Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions .opencode/plugins/ecc-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@
import type { PluginInput } from "@opencode-ai/plugin"
import * as fs from "fs"
import * as path from "path"
import {
initStore,
recordChange,
clearChanges,
} from "./lib/changed-files-store.js"
import changedFilesTool from "../tools/changed-files.js"
import dependencyAnalyzerTool from "../tools/dependency-analyzer.js"

Expand Down Expand Up @@ -80,7 +75,6 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
type HookProfile = "minimal" | "standard" | "strict"

const worktreePath = worktree || directory
initStore(worktreePath)

const editedFiles = new Set<string>()

Expand Down Expand Up @@ -110,6 +104,26 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
const log = (level: "debug" | "info" | "warn" | "error", message: string) =>
client.app.log({ body: { service: "ecc", level, message } })

// Loaded lazily (instead of via a top-level import) so that a missing or
// partially-installed `~/.opencode/plugins/lib` directory (e.g. an
// interrupted or partial ECC install on Termux/Android) only disables
// changed-files tracking, rather than throwing during module evaluation.
// This plugin is OpenCode's startup entry point, so a static import
// failure here previously crashed the whole plugin -- and with it, the
// entire OpenCode session -- before any hooks could load (see #2530).
let changedFilesStore: typeof import("./lib/changed-files-store.js") | undefined
try {
changedFilesStore = await import("./lib/changed-files-store.js")
changedFilesStore.initStore(worktreePath)
} catch (error) {
const reason = error instanceof Error ? error.message : String(error)
log(
"warn",
`[ECC] changed-files tracking disabled: could not load './lib/changed-files-store.js' (${reason}). ` +
"Run `ecc repair --target opencode` to restore the missing files. Other ECC hooks are unaffected."
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const normalizeProfile = (value: string | undefined): HookProfile => {
if (value === "minimal" || value === "strict") return value
return "standard"
Expand Down Expand Up @@ -154,7 +168,7 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
*/
"file.edited": async (event: { path: string }) => {
editedFiles.add(event.path)
recordChange(event.path, "modified")
changedFilesStore?.recordChange(event.path, "modified")

// Auto-format JS/TS files
if (hookEnabled("post:edit:format", ["strict"]) && event.path.match(/\.(ts|tsx|js|jsx)$/)) {
Expand Down Expand Up @@ -198,16 +212,16 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
) => {
const filePath = getFilePath(input.args)
if (input.tool === "edit" && filePath) {
recordChange(filePath, "modified")
changedFilesStore?.recordChange(filePath, "modified")
}
if (input.tool === "write" && filePath) {
const key = input.callID ?? `write-${++writeCounter}-${filePath}`
const pending = pendingToolChanges.get(key)
if (pending) {
recordChange(pending.path, pending.type)
changedFilesStore?.recordChange(pending.path, pending.type)
pendingToolChanges.delete(key)
} else {
recordChange(filePath, "modified")
changedFilesStore?.recordChange(filePath, "modified")
}
}

Expand Down Expand Up @@ -413,7 +427,7 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
if (!hookEnabled("session:end-marker", ["minimal", "standard", "strict"])) return
log("info", "[ECC] Session ended - cleaning up")
editedFiles.clear()
clearChanges()
changedFilesStore?.clearChanges()
pendingToolChanges.clear()
},

Expand All @@ -428,7 +442,7 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
let changeType: "added" | "modified" | "deleted" = "modified"
if (event.type === "create" || event.type === "add") changeType = "added"
else if (event.type === "delete" || event.type === "remove") changeType = "deleted"
recordChange(event.path, changeType)
changedFilesStore?.recordChange(event.path, changeType)
if (event.type === "change" && event.path.match(/\.(ts|tsx|js|jsx)$/)) {
editedFiles.add(event.path)
}
Expand Down
36 changes: 29 additions & 7 deletions .opencode/tools/changed-files.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
import {
buildTree,
getChangedPaths,
hasChanges,
type ChangeType,
type TreeNode,
} from "../plugins/lib/changed-files-store.js"
import type { ChangeType, TreeNode } from "../plugins/lib/changed-files-store.js"

const INDICATORS: Record<ChangeType, string> = {
added: "+",
Expand All @@ -26,6 +20,33 @@ function renderTree(nodes: TreeNode[], indent: string): string {
return lines.join("\n")
}

// Loaded lazily (instead of via a top-level import) so that a missing or
// partially-installed `~/.opencode/plugins` directory only breaks this one
// tool when it's actually invoked, rather than throwing during module
// evaluation. `tools/index.ts` re-exports every tool from a single barrel
// file, so a static import failure here previously took down the entire
// tools module -- and with it, the whole OpenCode session -- on the very
// first tool-loading pass (see #2530).
type ChangedFilesStore = typeof import("../plugins/lib/changed-files-store.js")
let changedFilesStorePromise: Promise<ChangedFilesStore> | undefined

async function loadChangedFilesStore(): Promise<ChangedFilesStore> {
if (!changedFilesStorePromise) {
changedFilesStorePromise = import("../plugins/lib/changed-files-store.js").catch((error) => {
changedFilesStorePromise = undefined
const reason = error instanceof Error ? error.message : String(error)
throw new Error(
"changed-files tool: could not load '../plugins/lib/changed-files-store.js'. " +
"This usually means the ~/.opencode/plugins directory is missing or incomplete " +
"(an interrupted or partial ECC install can leave tools/ populated without plugins/). " +
"Run `node scripts/repair.js --target opencode` (or `ecc repair --target opencode`) " +
`from the ECC repo to restore the missing files. Original error: ${reason}`
)
})
}
return changedFilesStorePromise
}

const changedFilesTool: ToolDefinition = tool({
description:
"List files changed by agents in this session as a navigable tree. Shows added (+), modified (~), and deleted (-) indicators. Use filter to show only specific change types. Returns paths for git diff.",
Expand All @@ -40,6 +61,7 @@ const changedFilesTool: ToolDefinition = tool({
.describe("Output format: tree for terminal display, json for structured data (default: tree)"),
},
async execute(args, context) {
const { buildTree, getChangedPaths, hasChanges } = await loadChangedFilesStore()
const filter = args.filter === "all" || !args.filter ? undefined : (args.filter as ChangeType)
const format = args.format ?? "tree"

Expand Down
40 changes: 39 additions & 1 deletion TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,44 @@ npm pkg set packageManager="pnpm@8.15.0"
rm package-lock.json # If using pnpm/yarn/bun
```

### OpenCode Fails to Start on Termux/Android

**Symptom:** Changed-files tracking silently stops working (a one-time
`[ECC] changed-files tracking disabled` warning appears in the OpenCode
logs), or (on older versions) `opencode` crashes on startup entirely with a
Bun `ResolveMessage`, e.g.:

```
ResolveMessage: Cannot find module '../plugins/lib/changed-files-store.js' from '.../.opencode/tools/changed-files.ts'
```

**Causes:**
- The `~/.opencode` install is missing or incomplete for this machine —
usually `tools/` and `plugins/` are present but `plugins/lib/` never
finished copying (an interrupted install, or a storage/permission hiccup
that's more common on Android's filesystem). Both the `changed-files` tool
and the `ecc-hooks` plugin depend on `plugins/lib/changed-files-store.js`;
since `ecc-hooks.ts` is OpenCode's plugin entry point (loaded once at
session startup, before `tools/index.ts`'s barrel file), a missing
dependency there used to crash the entire OpenCode session before any
hooks could load — not just the one tool.

**Solutions:**
```bash
# From the ECC repo, check for and repair missing/incomplete managed files
ecc doctor --target opencode
ecc repair --target opencode

# If that reports no drift but plugins/ is still missing on the device,
# re-run the ECC installer for the opencode target
```

**Note:** If you're also seeing `ProviderModelNotFoundError: Model not found: openai/gpt-5.5`
referencing `~/.config/opencode/oh-my-opencode-slim.json`, that file belongs to the
third-party [`oh-my-opencode-slim`](https://github.com/alvinunreal/oh-my-opencode-slim)
plugin, not ECC — ECC never writes to `~/.config/opencode/`. Fix the model prefix
(`opencode/...` instead of `openai/...`) there, or file it against that project.

---

## Performance Issues
Expand Down Expand Up @@ -429,4 +467,4 @@ find ~/.claude/plugins -name "*.sh" -exec dos2unix {} \;
- [README.md](./README.md) - Installation and features
Comment thread
shanujans marked this conversation as resolved.
- [CONTRIBUTING.md](./CONTRIBUTING.md) - Development guidelines
- [docs/](./docs/) - Detailed documentation
- [examples/](./examples/) - Usage examples
- [examples/](./examples/) - Usage examples