kimetsu-pi 0.1.2 → 0.1.3
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 +17 -4
- package/extensions/kimetsu.ts +161 -20
- package/package.json +9 -5
- package/skills/kimetsu-brain/SKILL.md +4 -2
package/README.md
CHANGED
|
@@ -24,7 +24,8 @@ Pi has no MCP layer, so this package brings Kimetsu to Pi through Pi's own
|
|
|
24
24
|
extension points:
|
|
25
25
|
|
|
26
26
|
- **Extension** (`extensions/kimetsu.ts`) — a TypeScript Pi extension that hooks
|
|
27
|
-
Pi lifecycle events (`session_start`, `
|
|
27
|
+
Pi lifecycle events (`session_start`, `before_agent_start`, `agent_end`,
|
|
28
|
+
`session_shutdown`) and shells
|
|
28
29
|
out to the `kimetsu` binary to warm, load, and persist brain context around each
|
|
29
30
|
session. Each call is capped by a timeout, so a slow or hung binary never stalls
|
|
30
31
|
Pi. If the binary is not on `PATH`, every hook silently no-ops and Pi is
|
|
@@ -55,9 +56,15 @@ extension points:
|
|
|
55
56
|
| **13×** | cheaper per solved task ($0.19 vs $2.47 on a Terminal-Bench slice) |
|
|
56
57
|
| **~1M** | memories in ~3 GB RAM with sub-2s retrieval, one SQLite file |
|
|
57
58
|
|
|
58
|
-
##
|
|
59
|
+
## Prerequisites
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
Install current Pi (Node.js 22.19 or newer):
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npm install -g @earendil-works/pi-coding-agent
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The `kimetsu` v2.7.0 or newer binary must be on `PATH`. Install it with:
|
|
61
68
|
|
|
62
69
|
```sh
|
|
63
70
|
npm install -g kimetsu-ai
|
|
@@ -77,10 +84,16 @@ pi install npm:kimetsu-pi
|
|
|
77
84
|
|
|
78
85
|
| Pi lifecycle event | Kimetsu command run |
|
|
79
86
|
| --- | --- |
|
|
80
|
-
| `session_start` | `kimetsu brain warm`
|
|
87
|
+
| `session_start` | `kimetsu brain warm` |
|
|
88
|
+
| `before_agent_start` | `kimetsu brain context-hook --warm-on-first-prompt` |
|
|
81
89
|
| `agent_end` | `kimetsu brain stop-hook` |
|
|
82
90
|
| `session_shutdown` | `kimetsu brain session-end-hook` |
|
|
83
91
|
|
|
92
|
+
The extension uses Pi's `SessionManager` identity, so `/new`, `/resume`, and
|
|
93
|
+
`/fork` keep separate Kimetsu warm-start and deduplication state. It also passes
|
|
94
|
+
Pi's persisted JSONL transcript to the stop and session-end hooks, enabling
|
|
95
|
+
Kimetsu's configured distiller and automatic work-episode capture.
|
|
96
|
+
|
|
84
97
|
## Development
|
|
85
98
|
|
|
86
99
|
```sh
|
package/extensions/kimetsu.ts
CHANGED
|
@@ -1,57 +1,198 @@
|
|
|
1
1
|
// Kimetsu brain extension for Pi (earendil-works/pi).
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
//
|
|
3
|
+
// CANONICAL SOURCE: kimetsu/crates/kimetsu-chat/assets/pi-extension.ts
|
|
4
|
+
// `kimetsu plugin install pi` writes this file verbatim, and the published
|
|
5
|
+
// `kimetsu-pi` npm package vendors a byte-identical copy (CI diffs the two).
|
|
6
|
+
// Edit it here — never in the installed or published copy.
|
|
7
|
+
//
|
|
8
|
+
// Pi exposes no MCP surface, so Kimetsu integrates by shelling out to the
|
|
9
|
+
// binary on lifecycle events. `before_agent_start` is the injection point:
|
|
10
|
+
// the hook payload goes in on stdin, the `additionalContext` block comes back
|
|
11
|
+
// on stdout, and Pi carries it into the turn as a context message.
|
|
12
|
+
//
|
|
13
|
+
// Every failure mode is a silent no-op: a missing binary, a hung binary, a
|
|
14
|
+
// crash, unparseable output. Kimetsu is a sidecar — it must never break Pi.
|
|
6
15
|
|
|
7
16
|
import { spawn } from "node:child_process";
|
|
17
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
18
|
|
|
9
|
-
|
|
19
|
+
/** Hard cap on any single kimetsu invocation. A hung binary must not stall a turn. */
|
|
20
|
+
const EXEC_TIMEOUT_MS = 10000;
|
|
21
|
+
|
|
22
|
+
/** Fallback session id when Pi's context does not expose one. Stable per process,
|
|
23
|
+
* which is what the brain's per-session dedupe and refractory windows need. */
|
|
24
|
+
const FALLBACK_SESSION_ID = `pi-${process.pid}`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Run `kimetsu <args>`, optionally writing `input` to its stdin, and resolve
|
|
28
|
+
* with whatever it printed to stdout ("" on any failure).
|
|
29
|
+
*
|
|
30
|
+
* stdout is PIPED, not ignored: the context hook communicates entirely through
|
|
31
|
+
* it. stderr stays ignored so diagnostics never mix into the parsed payload.
|
|
32
|
+
*/
|
|
33
|
+
function kimetsuRun(args: string[], input?: string): Promise<string> {
|
|
10
34
|
return new Promise((resolve) => {
|
|
11
35
|
let settled = false;
|
|
12
36
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
37
|
+
let stdout = "";
|
|
13
38
|
const done = () => {
|
|
14
39
|
if (settled) return;
|
|
15
40
|
settled = true;
|
|
16
41
|
if (timer !== undefined) clearTimeout(timer);
|
|
17
|
-
resolve();
|
|
42
|
+
resolve(stdout);
|
|
18
43
|
};
|
|
19
44
|
try {
|
|
20
45
|
const child = spawn("kimetsu", args, {
|
|
21
|
-
stdio: "ignore",
|
|
46
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
22
47
|
shell: false,
|
|
23
48
|
windowsHide: true,
|
|
24
49
|
});
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// the host process alive.
|
|
50
|
+
// Cap the wait and kill the child if it overruns. unref() so the timer
|
|
51
|
+
// alone can never keep the host process alive.
|
|
28
52
|
timer = setTimeout(() => {
|
|
29
53
|
child.kill();
|
|
30
54
|
done();
|
|
31
|
-
},
|
|
55
|
+
}, EXEC_TIMEOUT_MS);
|
|
32
56
|
if (typeof timer.unref === "function") timer.unref();
|
|
57
|
+
|
|
58
|
+
child.stdout?.setEncoding("utf8");
|
|
59
|
+
child.stdout?.on("data", (chunk: string) => {
|
|
60
|
+
stdout += chunk;
|
|
61
|
+
});
|
|
62
|
+
child.stdout?.on("error", () => {}); // torn pipe — resolve with what we have
|
|
63
|
+
child.stdin?.on("error", () => {}); // EPIPE when the child exits early
|
|
64
|
+
|
|
33
65
|
child.on("error", done); // binary not on PATH — silent no-op
|
|
34
|
-
child.on("close", done); //
|
|
66
|
+
child.on("close", done); // 'close' (not 'exit') so stdout is complete
|
|
67
|
+
|
|
68
|
+
child.stdin?.end(input ?? "");
|
|
35
69
|
} catch {
|
|
36
70
|
done(); // any unexpected error — silent no-op
|
|
37
71
|
}
|
|
38
72
|
});
|
|
39
73
|
}
|
|
40
74
|
|
|
41
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Pull `hookSpecificOutput.additionalContext` out of a hook's stdout.
|
|
77
|
+
*
|
|
78
|
+
* The hook prints a single JSON line, but scanning from the end tolerates any
|
|
79
|
+
* stray output ahead of it. Anything unparseable yields `undefined`, which the
|
|
80
|
+
* callers treat as "nothing to inject".
|
|
81
|
+
*/
|
|
82
|
+
function parseAdditionalContext(stdout: string): string | undefined {
|
|
83
|
+
const lines = stdout
|
|
84
|
+
.split("\n")
|
|
85
|
+
.map((line) => line.trim())
|
|
86
|
+
.filter((line) => line !== "")
|
|
87
|
+
.reverse();
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(line);
|
|
91
|
+
const context = parsed?.hookSpecificOutput?.additionalContext;
|
|
92
|
+
if (typeof context === "string" && context.trim() !== "") return context;
|
|
93
|
+
} catch {
|
|
94
|
+
// Not JSON — keep looking at earlier lines.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Best-effort session id from Pi's handler context, across naming variants. */
|
|
101
|
+
function sessionIdOf(ctx: any): string {
|
|
102
|
+
// Current Pi exposes the durable id through SessionManager. Prefer it over
|
|
103
|
+
// historical context-field variants so /new, /resume, and /fork each get a
|
|
104
|
+
// distinct Kimetsu session even when they happen in the same Pi process.
|
|
105
|
+
const getSessionId = ctx?.sessionManager?.getSessionId;
|
|
106
|
+
if (typeof getSessionId === "function") {
|
|
107
|
+
try {
|
|
108
|
+
const id = getSessionId.call(ctx.sessionManager);
|
|
109
|
+
if (typeof id === "string" && id.trim() !== "") return id;
|
|
110
|
+
} catch {
|
|
111
|
+
// A third-party/legacy SessionManager must not break the host.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const candidates = [ctx?.sessionId, ctx?.sessionID, ctx?.session_id, ctx?.session?.id];
|
|
115
|
+
for (const candidate of candidates) {
|
|
116
|
+
if (typeof candidate === "string" && candidate.trim() !== "") return candidate;
|
|
117
|
+
}
|
|
118
|
+
return FALLBACK_SESSION_ID;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Current Pi's persisted JSONL transcript, when the session is not ephemeral. */
|
|
122
|
+
function transcriptPathOf(ctx: any): string | undefined {
|
|
123
|
+
const getSessionFile = ctx?.sessionManager?.getSessionFile;
|
|
124
|
+
if (typeof getSessionFile !== "function") return undefined;
|
|
125
|
+
try {
|
|
126
|
+
const path = getSessionFile.call(ctx.sessionManager);
|
|
127
|
+
return typeof path === "string" && path.trim() !== "" ? path : undefined;
|
|
128
|
+
} catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Host-neutral hook payload understood by Kimetsu v2.7. */
|
|
134
|
+
function lifecyclePayload(ctx: any, transcript?: unknown[]): string {
|
|
135
|
+
const payload: Record<string, unknown> = { session_id: sessionIdOf(ctx) };
|
|
136
|
+
const transcriptPath = transcriptPathOf(ctx);
|
|
137
|
+
if (transcriptPath !== undefined) payload.transcript_path = transcriptPath;
|
|
138
|
+
else if (Array.isArray(transcript)) payload.transcript = transcript;
|
|
139
|
+
return JSON.stringify(payload);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** `--workspace <cwd>` when Pi tells us the working directory, else nothing
|
|
143
|
+
* (the CLI then defaults to its own cwd). */
|
|
144
|
+
function workspaceArgs(ctx: any): string[] {
|
|
145
|
+
const cwd = ctx?.cwd;
|
|
146
|
+
return typeof cwd === "string" && cwd.trim() !== "" ? ["--workspace", cwd] : [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default function (pi: ExtensionAPI) {
|
|
42
150
|
// session_start fires once when Pi starts up or a new session begins.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
151
|
+
// Warming spawns the embedder daemon so the first real retrieval is semantic
|
|
152
|
+
// rather than falling back to lexical FTS.
|
|
153
|
+
// (`brain warm` takes no --workspace: it resolves the project from its cwd.)
|
|
154
|
+
pi.on("session_start", async (_event, _ctx) => {
|
|
155
|
+
await kimetsuRun(["brain", "warm"]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// before_agent_start fires with the user's prompt, before the model is
|
|
159
|
+
// called, and can return a message that joins the turn. This is where brain
|
|
160
|
+
// context is injected. Pi has no session-start context surface, so
|
|
161
|
+
// --warm-on-first-prompt folds the repo digest and episodic resume into the
|
|
162
|
+
// first turn of each session.
|
|
163
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
164
|
+
const payload = JSON.stringify({
|
|
165
|
+
session_id: sessionIdOf(ctx),
|
|
166
|
+
prompt: typeof event?.prompt === "string" ? event.prompt : "",
|
|
167
|
+
});
|
|
168
|
+
const stdout = await kimetsuRun(
|
|
169
|
+
["brain", "context-hook", "--warm-on-first-prompt", ...workspaceArgs(ctx)],
|
|
170
|
+
payload,
|
|
171
|
+
);
|
|
172
|
+
const content = parseAdditionalContext(stdout);
|
|
173
|
+
if (content === undefined) return; // nothing relevant — zero tokens
|
|
174
|
+
return {
|
|
175
|
+
message: {
|
|
176
|
+
customType: "kimetsu-brain",
|
|
177
|
+
content,
|
|
178
|
+
display: false,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
46
181
|
});
|
|
47
182
|
|
|
48
183
|
// agent_end fires after the LLM turn completes (maps to Kimetsu stop-hook).
|
|
49
|
-
pi.on("agent_end", async (
|
|
50
|
-
await
|
|
184
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
185
|
+
await kimetsuRun(
|
|
186
|
+
["brain", "stop-hook", ...workspaceArgs(ctx)],
|
|
187
|
+
lifecyclePayload(ctx, event.messages),
|
|
188
|
+
);
|
|
51
189
|
});
|
|
52
190
|
|
|
53
191
|
// session_shutdown fires on clean session close (maps to session-end-hook).
|
|
54
|
-
pi.on("session_shutdown", async (_event
|
|
55
|
-
await
|
|
192
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
193
|
+
await kimetsuRun(
|
|
194
|
+
["brain", "session-end-hook", ...workspaceArgs(ctx)],
|
|
195
|
+
lifecyclePayload(ctx),
|
|
196
|
+
);
|
|
56
197
|
});
|
|
57
198
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimetsu-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Kimetsu brain as a Pi.dev package — local-first, cross-session memory for the Pi coding agent that gets sharper every run.",
|
|
5
5
|
"keywords": ["pi-package", "pi", "kimetsu", "memory", "brain", "rag", "mcp", "extension", "skill"],
|
|
6
6
|
"homepage": "https://kimetsu.dev",
|
|
@@ -13,14 +13,18 @@
|
|
|
13
13
|
"image": "https://raw.githubusercontent.com/RodCor/kimetsu/main/docs/assets/kimetsu-logo.png"
|
|
14
14
|
},
|
|
15
15
|
"files": ["extensions/", "skills/", "README.md", "LICENSE"],
|
|
16
|
-
"engines": { "node": ">=
|
|
16
|
+
"engines": { "node": ">=22.19.0" },
|
|
17
17
|
"scripts": {
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
19
|
"test": "vitest run"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@
|
|
23
|
-
"
|
|
24
|
-
"
|
|
22
|
+
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
23
|
+
"@types/node": "^26.0.0",
|
|
24
|
+
"typescript": "^7.0.0",
|
|
25
|
+
"vitest": "^4.1.10"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
25
29
|
}
|
|
26
30
|
}
|
|
@@ -10,8 +10,10 @@ Brain-first workflow:
|
|
|
10
10
|
1. Before planning or editing broad coding, review, debugging, or setup tasks,
|
|
11
11
|
run `kimetsu brain context <query>` and read the returned capsules as working
|
|
12
12
|
context before deciding on a plan.
|
|
13
|
-
2. After solving a non-obvious problem, run
|
|
14
|
-
|
|
13
|
+
2. After solving a non-obvious problem, run
|
|
14
|
+
`kimetsu brain memory add --scope project --kind <kind> "<lesson>"` with a
|
|
15
|
+
concrete, actionable lesson. Choose `fact`, `preference`, `convention`,
|
|
16
|
+
`command`, or `failure_pattern` for `<kind>`.
|
|
15
17
|
3. Run `kimetsu brain status` when you need to know whether the brain is
|
|
16
18
|
initialized, has accepted memories, or has pending proposals.
|
|
17
19
|
|