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 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
- 📌 Recall: auto-captured correction — Use pnpm, not npm
20
+ classified via claude -p (model=haiku, Claude subscription, no API key): correction — Use pnpm, not npm
19
21
  ```
20
22
 
21
- That one line did everything. A hook classified your prompt with an LLM, decided it was a durable rule (not chit-chat), and stored it locally. No "remember this" incantation, no tool call, no config file to edit.
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
- > **⚠️ One-time rollover:** after `kiro setup` or `--merge-into`, start **one fresh conversation** (no `--resume`). Kiro snapshots the agent config when a conversation is *created*, so older conversations never see the new hooks even resumed or after a restart. After that one fresh start, `--resume` works normally.
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
- Capture under Kiro runs on **Kiro's own LLM** no `ANTHROPIC_API_KEY`, no personal Anthropic subscription. It uses a dedicated fixed classifier model (default `claude-haiku-4.5`, independent of your chat model), costs ~0.06 Kiro credits per prompt, and never blocks your turn. Verify with `claude-recall kiro doctor`.
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 uses an LLM wherever one is available — Claude Code provides `ANTHROPIC_API_KEY` to its hooks; Kiro uses its own included LLM — with silent regex fallback. No configuration needed.
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)_ | LLM classification via Haiku. Not required Claude Code provides it to its hooks, and under Kiro the included LLM is used instead (and is preferred even if this is set). Regex is the final fallback. |
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)_ | Under Kiro, force `ANTHROPIC_API_KEY`-based classification ahead of Kiro's included LLM (uses your Anthropic credits — e.g. for a stronger model). No effect under Claude Code. |
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
- const { handleCorrectionDetector } = await Promise.resolve().then(() => __importStar(require('../../hooks/correction-detector')));
74
- await handleCorrectionDetector(input);
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 needs no adapter: wire `correction-detector` directly.
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 buildPrompt(text) {
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 a mistake ("no, use X not Y")\n' +
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
- buildPrompt(text),
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
- (0, shared_1.hookLog)('kiro-classifier', 'no parseable classification in kiro-cli output');
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
- * Picks up ANTHROPIC_API_KEY from the environment (Claude Code sets it
6
- * automatically for child processes). Falls back gracefully when unavailable.
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 a mistake ("no, use X not Y", "wrong, it should be...")
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. NOT observations, complaints, questions, debugging statements, or one-off instructions like "fix this" or "tell me about X"
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 a mistake ("no, use X not Y", "wrong, it should be...")
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. NOT observations, complaints, questions, debugging statements, or one-off instructions like "fix this" or "tell me about X"
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.
@@ -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
- { regex: /^no[,.]?\s+(.+)/i, confidence: 0.8 },
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
- { regex: /\bnever\s+(.+)/i, confidence: 0.75 },
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
- { regex: /\b(?:remember|recall)\s+(?:that\s+|this\s+|to\s+)?(.+)/i, confidence: 0.8 },
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
- * - Under Claude Code: Claude Haiku via ANTHROPIC_API_KEY (Claude Code
151
- * provides this to its hooks) → regex.
152
- * - Under Kiro (CLAUDE_RECALL_KIRO_CLASSIFIER set by the kiro-capture-worker):
153
- * Kiro's own headless LLM (`kiro-cli chat --no-interactive`)ANTHROPIC_
154
- * API_KEY if present → regex. Kiro's included LLM is preferred so a stray
155
- * key doesn't spend the user's Anthropic credits; CLAUDE_RECALL_PREFER_API_
156
- * KEY flips the order. The Kiro call is ~3s, so it runs only from the
157
- * detached worker, never inline. See docs/kiro-llm-capture.md.
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 import keeps kiro-classifier (and child_process) out of the module
178
- // graph for every non-Kiro hook invocation.
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
- // Order the two LLM backends. Under Kiro, prefer Kiro's INCLUDED LLM over a
181
- // stray ANTHROPIC_API_KEY: a key exported for other tools should not silently
182
- // spend the user's Anthropic credits when Kiro already ships an LLM. Set
183
- // CLAUDE_RECALL_PREFER_API_KEY to force the key first (e.g. to use a stronger
184
- // model you pay for). Under Claude Code the Kiro backend isn't available, so
185
- // the key path is the only LLM classifier.
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
  }
@@ -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. That classifier normally calls Claude Haiku through
8
- `ANTHROPIC_API_KEY`, which Claude Code sets automatically for its hook
9
- subprocesses.
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 there is no Kiro backend, so the key path is
102
- the only LLM classifier Claude Code provides the key to its hooks.)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-recall",
3
- "version": "0.30.2",
3
+ "version": "0.31.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": {