claude-recall 0.27.1 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Claude Recall is a **local memory engine** that gives coding agents something they're missing by default:
6
6
  **the ability to learn from you over time.**
7
7
 
8
- Works with **Claude Code** (via MCP server + hooks) and **[Pi](https://github.com/mariozechner/pi)** (via native extension). Both share the same local database — a preference learned in one agent is available in the other.
8
+ Works with **Claude Code** (via MCP server + hooks), **[Pi](https://github.com/mariozechner/pi)** (via native extension), and **[Kiro CLI](https://kiro.dev/cli/)** (via custom agent + hooks). All three share the same local database — a preference learned in one agent is available in the others.
9
9
 
10
10
  Your preferences, project structure, workflows, corrections, and coding style are captured automatically and applied in future sessions — **securely stored on your machine**.
11
11
 
@@ -69,30 +69,96 @@ pi install npm:claude-recall
69
69
 
70
70
  That's it. Ask Pi to *"Load my rules"* to verify.
71
71
 
72
+ ### Install for Kiro CLI
73
+
74
+ Requires claude-recall **≥ 0.28.0** (`claude-recall --version` to check; `claude-recall upgrade` to update). Uses the same global binary (install it once per machine as above). Two ways to wire it in — full integration is what most people want.
75
+
76
+ **Option A — full integration (recommended): memory + hooks via a custom agent.**
77
+
78
+ In your shell, in the project directory, **before** starting Kiro:
79
+
80
+ ```bash
81
+ claude-recall kiro setup
82
+ ```
83
+
84
+ This writes a Kiro custom agent at `.kiro/agents/recall.json` (use `--global` for `~/.kiro/agents` so it's available in every project). It does **not** touch your `mcp.json` — the agent config carries its own `mcpServers` entry for claude-recall, so no separate MCP registration is needed. It also sets `includeMcpJson: true`, so any other servers you have in `mcp.json` keep working alongside it.
85
+
86
+ Then start Kiro from your shell:
87
+
88
+ ```bash
89
+ kiro
90
+ ```
91
+
92
+ and inside the Kiro chat, switch to the agent:
93
+
94
+ ```
95
+ /agent swap recall
96
+ ```
97
+
98
+ You get: active rules injected into context automatically at agent start (no tool call needed), just-in-time rule injection before each tool call, automatic capture of corrections/preferences from your prompts, tool-outcome tracking with Bash fix-pairing, and the full MCP tool surface (read-only tools pre-approved). Memories are shared with Claude Code and Pi: same database, same per-project scoping.
99
+
100
+ **Already have a custom agent you live in?** Don't swap — merge Claude Recall into it instead:
101
+
102
+ ```bash
103
+ claude-recall kiro setup --merge-into <agent-name>
104
+ ```
105
+
106
+ This finds the agent config (workspace `.kiro/agents/` first, then `~/.kiro/agents/`; `--global` to target the global one directly), writes a timestamped backup, and appends the claude-recall pieces — MCP server, pre-approved read-only tools, and the four hooks — without touching anything the agent already had. Idempotent: re-running changes nothing. If the agent restricts tools with an explicit list, `@claude-recall` is added to it.
107
+
108
+ **Option B — MCP tools only (no hooks, works in Kiro's default agent).**
109
+
110
+ If you don't want a custom agent, register just the MCP server in Kiro's config instead. Create or merge into `.kiro/settings/mcp.json` (project) or `~/.kiro/settings/mcp.json` (all projects):
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "claude-recall": {
116
+ "command": "claude-recall",
117
+ "args": ["mcp", "start"],
118
+ "autoApprove": ["load_rules", "search_memory", "load_checkpoint"]
119
+ }
120
+ }
121
+ }
122
+ ```
123
+
124
+ With Option B the agent has the memory tools (`load_rules`, `store_memory`, `search_memory`, checkpoints) but nothing happens automatically — no rules at session start, no auto-capture. Ask it to *"load my rules"*.
125
+
126
+ **Not available under Kiro** with either option (Kiro's hooks expose no transcript): transcript-based failure detection and session-end auto-checkpoints.
127
+
72
128
  ### Shared Database
73
129
 
74
- Both agents use the same database at `~/.claude-recall/claude-recall.db`, scoped per project by working directory. A correction learned in one agent is available in the other.
130
+ All runtimes (Claude Code, Pi, Kiro CLI) use the same database at `~/.claude-recall/claude-recall.db`, scoped per project by working directory. A correction learned in one agent is available in the others.
75
131
 
76
132
  ### Upgrading
77
133
 
134
+ One command upgrades the shared binary for **all** runtimes (Claude Code, Pi, and Kiro use the same global install):
135
+
78
136
  ```bash
79
137
  claude-recall upgrade
80
138
  ```
81
139
 
82
- One command. Checks the registry, refreshes the global binary, clears any running MCP servers — Claude Code respawns them on the next tool call, picking up the new version. **No `claude mcp add` re-run needed** — existing registrations point at the `claude-recall` command, not a pinned path.
140
+ It checks the registry, refreshes the global binary, and clears any running MCP servers — they respawn on the next tool call with the new version.
141
+
142
+ Per-runtime notes:
83
143
 
84
- If the release notes mention new or changed hooks (a `hooksVersion` bump), also re-run `claude-recall setup --install` in each active project. It's safe to run any time: when your hooks are already current it's a no-op and touches nothing.
144
+ - **Claude Code** — nothing else needed; registrations point at the `claude-recall` command, not a pinned path. If the release notes mention new or changed hooks (a `hooksVersion` bump), also re-run `claude-recall setup --install` in each active project safe any time; a no-op when hooks are already current.
145
+ - **Pi** — run `pi update npm:claude-recall` and restart Pi.
146
+ - **Kiro CLI** — nothing else needed; the agent config points at the `claude-recall` command. If the release notes mention changes to the Kiro agent template, re-run `claude-recall kiro setup --force` in each project that uses it.
85
147
 
86
- > **Registered before v0.27.x?** Older versions auto-registered the MCP server with an `npx`-based command, which re-resolves the package on every server start and can be shadowed by stale project-local installs. Switch to the direct binary form (run in each affected project):
148
+ > **Claude Code registered before v0.27.x?** Older versions auto-registered the MCP server with an `npx`-based command, which re-resolves the package on every server start and can be shadowed by stale project-local installs. Switch to the direct binary form (run in each affected project):
87
149
  >
88
150
  > ```bash
89
151
  > claude mcp remove claude-recall
90
152
  > claude mcp add claude-recall -- claude-recall mcp start
91
153
  > ```
92
154
 
93
- For Pi, run `pi update npm:claude-recall` and restart Pi.
94
-
95
- > **Seeing `error: unknown command 'upgrade'`?** Your installed version predates 0.23.2 (the release that added the `upgrade` command). Bootstrap once with `npm install -g claude-recall@latest`, then all future upgrades use `claude-recall upgrade`.
155
+ > **Seeing `error: unknown command '<anything>'`?** Your installed global binary is older than the docs you're reading — commands ship with releases (`kiro` needs ≥ 0.28.0, `compact` 0.26.0, `upgrade` ≥ 0.23.2). Fix:
156
+ >
157
+ > ```bash
158
+ > claude-recall upgrade
159
+ > ```
160
+ >
161
+ > If `upgrade` itself is the unknown command (pre-0.23.2 install), bootstrap once with `npm install -g claude-recall@latest`.
96
162
 
97
163
  <details>
98
164
  <summary><b>If the install step reports <code>EACCES: permission denied</code></b></summary>
@@ -128,19 +194,19 @@ The prefix fix only tells npm *where* to install; it doesn't install anything it
128
194
 
129
195
  Once installed, Claude Recall works automatically in the background. Each row below is tagged with the runtime it applies to so you can skip what doesn't apply to you.
130
196
 
131
- | When | What happens | CC | Pi |
132
- |---|---|:-:|:-:|
133
- | **Session start** | Active rules are loaded before the first action and injected into the agent's context | ✓ | ✓ |
134
- | **As you work** | Every prompt is classified for corrections and preferences. Natural statements like *"we use tabs here"* are detected and stored | ✓ | ✓ |
135
- | **Before each tool call / agent turn** | **Just-in-time rule injection** — relevant rules are surfaced as a `<system-reminder>` block adjacent to the action so the agent sees them at the moment of decision (not 50,000 tokens upstream). Per-tool-call in CC; per-turn in Pi | ✓ | ✓ |
136
- | **Tool outcomes** | Tool results (Bash, Edit, Write, etc.) are captured. Failures are stored; Bash failures are paired with their successful fixes | ✓ | ✓ |
137
- | **Reask detection** | Frustration signals (*"still broken"*, *"that didn't work"*) are recorded as outcome events | ✓ | ✓ |
138
- | **Before context compression** | Aggressive memory sweep captures important context before the window shrinks | ✓ | ✓ |
139
- | **After context compression** | Rules are automatically re-injected into the new context so they're not lost | ✓ | |
140
- | **Sub-agent spawned** | Active rules are injected into the sub-agent's context. Sub-agent outcomes (completed/failed/killed) are captured | ✓ | |
141
- | **Rules sync** | Top 30 rules are exported as typed `.md` files to Claude Code's native memory directory | ✓ | |
142
- | **Session exit** | **Auto-checkpoint** — the most recent task is extracted into a `{completed, remaining, blockers}` snapshot and saved for the next session. Critical for Pi (no `--resume` flag); safety net for CC users who exit without resuming | ✓ | ✓ |
143
- | **End of session** | Session episodes are created, candidate lessons are extracted from failures, and validated patterns are promoted into active rules | ✓ | ✓ |
197
+ | When | What happens | CC | Pi | Kiro |
198
+ |---|---|:-:|:-:|:-:|
199
+ | **Session start** | Active rules are loaded before the first action and injected into the agent's context. Under Kiro this is fully automatic — rules land in context at `agentSpawn`, no tool call needed | ✓ | ✓ | ✓ |
200
+ | **As you work** | Every prompt is classified for corrections and preferences. Natural statements like *"we use tabs here"* are detected and stored | ✓ | ✓ | ✓ |
201
+ | **Before each tool call / agent turn** | **Just-in-time rule injection** — relevant rules are surfaced adjacent to the action so the agent sees them at the moment of decision (not 50,000 tokens upstream). Per-tool-call in CC and Kiro; per-turn in Pi | ✓ | ✓ | ✓ |
202
+ | **Tool outcomes** | Tool results (Bash, Edit, Write, etc.) are captured. Failures are stored; Bash failures are paired with their successful fixes | ✓ | ✓ | ✓ |
203
+ | **Reask detection** | Frustration signals (*"still broken"*, *"that didn't work"*) are recorded as outcome events | ✓ | ✓ | ✓ |
204
+ | **Before context compression** | Aggressive memory sweep captures important context before the window shrinks | ✓ | ✓ | |
205
+ | **After context compression** | Rules are automatically re-injected into the new context so they're not lost | ✓ | | |
206
+ | **Sub-agent spawned** | Active rules are injected into the sub-agent's context. Sub-agent outcomes (completed/failed/killed) are captured | ✓ | | |
207
+ | **Rules sync** | Top 30 rules are exported as typed `.md` files to Claude Code's native memory directory | ✓ | | |
208
+ | **Session exit** | **Auto-checkpoint** — the most recent task is extracted into a `{completed, remaining, blockers}` snapshot and saved for the next session. Critical for Pi (no `--resume` flag); safety net for CC users who exit without resuming. Kiro surfaces existing checkpoints at agent start but doesn't auto-create them (no transcript in its hooks) | ✓ | ✓ | |
209
+ | **End of session** | Session episodes are created, candidate lessons are extracted from failures, and validated patterns are promoted into active rules | ✓ | ✓ | |
144
210
 
145
211
  Classification and checkpoint extraction use Claude Haiku (via `ANTHROPIC_API_KEY`) with silent regex fallback. No configuration needed.
146
212
 
@@ -286,6 +352,11 @@ Manual `checkpoint save` is the explicit path. **Auto-checkpoint** is the safety
286
352
  ### Troubleshooting
287
353
 
288
354
  ```bash
355
+ # "error: unknown command 'kiro'" (or any other command)?
356
+ # Your global binary is older than the feature — upgrade it:
357
+ claude-recall --version # What you have
358
+ claude-recall upgrade # Get current
359
+
289
360
  # "Are my hooks installed?"
290
361
  claude-recall status # Shows hook registration status
291
362
  claude-recall hooks check # Verify hook files exist and are valid
@@ -325,7 +396,9 @@ claude-recall mcp cleanup --all # Stop all stale MCP servers
325
396
  ```bash
326
397
  # ── Setup & Diagnostics ─────────────────────────────────────────────
327
398
  claude-recall setup # Show activation instructions
328
- claude-recall setup --install # Install skills + hooks
399
+ claude-recall setup --install # Install skills + hooks (Claude Code, current project)
400
+ claude-recall kiro setup # Write Kiro custom agent (.kiro/agents/recall.json); --global for all projects
401
+ claude-recall kiro setup --merge-into <agent> # Merge Claude Recall into an existing Kiro agent (backup + append-only + idempotent)
329
402
  claude-recall upgrade # One-shot upgrade: global binary + clear stale MCP servers
330
403
  claude-recall status # Installation and system status
331
404
  claude-recall repair # Fix broken claude-recall hook paths (conservative: preserves user customizations)
@@ -48,6 +48,7 @@ const skill_generator_1 = require("../services/skill-generator");
48
48
  const mcp_commands_1 = require("./commands/mcp-commands");
49
49
  const project_commands_1 = require("./commands/project-commands");
50
50
  const hook_commands_1 = require("./commands/hook-commands");
51
+ const kiro_commands_1 = require("./commands/kiro-commands");
51
52
  const repair_1 = require("./commands/repair");
52
53
  // v14 = add PreToolUse rule-injector + Post resolver for JITRI.
53
54
  // Bump when the hook block template changes — setup skips the settings
@@ -1813,6 +1814,8 @@ async function main() {
1813
1814
  project_commands_1.ProjectCommands.register(program);
1814
1815
  // Hook commands (automatic memory capture)
1815
1816
  hook_commands_1.HookCommands.register(program);
1817
+ // Kiro CLI integration
1818
+ kiro_commands_1.KiroCommands.register(program);
1816
1819
  // Migration commands
1817
1820
  // Search command
1818
1821
  program
@@ -50,6 +50,9 @@ const AVAILABLE_HOOKS = [
50
50
  'session-end-checkpoint',
51
51
  'session-end-checkpoint-worker',
52
52
  'bash-failure-watcher',
53
+ 'kiro-agent-spawn',
54
+ 'kiro-rule-injector',
55
+ 'kiro-tool-outcome',
53
56
  ];
54
57
  /**
55
58
  * Hook CLI Commands
@@ -135,6 +138,23 @@ class HookCommands {
135
138
  await handleRuleInjectionResolver(input);
136
139
  break;
137
140
  }
141
+ // Kiro CLI adapters — see src/hooks/kiro-hooks.ts. Kiro's
142
+ // userPromptSubmit needs no adapter: wire `correction-detector` directly.
143
+ case 'kiro-agent-spawn': {
144
+ const { handleKiroAgentSpawn } = await Promise.resolve().then(() => __importStar(require('../../hooks/kiro-hooks')));
145
+ await handleKiroAgentSpawn(input);
146
+ break;
147
+ }
148
+ case 'kiro-rule-injector': {
149
+ const { handleKiroRuleInjector } = await Promise.resolve().then(() => __importStar(require('../../hooks/kiro-hooks')));
150
+ await handleKiroRuleInjector(input);
151
+ break;
152
+ }
153
+ case 'kiro-tool-outcome': {
154
+ const { handleKiroToolOutcome } = await Promise.resolve().then(() => __importStar(require('../../hooks/kiro-hooks')));
155
+ await handleKiroToolOutcome(input);
156
+ break;
157
+ }
138
158
  default:
139
159
  console.error(`Unknown hook: ${name}`);
140
160
  console.error(`Available: ${AVAILABLE_HOOKS.join(', ')}`);
@@ -149,14 +169,16 @@ class HookCommands {
149
169
  .description(`Run one or more hook handlers in a single process (${AVAILABLE_HOOKS.slice(0, 3).join(' | ')} | ...)`)
150
170
  .action(async (names) => {
151
171
  // Read stdin synchronously BEFORE dynamic import to avoid data loss;
152
- // every handler receives the same event payload.
172
+ // every handler receives the same event payload. An empty/unreadable
173
+ // stdin degrades to {} instead of aborting — lifecycle events with no
174
+ // payload (e.g. Kiro agentSpawn variants) must still run the handler.
153
175
  let input;
154
176
  try {
155
177
  input = (0, shared_1.readStdin)();
156
178
  }
157
179
  catch (err) {
158
- (0, shared_1.hookLog)('hook-dispatcher', `stdin read failed: ${(0, shared_1.safeErrorMessage)(err)}`);
159
- process.exit(0);
180
+ (0, shared_1.hookLog)('hook-dispatcher', `stdin read failed (continuing with empty payload): ${(0, shared_1.safeErrorMessage)(err)}`);
181
+ input = {};
160
182
  }
161
183
  for (const name of names) {
162
184
  try {
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.KiroCommands = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const repair_1 = require("./repair");
41
+ /**
42
+ * Kiro CLI integration commands.
43
+ *
44
+ * `claude-recall kiro setup` writes a Kiro custom agent config
45
+ * (.kiro/agents/recall.json) wiring the same shared memory database into
46
+ * Kiro CLI: MCP tools, rules auto-loaded into context at agentSpawn,
47
+ * just-in-time rule injection on preToolUse, user-prompt capture, and tool
48
+ * outcome tracking. See src/hooks/kiro-hooks.ts for the adapter details and
49
+ * kiro.dev/docs/cli/custom-agents for the config format.
50
+ */
51
+ class KiroCommands {
52
+ /** The claude-recall mcpServers entry (shared by fresh config and merge). */
53
+ static buildMcpServerEntry(mcpCommand, mcpArgs) {
54
+ return {
55
+ command: mcpCommand,
56
+ args: mcpArgs,
57
+ timeout: 120000,
58
+ };
59
+ }
60
+ /** The four lifecycle hook entries (shared by fresh config and merge). */
61
+ static buildHookEntries(hookCmd) {
62
+ return {
63
+ // stdout of agentSpawn is added to context → rules present from turn one
64
+ agentSpawn: [
65
+ { command: `${hookCmd} kiro-agent-spawn`, timeout_ms: 10000 },
66
+ ],
67
+ // Kiro's userPromptSubmit payload matches correction-detector exactly
68
+ userPromptSubmit: [
69
+ { command: `${hookCmd} correction-detector`, timeout_ms: 8000 },
70
+ ],
71
+ preToolUse: [
72
+ { matcher: '*', command: `${hookCmd} kiro-rule-injector`, timeout_ms: 5000 },
73
+ ],
74
+ postToolUse: [
75
+ { matcher: '*', command: `${hookCmd} kiro-tool-outcome`, timeout_ms: 5000 },
76
+ ],
77
+ };
78
+ }
79
+ static buildAgentConfig(hookCmd, mcpCommand, mcpArgs) {
80
+ return {
81
+ name: 'recall',
82
+ description: 'Kiro agent with Claude Recall persistent memory: rules auto-loaded at start, just-in-time injection per tool call, automatic capture of corrections and outcomes.',
83
+ // Also load any servers the user configured in .kiro/settings/mcp.json
84
+ includeMcpJson: true,
85
+ mcpServers: {
86
+ 'claude-recall': KiroCommands.buildMcpServerEntry(mcpCommand, mcpArgs),
87
+ },
88
+ tools: ['*'],
89
+ allowedTools: [...KiroCommands.ALLOWED_TOOLS],
90
+ hooks: KiroCommands.buildHookEntries(hookCmd),
91
+ };
92
+ }
93
+ /**
94
+ * Merge claude-recall into an EXISTING agent config, append-don't-replace:
95
+ * - mcpServers.claude-recall added only if absent (never overwrites)
96
+ * - allowedTools entries appended, deduplicated
97
+ * - hook entries appended per lifecycle array, skipped when an entry for
98
+ * the same claude-recall handler is already present (idempotent)
99
+ * - an explicit `tools` list (no '*') gains '@claude-recall'
100
+ * Returns a summary of what changed; mutates `config` in place.
101
+ */
102
+ static mergeIntoAgentConfig(config, hookCmd, mcpCommand, mcpArgs) {
103
+ const changes = [];
104
+ // mcpServers
105
+ if (!config.mcpServers || typeof config.mcpServers !== 'object') {
106
+ config.mcpServers = {};
107
+ }
108
+ if (config.mcpServers['claude-recall']) {
109
+ changes.push('mcpServers.claude-recall: already present — left untouched');
110
+ }
111
+ else {
112
+ config.mcpServers['claude-recall'] = KiroCommands.buildMcpServerEntry(mcpCommand, mcpArgs);
113
+ changes.push('mcpServers.claude-recall: added');
114
+ }
115
+ // allowedTools (dedup append)
116
+ if (!Array.isArray(config.allowedTools)) {
117
+ config.allowedTools = [];
118
+ }
119
+ const addedTools = KiroCommands.ALLOWED_TOOLS.filter(t => !config.allowedTools.includes(t));
120
+ config.allowedTools.push(...addedTools);
121
+ changes.push(addedTools.length > 0
122
+ ? `allowedTools: added ${addedTools.join(', ')}`
123
+ : 'allowedTools: already present — nothing added');
124
+ // tools: only when the agent restricts tools with an explicit list.
125
+ // '*' or '@claude-recall' already grants access; an absent field is left
126
+ // alone (the agent's default tool policy governs).
127
+ if (Array.isArray(config.tools) && !config.tools.includes('*') && !config.tools.includes('@claude-recall')) {
128
+ config.tools.push('@claude-recall');
129
+ changes.push('tools: added @claude-recall (explicit tool list detected)');
130
+ }
131
+ // hooks (append per lifecycle; skip when our handler is already wired)
132
+ if (!config.hooks || typeof config.hooks !== 'object') {
133
+ config.hooks = {};
134
+ }
135
+ const handlerMarkers = {
136
+ agentSpawn: 'kiro-agent-spawn',
137
+ userPromptSubmit: 'correction-detector',
138
+ preToolUse: 'kiro-rule-injector',
139
+ postToolUse: 'kiro-tool-outcome',
140
+ };
141
+ for (const [event, entries] of Object.entries(KiroCommands.buildHookEntries(hookCmd))) {
142
+ if (!Array.isArray(config.hooks[event])) {
143
+ config.hooks[event] = [];
144
+ }
145
+ const marker = handlerMarkers[event];
146
+ const alreadyWired = config.hooks[event].some((h) => typeof h?.command === 'string' && h.command.includes(marker));
147
+ if (alreadyWired) {
148
+ changes.push(`hooks.${event}: ${marker} already wired — skipped`);
149
+ }
150
+ else {
151
+ config.hooks[event].push(...entries);
152
+ changes.push(`hooks.${event}: added ${marker}`);
153
+ }
154
+ }
155
+ return changes;
156
+ }
157
+ /** Resolve hook/MCP command forms (portable PATH form, absolute fallback). */
158
+ static resolveCommands() {
159
+ const onPath = (0, repair_1.resolveOnPath)('claude-recall');
160
+ const cliScript = path.resolve(__dirname, '..', 'claude-recall-cli.js');
161
+ return {
162
+ onPath: !!onPath,
163
+ hookCmd: onPath ? 'claude-recall hook run' : `node ${cliScript} hook run`,
164
+ mcpCommand: onPath ? 'claude-recall' : 'node',
165
+ mcpArgs: onPath ? ['mcp', 'start'] : [cliScript, 'mcp', 'start'],
166
+ };
167
+ }
168
+ /**
169
+ * Merge claude-recall into an existing agent config file. Lookup follows
170
+ * Kiro's own precedence: workspace .kiro/agents first, then ~/.kiro/agents
171
+ * (--global restricts to the global directory). Fail-safe: a missing agent
172
+ * or malformed JSON changes nothing and exits 1; a timestamped backup is
173
+ * written before any mutation.
174
+ */
175
+ static runMergeInto(agentName, options) {
176
+ const safeName = agentName.replace(/\.json$/, '');
177
+ const candidates = options.global
178
+ ? [path.join(os.homedir(), '.kiro', 'agents', `${safeName}.json`)]
179
+ : [
180
+ path.join(process.cwd(), '.kiro', 'agents', `${safeName}.json`),
181
+ path.join(os.homedir(), '.kiro', 'agents', `${safeName}.json`),
182
+ ];
183
+ const agentPath = candidates.find(p => fs.existsSync(p));
184
+ if (!agentPath) {
185
+ console.error(`❌ Agent "${safeName}" not found. Looked in:`);
186
+ for (const p of candidates)
187
+ console.error(` ${p}`);
188
+ console.error(' (workspace agents take precedence; use --global to target ~/.kiro/agents only)');
189
+ process.exit(1);
190
+ }
191
+ const raw = fs.readFileSync(agentPath, 'utf8');
192
+ let config;
193
+ try {
194
+ config = JSON.parse(raw);
195
+ }
196
+ catch {
197
+ console.error(`❌ ${agentPath} is not valid JSON — refusing to modify it.`);
198
+ console.error(' Fix the file (or recreate the agent) and re-run.');
199
+ process.exit(1);
200
+ return; // unreachable; satisfies control-flow analysis
201
+ }
202
+ const { hookCmd, mcpCommand, mcpArgs, onPath } = KiroCommands.resolveCommands();
203
+ if (!onPath) {
204
+ console.log('⚠️ claude-recall not on PATH — merged commands will use absolute paths (breaks if the install moves).');
205
+ }
206
+ // Structural change detection — the summary strings are for humans
207
+ const before = JSON.stringify(config);
208
+ const changes = KiroCommands.mergeIntoAgentConfig(config, hookCmd, mcpCommand, mcpArgs);
209
+ const changed = JSON.stringify(config) !== before;
210
+ if (!changed) {
211
+ console.log(`✅ ${agentPath} already has Claude Recall fully wired — nothing to do.`);
212
+ process.exit(0);
213
+ }
214
+ const backupPath = `${agentPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
215
+ fs.writeFileSync(backupPath, raw);
216
+ fs.writeFileSync(agentPath, JSON.stringify(config, null, 2) + '\n');
217
+ console.log(`✅ Merged Claude Recall into: ${agentPath}`);
218
+ console.log(` Backup: ${backupPath}`);
219
+ console.log('');
220
+ for (const c of changes)
221
+ console.log(` • ${c}`);
222
+ console.log('');
223
+ console.log('Kiro hot-reloads agent configs on save — the changes apply to your next');
224
+ console.log(`interaction with the "${safeName}" agent, no restart needed.`);
225
+ process.exit(0);
226
+ }
227
+ /** Action body, separated from commander wiring so tests can call it directly. */
228
+ static runSetup(options) {
229
+ const baseDir = options.global
230
+ ? path.join(os.homedir(), '.kiro', 'agents')
231
+ : path.join(process.cwd(), '.kiro', 'agents');
232
+ const agentPath = path.join(baseDir, 'recall.json');
233
+ if (fs.existsSync(agentPath) && !options.force) {
234
+ console.log(`✅ ${agentPath} already exists — leaving it untouched (use --force to overwrite).`);
235
+ process.exit(0);
236
+ }
237
+ // Prefer the portable PATH form; absolute dist path only as fallback
238
+ // (same policy as Claude Code hook installation — move-proof commands)
239
+ const { hookCmd, mcpCommand, mcpArgs, onPath } = KiroCommands.resolveCommands();
240
+ if (!onPath) {
241
+ console.log('⚠️ claude-recall not on PATH — the agent config will use absolute paths (breaks if the install moves).');
242
+ console.log(' `npm install -g claude-recall` gives move-proof commands.');
243
+ }
244
+ fs.mkdirSync(baseDir, { recursive: true });
245
+ const config = KiroCommands.buildAgentConfig(hookCmd, mcpCommand, mcpArgs);
246
+ fs.writeFileSync(agentPath, JSON.stringify(config, null, 2) + '\n');
247
+ console.log(`✅ Wrote Kiro agent config: ${agentPath}`);
248
+ console.log('');
249
+ console.log('No mcp.json changes needed — the agent config carries its own claude-recall');
250
+ console.log('MCP server entry (and includeMcpJson keeps your other servers working).');
251
+ console.log('');
252
+ console.log('1. Start Kiro from your shell:');
253
+ console.log('');
254
+ console.log('kiro');
255
+ console.log('');
256
+ console.log('2. Inside the Kiro chat, switch to the agent:');
257
+ console.log('');
258
+ console.log('/agent swap recall');
259
+ console.log('');
260
+ console.log('Rules load into context automatically at agent start; corrections and');
261
+ console.log('preferences you state are captured; memories are shared with Claude Code');
262
+ console.log('(same database, same per-project scoping).');
263
+ console.log('');
264
+ console.log('Not available under Kiro: transcript-based failure detection and');
265
+ console.log('session-end checkpoints (Kiro exposes no transcript to hooks).');
266
+ process.exit(0);
267
+ }
268
+ static register(program) {
269
+ const kiroCmd = program
270
+ .command('kiro')
271
+ .description('Kiro CLI integration');
272
+ kiroCmd
273
+ .command('setup')
274
+ .description('Write a Kiro custom agent (.kiro/agents/recall.json) with Claude Recall memory wired in, or merge into an existing agent')
275
+ .option('--global', 'Target ~/.kiro/agents (all projects) instead of ./.kiro/agents')
276
+ .option('--force', 'Overwrite an existing recall.json (ignored with --merge-into)')
277
+ .option('--merge-into <agent>', 'Merge Claude Recall into an existing agent config instead of creating the "recall" agent (backup written first; append-only; idempotent)')
278
+ .action((options) => {
279
+ if (options.mergeInto) {
280
+ KiroCommands.runMergeInto(options.mergeInto, { global: options.global });
281
+ }
282
+ else {
283
+ KiroCommands.runSetup(options);
284
+ }
285
+ });
286
+ }
287
+ }
288
+ exports.KiroCommands = KiroCommands;
289
+ /** Read-only memory tools that run without prompting; store/delete still ask. */
290
+ KiroCommands.ALLOWED_TOOLS = [
291
+ '@claude-recall/load_rules',
292
+ '@claude-recall/search_memory',
293
+ '@claude-recall/load_checkpoint',
294
+ ];
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ /**
3
+ * Kiro CLI hook adapters.
4
+ *
5
+ * Kiro CLI's hook contract (kiro.dev/docs/cli/hooks) is nearly identical to
6
+ * Claude Code's: hooks receive JSON on stdin with hook_event_name, cwd,
7
+ * session_id, tool_name, tool_input. These adapters bridge the three
8
+ * differences:
9
+ *
10
+ * 1. Tool names are Kiro-internal (execute_bash, fs_write, fs_read) —
11
+ * mapped to the Claude Code names our handlers and the rule ranker
12
+ * understand. fs_write inputs use `path`, mapped to `file_path`.
13
+ * 2. PostToolUse carries `tool_response` (an object), not a `tool_output`
14
+ * string.
15
+ * 3. Hook stdout is added directly to the agent's context — no
16
+ * hookSpecificOutput JSON envelope — and the agentSpawn event gives a
17
+ * context slot at session start, which we use to load active rules
18
+ * up front (Claude Code needs the search-enforcer dance for this;
19
+ * Kiro gets it for free).
20
+ *
21
+ * userPromptSubmit needs no adapter: Kiro sends { prompt, session_id, cwd },
22
+ * exactly what correction-detector expects — wire it directly.
23
+ *
24
+ * Known v1 gaps (accepted): Kiro provides no tool_use_id, so rule-injection →
25
+ * outcome correlation is weaker than under Claude Code; the stop event carries
26
+ * only assistant_response (no transcript file), so transcript-based failure
27
+ * detectors and session extraction don't run under Kiro.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.normalizeKiroInput = normalizeKiroInput;
31
+ exports.handleKiroAgentSpawn = handleKiroAgentSpawn;
32
+ exports.handleKiroRuleInjector = handleKiroRuleInjector;
33
+ exports.handleKiroToolOutcome = handleKiroToolOutcome;
34
+ const shared_1 = require("./shared");
35
+ const directives_1 = require("../shared/directives");
36
+ const memory_1 = require("../services/memory");
37
+ const config_1 = require("../services/config");
38
+ const rule_injector_1 = require("./rule-injector");
39
+ const tool_outcome_watcher_1 = require("./tool-outcome-watcher");
40
+ const memory_tools_1 = require("../mcp/tools/memory-tools");
41
+ const HOOK_NAME = 'kiro';
42
+ /** Kiro built-in tool names → the Claude Code names our handlers understand. */
43
+ const KIRO_TOOL_NAME_MAP = {
44
+ execute_bash: 'Bash',
45
+ shell: 'Bash',
46
+ fs_write: 'Write',
47
+ fs_read: 'Read',
48
+ };
49
+ /**
50
+ * Normalize a Kiro hook payload to the Claude Code shape. Pure and
51
+ * non-destructive — returns a shallow copy; unknown tool names (use_aws,
52
+ * @mcp-server tools, ...) pass through unchanged.
53
+ */
54
+ function normalizeKiroInput(input) {
55
+ if (!input || typeof input !== 'object')
56
+ return input;
57
+ const normalized = { ...input };
58
+ if (typeof input.tool_name === 'string' && KIRO_TOOL_NAME_MAP[input.tool_name]) {
59
+ normalized.tool_name = KIRO_TOOL_NAME_MAP[input.tool_name];
60
+ }
61
+ // Kiro file tools use `path`; our handlers key on `file_path`
62
+ if (input.tool_input && typeof input.tool_input === 'object'
63
+ && typeof input.tool_input.path === 'string' && input.tool_input.file_path === undefined) {
64
+ normalized.tool_input = { ...input.tool_input, file_path: input.tool_input.path };
65
+ }
66
+ // PostToolUse: tool_response (object) → tool_output (string)
67
+ if (input.tool_response !== undefined && input.tool_output === undefined) {
68
+ normalized.tool_output = typeof input.tool_response === 'string'
69
+ ? input.tool_response
70
+ : JSON.stringify(input.tool_response);
71
+ }
72
+ return normalized;
73
+ }
74
+ /** Format active rules as markdown sections (mirrors the Pi extension). */
75
+ function formatRulesForContext() {
76
+ const projectId = config_1.ConfigService.getInstance().getProjectId();
77
+ const rules = memory_1.MemoryService.getInstance().loadActiveRules(projectId);
78
+ const section = (title, items) => {
79
+ if (items.length === 0)
80
+ return null;
81
+ return `## ${title}\n` + items.map(m => `- ${(0, memory_tools_1.formatRuleValue)(m.value)}`).join('\n');
82
+ };
83
+ const sections = [
84
+ section('Preferences', rules.preferences),
85
+ section('Corrections', rules.corrections),
86
+ section('Failures', rules.failures),
87
+ section('DevOps Rules', rules.devops),
88
+ ].filter((s) => s !== null);
89
+ const total = rules.preferences.length + rules.corrections.length
90
+ + rules.failures.length + rules.devops.length;
91
+ return { body: sections.join('\n\n'), total };
92
+ }
93
+ /**
94
+ * agentSpawn — runs once when a Kiro agent activates. Whatever we print is
95
+ * added to the agent's context, so this IS the load_rules call: rules are in
96
+ * context from turn one without any tool call or enforcement.
97
+ */
98
+ async function handleKiroAgentSpawn(_input) {
99
+ try {
100
+ const { body, total } = formatRulesForContext();
101
+ const parts = [];
102
+ if (body) {
103
+ parts.push(directives_1.LOAD_RULES_DIRECTIVE, '---', body);
104
+ }
105
+ // Surface a pending task checkpoint the same way load_rules hints at one
106
+ try {
107
+ const projectId = config_1.ConfigService.getInstance().getProjectId();
108
+ const checkpoint = memory_1.MemoryService.getInstance().loadCheckpoint(projectId);
109
+ if (checkpoint) {
110
+ parts.push(`📌 A task checkpoint exists for this project (updated ${new Date(checkpoint.updated_at).toLocaleString()}). ` +
111
+ `Remaining: ${checkpoint.remaining.substring(0, 200)}`);
112
+ }
113
+ }
114
+ catch { /* checkpoint hint is best-effort */ }
115
+ if (parts.length === 0) {
116
+ (0, shared_1.hookLog)(HOOK_NAME, 'agentSpawn: no active rules or checkpoint — nothing injected');
117
+ return;
118
+ }
119
+ process.stdout.write(parts.join('\n\n') + '\n');
120
+ (0, shared_1.hookLog)(HOOK_NAME, `agentSpawn: injected ${total} rule(s) into Kiro context`);
121
+ }
122
+ catch (err) {
123
+ // Never block agent startup
124
+ (0, shared_1.hookLog)(HOOK_NAME, `agentSpawn error: ${(0, shared_1.safeErrorMessage)(err)}`);
125
+ }
126
+ }
127
+ /**
128
+ * preToolUse — just-in-time rule injection. Same ranking/recording core as
129
+ * the Claude Code rule-injector, but emits plain text: Kiro adds hook stdout
130
+ * to context directly (no hookSpecificOutput envelope).
131
+ */
132
+ async function handleKiroRuleInjector(input) {
133
+ try {
134
+ const normalized = normalizeKiroInput(input);
135
+ const context = await (0, rule_injector_1.computeInjection)(normalized?.tool_name ?? '', normalized?.tool_input ?? {}, '');
136
+ if (context) {
137
+ process.stdout.write(context + '\n');
138
+ }
139
+ }
140
+ catch (err) {
141
+ // Best-effort — never block the tool call
142
+ (0, shared_1.hookLog)(HOOK_NAME, `rule-injector error: ${(0, shared_1.safeErrorMessage)(err)}`);
143
+ }
144
+ }
145
+ /**
146
+ * postToolUse — outcome capture and Bash fix-pairing, delegated to the shared
147
+ * tool-outcome-watcher after payload normalization.
148
+ */
149
+ async function handleKiroToolOutcome(input) {
150
+ try {
151
+ await (0, tool_outcome_watcher_1.handleToolOutcomeWatcher)(normalizeKiroInput(input));
152
+ }
153
+ catch (err) {
154
+ (0, shared_1.hookLog)(HOOK_NAME, `tool-outcome error: ${(0, shared_1.safeErrorMessage)(err)}`);
155
+ }
156
+ }
@@ -25,6 +25,7 @@
25
25
  * citation-detection regex.
26
26
  */
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.computeInjection = computeInjection;
28
29
  exports.handleRuleInjector = handleRuleInjector;
29
30
  const shared_1 = require("./shared");
30
31
  const memory_1 = require("../services/memory");
@@ -80,72 +81,79 @@ function formatInjection(matches, toolName) {
80
81
  `but defer to safety and correctness if any conflict.\n${lines.join('\n')}\n` +
81
82
  `</recalled-memory>`);
82
83
  }
83
- async function handleRuleInjector(input) {
84
- const toolName = input?.tool_name ?? '';
85
- const toolInput = input?.tool_input ?? {};
86
- const toolUseId = input?.tool_use_id ?? '';
87
- if (!toolName) {
88
- // Nothing to do print empty JSON so CC parses it cleanly
89
- process.stdout.write('{}\n');
90
- return;
91
- }
84
+ /**
85
+ * Runtime-agnostic core: rank active rules against this tool call, record
86
+ * the injections for outcome resolution, and return the formatted context
87
+ * block or null when there is nothing to inject. Emitters wrap this per
88
+ * runtime (Claude Code wants a hookSpecificOutput JSON envelope; Kiro adds
89
+ * raw stdout to context).
90
+ */
91
+ async function computeInjection(toolName, toolInput, toolUseId) {
92
+ if (!toolName)
93
+ return null;
92
94
  // Skip the hook for our own tools so we don't recursively inject rules
93
95
  // about claude-recall into claude-recall calls. The agent already has
94
96
  // claude-recall context when calling its own tools.
95
97
  if (toolName.startsWith('mcp__claude-recall__') || toolName.startsWith('mcp__claude_recall')) {
96
- process.stdout.write('{}\n');
97
- return;
98
+ return null;
99
+ }
100
+ const projectId = config_1.ConfigService.getInstance().getProjectId();
101
+ const memoryService = memory_1.MemoryService.getInstance();
102
+ // Fetch all active rules for this project. We pass them all to the ranker
103
+ // because the ranking function is fast and we want sticky rules to surface
104
+ // even when token overlap is low.
105
+ const activeRules = memoryService.loadActiveRules(projectId);
106
+ const allRules = [
107
+ ...activeRules.preferences,
108
+ ...activeRules.corrections,
109
+ ...activeRules.failures,
110
+ ...activeRules.devops,
111
+ ].map(m => ({
112
+ key: m.key,
113
+ type: m.type,
114
+ value: m.value,
115
+ is_active: m.is_active !== false,
116
+ timestamp: m.timestamp,
117
+ project_id: m.project_id,
118
+ }));
119
+ if (allRules.length === 0) {
120
+ (0, shared_1.hookLog)('rule-injector', `No active rules for project ${projectId} (tool=${toolName})`);
121
+ return null;
122
+ }
123
+ const matches = (0, rule_retrieval_1.rankRulesForToolCall)(toolName, toolInput, allRules);
124
+ if (matches.length === 0) {
125
+ (0, shared_1.hookLog)('rule-injector', `No relevant rules for ${toolName} (scanned ${allRules.length})`);
126
+ return null;
98
127
  }
128
+ // Record each injection so PostToolUse can resolve it with the outcome
99
129
  try {
100
- const projectId = config_1.ConfigService.getInstance().getProjectId();
101
- const memoryService = memory_1.MemoryService.getInstance();
102
- // Fetch all active rules for this project. We pass them all to the ranker
103
- // because the ranking function is fast and we want sticky rules to surface
104
- // even when token overlap is low.
105
- const activeRules = memoryService.loadActiveRules(projectId);
106
- const allRules = [
107
- ...activeRules.preferences,
108
- ...activeRules.corrections,
109
- ...activeRules.failures,
110
- ...activeRules.devops,
111
- ].map(m => ({
112
- key: m.key,
113
- type: m.type,
114
- value: m.value,
115
- is_active: m.is_active !== false,
116
- timestamp: m.timestamp,
117
- project_id: m.project_id,
118
- }));
119
- if (allRules.length === 0) {
120
- (0, shared_1.hookLog)('rule-injector', `No active rules for project ${projectId} (tool=${toolName})`);
121
- process.stdout.write('{}\n');
122
- return;
130
+ const outcomeStorage = outcome_storage_1.OutcomeStorage.getInstance();
131
+ for (const m of matches) {
132
+ outcomeStorage.recordRuleInjection({
133
+ rule_key: m.rule.key,
134
+ tool_name: toolName,
135
+ tool_use_id: toolUseId,
136
+ project_id: projectId,
137
+ match_score: m.score,
138
+ matched_tokens: m.matchedTokens,
139
+ });
123
140
  }
124
- const matches = (0, rule_retrieval_1.rankRulesForToolCall)(toolName, toolInput, allRules);
125
- if (matches.length === 0) {
126
- (0, shared_1.hookLog)('rule-injector', `No relevant rules for ${toolName} (scanned ${allRules.length})`);
141
+ }
142
+ catch (err) {
143
+ // Non-critical failure to record shouldn't block the injection itself
144
+ (0, shared_1.hookLog)('rule-injector', `Failed to record injections: ${err.message}`);
145
+ }
146
+ (0, shared_1.hookLog)('rule-injector', `Injected ${matches.length} rule(s) for ${toolName} (top score=${matches[0].score.toFixed(3)})`);
147
+ return formatInjection(matches, toolName);
148
+ }
149
+ async function handleRuleInjector(input) {
150
+ try {
151
+ const additionalContext = await computeInjection(input?.tool_name ?? '', input?.tool_input ?? {}, input?.tool_use_id ?? '');
152
+ if (!additionalContext) {
153
+ // Nothing to inject — print empty JSON so CC parses it cleanly
127
154
  process.stdout.write('{}\n');
128
155
  return;
129
156
  }
130
- // Record each injection so PostToolUse can resolve it with the outcome
131
- try {
132
- const outcomeStorage = outcome_storage_1.OutcomeStorage.getInstance();
133
- for (const m of matches) {
134
- outcomeStorage.recordRuleInjection({
135
- rule_key: m.rule.key,
136
- tool_name: toolName,
137
- tool_use_id: toolUseId,
138
- project_id: projectId,
139
- match_score: m.score,
140
- matched_tokens: m.matchedTokens,
141
- });
142
- }
143
- }
144
- catch (err) {
145
- // Non-critical — failure to record shouldn't block the injection itself
146
- (0, shared_1.hookLog)('rule-injector', `Failed to record injections: ${err.message}`);
147
- }
148
- const additionalContext = formatInjection(matches, toolName);
149
157
  const output = {
150
158
  hookSpecificOutput: {
151
159
  hookEventName: 'PreToolUse',
@@ -153,7 +161,6 @@ async function handleRuleInjector(input) {
153
161
  },
154
162
  };
155
163
  process.stdout.write(JSON.stringify(output) + '\n');
156
- (0, shared_1.hookLog)('rule-injector', `Injected ${matches.length} rule(s) for ${toolName} (top score=${matches[0].score.toFixed(3)})`);
157
164
  }
158
165
  catch (err) {
159
166
  (0, shared_1.hookLog)('rule-injector', `Error: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-recall",
3
- "version": "0.27.1",
3
+ "version": "0.28.1",
4
4
  "description": "Persistent memory for Claude Code and Pi with native Skills integration, automatic capture, failure learning, and project scoping",
5
5
  "main": "dist/index.js",
6
6
  "bin": {