From 7c01a8134a6816e4ef046d44001800283d570a68 Mon Sep 17 00:00:00 2001 From: Shivay Lamba Date: Tue, 18 Aug 2026 02:43:46 +0530 Subject: [PATCH 1/5] feat: klaatcode support (cli adapter, native hooks) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Klaat Code (KlaatAI/klaatcode) as the 22nd supported harness. `agentmemory connect klaatcode` merges the MCP entry into ~/.klaatai/mcp.json. Klaat Code keys its servers under `servers` rather than `mcpServers`, so the adapter reuses the same wrapperKey escape hatch Zed already needs for `context_servers`. `--with-hooks` installs six native auto-capture hooks into ~/.klaatai/hooks.json. Klaat Code's hooks config is flat — event name straight to a list of entries, where an entry is a bare shell string (v1) or { command, matcher?, timeout? } (v2) — and its event names are snake_case, so buildMergedHooks() from codex-hooks.ts does not apply. klaatcode-hooks.ts is a small sibling that keeps the same two guarantees: ${CLAUDE_PLUGIN_ROOT} is resolved to the absolute bundled plugin path, and re-installs strip our previous entries first so upgrading never duplicates and never touches the user's own hooks. Klaat Code names the project path `project_root` in its hook payload and exports KLAATAI_PROJECT_ROOT, neither of which hookCwd() knew about, so the hooks would have attached observations to the wrong project. Both are now in the resolution chain, alongside the existing Devin and Claude Code fallbacks. Context injection is left alone: Klaat Code discards hook stdout except for before_tool block decisions, and AGENTMEMORY_INJECT_CONTEXT is default-off anyway. Recall runs through the MCP tools and the 17 skills, which Klaat Code already picks up from .claude/skills without any extra wiring. Known limitation, documented in the README row and printed after install: hooks and MCP are wired into Klaat Code's interactive TUI only, so `klaatai run` (headless) and ACP sessions are not captured. Signed-off-by: Shivay Lamba --- README.md | 1 + plugin/hooks/hooks.klaatcode.json | 39 +++++ plugin/scripts/notification.mjs | 3 +- plugin/scripts/post-commit.mjs | 3 +- plugin/scripts/post-tool-failure.mjs | 3 +- plugin/scripts/post-tool-use.mjs | 3 +- plugin/scripts/pre-compact.mjs | 3 +- plugin/scripts/prompt-submit.mjs | 3 +- plugin/scripts/session-end.mjs | 3 +- plugin/scripts/session-start.mjs | 3 +- plugin/scripts/subagent-start.mjs | 3 +- plugin/scripts/subagent-stop.mjs | 3 +- plugin/scripts/task-completed.mjs | 3 +- plugin/skills/agentmemory-agents/REFERENCE.md | 3 +- src/cli/connect/index.ts | 2 + src/cli/connect/klaatcode-hooks.ts | 115 +++++++++++++++ src/cli/connect/klaatcode.ts | 92 ++++++++++++ src/cli/onboarding.ts | 1 + src/hooks/_project.ts | 8 +- test/cli-connect.test.ts | 3 +- test/klaatcode-connect-hooks.test.ts | 133 ++++++++++++++++++ 21 files changed, 416 insertions(+), 14 deletions(-) create mode 100644 plugin/hooks/hooks.klaatcode.json create mode 100644 src/cli/connect/klaatcode-hooks.ts create mode 100644 src/cli/connect/klaatcode.ts create mode 100644 test/klaatcode-connect-hooks.test.ts diff --git a/README.md b/README.md index 6a6462d56..836e46e60 100644 --- a/README.md +++ b/README.md @@ -709,6 +709,7 @@ The agentmemory entry is the **same MCP server block** across every host that us | **Antigravity** (replaces Gemini CLI) | `mcp_config.json` (in Antigravity's User dir) | `agentmemory connect antigravity` writes the standard `mcpServers` block. macOS: `~/Library/Application Support/Antigravity/User/`. Linux: `~/.config/Antigravity/User/`. Use after the 2026-06-18 Gemini CLI sunset. | | **Antigravity CLI** (`agy`) | `~/.gemini/config/mcp_config.json` | `agentmemory connect antigravity-cli`. The `agy` CLI keeps its own config under `~/.gemini/`, separate from the Antigravity IDE above. Pass `--with-hooks` for native auto-capture via `~/.gemini/config/hooks.json`. | | **Kiro** | `~/.kiro/settings/mcp.json` | `agentmemory connect kiro` writes the user-level config. Workspace overrides go in `.kiro/settings/mcp.json` next to your code. | +| **Klaat Code (MCP + hooks)** | `~/.klaatai/mcp.json` | `agentmemory connect klaatcode` merges the MCP entry under Klaat Code's `servers` key (not `mcpServers`); `--with-hooks` adds six native auto-capture hooks (`session_start`, `before_message`, `before_tool`, `after_tool`, `after_message`, `session_end`) into `~/.klaatai/hooks.json` with Klaat Code's snake_case tool matchers. Verify with `/mcp` and `/hooks` inside klaatcode. Hooks fire in the interactive TUI only — `klaatai run` (headless) and ACP sessions are not captured. Klaat Code also auto-loads `.claude/skills/`, so the 17 agentmemory skills work with no extra step. | | **Warp** | `~/.warp/.mcp.json` | `agentmemory connect warp` writes the standard `mcpServers` block. Warp also auto-discovers skills from `.claude/skills/`; once the Claude Code plugin is installed the 8 agentmemory skills (`remember`, `recall`, `recap`, `handoff`, `forget`, `commit-context`, `commit-history`, `session-history`) appear natively in Warp's slash-command palette. | | **Cline (CLI)** | `~/.cline/mcp.json` | `agentmemory connect cline` writes the standard `mcpServers` block. VS Code extension users: paste the same block via Cline Settings → MCP Servers → Edit JSON. | | **Continue.dev** | `~/.continue/config.yaml` (preferred) or `config.json` (legacy) | `agentmemory connect continue` creates `config.yaml` from scratch when neither exists, or modifies existing `config.json`. **If you already have `config.yaml`** the adapter prints the exact block to paste under `mcpServers:`; it won't silently rewrite your yaml because preserving comments and anchors safely needs a YAML parser the package doesn't ship. Continue uses array form (not object) for `mcpServers`. | diff --git a/plugin/hooks/hooks.klaatcode.json b/plugin/hooks/hooks.klaatcode.json new file mode 100644 index 000000000..9a4bac8b0 --- /dev/null +++ b/plugin/hooks/hooks.klaatcode.json @@ -0,0 +1,39 @@ +{ + "session_start": [ + { + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.mjs\"", + "timeout": 5 + } + ], + "before_message": [ + { + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/prompt-submit.mjs\"", + "timeout": 5 + } + ], + "before_tool": [ + { + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-use.mjs\"", + "matcher": "^(run_command|edit_file|multi_edit|write_file|read_file|apply_patch|grep|glob)$", + "timeout": 5 + } + ], + "after_tool": [ + { + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-use.mjs\"", + "timeout": 5 + } + ], + "after_message": [ + { + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop.mjs\"", + "timeout": 5 + } + ], + "session_end": [ + { + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-end.mjs\"", + "timeout": 5 + } + ] +} diff --git a/plugin/scripts/notification.mjs b/plugin/scripts/notification.mjs index dcf2e3931..e4b1c4abf 100755 --- a/plugin/scripts/notification.mjs +++ b/plugin/scripts/notification.mjs @@ -27,7 +27,8 @@ function hookCwd(data) { if (Array.isArray(roots)) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; + const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; } //#endregion diff --git a/plugin/scripts/post-commit.mjs b/plugin/scripts/post-commit.mjs index 56a17ccea..f5867fbbe 100755 --- a/plugin/scripts/post-commit.mjs +++ b/plugin/scripts/post-commit.mjs @@ -9,7 +9,8 @@ function hookCwd(data) { if (Array.isArray(roots)) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; + const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; } //#endregion diff --git a/plugin/scripts/post-tool-failure.mjs b/plugin/scripts/post-tool-failure.mjs index 0af94e7cd..9f9d27387 100755 --- a/plugin/scripts/post-tool-failure.mjs +++ b/plugin/scripts/post-tool-failure.mjs @@ -27,7 +27,8 @@ function hookCwd(data) { if (Array.isArray(roots)) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; + const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; } //#endregion diff --git a/plugin/scripts/post-tool-use.mjs b/plugin/scripts/post-tool-use.mjs index 1189e35fe..207651236 100755 --- a/plugin/scripts/post-tool-use.mjs +++ b/plugin/scripts/post-tool-use.mjs @@ -27,7 +27,8 @@ function hookCwd(data) { if (Array.isArray(roots)) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; + const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; } //#endregion diff --git a/plugin/scripts/pre-compact.mjs b/plugin/scripts/pre-compact.mjs index 753094359..eb4b78576 100755 --- a/plugin/scripts/pre-compact.mjs +++ b/plugin/scripts/pre-compact.mjs @@ -27,7 +27,8 @@ function hookCwd(data) { if (Array.isArray(roots)) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; + const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; } //#endregion diff --git a/plugin/scripts/prompt-submit.mjs b/plugin/scripts/prompt-submit.mjs index 53daba26c..61eb61eeb 100755 --- a/plugin/scripts/prompt-submit.mjs +++ b/plugin/scripts/prompt-submit.mjs @@ -27,7 +27,8 @@ function hookCwd(data) { if (Array.isArray(roots)) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; + const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; } //#endregion diff --git a/plugin/scripts/session-end.mjs b/plugin/scripts/session-end.mjs index f2d8f79b1..0acf2f4e3 100755 --- a/plugin/scripts/session-end.mjs +++ b/plugin/scripts/session-end.mjs @@ -28,7 +28,8 @@ function hookCwd(data) { if (Array.isArray(roots)) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; + const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; } //#endregion diff --git a/plugin/scripts/session-start.mjs b/plugin/scripts/session-start.mjs index 7c112250c..41ca01b2d 100755 --- a/plugin/scripts/session-start.mjs +++ b/plugin/scripts/session-start.mjs @@ -27,7 +27,8 @@ function hookCwd(data) { if (Array.isArray(roots)) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; + const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; } //#endregion diff --git a/plugin/scripts/subagent-start.mjs b/plugin/scripts/subagent-start.mjs index 722f0c7f0..97b45b96a 100755 --- a/plugin/scripts/subagent-start.mjs +++ b/plugin/scripts/subagent-start.mjs @@ -27,7 +27,8 @@ function hookCwd(data) { if (Array.isArray(roots)) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; + const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; } //#endregion diff --git a/plugin/scripts/subagent-stop.mjs b/plugin/scripts/subagent-stop.mjs index 8927c1af6..49c800968 100755 --- a/plugin/scripts/subagent-stop.mjs +++ b/plugin/scripts/subagent-stop.mjs @@ -27,7 +27,8 @@ function hookCwd(data) { if (Array.isArray(roots)) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; + const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; } //#endregion diff --git a/plugin/scripts/task-completed.mjs b/plugin/scripts/task-completed.mjs index 613a9cd9b..8eed3f8e5 100755 --- a/plugin/scripts/task-completed.mjs +++ b/plugin/scripts/task-completed.mjs @@ -27,7 +27,8 @@ function hookCwd(data) { if (Array.isArray(roots)) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; + const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; } //#endregion diff --git a/plugin/skills/agentmemory-agents/REFERENCE.md b/plugin/skills/agentmemory-agents/REFERENCE.md index 84d637d8e..32d165d66 100644 --- a/plugin/skills/agentmemory-agents/REFERENCE.md +++ b/plugin/skills/agentmemory-agents/REFERENCE.md @@ -3,7 +3,7 @@ Generated from `src/cli/connect/index.ts`. Do not edit the block below by hand; run `npm run skills:gen` after adding or removing an adapter. -`agentmemory connect ` wires the memory server into a host agent. 21 adapters: +`agentmemory connect ` wires the memory server into a host agent. 22 adapters: | Agent | Name | Protocol | | --- | --- | --- | @@ -21,6 +21,7 @@ Generated from `src/cli/connect/index.ts`. Do not edit the block below by hand; | Gemini CLI | `gemini-cli` | Using MCP (the only protocol Gemini CLI speaks). Memory bridge runs at :3111 underneath. | | Hermes Agent | `hermes` | Using MCP. Hooks are also available, see https://github.com/rohitg00/agentmemory/tree/main/integrations/hermes. | | Kiro | `kiro` | Using MCP via ~/.kiro/settings/mcp.json (user-level). Workspace overrides live in .kiro/settings/mcp.json. | +| Klaat Code | `klaatcode` | Using MCP via ~/.klaatai/mcp.json (key: servers). Pass --with-hooks for native auto-capture. | | OpenClaw | `openclaw` | Using MCP. Hooks are also available, see https://github.com/rohitg00/agentmemory/tree/main/integrations/openclaw. | | OpenCode | `opencode` | Using MCP via ~/.config/opencode/opencode.json (top-level `mcp` key). For full auto-capture, also install the bundled plugin in plugin/opencode/. | | OpenHuman | `openhuman` | Using native hooks (REST API at :3111). MCP not required. | diff --git a/src/cli/connect/index.ts b/src/cli/connect/index.ts index 981b7d65f..cb0be153c 100644 --- a/src/cli/connect/index.ts +++ b/src/cli/connect/index.ts @@ -17,6 +17,7 @@ import { adapter as dsh } from "./dsh.js"; import { adapter as geminiCli } from "./gemini-cli.js"; import { adapter as hermes } from "./hermes.js"; import { adapter as kiro } from "./kiro.js"; +import { adapter as klaatcode } from "./klaatcode.js"; import { adapter as openclaw } from "./openclaw.js"; import { adapter as opencode } from "./opencode.js"; import { adapter as openhuman } from "./openhuman.js"; @@ -36,6 +37,7 @@ export const ADAPTERS: readonly ConnectAdapter[] = [ antigravity, antigravityCli, kiro, + klaatcode, warp, cline, continueDev, diff --git a/src/cli/connect/klaatcode-hooks.ts b/src/cli/connect/klaatcode-hooks.ts new file mode 100644 index 000000000..7dc2085f2 --- /dev/null +++ b/src/cli/connect/klaatcode-hooks.ts @@ -0,0 +1,115 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +/** + * Merge engine for Klaat Code's hooks file (`~/.klaatai/hooks.json`). + * + * Klaat Code does not use the nested Claude-Code hook shape that + * `codex-hooks.ts#buildMergedHooks` handles. Its config is flat — event + * name maps straight to a list of entries, where an entry is either a bare + * shell string (v1) or `{ command, matcher?, timeout? }` (v2): + * + * { "session_start": ["cmd"], + * "before_tool": [{ "command": "…", "matcher": "^edit_file$", "timeout": 5 }] } + * + * Event names differ too (`session_start` / `before_tool` / … rather than + * `SessionStart` / `PreToolUse` / …), so the bundled manifest for Klaat Code + * is authored in its native shape and this module only resolves paths and + * de-duplicates. See KlaatAI/klaatcode `src/screens/repl.ts` for the loader. + * + * The two guarantees match the Codex/Claude engine: + * 1. `${CLAUDE_PLUGIN_ROOT}` is rewritten to the absolute bundled + * `plugin/` path, so the written file needs no env expansion (Klaat + * Code does not inject that variable — it is used here purely as the + * internal placeholder token every bundled manifest shares). + * 2. Re-installs are idempotent: any pre-existing entry whose command + * points under `/scripts/` is stripped before ours are + * appended, so upgrading never leaves stale duplicates and never + * touches the user's own hooks. + */ + +export type KlaatcodeHookEvent = + | "before_tool" + | "after_tool" + | "before_message" + | "after_message" + | "session_start" + | "session_end"; + +export type KlaatcodeHookEntry = + | string + | { command: string; matcher?: string; timeout?: number }; + +export type KlaatcodeHooksConfig = Partial< + Record +>; + +function entryCommand(entry: KlaatcodeHookEntry): string { + return typeof entry === "string" ? entry : entry.command; +} + +function normalizePathForCommandMatch(value: string): string { + return value.replace(/\\/g, "/"); +} + +function isAgentmemoryEntry( + entry: KlaatcodeHookEntry, + scriptsDir: string, +): boolean { + return normalizePathForCommandMatch(entryCommand(entry)).includes( + normalizePathForCommandMatch(scriptsDir), + ); +} + +function resolveEntry( + entry: KlaatcodeHookEntry, + pluginRoot: string, +): KlaatcodeHookEntry { + const command = entryCommand(entry).replace( + /\$\{CLAUDE_PLUGIN_ROOT\}/g, + pluginRoot, + ); + if (typeof entry === "string") return command; + const next: { command: string; matcher?: string; timeout?: number } = { + command, + }; + if (entry.matcher !== undefined) next.matcher = entry.matcher; + if (entry.timeout !== undefined) next.timeout = entry.timeout; + return next; +} + +export function buildMergedKlaatcodeHooks( + existing: KlaatcodeHooksConfig | null, + pluginRoot: string, + manifestFile = "hooks.klaatcode.json", +): KlaatcodeHooksConfig { + const ours = JSON.parse( + readFileSync(join(pluginRoot, "hooks", manifestFile), "utf-8"), + ) as KlaatcodeHooksConfig; + const scriptsDir = join(pluginRoot, "scripts"); + + const out: KlaatcodeHooksConfig = {}; + + if (existing) { + for (const [event, entries] of Object.entries(existing) as [ + KlaatcodeHookEvent, + KlaatcodeHookEntry[], + ][]) { + if (!Array.isArray(entries)) continue; + const kept = entries.filter( + (entry) => !isAgentmemoryEntry(entry, scriptsDir), + ); + if (kept.length > 0) out[event] = kept; + } + } + + for (const [event, entries] of Object.entries(ours) as [ + KlaatcodeHookEvent, + KlaatcodeHookEntry[], + ][]) { + const resolved = entries.map((entry) => resolveEntry(entry, pluginRoot)); + out[event] = [...(out[event] ?? []), ...resolved]; + } + + return out; +} diff --git a/src/cli/connect/klaatcode.ts b/src/cli/connect/klaatcode.ts new file mode 100644 index 000000000..7edff2de5 --- /dev/null +++ b/src/cli/connect/klaatcode.ts @@ -0,0 +1,92 @@ +import { existsSync, mkdirSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import * as p from "@clack/prompts"; +import { createJsonMcpAdapter } from "./json-mcp-adapter.js"; +import type { ConnectOptions, ConnectResult } from "./types.js"; +import { findPluginRoot } from "./codex-hooks.js"; +import { + buildMergedKlaatcodeHooks, + type KlaatcodeHooksConfig, +} from "./klaatcode-hooks.js"; +import { + backupFile, + logBackup, + logInstalled, + readJsonSafe, + writeJsonAtomic, +} from "./util.js"; + +// Klaat Code keeps everything under ~/.klaatai (relocatable via KLAATAI_DIR), +// with MCP servers and lifecycle hooks in separate files rather than one +// settings blob. MCP entries live under the top-level "servers" key — the +// same situation as Zed's "context_servers", handled by wrapperKey. +function klaatDir(): string { + const override = process.env["KLAATAI_DIR"]; + if (override && override.trim()) return override; + return join(homedir(), ".klaatai"); +} + +const KLAAT_DIR = klaatDir(); +const KLAAT_MCP = join(KLAAT_DIR, "mcp.json"); +const KLAAT_HOOKS = join(KLAAT_DIR, "hooks.json"); + +export const adapter = createJsonMcpAdapter({ + name: "klaatcode", + displayName: "Klaat Code", + detectDir: KLAAT_DIR, + configPath: KLAAT_MCP, + wrapperKey: "servers", + category: "native", + docs: "https://github.com/rohitg00/agentmemory#other-agents", + protocolNote: + "→ Using MCP via ~/.klaatai/mcp.json (key: servers). Pass --with-hooks for native auto-capture.", + installHooks: installKlaatcodeHooks, +}); + +function installKlaatcodeHooks(opts: ConnectOptions): ConnectResult { + let pluginRoot: string; + try { + pluginRoot = findPluginRoot(); + } catch (err) { + return { + kind: "skipped", + reason: err instanceof Error ? err.message : String(err), + }; + } + + const existing = readJsonSafe(KLAAT_HOOKS); + const merged = buildMergedKlaatcodeHooks( + existing, + pluginRoot, + "hooks.klaatcode.json", + ); + + if (opts.dryRun) { + p.log.info( + `[dry-run] Would write ${Object.keys(merged).length} hook event(s) into ${KLAAT_HOOKS}`, + ); + return { kind: "installed", mutatedPath: KLAAT_HOOKS }; + } + + let backupPath: string | undefined; + if (existsSync(KLAAT_HOOKS)) { + backupPath = backupFile(KLAAT_HOOKS, "klaatcode-hooks", "json"); + logBackup(backupPath); + } else { + mkdirSync(KLAAT_DIR, { recursive: true }); + } + + writeJsonAtomic(KLAAT_HOOKS, merged); + + logInstalled("Klaat Code hooks", KLAAT_HOOKS); + p.log.info( + "Verify with `/hooks` inside klaatcode. Hooks fire in the interactive TUI only — headless (`klaatai run`) and ACP sessions are not captured. Re-run `agentmemory connect klaatcode --with-hooks` after upgrading agentmemory so the plugin paths stay current.", + ); + + return { + kind: "installed", + mutatedPath: KLAAT_HOOKS, + ...(backupPath !== undefined && { backupPath }), + }; +} diff --git a/src/cli/onboarding.ts b/src/cli/onboarding.ts index 926cdbea9..6d8b8a39f 100644 --- a/src/cli/onboarding.ts +++ b/src/cli/onboarding.ts @@ -45,6 +45,7 @@ const AGENT_GLYPH: Record = { codex: "◎", cursor: "◫", "gemini-cli": "✦", + klaatcode: "◭", opencode: "⬡", }; diff --git a/src/hooks/_project.ts b/src/hooks/_project.ts index 9f0320c5c..8f0f62700 100644 --- a/src/hooks/_project.ts +++ b/src/hooks/_project.ts @@ -28,8 +28,14 @@ export function hookCwd(data: Record | null | undefined): strin if (typeof root === "string" && root.trim()) return root; } } + // Klaat Code names the field project_root rather than cwd. + if (typeof data.project_root === "string" && data.project_root.trim()) { + return data.project_root; + } const projectDir = - process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"]; + process.env["DEVIN_PROJECT_DIR"] || + process.env["CLAUDE_PROJECT_DIR"] || + process.env["KLAATAI_PROJECT_ROOT"]; if (projectDir && projectDir.trim()) return projectDir; return undefined; } diff --git a/test/cli-connect.test.ts b/test/cli-connect.test.ts index ce1cc3440..4c741634b 100644 --- a/test/cli-connect.test.ts +++ b/test/cli-connect.test.ts @@ -57,6 +57,7 @@ describe("agentmemory connect — dispatcher", () => { "gemini-cli", "hermes", "kiro", + "klaatcode", "opencode", "openclaw", "openhuman", @@ -66,7 +67,7 @@ describe("agentmemory connect — dispatcher", () => { "zed", ].sort(), ); - expect(ADAPTERS.length).toBe(21); + expect(ADAPTERS.length).toBe(22); }); it("every adapter exposes detect() and install()", () => { diff --git a/test/klaatcode-connect-hooks.test.ts b/test/klaatcode-connect-hooks.test.ts new file mode 100644 index 000000000..7dffe9b5c --- /dev/null +++ b/test/klaatcode-connect-hooks.test.ts @@ -0,0 +1,133 @@ +import { describe, it, expect } from "vitest"; +import { resolve } from "node:path"; +import { spawn } from "node:child_process"; +import { findPluginRoot } from "../src/cli/connect/codex-hooks.js"; +import { + buildMergedKlaatcodeHooks, + type KlaatcodeHooksConfig, +} from "../src/cli/connect/klaatcode-hooks.js"; + +const PLUGIN_ROOT = resolve(__dirname, "..", "plugin"); + +const KLAATCODE_EVENTS = [ + "session_start", + "before_message", + "before_tool", + "after_tool", + "after_message", + "session_end", +]; + +const KLAATCODE_TOOL_NAMES = [ + "run_command", + "edit_file", + "multi_edit", + "write_file", + "read_file", + "apply_patch", + "grep", + "glob", +]; + +function entryCommand(entry: string | { command: string }): string { + return typeof entry === "string" ? entry : entry.command; +} + +function runHook( + script: string, + payload: Record, + env: Record = {}, +): Promise<{ out: string; code: number | null }> { + return new Promise((resolve_) => { + const child = spawn("node", [`plugin/scripts/${script}.mjs`], { + env: { ...process.env, AGENTMEMORY_URL: "http://127.0.0.1:1", ...env }, + }); + let out = ""; + child.stdout.on("data", (c) => (out += c)); + child.on("exit", (code) => resolve_({ out, code })); + child.stdin.write(JSON.stringify(payload)); + child.stdin.end(); + }); +} + +describe("buildMergedKlaatcodeHooks (Klaat Code manifest)", () => { + it("rewrites ${CLAUDE_PLUGIN_ROOT} to absolute pluginRoot in every command", () => { + const merged = buildMergedKlaatcodeHooks(null, findPluginRoot()); + for (const entries of Object.values(merged)) { + for (const entry of entries!) { + const cmd = entryCommand(entry); + expect(cmd).not.toContain("${CLAUDE_PLUGIN_ROOT}"); + expect(cmd).toContain(`${PLUGIN_ROOT}/scripts/`); + } + } + }); + + it("wires exactly the six events Klaat Code dispatches", () => { + const merged = buildMergedKlaatcodeHooks(null, findPluginRoot()); + expect(Object.keys(merged).sort()).toEqual([...KLAATCODE_EVENTS].sort()); + }); + + it("matches Klaat Code's snake_case tool names, not Claude Code's", () => { + const merged = buildMergedKlaatcodeHooks(null, findPluginRoot()); + const entry = merged["before_tool"]?.[0]; + expect(typeof entry).toBe("object"); + const matcher = (entry as { matcher?: string }).matcher; + expect(matcher).toBeDefined(); + const re = new RegExp(matcher!); + for (const tool of KLAATCODE_TOOL_NAMES) expect(re.test(tool)).toBe(true); + for (const claudeTool of ["Edit", "Write", "Bash"]) { + expect(re.test(claudeTool)).toBe(false); + } + }); + + it("preserves user-authored hooks in both v1 and v2 form", () => { + const existing: KlaatcodeHooksConfig = { + after_message: ["afplay /System/Library/Sounds/Glass.aiff"], + before_tool: [{ command: "./scripts/guard-shell.sh", matcher: "run_command" }], + }; + const merged = buildMergedKlaatcodeHooks(existing, findPluginRoot()); + expect(merged["after_message"]?.[0]).toBe( + "afplay /System/Library/Sounds/Glass.aiff", + ); + expect(entryCommand(merged["before_tool"]![0]!)).toBe( + "./scripts/guard-shell.sh", + ); + }); + + it("is idempotent across re-installs", () => { + const root = findPluginRoot(); + const once = buildMergedKlaatcodeHooks(null, root); + const twice = buildMergedKlaatcodeHooks(once, root); + expect(twice).toEqual(once); + }); +}); + +describe("hook payload compatibility with Klaat Code", () => { + it("resolves the project from the payload's project_root field", async () => { + const { resolveProject, hookCwd } = await import("../src/hooks/_project.js"); + const payload = { session_id: "s1", project_root: "/tmp" }; + expect(hookCwd(payload)).toBe("/tmp"); + expect(resolveProject(hookCwd(payload))).toBe("tmp"); + }); + + it("falls back to KLAATAI_PROJECT_ROOT when the payload carries no path", async () => { + const { hookCwd } = await import("../src/hooks/_project.js"); + const before = process.env["KLAATAI_PROJECT_ROOT"]; + process.env["KLAATAI_PROJECT_ROOT"] = "/tmp"; + try { + expect(hookCwd({ session_id: "s1" })).toBe("/tmp"); + } finally { + if (before === undefined) delete process.env["KLAATAI_PROJECT_ROOT"]; + else process.env["KLAATAI_PROJECT_ROOT"] = before; + } + }); + + it("fails open when the memory server is unreachable", async () => { + const { code } = await runHook("session-start", { + event: "session_start", + session_id: "s1", + project_root: "/tmp", + }); + expect(code).toBe(0); + }); +}); From 4e554ae4694226216f25056ec5e96ff8eca17ad3 Mon Sep 17 00:00:00 2001 From: Shivay Lamba Date: Tue, 18 Aug 2026 03:06:22 +0530 Subject: [PATCH 2/5] docs: add Klaat Code logo to README grid and website Signed-off-by: Shivay Lamba --- README.md | 5 +++++ website/components/Agents.tsx | 8 ++++++++ website/public/klaatcode.png | Bin 0 -> 4769 bytes 3 files changed, 13 insertions(+) create mode 100644 website/public/klaatcode.png diff --git a/README.md b/README.md index 836e46e60..3868fce6b 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,11 @@ agentmemory works with any agent that supports hooks, MCP, or REST API. All agen Warp
connect + MCP + skills + +Klaat Code
+Klaat Code
+6 hooks + MCP + diff --git a/website/components/Agents.tsx b/website/components/Agents.tsx index d8f4fa3ca..2cf05e6be 100644 --- a/website/components/Agents.tsx +++ b/website/components/Agents.tsx @@ -233,6 +233,14 @@ const MARQUEE: Agent[] = [ accent: "#317CFF", href: "https://devin.ai", }, + { + id: "klaatcode", + name: "Klaat Code", + from: "KlaatAI", + logo: "/klaatcode.png", + accent: "#8B5CF6", + href: "https://github.com/KlaatAI/klaatcode", + }, ]; function FeaturedCard({ a }: { a: Agent }) { diff --git a/website/public/klaatcode.png b/website/public/klaatcode.png new file mode 100644 index 0000000000000000000000000000000000000000..5412271c2bd953e7ff687d4de012f950ec115d6f GIT binary patch literal 4769 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 Date: Tue, 18 Aug 2026 03:11:52 +0530 Subject: [PATCH 3/5] fix: take first non-blank project dir env var A `||` chain stops at a whitespace-only earlier value, so a blank DEVIN_PROJECT_DIR would shadow a valid KLAATAI_PROJECT_ROOT and leave the project unresolved. Select the first non-blank var instead. Raised by CodeRabbit on #1222. Signed-off-by: Shivay Lamba --- plugin/scripts/notification.mjs | 10 ++++++++-- plugin/scripts/post-commit.mjs | 10 ++++++++-- plugin/scripts/post-tool-failure.mjs | 10 ++++++++-- plugin/scripts/post-tool-use.mjs | 10 ++++++++-- plugin/scripts/pre-compact.mjs | 10 ++++++++-- plugin/scripts/prompt-submit.mjs | 10 ++++++++-- plugin/scripts/session-end.mjs | 10 ++++++++-- plugin/scripts/session-start.mjs | 10 ++++++++-- plugin/scripts/subagent-start.mjs | 10 ++++++++-- plugin/scripts/subagent-stop.mjs | 10 ++++++++-- plugin/scripts/task-completed.mjs | 10 ++++++++-- src/hooks/_project.ts | 15 ++++++++++----- test/klaatcode-connect-hooks.test.ts | 21 +++++++++++++++++++++ 13 files changed, 119 insertions(+), 27 deletions(-) diff --git a/plugin/scripts/notification.mjs b/plugin/scripts/notification.mjs index e4b1c4abf..6e35018f4 100755 --- a/plugin/scripts/notification.mjs +++ b/plugin/scripts/notification.mjs @@ -28,8 +28,14 @@ function hookCwd(data) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT" + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } } //#endregion //#region src/hooks/notification.ts diff --git a/plugin/scripts/post-commit.mjs b/plugin/scripts/post-commit.mjs index f5867fbbe..995f11415 100755 --- a/plugin/scripts/post-commit.mjs +++ b/plugin/scripts/post-commit.mjs @@ -10,8 +10,14 @@ function hookCwd(data) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT" + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } } //#endregion //#region src/hooks/post-commit.ts diff --git a/plugin/scripts/post-tool-failure.mjs b/plugin/scripts/post-tool-failure.mjs index 9f9d27387..0f4532f5f 100755 --- a/plugin/scripts/post-tool-failure.mjs +++ b/plugin/scripts/post-tool-failure.mjs @@ -28,8 +28,14 @@ function hookCwd(data) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT" + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } } //#endregion //#region src/hooks/post-tool-failure.ts diff --git a/plugin/scripts/post-tool-use.mjs b/plugin/scripts/post-tool-use.mjs index 207651236..a164d8eda 100755 --- a/plugin/scripts/post-tool-use.mjs +++ b/plugin/scripts/post-tool-use.mjs @@ -28,8 +28,14 @@ function hookCwd(data) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT" + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } } //#endregion //#region src/hooks/post-tool-use.ts diff --git a/plugin/scripts/pre-compact.mjs b/plugin/scripts/pre-compact.mjs index eb4b78576..50352a5cb 100755 --- a/plugin/scripts/pre-compact.mjs +++ b/plugin/scripts/pre-compact.mjs @@ -28,8 +28,14 @@ function hookCwd(data) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT" + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } } //#endregion //#region src/hooks/pre-compact.ts diff --git a/plugin/scripts/prompt-submit.mjs b/plugin/scripts/prompt-submit.mjs index 61eb61eeb..c1c600e40 100755 --- a/plugin/scripts/prompt-submit.mjs +++ b/plugin/scripts/prompt-submit.mjs @@ -28,8 +28,14 @@ function hookCwd(data) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT" + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } } //#endregion //#region src/hooks/prompt-submit.ts diff --git a/plugin/scripts/session-end.mjs b/plugin/scripts/session-end.mjs index 0acf2f4e3..20d845e8a 100755 --- a/plugin/scripts/session-end.mjs +++ b/plugin/scripts/session-end.mjs @@ -29,8 +29,14 @@ function hookCwd(data) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT" + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } } //#endregion //#region src/hooks/session-end.ts diff --git a/plugin/scripts/session-start.mjs b/plugin/scripts/session-start.mjs index 41ca01b2d..40ba01ffc 100755 --- a/plugin/scripts/session-start.mjs +++ b/plugin/scripts/session-start.mjs @@ -28,8 +28,14 @@ function hookCwd(data) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT" + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } } //#endregion //#region src/hooks/session-start.ts diff --git a/plugin/scripts/subagent-start.mjs b/plugin/scripts/subagent-start.mjs index 97b45b96a..82eb74d7f 100755 --- a/plugin/scripts/subagent-start.mjs +++ b/plugin/scripts/subagent-start.mjs @@ -28,8 +28,14 @@ function hookCwd(data) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT" + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } } //#endregion //#region src/hooks/subagent-start.ts diff --git a/plugin/scripts/subagent-stop.mjs b/plugin/scripts/subagent-stop.mjs index 49c800968..2e84ecfcd 100755 --- a/plugin/scripts/subagent-stop.mjs +++ b/plugin/scripts/subagent-stop.mjs @@ -28,8 +28,14 @@ function hookCwd(data) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT" + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } } //#endregion //#region src/hooks/subagent-stop.ts diff --git a/plugin/scripts/task-completed.mjs b/plugin/scripts/task-completed.mjs index 8eed3f8e5..b0a9a03bd 100755 --- a/plugin/scripts/task-completed.mjs +++ b/plugin/scripts/task-completed.mjs @@ -28,8 +28,14 @@ function hookCwd(data) { for (const root of roots) if (typeof root === "string" && root.trim()) return root; } if (typeof data.project_root === "string" && data.project_root.trim()) return data.project_root; - const projectDir = process.env["DEVIN_PROJECT_DIR"] || process.env["CLAUDE_PROJECT_DIR"] || process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT" + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } } //#endregion //#region src/hooks/task-completed.ts diff --git a/src/hooks/_project.ts b/src/hooks/_project.ts index 8f0f62700..e5be5b627 100644 --- a/src/hooks/_project.ts +++ b/src/hooks/_project.ts @@ -32,10 +32,15 @@ export function hookCwd(data: Record | null | undefined): strin if (typeof data.project_root === "string" && data.project_root.trim()) { return data.project_root; } - const projectDir = - process.env["DEVIN_PROJECT_DIR"] || - process.env["CLAUDE_PROJECT_DIR"] || - process.env["KLAATAI_PROJECT_ROOT"]; - if (projectDir && projectDir.trim()) return projectDir; + // Take the first non-blank var: a `||` chain would stop at a + // whitespace-only earlier value and shadow a valid later one. + for (const name of [ + "DEVIN_PROJECT_DIR", + "CLAUDE_PROJECT_DIR", + "KLAATAI_PROJECT_ROOT", + ]) { + const value = process.env[name]; + if (value && value.trim()) return value; + } return undefined; } diff --git a/test/klaatcode-connect-hooks.test.ts b/test/klaatcode-connect-hooks.test.ts index 7dffe9b5c..195046a1f 100644 --- a/test/klaatcode-connect-hooks.test.ts +++ b/test/klaatcode-connect-hooks.test.ts @@ -122,6 +122,27 @@ describe("hook payload compatibility with Klaat Code", () => { } }); + it("skips a blank earlier env var instead of letting it shadow the fallback", async () => { + const { hookCwd } = await import("../src/hooks/_project.js"); + const before = { + devin: process.env["DEVIN_PROJECT_DIR"], + klaat: process.env["KLAATAI_PROJECT_ROOT"], + }; + process.env["DEVIN_PROJECT_DIR"] = " "; + process.env["KLAATAI_PROJECT_ROOT"] = "/tmp"; + try { + expect(hookCwd({ session_id: "s1" })).toBe("/tmp"); + } finally { + for (const [key, value] of [ + ["DEVIN_PROJECT_DIR", before.devin], + ["KLAATAI_PROJECT_ROOT", before.klaat], + ] as const) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + } + }); + it("fails open when the memory server is unreachable", async () => { const { code } = await runHook("session-start", { event: "session_start", From 6da0e4bf10940fdc56603030b7ebf7c62722926d Mon Sep 17 00:00:00 2001 From: Shivay Lamba Date: Tue, 18 Aug 2026 03:20:33 +0530 Subject: [PATCH 4/5] style: drop what-explaining comments per AGENTS.md Trims the comment blocks in klaatcode.ts, klaatcode-hooks.ts and _project.ts down to the non-obvious why, per the 'no comments explaining WHAT' rule in AGENTS.md and CONTRIBUTING.md. Raised by CodeRabbit on #1222. Signed-off-by: Shivay Lamba --- src/cli/connect/klaatcode-hooks.ts | 29 ++++++----------------------- src/cli/connect/klaatcode.ts | 4 ---- src/hooks/_project.ts | 3 --- 3 files changed, 6 insertions(+), 30 deletions(-) diff --git a/src/cli/connect/klaatcode-hooks.ts b/src/cli/connect/klaatcode-hooks.ts index 7dc2085f2..3252c7b1a 100644 --- a/src/cli/connect/klaatcode-hooks.ts +++ b/src/cli/connect/klaatcode-hooks.ts @@ -2,30 +2,13 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; /** - * Merge engine for Klaat Code's hooks file (`~/.klaatai/hooks.json`). + * Separate from `codex-hooks.ts` because Klaat Code's hooks file is flat and + * snake_case rather than the nested Claude-Code shape `buildMergedHooks` + * handles. Loader: KlaatAI/klaatcode `src/screens/repl.ts`. * - * Klaat Code does not use the nested Claude-Code hook shape that - * `codex-hooks.ts#buildMergedHooks` handles. Its config is flat — event - * name maps straight to a list of entries, where an entry is either a bare - * shell string (v1) or `{ command, matcher?, timeout? }` (v2): - * - * { "session_start": ["cmd"], - * "before_tool": [{ "command": "…", "matcher": "^edit_file$", "timeout": 5 }] } - * - * Event names differ too (`session_start` / `before_tool` / … rather than - * `SessionStart` / `PreToolUse` / …), so the bundled manifest for Klaat Code - * is authored in its native shape and this module only resolves paths and - * de-duplicates. See KlaatAI/klaatcode `src/screens/repl.ts` for the loader. - * - * The two guarantees match the Codex/Claude engine: - * 1. `${CLAUDE_PLUGIN_ROOT}` is rewritten to the absolute bundled - * `plugin/` path, so the written file needs no env expansion (Klaat - * Code does not inject that variable — it is used here purely as the - * internal placeholder token every bundled manifest shares). - * 2. Re-installs are idempotent: any pre-existing entry whose command - * points under `/scripts/` is stripped before ours are - * appended, so upgrading never leaves stale duplicates and never - * touches the user's own hooks. + * Klaat Code never injects `${CLAUDE_PLUGIN_ROOT}` — the token is the shared + * placeholder in every bundled manifest, resolved here so the written file + * needs no env expansion. */ export type KlaatcodeHookEvent = diff --git a/src/cli/connect/klaatcode.ts b/src/cli/connect/klaatcode.ts index 7edff2de5..62f184e4d 100644 --- a/src/cli/connect/klaatcode.ts +++ b/src/cli/connect/klaatcode.ts @@ -17,10 +17,6 @@ import { writeJsonAtomic, } from "./util.js"; -// Klaat Code keeps everything under ~/.klaatai (relocatable via KLAATAI_DIR), -// with MCP servers and lifecycle hooks in separate files rather than one -// settings blob. MCP entries live under the top-level "servers" key — the -// same situation as Zed's "context_servers", handled by wrapperKey. function klaatDir(): string { const override = process.env["KLAATAI_DIR"]; if (override && override.trim()) return override; diff --git a/src/hooks/_project.ts b/src/hooks/_project.ts index e5be5b627..3a030454e 100644 --- a/src/hooks/_project.ts +++ b/src/hooks/_project.ts @@ -28,12 +28,9 @@ export function hookCwd(data: Record | null | undefined): strin if (typeof root === "string" && root.trim()) return root; } } - // Klaat Code names the field project_root rather than cwd. if (typeof data.project_root === "string" && data.project_root.trim()) { return data.project_root; } - // Take the first non-blank var: a `||` chain would stop at a - // whitespace-only earlier value and shadow a valid later one. for (const name of [ "DEVIN_PROJECT_DIR", "CLAUDE_PROJECT_DIR", From 0789f27de833bea58bf4bd2ff7c2b6f4bc7e00cc Mon Sep 17 00:00:00 2001 From: Shivay Lamba Date: Tue, 18 Aug 2026 03:53:10 +0530 Subject: [PATCH 5/5] feat: write the memory guideline for klaatcode too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit guidelineTargets deliberately omits Claude Code and Codex because their hooks already auto-capture, and I originally skipped Klaat Code on the same reasoning. That was wrong: Klaat Code discards hook stdout except for before_tool block decisions, so unlike those two it has no path for injecting recalled context. Capture worked, but nothing ever prompted the model to call memory_recall, which left recall — the actual point of the integration — inert. Targets ~/.klaatai/rules.md, which system-prompt.ts reads globally before the per-project rules, using the same marked-block format as the other shared instruction files. Verified end to end against a real Klaat Code session: with the guideline in place a fresh session calls memory_recall and memory_smart_search unprompted and gets back a fact saved by an earlier session. Signed-off-by: Shivay Lamba --- src/cli/connect/guidelines.ts | 10 ++++++++++ test/connect-guidelines.test.ts | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/cli/connect/guidelines.ts b/src/cli/connect/guidelines.ts index 26770b6a7..a9614f457 100644 --- a/src/cli/connect/guidelines.ts +++ b/src/cli/connect/guidelines.ts @@ -84,6 +84,16 @@ export function guidelineTargets( scope: "global", source: "https://kiro.dev/docs/steering", }, + // Klaat Code has capture hooks but discards their stdout, so unlike Claude + // Code and Codex it has no path for injecting recalled context. Without a + // guideline nothing ever prompts the model to call memory_recall. + klaatcode: { + globalPath: join(home, ".klaatai", "rules.md"), + projectPath: join(".klaatai", "rules.md"), + format: "block", + scope: "global", + source: "https://github.com/KlaatAI/klaatcode#project-rules", + }, "gemini-cli": { globalPath: join(home, ".gemini", "GEMINI.md"), projectPath: "GEMINI.md", diff --git a/test/connect-guidelines.test.ts b/test/connect-guidelines.test.ts index 633aedc5e..8023d1586 100644 --- a/test/connect-guidelines.test.ts +++ b/test/connect-guidelines.test.ts @@ -29,6 +29,17 @@ describe("writeGuideline", () => { expect(body).toContain("memory_save"); }); + it("writes a marked block into Klaat Code's global rules.md", () => { + const r = writeGuideline("klaatcode", { cwd, home }); + expect(r.kind).toBe("written"); + if (r.kind === "written") expect(r.scope).toBe("global"); + const path = join(home, ".klaatai", "rules.md"); + expect(existsSync(path)).toBe(true); + const body = readFileSync(path, "utf8"); + expect(body).toContain("memory_recall"); + expect(body).toContain("memory_save"); + }); + it("writes a Kiro steering file with inclusion: always (global)", () => { const r = writeGuideline("kiro", { cwd, home }); expect(r.kind).toBe("written"); @@ -141,6 +152,7 @@ describe("guidelineTargets coverage", () => { "droid", "gemini-cli", "kiro", + "klaatcode", "opencode", "qwen", "warp",