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