claude-recall 0.28.7 → 0.29.0

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
@@ -107,7 +107,7 @@ This finds the agent config (workspace `.kiro/agents/` first, then `~/.kiro/agen
107
107
 
108
108
  > **⚠️ After `kiro setup` or `--merge-into`: start ONE fresh conversation per project (no `--resume`).** Kiro snapshots the agent config into each conversation **at creation** — `--resume` restores that snapshot and ignores agent-config changes made since. So conversations created *before* you wired claude-recall will **never** run its hooks, no matter how often you resume them or restart Kiro. Start one fresh conversation after wiring; every conversation created from then on carries the hooks, **including when resumed** (`--resume` works normally afterwards — this is a one-time rollover per project).
109
109
 
110
- **What you need to do — once per project, no code changes needed.** In each project, start one conversation *without* `--resume` (add your usual flags, e.g. `--classic`, `--trust-all-tools`):
110
+ The rollover, concretely (add your usual flags, e.g. `--classic`, `--trust-all-tools`):
111
111
 
112
112
  ```bash
113
113
  cd ~/path/to/your-project
@@ -121,9 +121,9 @@ claude-recall search "helm"
121
121
  tail -5 ~/.claude-recall/hook-logs/hook-dispatcher.log
122
122
  ```
123
123
 
124
- You should see a `scope [...] → project=your-project` line. From then on your normal `--resume` command works — every conversation created after wiring carries the hooks permanently, including when resumed. `claude-recall kiro doctor` gives a fuller health report.
124
+ The log should show a `scope [...] → project=your-project` line; `claude-recall kiro doctor` gives a fuller health report.
125
125
 
126
- > **Tip:** export `ANTHROPIC_API_KEY` in the shell you launch Kiro from. Hooks then use Claude Haiku to classify what's worth remembering; without it a conservative regex fallback runs, which catches explicit phrasings ("remember ...", "always ...", "never ...", "I prefer ...") but misses subtler ones.
126
+ > **Capture uses Kiro's own LLM no API key needed.** To decide what's worth remembering, the capture hook classifies each prompt with Kiro's model via a headless `kiro-cli chat --no-interactive` call (through a bundled bare `claude-recall-classifier` agent). So natural statements like "my favourite color is green" are captured without any `ANTHROPIC_API_KEY` and without the `store_memory` MCP tool — which matters under enterprise governance that blocks the MCP server. It runs in a detached background worker, so your turn is never blocked; it spends ~0.06 Kiro credits per prompt. Precedence: `ANTHROPIC_API_KEY` (if you set one) Kiro's LLM → a regex fallback. Tune with `CLAUDE_RECALL_KIRO_MODEL` (default `claude-haiku-4.5`). Details in [docs/kiro-llm-capture.md](docs/kiro-llm-capture.md).
127
127
 
128
128
  **Option B — MCP tools only (no hooks, works in Kiro's default agent).**
129
129
 
@@ -150,7 +150,7 @@ With Option B the agent has the memory tools (`load_rules`, `store_memory`, `sea
150
150
  > To force a fixed project id regardless of directory, pin it with `CLAUDE_RECALL_PROJECT_ID`. A per-project shell alias makes it seamless:
151
151
  >
152
152
  > ```bash
153
- > alias kiro-epic='CLAUDE_RECALL_PROJECT_ID=epic-workflow-cicd kiro-cli chat --agent mcp-agent-env --resume'
153
+ > alias kiro-myproj='CLAUDE_RECALL_PROJECT_ID=my-project kiro-cli chat --agent <your-agent> --resume'
154
154
  > ```
155
155
  >
156
156
  > `claude-recall kiro doctor` always prints the resolved project (and whether it's pinned) so you can confirm where memories are landing before trusting it.
@@ -522,6 +522,8 @@ claude-recall hook run memory-sync # Stop + PreCompact hook (syncs rul
522
522
 
523
523
  Each project gets isolated memory based on its working directory. **Project ID** is derived from the `cwd` passed by the agent. Universal memories (no project scope) are available everywhere. Switching projects switches memory automatically.
524
524
 
525
+ To pin the project id explicitly — e.g. one logical project spanning several directories (worktrees, subrepos) — set `CLAUDE_RECALL_PROJECT_ID` (see [Environment Variables](#environment-variables)).
526
+
525
527
  Database location: `~/.claude-recall/claude-recall.db` (shared file, scoped by `project_id` column).
526
528
 
527
529
  ---
@@ -560,7 +562,9 @@ Runtime behavior can be tuned via environment variables. Defaults are chosen so
560
562
  | Variable | Default | Effect |
561
563
  | ---------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
562
564
  | `CLAUDE_RECALL_DB_PATH` | `~/.claude-recall/` | Database directory. |
563
- | `ANTHROPIC_API_KEY` | _(unset)_ | Enables LLM-based classification (Haiku). Falls back to regex silently when missing. |
565
+ | `ANTHROPIC_API_KEY` | _(unset)_ | Enables LLM-based classification (Haiku). Under Kiro, falls back to Kiro's own headless LLM when missing; otherwise falls back to regex. |
566
+ | `CLAUDE_RECALL_KIRO_MODEL` | `claude-haiku-4.5` | Model for Kiro-LLM capture classification. Raise to `claude-sonnet-4.6` for steadier judgement at more credits. See [docs/kiro-llm-capture.md](docs/kiro-llm-capture.md). |
567
+ | `CLAUDE_RECALL_KIRO_LLM_TIMEOUT_MS` | `30000` | Hard cap on the headless `kiro-cli` classify call before the capture worker gives up and falls back to regex. |
564
568
  | `CLAUDE_RECALL_LOAD_BUDGET_TOKENS` | `2000` | Token budget for the `load_rules` payload. Rules are emitted in priority order (corrections → preferences by citation → devops by citation → failures) and dropped rules surface via `search_memory`. |
565
569
  | `CLAUDE_RECALL_AUTO_DEMOTE` | `false` | When `true`, auto-demote rules on MCP boot where `load_count >= CLAUDE_RECALL_DEMOTE_MIN_LOADS`, `cite_count = 0`, and age `> CLAUDE_RECALL_DEMOTE_MIN_AGE_DAYS`. Still reversible via `rules promote <id>`. |
566
570
  | `CLAUDE_RECALL_DEMOTE_MIN_LOADS` | `20` | Minimum load count before a rule qualifies for auto-demotion. |
@@ -53,6 +53,8 @@ const AVAILABLE_HOOKS = [
53
53
  'kiro-agent-spawn',
54
54
  'kiro-rule-injector',
55
55
  'kiro-tool-outcome',
56
+ 'kiro-capture',
57
+ 'kiro-capture-worker',
56
58
  ];
57
59
  /**
58
60
  * Hook CLI Commands
@@ -155,6 +157,16 @@ class HookCommands {
155
157
  await handleKiroToolOutcome(input);
156
158
  break;
157
159
  }
160
+ case 'kiro-capture': {
161
+ const { handleKiroCapture } = await Promise.resolve().then(() => __importStar(require('../../hooks/kiro-hooks')));
162
+ await handleKiroCapture(input);
163
+ break;
164
+ }
165
+ case 'kiro-capture-worker': {
166
+ const { handleKiroCaptureWorker } = await Promise.resolve().then(() => __importStar(require('../../hooks/kiro-hooks')));
167
+ await handleKiroCaptureWorker(input);
168
+ break;
169
+ }
158
170
  default:
159
171
  console.error(`Unknown hook: ${name}`);
160
172
  console.error(`Available: ${AVAILABLE_HOOKS.join(', ')}`);
@@ -57,6 +57,30 @@ class KiroCommands {
57
57
  timeout: 120000,
58
58
  };
59
59
  }
60
+ /** The bare classifier agent config — no MCP, no hooks, no tools. */
61
+ static buildClassifierAgentConfig() {
62
+ return {
63
+ name: KiroCommands.CLASSIFIER_AGENT_NAME,
64
+ description: 'Headless memory classifier for Claude Recall. No MCP servers, hooks, or tools — invoked by the capture worker to decide what to remember, using Kiro\'s own LLM.',
65
+ mcpServers: {},
66
+ includeMcpJson: false,
67
+ tools: [],
68
+ allowedTools: [],
69
+ hooks: {},
70
+ };
71
+ }
72
+ /**
73
+ * Write the bare classifier agent to ~/.kiro/agents/ (always global, so any
74
+ * project's capture worker can invoke `--agent claude-recall-classifier`).
75
+ * Idempotent overwrite — the file is entirely ours. Returns its path.
76
+ */
77
+ static writeClassifierAgent() {
78
+ const dir = path.join(os.homedir(), '.kiro', 'agents');
79
+ fs.mkdirSync(dir, { recursive: true });
80
+ const p = path.join(dir, `${KiroCommands.CLASSIFIER_AGENT_NAME}.json`);
81
+ fs.writeFileSync(p, JSON.stringify(KiroCommands.buildClassifierAgentConfig(), null, 2) + '\n');
82
+ return p;
83
+ }
60
84
  /** The four lifecycle hook entries (shared by fresh config and merge). */
61
85
  static buildHookEntries(hookCmd) {
62
86
  return {
@@ -64,9 +88,12 @@ class KiroCommands {
64
88
  agentSpawn: [
65
89
  { command: `${hookCmd} kiro-agent-spawn`, timeout_ms: 10000 },
66
90
  ],
67
- // Kiro's userPromptSubmit payload matches correction-detector exactly
91
+ // Capture. kiro-capture spawns a detached worker that classifies the
92
+ // prompt via Kiro's own headless LLM (no ANTHROPIC_API_KEY needed) and
93
+ // stores in the background — see src/hooks/kiro-classifier.ts. The hook
94
+ // itself returns in milliseconds, so the short timeout is ample.
68
95
  userPromptSubmit: [
69
- { command: `${hookCmd} correction-detector`, timeout_ms: 8000 },
96
+ { command: `${hookCmd} kiro-capture`, timeout_ms: 8000 },
70
97
  ],
71
98
  preToolUse: [
72
99
  { matcher: '*', command: `${hookCmd} kiro-rule-injector`, timeout_ms: 5000 },
@@ -134,7 +161,7 @@ class KiroCommands {
134
161
  }
135
162
  const handlerMarkers = {
136
163
  agentSpawn: 'kiro-agent-spawn',
137
- userPromptSubmit: 'correction-detector',
164
+ userPromptSubmit: 'kiro-capture',
138
165
  preToolUse: 'kiro-rule-injector',
139
166
  postToolUse: 'kiro-tool-outcome',
140
167
  };
@@ -214,8 +241,11 @@ class KiroCommands {
214
241
  const backupPath = `${agentPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
215
242
  fs.writeFileSync(backupPath, raw);
216
243
  fs.writeFileSync(agentPath, JSON.stringify(config, null, 2) + '\n');
244
+ // Capture uses Kiro's own headless LLM via a bare classifier agent.
245
+ const classifierPath = KiroCommands.writeClassifierAgent();
217
246
  console.log(`✅ Merged Claude Recall into: ${agentPath}`);
218
247
  console.log(` Backup: ${backupPath}`);
248
+ console.log(` Classifier agent: ${classifierPath}`);
219
249
  console.log('');
220
250
  for (const c of changes)
221
251
  console.log(` • ${c}`);
@@ -251,7 +281,10 @@ class KiroCommands {
251
281
  fs.mkdirSync(baseDir, { recursive: true });
252
282
  const config = KiroCommands.buildAgentConfig(hookCmd, mcpCommand, mcpArgs);
253
283
  fs.writeFileSync(agentPath, JSON.stringify(config, null, 2) + '\n');
284
+ // Capture uses Kiro's own headless LLM via a bare classifier agent.
285
+ const classifierPath = KiroCommands.writeClassifierAgent();
254
286
  console.log(`✅ Wrote Kiro agent config: ${agentPath}`);
287
+ console.log(`✅ Wrote classifier agent: ${classifierPath}`);
255
288
  console.log('');
256
289
  console.log('No mcp.json changes needed — the agent config carries its own claude-recall');
257
290
  console.log('MCP server entry (and includeMcpJson keeps your other servers working).');
@@ -266,7 +299,9 @@ class KiroCommands {
266
299
  console.log('');
267
300
  console.log('Rules load into context automatically at agent start; corrections and');
268
301
  console.log('preferences you state are captured; memories are shared with Claude Code');
269
- console.log('(same database, same per-project scoping).');
302
+ console.log('(same database, same per-project scoping). Capture classifies each prompt');
303
+ console.log('with Kiro\'s own LLM — no ANTHROPIC_API_KEY needed (spends ~0.06 Kiro');
304
+ console.log('credits/prompt; set CLAUDE_RECALL_KIRO_MODEL to change the model).');
270
305
  console.log('');
271
306
  console.log('⚠️ Kiro snapshots the agent config into each conversation at creation —');
272
307
  console.log('conversations created before this setup never run the hooks, even when');
@@ -406,6 +441,25 @@ class KiroCommands {
406
441
  line('⚠', 'none found. Run `claude-recall kiro setup` (new agent) or');
407
442
  line(' ', ' `claude-recall kiro setup --merge-into <agent>` (existing agent).');
408
443
  }
444
+ // --- Capture backend (Kiro LLM classifier) ---
445
+ console.log('\nCapture backend (no ANTHROPIC_API_KEY needed under Kiro)');
446
+ const classifierPath = path.join(os.homedir(), '.kiro', 'agents', `${KiroCommands.CLASSIFIER_AGENT_NAME}.json`);
447
+ if (fs.existsSync(classifierPath)) {
448
+ line('✓', `classifier agent present: ${classifierPath}`);
449
+ }
450
+ else {
451
+ line('⚠', `classifier agent missing (${KiroCommands.CLASSIFIER_AGENT_NAME}.json) — re-run \`claude-recall kiro setup\` or \`--merge-into\`. Capture falls back to regex without it.`);
452
+ }
453
+ if ((0, repair_1.resolveOnPath)('kiro-cli')) {
454
+ const model = process.env.CLAUDE_RECALL_KIRO_MODEL || 'claude-haiku-4.5';
455
+ line('✓', `kiro-cli on PATH — capture classifies with Kiro's LLM (model: ${model})`);
456
+ }
457
+ else {
458
+ line('⚠', 'kiro-cli not on PATH — capture cannot reach Kiro\'s LLM and falls back to regex.');
459
+ }
460
+ if (process.env.ANTHROPIC_API_KEY) {
461
+ line('•', 'ANTHROPIC_API_KEY is set — it takes precedence over the Kiro LLM for capture.');
462
+ }
409
463
  // --- Hook activity ---
410
464
  console.log('\nRecent hook activity');
411
465
  const dir = process.env.CLAUDE_RECALL_DB_PATH || path.join(os.homedir(), '.claude-recall');
@@ -462,3 +516,9 @@ KiroCommands.ALLOWED_TOOLS = [
462
516
  '@claude-recall/search_memory',
463
517
  '@claude-recall/load_checkpoint',
464
518
  ];
519
+ /**
520
+ * Name of the bare agent used for headless memory classification. Kept in
521
+ * sync with CLASSIFIER_AGENT in src/hooks/kiro-classifier.ts (duplicated as a
522
+ * plain string so this module doesn't pull the hooks graph into jest).
523
+ */
524
+ KiroCommands.CLASSIFIER_AGENT_NAME = 'claude-recall-classifier';
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ /**
3
+ * Kiro-LLM memory classifier.
4
+ *
5
+ * When Claude Recall runs under Kiro CLI there is no ANTHROPIC_API_KEY, so the
6
+ * usual Haiku classifier (llm-classifier.ts) is unavailable and capture would
7
+ * degrade to regex. But Kiro CLI ships its own LLM and exposes it headlessly:
8
+ * `kiro-cli chat --no-interactive "<prompt>"` runs a one-shot completion on
9
+ * Kiro's model using Kiro's own auth — no API key, no personal subscription.
10
+ * See docs/kiro-llm-capture.md for the full findings.
11
+ *
12
+ * This module shells out to that headless mode to classify a user prompt into
13
+ * the same ClassifyResult shape the rest of the pipeline expects. It runs from
14
+ * a DETACHED worker (kiro-capture-worker), never inline on the user's turn,
15
+ * because a cold `kiro-cli` boot can take ~15s.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.CLASSIFIER_AGENT = void 0;
19
+ exports.extractClassification = extractClassification;
20
+ exports.classifyWithKiro = classifyWithKiro;
21
+ const child_process_1 = require("child_process");
22
+ const shared_1 = require("./shared");
23
+ /** Bare Kiro agent used for classification — no MCP, no hooks, no tools. */
24
+ exports.CLASSIFIER_AGENT = 'claude-recall-classifier';
25
+ const DEFAULT_MODEL = 'claude-haiku-4.5';
26
+ const DEFAULT_TIMEOUT_MS = 30000;
27
+ const VALID_TYPES = new Set([
28
+ 'correction',
29
+ 'preference',
30
+ 'failure',
31
+ 'devops',
32
+ 'project-knowledge',
33
+ ]);
34
+ /**
35
+ * The classify instruction, prepended to the user's message. kiro-cli takes a
36
+ * single INPUT argument (no separate system prompt), so instruction + payload
37
+ * are combined. Mirrors the contract of llm-classifier's SYSTEM_PROMPT so
38
+ * downstream thresholds/dedup behave identically regardless of which LLM ran.
39
+ */
40
+ function buildPrompt(text) {
41
+ return ('You are a memory classifier for a developer tool. Classify the USER MESSAGE ' +
42
+ 'into exactly one type and respond with ONLY minified JSON — no markdown, no ' +
43
+ 'prose, no code fence:\n' +
44
+ '{"type":"correction|preference|failure|devops|project-knowledge|none","confidence":0.0-1.0,"extract":"<concise imperative rule to remember, or empty>"}\n\n' +
45
+ 'Types:\n' +
46
+ '- correction: user correcting a mistake ("no, use X not Y")\n' +
47
+ '- preference: a reusable directive about how the user wants things done ("I prefer X", "we use tabs", "my favourite color is green"). Must apply beyond this one message.\n' +
48
+ '- failure: something broke ("build failed")\n' +
49
+ '- devops: durable CI/CD, git, deployment, or Docker rules\n' +
50
+ '- project-knowledge: architecture, stack, database, or API facts\n' +
51
+ '- none: questions, chitchat, task instructions, observations, or anything not worth remembering across sessions\n\n' +
52
+ 'Be conservative: when in doubt use "none" with confidence 0. Use confidence >= 0.75 for correction/preference/devops. ' +
53
+ 'extract must be a clean standalone rule (e.g. "Favourite colour is green"), or empty when type is none.\n\n' +
54
+ 'USER MESSAGE: ' + text);
55
+ }
56
+ /** Strip ANSI/VT control sequences that kiro-cli writes around its output. */
57
+ function stripAnsi(s) {
58
+ // Anchor every strip to the ESC byte (\x1b) so ordinary JSON text —
59
+ // uppercase letters, hyphens, braces — is never touched. First form: CSI
60
+ // (ESC [ ... final byte); second: two-byte ESC <char>. The control char in
61
+ // the pattern is deliberate — that's exactly what we're stripping.
62
+ // eslint-disable-next-line no-control-regex
63
+ return s.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '').replace(/\x1b[@-_]/g, '');
64
+ }
65
+ /**
66
+ * Pull the first balanced JSON object out of kiro-cli's decorated stdout.
67
+ * The output looks like `> {"type":...}` or `> json\n{...}`, possibly wrapped
68
+ * in a ```json fence and coloured with ANSI. Returns null if no object parses.
69
+ */
70
+ function extractClassification(raw) {
71
+ const cleaned = stripAnsi(raw);
72
+ const start = cleaned.indexOf('{');
73
+ const end = cleaned.lastIndexOf('}');
74
+ if (start === -1 || end === -1 || end <= start)
75
+ return null;
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(cleaned.slice(start, end + 1));
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ if (!parsed || typeof parsed !== 'object')
84
+ return null;
85
+ if (parsed.type === 'none' || !VALID_TYPES.has(parsed.type))
86
+ return null;
87
+ if (typeof parsed.extract !== 'string' || parsed.extract.trim().length === 0)
88
+ return null;
89
+ const confidence = typeof parsed.confidence === 'number' ? parsed.confidence : 0.7;
90
+ return {
91
+ type: parsed.type,
92
+ confidence,
93
+ extract: parsed.extract.trim(),
94
+ };
95
+ }
96
+ /**
97
+ * Classify a prompt by invoking Kiro's headless LLM. Returns null on any
98
+ * failure (kiro-cli absent, timeout, non-zero exit, unparseable output) so the
99
+ * caller falls back to regex. Never throws.
100
+ */
101
+ function classifyWithKiro(text) {
102
+ const model = process.env.CLAUDE_RECALL_KIRO_MODEL || DEFAULT_MODEL;
103
+ const timeoutMs = parseInt(process.env.CLAUDE_RECALL_KIRO_LLM_TIMEOUT_MS || '', 10);
104
+ const timeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
105
+ return new Promise((resolve) => {
106
+ let settled = false;
107
+ const done = (result) => {
108
+ if (settled)
109
+ return;
110
+ settled = true;
111
+ resolve(result);
112
+ };
113
+ let child;
114
+ try {
115
+ // args array (no shell) — the user's text is passed as a single argv
116
+ // entry, so no shell escaping or injection is possible.
117
+ child = (0, child_process_1.spawn)('kiro-cli', [
118
+ 'chat',
119
+ '--no-interactive',
120
+ '--agent', exports.CLASSIFIER_AGENT,
121
+ '--model', model,
122
+ buildPrompt(text),
123
+ ], { stdio: ['ignore', 'pipe', 'ignore'] });
124
+ }
125
+ catch (err) {
126
+ (0, shared_1.hookLog)('kiro-classifier', `spawn threw: ${(0, shared_1.safeErrorMessage)(err)}`);
127
+ return done(null);
128
+ }
129
+ const timer = setTimeout(() => {
130
+ (0, shared_1.hookLog)('kiro-classifier', `timeout after ${timeout}ms — killing kiro-cli`);
131
+ try {
132
+ child.kill('SIGKILL');
133
+ }
134
+ catch { /* already gone */ }
135
+ done(null);
136
+ }, timeout);
137
+ let stdout = '';
138
+ child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
139
+ child.on('error', (err) => {
140
+ clearTimeout(timer);
141
+ // ENOENT = kiro-cli not on PATH; anything else = spawn failure
142
+ (0, shared_1.hookLog)('kiro-classifier', `kiro-cli error: ${err?.code ?? ''} ${err?.message ?? err}`);
143
+ done(null);
144
+ });
145
+ child.on('close', (code) => {
146
+ clearTimeout(timer);
147
+ if (settled)
148
+ return;
149
+ if (code !== 0) {
150
+ (0, shared_1.hookLog)('kiro-classifier', `kiro-cli exited ${code}`);
151
+ return done(null);
152
+ }
153
+ const result = extractClassification(stdout);
154
+ if (!result) {
155
+ (0, shared_1.hookLog)('kiro-classifier', 'no parseable classification in kiro-cli output');
156
+ }
157
+ done(result);
158
+ });
159
+ });
160
+ }
@@ -31,12 +31,16 @@ exports.normalizeKiroInput = normalizeKiroInput;
31
31
  exports.handleKiroAgentSpawn = handleKiroAgentSpawn;
32
32
  exports.handleKiroRuleInjector = handleKiroRuleInjector;
33
33
  exports.handleKiroToolOutcome = handleKiroToolOutcome;
34
+ exports.handleKiroCapture = handleKiroCapture;
35
+ exports.handleKiroCaptureWorker = handleKiroCaptureWorker;
36
+ const child_process_1 = require("child_process");
34
37
  const shared_1 = require("./shared");
35
38
  const directives_1 = require("../shared/directives");
36
39
  const memory_1 = require("../services/memory");
37
40
  const config_1 = require("../services/config");
38
41
  const rule_injector_1 = require("./rule-injector");
39
42
  const tool_outcome_watcher_1 = require("./tool-outcome-watcher");
43
+ const correction_detector_1 = require("./correction-detector");
40
44
  const memory_tools_1 = require("../mcp/tools/memory-tools");
41
45
  const HOOK_NAME = 'kiro';
42
46
  /** Kiro built-in tool names → the Claude Code names our handlers understand. */
@@ -180,3 +184,58 @@ async function handleKiroToolOutcome(input) {
180
184
  (0, shared_1.hookLog)(HOOK_NAME, `tool-outcome error: ${(0, shared_1.safeErrorMessage)(err)}`);
181
185
  }
182
186
  }
187
+ /**
188
+ * userPromptSubmit — the synchronous gate Kiro waits on. Under Kiro there is no
189
+ * ANTHROPIC_API_KEY, so classification uses Kiro's own headless LLM
190
+ * (`kiro-cli chat --no-interactive`), which cold-boots in ~3–15s — too slow to
191
+ * run inline within Kiro's hook timeout, and a poor UX blocking every turn.
192
+ *
193
+ * So this handler does NOT classify. It spawns a DETACHED worker
194
+ * (kiro-capture-worker) that survives this process, pipes the prompt payload to
195
+ * it over stdin, and returns in milliseconds. The worker performs the slow Kiro
196
+ * classify call and stores the memory in the background. Same pattern as
197
+ * session-end-checkpoint. See docs/kiro-llm-capture.md.
198
+ */
199
+ async function handleKiroCapture(input) {
200
+ const prompt = input?.prompt ?? '';
201
+ // Cheap pre-checks mirror correction-detector so we don't spawn a worker (and
202
+ // spend Kiro credits) on input that could never be stored.
203
+ if (prompt.length < 20 || prompt.length > 2000)
204
+ return;
205
+ if (prompt.startsWith('```') || prompt.startsWith('{'))
206
+ return;
207
+ try {
208
+ const cliPath = process.argv[1]; // absolute path to claude-recall-cli.js
209
+ const child = (0, child_process_1.spawn)(process.execPath, [cliPath, 'hook', 'run', 'kiro-capture-worker'], { detached: true, stdio: ['pipe', 'ignore', 'ignore'] });
210
+ child.on('error', (err) => {
211
+ (0, shared_1.hookLog)(HOOK_NAME, `capture worker spawn error: ${err?.message ?? err}`);
212
+ });
213
+ if (child.stdin) {
214
+ child.stdin.on('error', (err) => {
215
+ (0, shared_1.hookLog)(HOOK_NAME, `capture worker stdin error: ${err?.message ?? err}`);
216
+ });
217
+ child.stdin.write(JSON.stringify(input));
218
+ child.stdin.end();
219
+ }
220
+ child.unref();
221
+ (0, shared_1.hookLog)(HOOK_NAME, `capture: spawned detached worker (pid=${child.pid})`);
222
+ }
223
+ catch (err) {
224
+ (0, shared_1.hookLog)(HOOK_NAME, `capture spawn failed: ${(0, shared_1.safeErrorMessage)(err)}`);
225
+ }
226
+ }
227
+ /**
228
+ * kiro-capture-worker — the background half of handleKiroCapture. Enables the
229
+ * Kiro-LLM classifier path (CLAUDE_RECALL_KIRO_CLASSIFIER) and delegates to the
230
+ * standard correction-detector, which now classifies via Kiro's headless LLM
231
+ * and stores. Runs detached, so its output goes nowhere and capture is silent.
232
+ */
233
+ async function handleKiroCaptureWorker(input) {
234
+ process.env.CLAUDE_RECALL_KIRO_CLASSIFIER = '1';
235
+ try {
236
+ await (0, correction_detector_1.handleCorrectionDetector)(input);
237
+ }
238
+ catch (err) {
239
+ (0, shared_1.hookLog)(HOOK_NAME, `capture worker error: ${(0, shared_1.safeErrorMessage)(err)}`);
240
+ }
241
+ }
@@ -133,13 +133,26 @@ function classifyContentRegex(text) {
133
133
  }
134
134
  /**
135
135
  * Classify text content — LLM-first, regex fallback.
136
- * Tries Claude Haiku via ANTHROPIC_API_KEY (set by Claude Code automatically).
137
- * Falls back to regex patterns if API is unavailable or call fails.
136
+ * Precedence:
137
+ * 1. Claude Haiku via ANTHROPIC_API_KEY (Claude Code sets this automatically).
138
+ * 2. Kiro's headless LLM (`kiro-cli chat --no-interactive`) when running under
139
+ * Kiro — no API key needed. Gated on CLAUDE_RECALL_KIRO_CLASSIFIER, which
140
+ * the kiro-capture-worker sets; the classify call is ~3s so it only runs
141
+ * from that detached worker, never inline. See docs/kiro-llm-capture.md.
142
+ * 3. Regex patterns, if neither LLM path yields a result.
138
143
  */
139
144
  async function classifyContent(text) {
140
145
  const llmResult = await (0, llm_classifier_1.classifyWithLLM)(text);
141
146
  if (llmResult)
142
147
  return llmResult;
148
+ if (process.env.CLAUDE_RECALL_KIRO_CLASSIFIER) {
149
+ // Dynamic import keeps kiro-classifier (and child_process) out of the
150
+ // module graph for every non-Kiro hook invocation.
151
+ const { classifyWithKiro } = await Promise.resolve().then(() => __importStar(require('./kiro-classifier')));
152
+ const kiroResult = await classifyWithKiro(text);
153
+ if (kiroResult)
154
+ return kiroResult;
155
+ }
143
156
  return classifyContentRegex(text);
144
157
  }
145
158
  /**
@@ -0,0 +1,128 @@
1
+ # Using Kiro's LLM for memory capture (no API key)
2
+
3
+ ## The problem
4
+
5
+ Claude Recall captures memories with an LLM classifier: it reads each user
6
+ prompt and decides whether it contains a durable preference, correction, or
7
+ project fact worth storing. That classifier normally calls Claude Haiku through
8
+ `ANTHROPIC_API_KEY`, which Claude Code sets automatically for its hook
9
+ subprocesses.
10
+
11
+ Kiro CLI does **not** set `ANTHROPIC_API_KEY`. So under Kiro the classifier fell
12
+ back to a conservative regex that only fires on explicit phrasings
13
+ ("remember …", "always …", "I prefer …"). A natural statement like
14
+ "my favourite color is green" was examined and rejected
15
+ (`no rule detected in prompt` in `correction-detector.log`), and nothing was
16
+ stored. Worse, under enterprise MCP governance the `store_memory` tool is
17
+ dropped from the agent's toolset, so the agent itself can't store on demand
18
+ either — leaving regex as the *only* capture path.
19
+
20
+ Two non-answers:
21
+
22
+ - **"Just export `ANTHROPIC_API_KEY`."** Works, but forces the user to bring
23
+ their own Claude subscription/credits when Kiro already includes an LLM.
24
+ - **"Broaden the regex."** Trades one brittle heuristic for a slightly larger
25
+ brittle heuristic; still misses anything not phrased as a known trigger.
26
+
27
+ ## The finding
28
+
29
+ **`kiro-cli chat --no-interactive "<prompt>"` is a headless one-shot completion
30
+ that runs on Kiro's own model and Kiro's own authentication.** It is exactly the
31
+ subprocess-callable LLM I had wrongly assumed didn't exist. A background hook can
32
+ shell out to it to classify text, with no API key and no personal subscription.
33
+
34
+ Verified on Kiro CLI 2.7.0 (Kiro PRO):
35
+
36
+ | Property | Result |
37
+ | --- | --- |
38
+ | Auth | Uses the Kiro login. No `ANTHROPIC_API_KEY`. Consumes Kiro credits (~0.06 per call on Haiku). |
39
+ | Model | `--list-models` exposes `claude-haiku-4.5` (rate 0.4 — cheapest Claude), plus Sonnet/Opus tiers. |
40
+ | Classify **and** extract in one call | `{"store": true, "type": "preference", "value": "favourite color is green"}` |
41
+ | Latency (bare agent, warm) | ~2.9–3.4 s |
42
+ | Latency (default agent, cold) | ~16 s — dominated by loading the session's MCP servers and firing that agent's own hooks |
43
+ | Reachable from a minimal `PATH` | Yes — `~/.local/bin/kiro-cli` resolves |
44
+
45
+ ### Why a dedicated "bare" agent matters
46
+
47
+ Running the classify call through the *default* agent is slow and unsafe:
48
+
49
+ - It loads all of the session's MCP servers (the enterprise setup loads five),
50
+ adding ~5 s.
51
+ - It fires that agent's own `agentSpawn`/hook chain — including Claude Recall's
52
+ own hooks if they're wired — which risks **recursion** (a capture call that
53
+ triggers another capture call).
54
+
55
+ So `kiro setup` writes a minimal `claude-recall-classifier` agent to
56
+ `~/.kiro/agents/` with **no MCP servers, no hooks, and no tools**. The classify
57
+ call runs `--agent claude-recall-classifier`, which cold-boots in ~3 s and
58
+ cannot recurse.
59
+
60
+ ### Why capture must run asynchronously
61
+
62
+ Kiro enforces a per-hook timeout (we wire `userPromptSubmit` at 8 s). A cold
63
+ classify call can exceed that, which would kill the hook on the first — and most
64
+ important — prompt of a session. Even when it fits, blocking every turn on a
65
+ ~3 s LLM call is a poor experience for an interactive session.
66
+
67
+ So the `userPromptSubmit` hook does **not** classify inline. It spawns a
68
+ **detached worker** (the same pattern as `session-end-checkpoint`), pipes the
69
+ prompt to it over stdin, and returns in milliseconds. The worker performs the
70
+ ~3 s Kiro classify call and stores the memory in the background. The trade-off:
71
+ no synchronous "captured" echo, and a brief window where a memory stated in one
72
+ turn isn't queryable until the worker finishes (~3 s later) — acceptable for
73
+ capture.
74
+
75
+ ## The design
76
+
77
+ ```
78
+ Kiro userPromptSubmit
79
+
80
+
81
+ hook run kiro-capture (returns in <100 ms)
82
+ │ spawn detached, pipe payload via stdin
83
+
84
+ hook run kiro-capture-worker (background, survives parent exit)
85
+ │ sets CLAUDE_RECALL_KIRO_CLASSIFIER=1
86
+
87
+ handleCorrectionDetector → classifyContent()
88
+
89
+ ├─ classifyWithLLM() → ANTHROPIC_API_KEY (unset under Kiro) → null
90
+ ├─ classifyWithKiro() → kiro-cli chat --no-interactive --agent
91
+ │ claude-recall-classifier --model claude-haiku-4.5
92
+ │ → {"type","confidence","extract"}
93
+ └─ regex fallback (only if both above yield nothing)
94
+ ```
95
+
96
+ Capture precedence under Kiro becomes: **your own `ANTHROPIC_API_KEY` if you set
97
+ one → Kiro's headless LLM → regex as a last resort.** In the governance-locked,
98
+ no-key setup the Kiro LLM is the primary path and regex is just a safety net.
99
+
100
+ ### Output parsing
101
+
102
+ `kiro-cli` decorates stdout with a `> ` prompt marker, occasional ```` ```json ````
103
+ fences, and ANSI control sequences (the spinner and credits footer go to
104
+ stderr). `classifyWithKiro` strips ANSI, removes the marker and fences, extracts
105
+ the first balanced `{ … }` block, and `JSON.parse`s it. Any failure at any step
106
+ returns `null`, degrading to regex — a hook must never throw.
107
+
108
+ ### Configuration
109
+
110
+ | Variable | Default | Effect |
111
+ | --- | --- | --- |
112
+ | `CLAUDE_RECALL_KIRO_CLASSIFIER` | *(set by the worker)* | Enables the Kiro-LLM path in `classifyContent`. Set automatically by `kiro-capture-worker`; never needed by hand. |
113
+ | `CLAUDE_RECALL_KIRO_MODEL` | `claude-haiku-4.5` | Model for the classify call. Raise to `claude-sonnet-4.6` for steadier judgement at ~3× the credits. |
114
+ | `CLAUDE_RECALL_KIRO_LLM_TIMEOUT_MS` | `30000` | Hard cap on the headless call before the worker gives up and falls back to regex. |
115
+
116
+ ## Caveats
117
+
118
+ - **Consistency.** Haiku returned YES then NO on near-identical prompt wording
119
+ during testing. The classify prompt is written to be explicit, and
120
+ `CLAUDE_RECALL_KIRO_MODEL` lets you trade credits for a steadier model.
121
+ - **Cost.** Each classified prompt spends ~0.06 Kiro credits on Haiku. That is
122
+ the deliberate trade for using Kiro's LLM instead of a personal subscription.
123
+ - **Concurrency.** The worker opens a second, throwaway headless conversation
124
+ while the interactive session runs. They use different agents and no
125
+ `--resume`, so they're independent; SQLite WAL handles concurrent writes.
126
+ - **Requires `kiro-cli` on `PATH`.** If it's absent the classify call errors and
127
+ the worker falls back to regex — no crash.
128
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-recall",
3
- "version": "0.28.7",
3
+ "version": "0.29.0",
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": {