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 +9 -5
- package/dist/cli/commands/hook-commands.js +12 -0
- package/dist/cli/commands/kiro-commands.js +64 -4
- package/dist/hooks/kiro-classifier.js +160 -0
- package/dist/hooks/kiro-hooks.js +59 -0
- package/dist/hooks/shared.js +15 -2
- package/docs/kiro-llm-capture.md +128 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
124
|
+
The log should show a `scope [...] → project=your-project` line; `claude-recall kiro doctor` gives a fuller health report.
|
|
125
125
|
|
|
126
|
-
> **
|
|
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-
|
|
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).
|
|
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
|
-
//
|
|
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}
|
|
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: '
|
|
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
|
+
}
|
package/dist/hooks/kiro-hooks.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/hooks/shared.js
CHANGED
|
@@ -133,13 +133,26 @@ function classifyContentRegex(text) {
|
|
|
133
133
|
}
|
|
134
134
|
/**
|
|
135
135
|
* Classify text content — LLM-first, regex fallback.
|
|
136
|
-
*
|
|
137
|
-
*
|
|
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