claude-recall 0.30.2 → 0.31.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 +48 -8
- package/dist/cli/commands/hook-commands.js +13 -3
- package/dist/hooks/cc-capture.js +76 -0
- package/dist/hooks/cc-classifier.js +142 -0
- package/dist/hooks/kiro-classifier.js +22 -5
- package/dist/hooks/llm-classifier.js +39 -14
- package/dist/hooks/shared.js +54 -20
- package/docs/kiro-llm-capture.md +8 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,11 +14,13 @@ It works with **Claude Code**, **[Pi](https://github.com/mariozechner/pi)**, and
|
|
|
14
14
|
|
|
15
15
|
> **You:** use pnpm here, not npm
|
|
16
16
|
|
|
17
|
+
Seconds later, silently in the background (`~/.claude-recall/hook-logs/cc-classifier.log`):
|
|
18
|
+
|
|
17
19
|
```text
|
|
18
|
-
|
|
20
|
+
classified via claude -p (model=haiku, Claude subscription, no API key): correction — Use pnpm, not npm
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
That
|
|
23
|
+
That's the whole workflow. A background hook classified your prompt with an LLM — **the agent's own LLM** (your Claude subscription under Claude Code, Kiro's credits under Kiro; no API key involved) — decided it was a durable rule and stored it locally. No "remember this" incantation, no tool call, no config file to edit.
|
|
22
24
|
|
|
23
25
|
**Friday — brand-new session, no shared history:**
|
|
24
26
|
|
|
@@ -42,7 +44,7 @@ $ claude-recall search "pnpm"
|
|
|
42
44
|
|
|
43
45
|
## Features
|
|
44
46
|
|
|
45
|
-
- **Automatic capture** — an LLM classifier detects preferences, corrections, and project facts in your normal prompts (regex fallback when no LLM is available
|
|
47
|
+
- **Automatic capture** — an LLM classifier detects preferences, corrections, and project facts in your normal prompts, running on **the agent's own LLM** (Claude subscription / Kiro credits — never a separate API key), with regex fallback when no LLM is available
|
|
46
48
|
- **Applied where it counts** — rules load at session start *and* are re-surfaced just-in-time before each tool call
|
|
47
49
|
- **Project-scoped** — each project gets its own memory namespace; switch directories and the agent switches context
|
|
48
50
|
- **Learns from failures** — records what broke, why, and what fixed it, so mistakes aren't repeated
|
|
@@ -83,6 +85,13 @@ claude mcp add --scope user claude-recall -- claude-recall mcp start
|
|
|
83
85
|
|
|
84
86
|
Hook-based auto-capture remains a per-project opt-in via `claude-recall setup --install`.
|
|
85
87
|
|
|
88
|
+
**Capture runs on your Claude subscription — no API key.** The capture hook classifies each prompt via a headless `claude -p` call on the same login that powers your session, in a detached background worker (your turn is never blocked; capture is silent and lands a few seconds later). If you happen to have `ANTHROPIC_API_KEY` exported for other tools, it is deliberately **not** used unless you set `CLAUDE_RECALL_PREFER_API_KEY=1` — a stray key shouldn't quietly spend your Anthropic API credits. Verify captures any time:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
tail -5 ~/.claude-recall/hook-logs/cc-classifier.log # which model ran, what was stored
|
|
92
|
+
claude-recall search "something you said"
|
|
93
|
+
```
|
|
94
|
+
|
|
86
95
|
### Pi
|
|
87
96
|
|
|
88
97
|
```bash
|
|
@@ -106,9 +115,38 @@ Already living in a custom agent of your own? Merge Claude Recall into it instea
|
|
|
106
115
|
claude-recall kiro setup --merge-into <agent-name>
|
|
107
116
|
```
|
|
108
117
|
|
|
109
|
-
> **⚠️
|
|
118
|
+
> **⚠️ You must start ONE fresh conversation after setup — this is the most common reason "it does nothing."**
|
|
119
|
+
>
|
|
120
|
+
> After `kiro setup` or `--merge-into`, start **one new conversation without `--resume`**:
|
|
121
|
+
>
|
|
122
|
+
> ```bash
|
|
123
|
+
> kiro-cli chat --agent recall # or: --agent <your-agent> if you merged
|
|
124
|
+
> ```
|
|
125
|
+
>
|
|
126
|
+
> Kiro snapshots the agent config at the moment a conversation is *created*. Any conversation that already existed — including one you reach with `--resume` or after restarting Kiro — was snapshotted **before** claude-recall was wired in, so it will **never** run the hooks and capture will silently do nothing. Once you've started that one fresh conversation, every conversation from then on carries the hooks and `--resume` works normally. This is a one-time rollover, once per project.
|
|
127
|
+
>
|
|
128
|
+
> Confirm the wiring is live before you rely on it:
|
|
129
|
+
>
|
|
130
|
+
> ```bash
|
|
131
|
+
> claude-recall kiro doctor # green checks = hooks are active for this project
|
|
132
|
+
> ```
|
|
133
|
+
|
|
134
|
+
**Capture runs on Kiro's own LLM — no API key, no personal subscription.** It classifies each prompt with a dedicated fixed model (default `claude-haiku-4.5`, independent of your chat model) and costs **~0.06 Kiro credits per prompt** — cheap, but note it's *every* prompt, so budget accordingly across a team.
|
|
135
|
+
|
|
136
|
+
**What to expect from capture — read this so it doesn't feel broken:**
|
|
137
|
+
|
|
138
|
+
- **It's silent and asynchronous.** Capture runs in a background worker so it never blocks your turn — which also means there's **no "captured ✓" message** in the Kiro chat. Stating a preference and seeing nothing happen is normal.
|
|
139
|
+
- **It's a best-effort LLM judgement, not a guarantee.** The classifier decides what's a durable rule vs. chit-chat; it won't catch every phrasing, and near-identical wording can occasionally be judged differently. State preferences plainly ("use pnpm here, not npm") for the best hit rate.
|
|
140
|
+
- **There's a ~3s lag.** A preference you just stated isn't queryable for a couple of seconds while the worker finishes.
|
|
141
|
+
|
|
142
|
+
**To verify capture actually worked** — from a second terminal (Kiro's chat can't shell out):
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
claude-recall search "pnpm" # did the rule land?
|
|
146
|
+
tail -5 ~/.claude-recall/hook-logs/kiro-classifier.log # what the classifier decided, and which model ran
|
|
147
|
+
```
|
|
110
148
|
|
|
111
|
-
|
|
149
|
+
Or, from **inside the Kiro session**, just ask the agent to recall it (*"what do you remember about my package manager?"*) — it reads the same DB and will surface the stored rule if capture succeeded.
|
|
112
150
|
|
|
113
151
|
**Everything else Kiro** — MCP-only mode, project scoping and `--resume`, the classifier internals, enterprise-governance notes, troubleshooting: **[docs/kiro.md](docs/kiro.md)**.
|
|
114
152
|
|
|
@@ -131,7 +169,7 @@ Once installed, Claude Recall works in the background (CC = Claude Code):
|
|
|
131
169
|
| **Session exit** | An auto-checkpoint (`{completed, remaining, blockers}`) is saved for next time | ✓ | ✓ | |
|
|
132
170
|
| **End of session** | Failure patterns become candidate lessons; validated ones are promoted to rules | ✓ | ✓ | |
|
|
133
171
|
|
|
134
|
-
Classification
|
|
172
|
+
Classification runs on each runtime's **own** LLM — Claude Code via headless `claude -p` on your subscription; Kiro via `kiro-cli chat --no-interactive` on Kiro credits — with `ANTHROPIC_API_KEY` (if you exported one *and* opted in) and regex as fallbacks. No API key is ever required; no configuration needed.
|
|
135
173
|
|
|
136
174
|
```bash
|
|
137
175
|
# Verify it's working
|
|
@@ -349,9 +387,11 @@ Defaults work out of the box; tune via environment variables as needed.
|
|
|
349
387
|
| Variable | Default | Effect |
|
|
350
388
|
| ---------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
|
|
351
389
|
| `CLAUDE_RECALL_DB_PATH` | `~/.claude-recall/` | Database directory. |
|
|
352
|
-
| `ANTHROPIC_API_KEY` | _(unset)_ |
|
|
390
|
+
| `ANTHROPIC_API_KEY` | _(unset)_ | Optional personal API key for Haiku-based classification. **Never required and never provided by Claude Code** — capture uses each runtime's own LLM first (Claude subscription via `claude -p`; Kiro credits via `kiro-cli`). A key you exported is only consulted as a fallback, or first with `CLAUDE_RECALL_PREFER_API_KEY=1`. Regex is the final fallback. |
|
|
391
|
+
| `CLAUDE_RECALL_CC_MODEL` | `haiku` | Dedicated model for capture classification under Claude Code (passed to `claude -p --model`) — independent of your interactive session model. |
|
|
392
|
+
| `CLAUDE_RECALL_CC_LLM_TIMEOUT_MS` | `30000` | Hard cap on the headless `claude -p` classify call before the capture worker gives up and falls through. |
|
|
353
393
|
| `CLAUDE_RECALL_KIRO_MODEL` | `claude-haiku-4.5` | Dedicated model for Kiro-LLM capture classification — **independent of your interactive Kiro chat model**. Raise to `claude-sonnet-4.6` for steadier judgement at more credits. See [docs/kiro-llm-capture.md](docs/kiro-llm-capture.md). |
|
|
354
|
-
| `CLAUDE_RECALL_PREFER_API_KEY` | _(unset)_ |
|
|
394
|
+
| `CLAUDE_RECALL_PREFER_API_KEY` | _(unset)_ | Force `ANTHROPIC_API_KEY`-based classification ahead of the runtime's included LLM (uses your Anthropic API credits — e.g. for a model you pay for). Applies under both Claude Code and Kiro. |
|
|
355
395
|
| `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. |
|
|
356
396
|
| `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`. |
|
|
357
397
|
| `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>`. |
|
|
@@ -37,6 +37,7 @@ exports.HookCommands = void 0;
|
|
|
37
37
|
const shared_1 = require("../../hooks/shared");
|
|
38
38
|
const AVAILABLE_HOOKS = [
|
|
39
39
|
'correction-detector',
|
|
40
|
+
'cc-capture-worker',
|
|
40
41
|
'memory-stop',
|
|
41
42
|
'memory-sync',
|
|
42
43
|
'precompact-preserve',
|
|
@@ -70,8 +71,17 @@ class HookCommands {
|
|
|
70
71
|
static async runOne(name, input) {
|
|
71
72
|
switch (name) {
|
|
72
73
|
case 'correction-detector': {
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
// Kept as the registered name in every existing settings.json, but
|
|
75
|
+
// capture no longer classifies inline: the handler spawns a detached
|
|
76
|
+
// cc-capture-worker so the ~4s `claude -p` subscription classifier
|
|
77
|
+
// never blocks the user's turn. See src/hooks/cc-capture.ts.
|
|
78
|
+
const { handleCcCapture } = await Promise.resolve().then(() => __importStar(require('../../hooks/cc-capture')));
|
|
79
|
+
await handleCcCapture(input);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'cc-capture-worker': {
|
|
83
|
+
const { handleCcCaptureWorker } = await Promise.resolve().then(() => __importStar(require('../../hooks/cc-capture')));
|
|
84
|
+
await handleCcCaptureWorker(input);
|
|
75
85
|
break;
|
|
76
86
|
}
|
|
77
87
|
case 'memory-stop': {
|
|
@@ -141,7 +151,7 @@ class HookCommands {
|
|
|
141
151
|
break;
|
|
142
152
|
}
|
|
143
153
|
// Kiro CLI adapters — see src/hooks/kiro-hooks.ts. Kiro's
|
|
144
|
-
// userPromptSubmit
|
|
154
|
+
// userPromptSubmit wires `kiro-capture` (detached-worker capture).
|
|
145
155
|
case 'kiro-agent-spawn': {
|
|
146
156
|
const { handleKiroAgentSpawn } = await Promise.resolve().then(() => __importStar(require('../../hooks/kiro-hooks')));
|
|
147
157
|
await handleKiroAgentSpawn(input);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code capture — detached-worker pattern for UserPromptSubmit.
|
|
4
|
+
*
|
|
5
|
+
* Classification under Claude Code now prefers the user's SUBSCRIPTION (via
|
|
6
|
+
* headless `claude -p`, see cc-classifier.ts) over an ANTHROPIC_API_KEY. That
|
|
7
|
+
* call is ~4s cold — too slow to run inline while Claude Code blocks the
|
|
8
|
+
* user's prompt on the hook. So the registered hook (`hook run
|
|
9
|
+
* correction-detector`, unchanged in settings.json) spawns a DETACHED worker,
|
|
10
|
+
* pipes the payload to it over stdin, and returns in milliseconds — the same
|
|
11
|
+
* pattern as Kiro's kiro-capture (kiro-hooks.ts) and session-end-checkpoint.
|
|
12
|
+
*
|
|
13
|
+
* Trade-off: the synchronous "📌 Recall: auto-captured …" echo is gone —
|
|
14
|
+
* capture is silent and lands ~4s later. Verify via
|
|
15
|
+
* ~/.claude-recall/hook-logs/cc-classifier.log or `claude-recall search`.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.handleCcCapture = handleCcCapture;
|
|
19
|
+
exports.handleCcCaptureWorker = handleCcCaptureWorker;
|
|
20
|
+
const child_process_1 = require("child_process");
|
|
21
|
+
const shared_1 = require("./shared");
|
|
22
|
+
const correction_detector_1 = require("./correction-detector");
|
|
23
|
+
const HOOK_NAME = 'cc-capture';
|
|
24
|
+
/**
|
|
25
|
+
* The inline half — fires on UserPromptSubmit, spawns the worker, returns
|
|
26
|
+
* immediately. Pre-checks mirror correction-detector so we don't spawn a
|
|
27
|
+
* process (or spend subscription usage) on input that could never be stored.
|
|
28
|
+
*/
|
|
29
|
+
async function handleCcCapture(input) {
|
|
30
|
+
// Recursion guard: if we're already inside a classifier's nested headless
|
|
31
|
+
// session (claude -p or kiro-cli fired hooks of its own), do NOT spawn
|
|
32
|
+
// another worker — that would classify the classify prompt, forever.
|
|
33
|
+
if (process.env.CLAUDE_RECALL_CC_CLASSIFIER || process.env.CLAUDE_RECALL_KIRO_CLASSIFIER) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const prompt = input?.prompt ?? '';
|
|
37
|
+
if (prompt.length < 20 || prompt.length > 2000)
|
|
38
|
+
return;
|
|
39
|
+
if (prompt.startsWith('```') || prompt.startsWith('{'))
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
const cliPath = process.argv[1]; // absolute path to claude-recall-cli.js
|
|
43
|
+
const child = (0, child_process_1.spawn)(process.execPath, [cliPath, 'hook', 'run', 'cc-capture-worker'], { detached: true, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
44
|
+
child.on('error', (err) => {
|
|
45
|
+
(0, shared_1.hookLog)(HOOK_NAME, `capture worker spawn error: ${err?.message ?? err}`);
|
|
46
|
+
});
|
|
47
|
+
if (child.stdin) {
|
|
48
|
+
child.stdin.on('error', (err) => {
|
|
49
|
+
(0, shared_1.hookLog)(HOOK_NAME, `capture worker stdin error: ${err?.message ?? err}`);
|
|
50
|
+
});
|
|
51
|
+
child.stdin.write(JSON.stringify(input));
|
|
52
|
+
child.stdin.end();
|
|
53
|
+
}
|
|
54
|
+
child.unref();
|
|
55
|
+
(0, shared_1.hookLog)(HOOK_NAME, `capture: spawned detached worker (pid=${child.pid})`);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
(0, shared_1.hookLog)(HOOK_NAME, `capture spawn failed: ${(0, shared_1.safeErrorMessage)(err)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* cc-capture-worker — the background half. Enables the Claude-CLI classifier
|
|
63
|
+
* path (CLAUDE_RECALL_CC_CLASSIFIER) and delegates to the standard
|
|
64
|
+
* correction-detector, which classifies via `claude -p` (subscription) →
|
|
65
|
+
* ANTHROPIC_API_KEY → regex, and stores. Runs detached, so output goes
|
|
66
|
+
* nowhere and capture is silent.
|
|
67
|
+
*/
|
|
68
|
+
async function handleCcCaptureWorker(input) {
|
|
69
|
+
process.env.CLAUDE_RECALL_CC_CLASSIFIER = '1';
|
|
70
|
+
try {
|
|
71
|
+
await (0, correction_detector_1.handleCorrectionDetector)(input);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
(0, shared_1.hookLog)(HOOK_NAME, `capture worker error: ${(0, shared_1.safeErrorMessage)(err)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Claude-Code-LLM memory classifier.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors kiro-classifier for the Claude Code runtime: `claude -p "<prompt>"`
|
|
6
|
+
* is a headless one-shot completion that runs on the user's Claude Code
|
|
7
|
+
* SUBSCRIPTION auth (the same login powering their interactive session) — no
|
|
8
|
+
* ANTHROPIC_API_KEY, no separate pay-as-you-go credits. This makes the "each
|
|
9
|
+
* runtime brings its own LLM" promise true under Claude Code, not just Kiro.
|
|
10
|
+
*
|
|
11
|
+
* Two hard-won details:
|
|
12
|
+
*
|
|
13
|
+
* 1. `claude -p` prefers ANTHROPIC_API_KEY over subscription auth when the
|
|
14
|
+
* key is present in the environment. A stray key exported for other tools
|
|
15
|
+
* (possibly dead or out of credits) would silently hijack the call — so
|
|
16
|
+
* the key is STRIPPED from the child env to force subscription auth.
|
|
17
|
+
* 2. The child inherits CLAUDE_RECALL_CC_CLASSIFIER=1 (set by the capture
|
|
18
|
+
* worker), so if the nested headless session fires claude-recall hooks of
|
|
19
|
+
* its own, handleCcCapture sees the flag and refuses to spawn another
|
|
20
|
+
* worker — no recursion. The call also runs from the OS temp dir so the
|
|
21
|
+
* current project's hooks/settings don't load at all.
|
|
22
|
+
*
|
|
23
|
+
* Cold latency is ~4s, so this runs only from a DETACHED worker
|
|
24
|
+
* (cc-capture-worker), never inline on the user's turn.
|
|
25
|
+
*/
|
|
26
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
29
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
30
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
31
|
+
}
|
|
32
|
+
Object.defineProperty(o, k2, desc);
|
|
33
|
+
}) : (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
o[k2] = m[k];
|
|
36
|
+
}));
|
|
37
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
38
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
39
|
+
}) : function(o, v) {
|
|
40
|
+
o["default"] = v;
|
|
41
|
+
});
|
|
42
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
43
|
+
var ownKeys = function(o) {
|
|
44
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
45
|
+
var ar = [];
|
|
46
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
47
|
+
return ar;
|
|
48
|
+
};
|
|
49
|
+
return ownKeys(o);
|
|
50
|
+
};
|
|
51
|
+
return function (mod) {
|
|
52
|
+
if (mod && mod.__esModule) return mod;
|
|
53
|
+
var result = {};
|
|
54
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
55
|
+
__setModuleDefault(result, mod);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.classifyWithClaudeCli = classifyWithClaudeCli;
|
|
61
|
+
const child_process_1 = require("child_process");
|
|
62
|
+
const os = __importStar(require("os"));
|
|
63
|
+
const shared_1 = require("./shared");
|
|
64
|
+
const kiro_classifier_1 = require("./kiro-classifier");
|
|
65
|
+
const DEFAULT_MODEL = 'haiku';
|
|
66
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
67
|
+
/**
|
|
68
|
+
* Classify a prompt by invoking Claude Code's headless mode. Returns null on
|
|
69
|
+
* any failure (claude not on PATH, timeout, non-zero exit, unparseable output)
|
|
70
|
+
* so the caller falls back to the next backend. Never throws.
|
|
71
|
+
*/
|
|
72
|
+
function classifyWithClaudeCli(text) {
|
|
73
|
+
const model = process.env.CLAUDE_RECALL_CC_MODEL || DEFAULT_MODEL;
|
|
74
|
+
const timeoutMs = parseInt(process.env.CLAUDE_RECALL_CC_LLM_TIMEOUT_MS || '', 10);
|
|
75
|
+
const timeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
76
|
+
// Force subscription auth: with ANTHROPIC_API_KEY set, `claude -p` bills the
|
|
77
|
+
// key instead of the login — the exact behavior this classifier exists to
|
|
78
|
+
// avoid. CLAUDE_RECALL_CC_CLASSIFIER rides along as the recursion guard.
|
|
79
|
+
const env = { ...process.env, CLAUDE_RECALL_CC_CLASSIFIER: '1' };
|
|
80
|
+
delete env.ANTHROPIC_API_KEY;
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
let settled = false;
|
|
83
|
+
const done = (result) => {
|
|
84
|
+
if (settled)
|
|
85
|
+
return;
|
|
86
|
+
settled = true;
|
|
87
|
+
resolve(result);
|
|
88
|
+
};
|
|
89
|
+
let child;
|
|
90
|
+
try {
|
|
91
|
+
// args array (no shell) — the user's text is a single argv entry, so no
|
|
92
|
+
// shell escaping or injection is possible. cwd is the temp dir so the
|
|
93
|
+
// nested session loads no project settings or hooks.
|
|
94
|
+
child = (0, child_process_1.spawn)('claude', ['-p', '--model', model, (0, kiro_classifier_1.buildClassifyPrompt)(text)], { cwd: os.tmpdir(), env, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
(0, shared_1.hookLog)('cc-classifier', `spawn threw: ${(0, shared_1.safeErrorMessage)(err)}`);
|
|
98
|
+
return done(null);
|
|
99
|
+
}
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
(0, shared_1.hookLog)('cc-classifier', `timeout after ${timeout}ms — killing claude -p`);
|
|
102
|
+
try {
|
|
103
|
+
child.kill('SIGKILL');
|
|
104
|
+
}
|
|
105
|
+
catch { /* already gone */ }
|
|
106
|
+
done(null);
|
|
107
|
+
}, timeout);
|
|
108
|
+
let stdout = '';
|
|
109
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
110
|
+
child.on('error', (err) => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
// ENOENT = claude not on PATH; anything else = spawn failure
|
|
113
|
+
(0, shared_1.hookLog)('cc-classifier', `claude error: ${err?.code ?? ''} ${err?.message ?? err}`);
|
|
114
|
+
done(null);
|
|
115
|
+
});
|
|
116
|
+
child.on('close', (code) => {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
if (settled)
|
|
119
|
+
return;
|
|
120
|
+
if (code !== 0) {
|
|
121
|
+
(0, shared_1.hookLog)('cc-classifier', `claude -p exited ${code}`);
|
|
122
|
+
return done(null);
|
|
123
|
+
}
|
|
124
|
+
const result = (0, kiro_classifier_1.extractClassification)(stdout);
|
|
125
|
+
if (!result) {
|
|
126
|
+
// Distinguish a deliberate "none" verdict from unparseable output —
|
|
127
|
+
// "the model said not a rule" and "the call broke" are different
|
|
128
|
+
// diagnoses when reading the log.
|
|
129
|
+
(0, shared_1.hookLog)('cc-classifier', /"type"\s*:\s*"none"/.test(stdout)
|
|
130
|
+
? 'classified as none (not a durable rule)'
|
|
131
|
+
: 'no parseable classification in claude -p output');
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// Same observability contract as kiro-classifier: spell out which
|
|
135
|
+
// backend ran and on whose account, so "which LLM classified this?"
|
|
136
|
+
// is always answerable from the log.
|
|
137
|
+
(0, shared_1.hookLog)('cc-classifier', `classified via claude -p (model=${model}, Claude subscription, no API key): ${result.type} — ${result.extract.slice(0, 60)}`);
|
|
138
|
+
}
|
|
139
|
+
done(result);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.CLASSIFIER_AGENT = void 0;
|
|
19
|
+
exports.buildClassifyPrompt = buildClassifyPrompt;
|
|
19
20
|
exports.extractClassification = extractClassification;
|
|
20
21
|
exports.classifyWithKiro = classifyWithKiro;
|
|
21
22
|
const child_process_1 = require("child_process");
|
|
@@ -36,21 +37,32 @@ const VALID_TYPES = new Set([
|
|
|
36
37
|
* single INPUT argument (no separate system prompt), so instruction + payload
|
|
37
38
|
* are combined. Mirrors the contract of llm-classifier's SYSTEM_PROMPT so
|
|
38
39
|
* downstream thresholds/dedup behave identically regardless of which LLM ran.
|
|
40
|
+
*
|
|
41
|
+
* Exported for cc-classifier: `claude -p` is the same single-argument headless
|
|
42
|
+
* shape, so both CLI backends share one prompt (and one parser, below).
|
|
39
43
|
*/
|
|
40
|
-
function
|
|
44
|
+
function buildClassifyPrompt(text) {
|
|
41
45
|
return ('You are a memory classifier for a developer tool. Classify the USER MESSAGE ' +
|
|
42
46
|
'into exactly one type and respond with ONLY minified JSON — no markdown, no ' +
|
|
43
47
|
'prose, no code fence:\n' +
|
|
44
48
|
'{"type":"correction|preference|failure|devops|project-knowledge|none","confidence":0.0-1.0,"extract":"<concise imperative rule to remember, or empty>"}\n\n' +
|
|
45
49
|
'Types:\n' +
|
|
46
|
-
'- correction: user correcting
|
|
50
|
+
'- correction: user durably correcting how something should ALWAYS be done ("no, use X not Y"). Correcting a one-off action in the current task is NOT durable.\n' +
|
|
47
51
|
'- 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
52
|
'- failure: something broke ("build failed")\n' +
|
|
49
53
|
'- devops: durable CI/CD, git, deployment, or Docker rules\n' +
|
|
50
54
|
'- 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' +
|
|
55
|
+
'- none: questions, chitchat, task instructions, meta-conversation, sentence fragments, observations, or anything not worth remembering across sessions\n\n' +
|
|
56
|
+
'THE STANDALONE TEST — apply before choosing correction/preference/devops: a durable rule must make complete sense on its own, read cold in a future session with no knowledge of this chat. If the message needs the surrounding conversation to be understood, use "none".\n' +
|
|
57
|
+
'- Reject text referencing the current moment with no standalone subject ("this", "that", "the sentence", "here", "it", "first", "now").\n' +
|
|
58
|
+
'- Reject one-off imperatives aimed at the task in progress ("fix the sentence", "change that", "do X first") even when forceful.\n' +
|
|
59
|
+
'- Reject sentence fragments and mid-thought clauses ("is used in practice", "then run the setup command").\n' +
|
|
60
|
+
'- Reject meta-conversation about this tool or what to write/do next.\n\n' +
|
|
52
61
|
'Be conservative: when in doubt use "none" with confidence 0. Use confidence >= 0.75 for correction/preference/devops. ' +
|
|
53
62
|
'extract must be a clean standalone rule (e.g. "Favourite colour is green"), or empty when type is none.\n\n' +
|
|
63
|
+
'Examples: "we use pnpm, not npm" → {"type":"preference","confidence":0.9,"extract":"Use pnpm, not npm"}; ' +
|
|
64
|
+
'"first fix the sentence" → {"type":"none","confidence":0,"extract":""}; ' +
|
|
65
|
+
'"then run claude-recall kiro setup" → {"type":"none","confidence":0,"extract":""}.\n\n' +
|
|
54
66
|
'USER MESSAGE: ' + text);
|
|
55
67
|
}
|
|
56
68
|
/** Strip ANSI/VT control sequences that kiro-cli writes around its output. */
|
|
@@ -119,7 +131,7 @@ function classifyWithKiro(text) {
|
|
|
119
131
|
'--no-interactive',
|
|
120
132
|
'--agent', exports.CLASSIFIER_AGENT,
|
|
121
133
|
'--model', model,
|
|
122
|
-
|
|
134
|
+
buildClassifyPrompt(text),
|
|
123
135
|
], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
124
136
|
}
|
|
125
137
|
catch (err) {
|
|
@@ -152,7 +164,12 @@ function classifyWithKiro(text) {
|
|
|
152
164
|
}
|
|
153
165
|
const result = extractClassification(stdout);
|
|
154
166
|
if (!result) {
|
|
155
|
-
|
|
167
|
+
// Distinguish a deliberate "none" verdict from unparseable output —
|
|
168
|
+
// "the model said not a rule" and "the call broke" are different
|
|
169
|
+
// diagnoses when reading the log.
|
|
170
|
+
(0, shared_1.hookLog)('kiro-classifier', /"type"\s*:\s*"none"/.test(stdout)
|
|
171
|
+
? 'classified as none (not a durable rule)'
|
|
172
|
+
: 'no parseable classification in kiro-cli output');
|
|
156
173
|
}
|
|
157
174
|
else {
|
|
158
175
|
// Log success too, not just failures — otherwise a successful Kiro
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* LLM-powered memory classification using Claude Haiku.
|
|
3
|
+
* LLM-powered memory classification using Claude Haiku via the Anthropic API.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Requires ANTHROPIC_API_KEY in the environment — a personal pay-as-you-go
|
|
6
|
+
* API key the user exported themselves. Claude Code does NOT mint one from
|
|
7
|
+
* the user's subscription (hooks just inherit the user's environment), which
|
|
8
|
+
* is why this is a FALLBACK backend: the capture workers prefer each
|
|
9
|
+
* runtime's included LLM (claude -p / kiro-cli — see cc-classifier.ts and
|
|
10
|
+
* kiro-classifier.ts) and only land here when a key is present. Falls back
|
|
11
|
+
* gracefully (returns null) when unavailable.
|
|
7
12
|
*/
|
|
8
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
14
|
exports.classifyWithLLM = classifyWithLLM;
|
|
@@ -16,45 +21,65 @@ let clientInstance; // undefined = not yet checked
|
|
|
16
21
|
const MODEL = 'claude-haiku-4-5-20251001';
|
|
17
22
|
const SYSTEM_PROMPT = `You are a memory classifier for a developer tool. Classify user text into one of these types:
|
|
18
23
|
|
|
19
|
-
- correction: User correcting
|
|
20
|
-
- preference: User stating a clear, reusable directive about how they want things done going forward ("we use tabs", "always use TypeScript", "I prefer X"). Must be a rule that applies beyond this conversation.
|
|
24
|
+
- correction: User durably correcting how something should ALWAYS be done ("no, use X not Y", "wrong, it should be..."). Correcting a one-off action in the current task is NOT durable — only store it if the rule generalizes beyond this moment.
|
|
25
|
+
- preference: User stating a clear, reusable directive about how they want things done going forward ("we use tabs", "always use TypeScript", "I prefer X"). Must be a rule that applies beyond this conversation.
|
|
21
26
|
- failure: Something broke or failed ("build failed", "error in deployment")
|
|
22
27
|
- devops: CI/CD, deployment, Docker, git workflow patterns. ONLY durable rules — not transient operational events like "sandbox rebuilt" or "background task exited"
|
|
23
28
|
- project-knowledge: Architecture, stack, database, API patterns
|
|
24
|
-
- none: Casual conversation, questions, code snippets, or anything not worth remembering
|
|
29
|
+
- none: Casual conversation, questions, code snippets, one-off task instructions, meta-conversation, sentence fragments, or anything not worth remembering across sessions
|
|
25
30
|
|
|
26
31
|
Respond with ONLY valid JSON (no markdown fences). Format:
|
|
27
32
|
{"type":"<type>","confidence":<0.0-1.0>,"extract":"<the key fact to remember, concise>"}
|
|
28
33
|
|
|
34
|
+
THE STANDALONE TEST — apply this BEFORE classifying anything as correction/preference/devops:
|
|
35
|
+
A durable rule must make complete sense on its own, read cold in a future session with NO knowledge of this conversation. If the text needs the surrounding chat to be understood, it is "none".
|
|
36
|
+
- Reject text that references the current task or moment with no concrete standalone subject: "this", "that", "the sentence", "here", "it", "again", "first", "now".
|
|
37
|
+
- Reject one-off imperatives aimed at the task in progress ("fix the sentence", "change that", "add a line", "do X first") — even when phrased forcefully. A preference describes what the user ALWAYS wants, not what to do right now.
|
|
38
|
+
- Reject sentence fragments and mid-thought clauses (e.g. "is used in practice", "then run the setup command"). A rule is a complete directive, not a snippet.
|
|
39
|
+
- Reject meta-conversation about this tool, this chat, or what to write/say/do next.
|
|
40
|
+
|
|
29
41
|
Rules:
|
|
30
42
|
- Be very conservative — when in doubt, classify as "none". Only store things worth remembering across sessions
|
|
31
43
|
- Questions, observations, complaints, task instructions, and sentence fragments are "none" — not preferences
|
|
32
|
-
- If the text doesn't make sense as a standalone rule or directive, classify as "none"
|
|
33
44
|
- confidence >= 0.7 for corrections and preferences
|
|
34
45
|
- confidence >= 0.6 for other types
|
|
35
46
|
- "none" type should have confidence 0.0
|
|
36
|
-
- extract should be a clean, imperative statement of the rule/fact (e.g. "Use tabs for indentation")
|
|
37
|
-
- If the text is a question, greeting, or code block, classify as "none"
|
|
47
|
+
- extract should be a clean, imperative statement of the rule/fact, understandable standalone (e.g. "Use tabs for indentation")
|
|
48
|
+
- If the text is a question, greeting, or code block, classify as "none"
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
- "we use pnpm here, not npm" → {"type":"preference","confidence":0.9,"extract":"Use pnpm, not npm"}
|
|
52
|
+
- "always run tests before pushing" → {"type":"preference","confidence":0.85,"extract":"Run tests before pushing"}
|
|
53
|
+
- "first fix the sentence" → {"type":"none","confidence":0.0,"extract":""}
|
|
54
|
+
- "is used in practice" → {"type":"none","confidence":0.0,"extract":""}
|
|
55
|
+
- "then run claude-recall kiro setup" → {"type":"none","confidence":0.0,"extract":""}
|
|
56
|
+
- "what memories do you have?" → {"type":"none","confidence":0.0,"extract":""}`;
|
|
38
57
|
const BATCH_SYSTEM_PROMPT = `You are a memory classifier for a developer tool. You will receive a JSON array of texts. Classify each into one of these types:
|
|
39
58
|
|
|
40
|
-
- correction: User correcting
|
|
41
|
-
- preference: User stating a clear, reusable directive about how they want things done going forward ("we use tabs", "always use TypeScript", "I prefer X"). Must be a rule that applies beyond this conversation.
|
|
59
|
+
- correction: User durably correcting how something should ALWAYS be done ("no, use X not Y", "wrong, it should be..."). Correcting a one-off action in the current task is NOT durable — only store it if the rule generalizes beyond this moment.
|
|
60
|
+
- preference: User stating a clear, reusable directive about how they want things done going forward ("we use tabs", "always use TypeScript", "I prefer X"). Must be a rule that applies beyond this conversation.
|
|
42
61
|
- failure: Something broke or failed ("build failed", "error in deployment")
|
|
43
62
|
- devops: CI/CD, deployment, Docker, git workflow patterns. ONLY durable rules — not transient operational events like "sandbox rebuilt" or "background task exited"
|
|
44
63
|
- project-knowledge: Architecture, stack, database, API patterns
|
|
45
|
-
- none: Casual conversation, questions, code snippets, or anything not worth remembering
|
|
64
|
+
- none: Casual conversation, questions, code snippets, one-off task instructions, meta-conversation, sentence fragments, or anything not worth remembering across sessions
|
|
46
65
|
|
|
47
66
|
Respond with ONLY a valid JSON array (no markdown fences). One object per input text, in order:
|
|
48
67
|
[{"type":"<type>","confidence":<0.0-1.0>,"extract":"<the key fact to remember, concise>"}, ...]
|
|
49
68
|
|
|
69
|
+
THE STANDALONE TEST — apply this BEFORE classifying anything as correction/preference/devops:
|
|
70
|
+
A durable rule must make complete sense on its own, read cold in a future session with NO knowledge of this conversation. If a text needs the surrounding chat to be understood, it is "none".
|
|
71
|
+
- Reject text that references the current task or moment with no concrete standalone subject: "this", "that", "the sentence", "here", "it", "again", "first", "now".
|
|
72
|
+
- Reject one-off imperatives aimed at the task in progress ("fix the sentence", "change that", "add a line", "do X first") — even when phrased forcefully.
|
|
73
|
+
- Reject sentence fragments and mid-thought clauses (e.g. "is used in practice", "then run the setup command").
|
|
74
|
+
- Reject meta-conversation about this tool, this chat, or what to write/say/do next.
|
|
75
|
+
|
|
50
76
|
Rules:
|
|
51
77
|
- Be very conservative — when in doubt, classify as "none". Only store things worth remembering across sessions
|
|
52
78
|
- Questions, observations, complaints, task instructions, and sentence fragments are "none" — not preferences
|
|
53
|
-
- If the text doesn't make sense as a standalone rule or directive, classify as "none"
|
|
54
79
|
- confidence >= 0.7 for corrections and preferences
|
|
55
80
|
- confidence >= 0.6 for other types
|
|
56
81
|
- "none" type should have confidence 0.0
|
|
57
|
-
- extract should be a clean, imperative statement of the rule/fact
|
|
82
|
+
- extract should be a clean, imperative statement of the rule/fact, understandable standalone
|
|
58
83
|
- If a text is a question, greeting, or code block, classify as "none"`;
|
|
59
84
|
/**
|
|
60
85
|
* Lazy-init the Anthropic client. Returns null if SDK or API key unavailable.
|
package/dist/hooks/shared.js
CHANGED
|
@@ -67,9 +67,16 @@ const llm_classifier_1 = require("./llm-classifier");
|
|
|
67
67
|
// "I want", "I use" — were deleted rather than promoted because they match
|
|
68
68
|
// ordinary conversation far too often.)
|
|
69
69
|
const CORRECTION_PATTERNS = [
|
|
70
|
-
|
|
70
|
+
// "no ..." only counts as a correction when the remainder carries a durable-
|
|
71
|
+
// rule signal (use/not/instead/always/...). Without the lookahead, one-off
|
|
72
|
+
// task imperatives were stored: "no first fix the sentence" became the
|
|
73
|
+
// correction "first fix the sentence".
|
|
74
|
+
{ regex: /^no[,.]?\s+(?=.*\b(?:use|not|instead|never|always|don'?t|stop|should|must)\b)(.+)/i, confidence: 0.8 },
|
|
71
75
|
{ regex: /^wrong[,.]?\s+(.+)/i, confidence: 0.8 },
|
|
72
|
-
|
|
76
|
+
// "never" must start the message or a sentence. Mid-clause "never" fired on
|
|
77
|
+
// QUOTED text the user was discussing: pasting "it never touches your token
|
|
78
|
+
// allowance" stored "touches your token allowance..." as a correction.
|
|
79
|
+
{ regex: /(?:^|[.!?;:]\s+)never\s+(.+)/i, confidence: 0.75 },
|
|
73
80
|
{ regex: /\bdon'?t\s+ever\s+(.+)/i, confidence: 0.8 },
|
|
74
81
|
{ regex: /\bstop\s+(doing|using|adding)\s+(.+)/i, confidence: 0.75 },
|
|
75
82
|
];
|
|
@@ -80,7 +87,16 @@ const PREFERENCE_PATTERNS = [
|
|
|
80
87
|
// no-MCP) capture path, which matters under enterprise Kiro governance that
|
|
81
88
|
// blocks the MCP tools. The interrogative/question-mark guards below keep
|
|
82
89
|
// "do you remember that config file?" / "do you recall X?" out.
|
|
83
|
-
|
|
90
|
+
//
|
|
91
|
+
// Two hazards from talking ABOUT this tool (both observed in the wild):
|
|
92
|
+
// 1. The product name: "claude-recall" / "claude recall" contains the
|
|
93
|
+
// trigger word — "how claude recall is used" stored "is used...". The
|
|
94
|
+
// lookbehind skips "recall" when preceded by "claude-"/"claude ".
|
|
95
|
+
// 2. Non-imperative uses where "recall" is a noun/subject followed by a
|
|
96
|
+
// copula or auxiliary ("the recall is broken") — an imperative
|
|
97
|
+
// "remember/recall X" is never followed by a bare auxiliary, so the
|
|
98
|
+
// negative lookahead rejects those.
|
|
99
|
+
{ regex: /(?<!claude[- ])\b(?:remember|recall)\s+(?:that\s+|this\s+|to\s+)?(?!(?:is|was|are|were|has|have|had|does|did|will|would|can|could|should|may|might)\b)(.+)/i, confidence: 0.8 },
|
|
84
100
|
{ regex: /\bfrom\s+now\s+on[,.]?\s+(.+)/i, confidence: 0.8 },
|
|
85
101
|
{ regex: /\bgoing\s+forward[,.]?\s+(.+)/i, confidence: 0.8 },
|
|
86
102
|
{ regex: /\balways\s+(.+)/i, confidence: 0.75 },
|
|
@@ -146,15 +162,22 @@ function classifyContentRegex(text) {
|
|
|
146
162
|
* Classify text content — LLM-first, regex fallback. No API key is ever
|
|
147
163
|
* required: each runtime brings its own LLM.
|
|
148
164
|
*
|
|
149
|
-
* Precedence
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* - Under
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
165
|
+
* Precedence (the included-LLM backends run only inside the detached capture
|
|
166
|
+
* workers, which set the *_CLASSIFIER env vars — a ~4s CLI call can't run
|
|
167
|
+
* inline on a turn):
|
|
168
|
+
* - Under Claude Code (CLAUDE_RECALL_CC_CLASSIFIER set by cc-capture-worker):
|
|
169
|
+
* headless `claude -p` on the user's Claude SUBSCRIPTION → ANTHROPIC_API_KEY
|
|
170
|
+
* if present → regex.
|
|
171
|
+
* - Under Kiro (CLAUDE_RECALL_KIRO_CLASSIFIER set by kiro-capture-worker):
|
|
172
|
+
* Kiro's own headless LLM (`kiro-cli chat --no-interactive`, Kiro credits)
|
|
173
|
+
* → ANTHROPIC_API_KEY if present → regex.
|
|
174
|
+
* - Inline callers (memory-stop batch, etc.): ANTHROPIC_API_KEY → regex.
|
|
175
|
+
*
|
|
176
|
+
* The included LLM is preferred so a stray exported key doesn't silently
|
|
177
|
+
* spend the user's Anthropic API credits; CLAUDE_RECALL_PREFER_API_KEY flips
|
|
178
|
+
* the order. NOTE: ANTHROPIC_API_KEY is always a personal key the user
|
|
179
|
+
* exported — Claude Code does NOT provide one from the subscription.
|
|
180
|
+
* See docs/kiro-llm-capture.md.
|
|
158
181
|
*/
|
|
159
182
|
async function classifyContent(text) {
|
|
160
183
|
// Guard the LLM paths the same way the regex path is guarded: a question or
|
|
@@ -172,17 +195,22 @@ async function classifyContent(text) {
|
|
|
172
195
|
}
|
|
173
196
|
async function classifyContentInner(text) {
|
|
174
197
|
const underKiro = !!process.env.CLAUDE_RECALL_KIRO_CLASSIFIER;
|
|
198
|
+
const underCc = !!process.env.CLAUDE_RECALL_CC_CLASSIFIER;
|
|
175
199
|
const preferApiKey = !!process.env.CLAUDE_RECALL_PREFER_API_KEY;
|
|
176
200
|
const tryApiKey = () => (0, llm_classifier_1.classifyWithLLM)(text);
|
|
177
|
-
// Dynamic
|
|
178
|
-
// graph for
|
|
201
|
+
// Dynamic imports keep the CLI classifiers (and child_process) out of the
|
|
202
|
+
// module graph for hook invocations that don't run in a capture worker.
|
|
179
203
|
const tryKiro = async () => (await Promise.resolve().then(() => __importStar(require('./kiro-classifier')))).classifyWithKiro(text);
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
204
|
+
const tryCc = async () => (await Promise.resolve().then(() => __importStar(require('./cc-classifier')))).classifyWithClaudeCli(text);
|
|
205
|
+
// Order the LLM backends. Each runtime's INCLUDED LLM comes before a stray
|
|
206
|
+
// ANTHROPIC_API_KEY: a key exported for other tools should not silently
|
|
207
|
+
// spend the user's Anthropic API credits when the runtime already provides
|
|
208
|
+
// an LLM — Kiro via `kiro-cli chat --no-interactive` (Kiro credits), Claude
|
|
209
|
+
// Code via `claude -p` (the user's Claude subscription). Set
|
|
210
|
+
// CLAUDE_RECALL_PREFER_API_KEY to force the key first (e.g. to use a
|
|
211
|
+
// stronger model you pay for). The CLI backends only run inside their
|
|
212
|
+
// detached capture workers (which set the *_CLASSIFIER env vars) — inline
|
|
213
|
+
// hook paths stay key → regex, since a ~4s CLI call can't block a turn.
|
|
186
214
|
const backends = [];
|
|
187
215
|
if (underKiro && !preferApiKey) {
|
|
188
216
|
backends.push(tryKiro, tryApiKey);
|
|
@@ -190,6 +218,12 @@ async function classifyContentInner(text) {
|
|
|
190
218
|
else if (underKiro) {
|
|
191
219
|
backends.push(tryApiKey, tryKiro);
|
|
192
220
|
}
|
|
221
|
+
else if (underCc && !preferApiKey) {
|
|
222
|
+
backends.push(tryCc, tryApiKey);
|
|
223
|
+
}
|
|
224
|
+
else if (underCc) {
|
|
225
|
+
backends.push(tryApiKey, tryCc);
|
|
226
|
+
}
|
|
193
227
|
else {
|
|
194
228
|
backends.push(tryApiKey);
|
|
195
229
|
}
|
package/docs/kiro-llm-capture.md
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
Claude Recall captures memories with an LLM classifier: it reads each user
|
|
6
6
|
prompt and decides whether it contains a durable preference, correction, or
|
|
7
|
-
project fact worth storing.
|
|
8
|
-
`ANTHROPIC_API_KEY
|
|
9
|
-
|
|
7
|
+
project fact worth storing. At the time of this finding, that classifier called
|
|
8
|
+
Claude Haiku through `ANTHROPIC_API_KEY` — a personal API key the user had
|
|
9
|
+
exported themselves. (Claude Code does **not** mint a key from the user's
|
|
10
|
+
subscription; hooks merely inherit the environment. The headless-CLI approach
|
|
11
|
+
documented here was later applied back to Claude Code via `claude -p` on
|
|
12
|
+
subscription auth — see `src/hooks/cc-classifier.ts`.)
|
|
10
13
|
|
|
11
14
|
Kiro CLI does **not** set `ANTHROPIC_API_KEY`. So under Kiro the classifier fell
|
|
12
15
|
back to a conservative regex that only fires on explicit phrasings
|
|
@@ -98,8 +101,8 @@ present → regex as a last resort.** The Kiro LLM comes first *even when a key
|
|
|
98
101
|
set*, so a key exported for other tools never silently spends the user's
|
|
99
102
|
Anthropic credits — Kiro already ships an LLM. `CLAUDE_RECALL_PREFER_API_KEY=1`
|
|
100
103
|
flips the order back to key-first for anyone who deliberately wants to pay for a
|
|
101
|
-
stronger model. (Under Claude Code
|
|
102
|
-
the
|
|
104
|
+
stronger model. (Under Claude Code the same pattern applies with `claude -p` on
|
|
105
|
+
the user's subscription as the included LLM — see `src/hooks/cc-classifier.ts`.)
|
|
103
106
|
|
|
104
107
|
### Output parsing
|
|
105
108
|
|
package/package.json
CHANGED