Add plugin/hook/skill support, images, @mentions, dashboard, and stability fixes

Builds out locode's Claude Code plugin parity: MCP servers, slash commands,
sub-agents, hooks (12 lifecycle events), and skills, all loadable from a
local path or git URL. Also adds image support (read_file, /import), @-mention
file autocomplete, a /dashboard stats view, /export with an editable filename
prompt, an expanded git tool (reset/stash/merge/rebase/delete_branch), and a
redesigned status bar.

On top of that, a review pass found and fixed 10 correctness/stability bugs:
argument-injection in git reset/merge/rebase (a ref like "--hard" was parsed
as a flag), tool-call image results interleaving with native tool_call_id
messages and breaking OpenAI-compatible message ordering, backgrounded bash
jobs still being silently killed by their original timeout with the kill
masked as a clean exit, a SubagentStart hook's block being ignored, the
sub-agent timeout clock starting before the hook it should exclude, a
template-expansion bug that could re-substitute $1..$9 placeholders,
unbounded background-job output buffers, a missing directory-target fallback
in /export, and image size caps checked after reading the whole file instead
of before.

Session saves and exports now go through a shared atomic
write-then-rename helper so a crash can't leave a truncated file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-07-07 18:17:45 +09:00
co-authored by Claude Sonnet 5
parent 265f8930a3
commit 2a35f40c73
64 changed files with 3840 additions and 338 deletions
+144
View File
@@ -0,0 +1,144 @@
# Plan: Resolve locode's 9 known issues
## Goals
Fix all nine documented issues/limitations in one coherent pass, keep `typecheck` green, and add enough tests so `npm test` passes.
## Issues and implementation details
### 1. Grep bug: patterns starting with `-` are parsed as ripgrep flags
**File:** `src/tools/grep.ts`
**Approach:** Pass the pattern with ripgrep's `-e` option instead of as a positional argument, and put `--` before the path argument. This prevents any pattern (including `-foo`, `--foo`, `+foo`) from being interpreted as a flag.
**Test:** Add `src/tools/grep.test.ts` that mocks `execa` and verifies the generated args for a leading-dash pattern.
### 2. Security: session id is not sanitized before building the file path
**File:** `src/persistence/sessionStore.ts`
**Approach:**
- Derive a safe filename from the user-supplied id by replacing path separators and other unsafe characters with `_`.
- Keep the original id inside the record (it already is), but use the sanitized id for the filesystem lookup.
- Add a `safeId` helper and use it in `filePath`, `loadSession`, `deleteSession`, and `listSessions` so `--resume ../../etc/passwd` cannot escape the sessions directory.
**Test:** Add `src/persistence/sessionStore.test.ts` verifying that malicious ids are contained, normal ids still work, and `listSessions` ignores non-`.json` files.
### 3. Tests: `npm test` fails because there are no test files
**Approach:** Create the first test suite covering the fixes above. Add `vitest.config.ts` with `globals: true` and a `test/`/`src/**/*.test.ts` include. The tests will be pure unit tests that don't require a backend.
**Files to add:**
- `vitest.config.ts`
- `src/tools/grep.test.ts`
- `src/persistence/sessionStore.test.ts`
- `src/plugins/expandTemplate.test.ts` (small sanity test for existing behavior)
### 4. MCP content types: non-text results are placeholder-only
**Files:** `src/mcp/toolAdapter.ts`, `src/mcp/client.ts` (type update)
**Approach:**
- Update `McpToolInfo` and the call-tool handler to support `image`, `audio`, and `resource` content blocks from the MCP spec.
- For `image`: include `mimeType` and a truncated base64 note; if the model path is vision-capable, return the actual `data:` URI so the model can see it (same pattern as `read_file`).
- For `audio`: include `mimeType` and a note.
- For `resource`: if it's a text resource, inline the text; if binary, note the URI and mime type.
- Keep text blocks unchanged.
**Test:** Add `src/mcp/toolAdapter.test.ts` with mocked MCP content blocks of each type.
### 5. Hooks: only 6 events; no structured JSON output; no HTTP/prompt/agent hook types
**Files:** `src/hooks/types.ts`, `src/hooks/runner.ts`, `src/hooks/config.ts`
**Approach (scoped but complete within reason):**
- Expand the hook event set to the most useful missing events: `PermissionRequest`, `SubagentStart`, `SubagentStop`, `CwdChanged`, `FileChanged`, `ConfigChange`, `Stop` is already present. Final set: all 12 events from Claude Code's common set:
`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PermissionRequest`, `SubagentStart`, `SubagentStop`, `CwdChanged`, `FileChanged`, `ConfigChange`, `Stop`, `SessionEnd`.
- Add typed payload shapes for each event in `src/hooks/types.ts`.
- Add a structured output schema option: hooks can specify `outputSchema: "json"` in their config; when set, stdout is parsed as JSON and validated against a simple zod schema. If parsing fails, treat as a warning.
- Add an `HTTPHook` type (simple GET/POST with optional headers/body) and a `PromptHook` type (shows a yes/no prompt to the user). Wire `HTTPHook` in the runner; `PromptHook` will be added to types but marked as not yet implemented in the runner to avoid UI-blocking complexity in this pass.
- Fire `PermissionRequest` right before the user confirm gate in `agent/loop.ts gateAndRun`.
- Fire `SubagentStart`/`SubagentStop` around `runSubAgentTurn`.
- Fire `CwdChanged` when the session `cwd` would change (currently static; hook is informational for future use).
- Fire `FileChanged` after a mutating `write_file`/`edit_file`/`bash` succeeds.
- Fire `ConfigChange` when `/model`, `/backend`, `/mode`, or `locode config set` changes config.
**Test:** Add `src/hooks/runner.test.ts` testing exit-code semantics (0/2/other), JSON output parsing, and tool-scoped matching.
### 6. Plugin collisions: MCP and skill name collisions are not resolved cleanly
**Files:** `src/mcp/config.ts`, `src/plugins/skillTool.ts`, `src/plugins/registry.ts`
**Approach:**
- MCP servers: when a plugin provides a server whose name collides with a user or project server, log/record the collision and keep the more-specific tier (project > user > plugin). Add a `collisions` field to the returned merge result and surface it in `/mcp`.
- Skills: namespace skills internally as `<pluginName>/<skillName>` in the skill tool, but still accept the bare skill name for the `/name` shortcut. If two skills share a bare name, prefer the first loaded plugin and emit a warning in `/skills` listing duplicates.
- Plugin agent names are already namespaced (`agent__<plugin>__<agent>`); commands are already namespaced by the fact that they share a single command namespace. Add a warning list for duplicate command names too.
- Add a `PluginCollisionWarning` type and expose it via `getLoadedPlugins()` metadata.
**Test:** Add `src/plugins/skillTool.test.ts` and `src/mcp/config.test.ts` for collision behavior.
### 7. Skill references: cannot load sibling reference files for a skill
**Files:** `src/plugins/loader.ts`, `src/plugins/types.ts`, `src/plugins/skillTool.ts`
**Approach:**
- When loading a skill, also read any `references/*.md` files in the same skill directory.
- Store them as `references: { name, content }[]` on `PluginSkill`.
- When the skill is invoked (via the `skill` tool or `/name`), concatenate the references after the main SKILL.md body under a clear header so the model sees them.
**Test:** Add `src/plugins/loader.test.ts` with a temporary in-memory plugin layout.
### 8. Git operations: `git_commit` covers a subset
**File:** `src/tools/git.ts`
**Approach:** Extend `git_commit` with these additional operations:
- `reset` — `git reset` (mixed by default) with optional `ref` and `mode` (soft/mixed/hard). Requires confirmation; preview shows affected commits/files.
- `stash` — `git stash push` (with optional message and paths) and `git stash pop` (with optional stash ref). Preview shows what will be stashed/popped.
- `merge` — `git merge <branchName>` with optional `--no-ff`/`--ff-only`. Preview shows branches and commits.
- `rebase` — `git rebase <branchName>` with optional `--onto`. Preview shows commits.
- `delete_branch` — `git branch -d/-D <branchName>`. Preview shows the branch and whether it has unmerged commits.
- Add `operation` union entries and the necessary parameters (`ref`, `mode`, `stashRef`, `branchName`, `strategy`, `message`, `paths`, `force`).
- Keep the existing preview/handler pattern.
**Test:** Add `src/tools/git.test.ts` that validates argument generation for each operation (no real git exec).
### 9. Context window: 85% auto-compact threshold is hardcoded
**Files:** `src/agent/loop.ts`, `src/config/config.ts`, `src/config/store.ts`, `src/config/defaults.ts`, `src/cli.ts`
**Approach:**
- Add `autoCompactThreshold` to `StoredConfig` and env var `LOCODE_AUTO_COMPACT_THRESHOLD`.
- Default remains 0.85; allow values 0.1–0.95.
- Add `locode config set autoCompactThreshold <0.0-1.0>` and `locode config get autoCompactThreshold`.
- Read it in `resolveAutoCompactThreshold()` and use it in `shouldAutoCompact` in `agent/loop.ts`.
- Pass the threshold into the `Session` object so sub-agents inherit it.
**Test:** Add `src/config/config.test.ts` for threshold resolution and `src/agent/loop.test.ts` for the compact check.
## Files to modify
1. `src/tools/grep.ts`
2. `src/persistence/sessionStore.ts`
3. `src/mcp/toolAdapter.ts`
4. `src/mcp/client.ts`
5. `src/mcp/types.ts` (add status/collision type)
6. `src/mcp/config.ts`
7. `src/mcp/manager.ts` (surface collision warnings)
8. `src/hooks/types.ts`
9. `src/hooks/runner.ts`
10. `src/hooks/config.ts`
11. `src/agent/loop.ts` (fire new hooks, use threshold)
12. `src/agent/session.ts` (store threshold)
13. `src/plugins/loader.ts`
14. `src/plugins/types.ts`
15. `src/plugins/skillTool.ts`
16. `src/plugins/registry.ts` (collision tracking)
17. `src/plugins/agentTool.ts` (add command dup tracking if needed)
18. `src/tools/git.ts`
19. `src/config/config.ts`
20. `src/config/store.ts`
21. `src/config/defaults.ts`
22. `src/config/types.ts`
23. `src/cli.ts` (add config key)
24. `src/ui/ink/App.tsx` (pass threshold, fire ConfigChange)
25. `src/ui/ink/HistoryItemView.tsx` (show MCP collision warnings)
26. `README.md` (update limitations)
## Files to add
1. `vitest.config.ts`
2. `src/tools/grep.test.ts`
3. `src/persistence/sessionStore.test.ts`
4. `src/plugins/expandTemplate.test.ts`
5. `src/mcp/toolAdapter.test.ts`
6. `src/hooks/runner.test.ts`
7. `src/plugins/skillTool.test.ts`
8. `src/mcp/config.test.ts`
9. `src/plugins/loader.test.ts`
10. `src/tools/git.test.ts`
11. `src/config/config.test.ts`
12. `src/agent/loop.test.ts`
## Validation
- Run `npm run typecheck` — must pass.
- Run `npm test` — must pass.
- Run `npm run build` — must succeed.
## Risks / trade-offs
- Expanding hooks to 12 events touches `agent/loop.ts` in several places; need to keep event payloads consistent.
- MCP content type support is best-effort; real vision models may still ignore audio/resource blocks.
- Git operation expansion increases the chance of destructive commands (`reset --hard`, `rebase`, `delete_branch`); previews must be clear and the tool remains mutating/confirm-gated.
- Making the auto-compact threshold configurable requires threading a new field through `Session` creation/resumption.