kimetsu-pi 0.1.4 → 0.2.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/CHANGELOG.md +34 -0
- package/README.md +91 -9
- package/extensions/kimetsu.ts +468 -3
- package/package.json +7 -3
- package/skills/kimetsu-brain/SKILL.md +36 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Support for the Kimetsu 2.11.0 peer network. Pi tools `kimetsu_network_status`
|
|
8
|
+
and `kimetsu_network_request`, plus `/kimetsu-network-status`,
|
|
9
|
+
`/kimetsu-network-request`, and `/kimetsu-network-share`.
|
|
10
|
+
- A `kimetsu --version` check before any network command. Older binaries and
|
|
11
|
+
builds without the `network` feature explain the gap. Nothing listens in
|
|
12
|
+
that case.
|
|
13
|
+
- Sharing stays a terminal command. The extension never starts
|
|
14
|
+
`kimetsu network share`. `/kimetsu-network-share` only prints it. The relay
|
|
15
|
+
stays off, and only a signed, scrubbed repo pack is sent.
|
|
16
|
+
- Request passes `--no-mdns` unless discovery is requested, and `--map-ports`
|
|
17
|
+
only when asked. The process is killed on the 60-second deadline or when Pi
|
|
18
|
+
cancels the call.
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
|
|
22
|
+
- Pin the development-only `undici` dependency to 8.12.4 so the audit gate
|
|
23
|
+
clears the high-severity advisories in 8.12.1 and earlier. Pi itself stays
|
|
24
|
+
on 1.2.0.
|
|
25
|
+
|
|
26
|
+
## 0.1.5
|
|
27
|
+
|
|
28
|
+
- Pi 1.2.0 compatibility for custom-triggered turns, and integration coverage
|
|
29
|
+
against the Kimetsu 2.8.0 CLI.
|
|
30
|
+
|
|
31
|
+
## 0.1.4
|
|
32
|
+
|
|
33
|
+
- Fresh memory per task, a five-minute session-save deadline, and guidance for
|
|
34
|
+
corrections and usefulness feedback.
|
package/README.md
CHANGED
|
@@ -30,10 +30,31 @@ This package connects Kimetsu through Pi's native extension and skill interfaces
|
|
|
30
30
|
on `PATH`, every hook silently no-ops and Pi is
|
|
31
31
|
completely unaffected.
|
|
32
32
|
- **Skill** (`skills/kimetsu-brain/SKILL.md`) — a Pi skill that teaches the agent
|
|
33
|
-
to reuse injected context, correct outdated memories,
|
|
34
|
-
actually helped during a task
|
|
33
|
+
to reuse injected context, correct outdated memories, cite memories that
|
|
34
|
+
actually helped during a task, and use the peer memory network only when
|
|
35
|
+
asked.
|
|
35
36
|
|
|
36
|
-
## What's new in 0.
|
|
37
|
+
## What's new in 0.2.0
|
|
38
|
+
|
|
39
|
+
- Peer network tools for Kimetsu 2.11.0: `kimetsu_network_status` and
|
|
40
|
+
`kimetsu_network_request`, plus `/kimetsu-network-status`,
|
|
41
|
+
`/kimetsu-network-request`, and `/kimetsu-network-share`.
|
|
42
|
+
- `kimetsu --version` is checked before any network command. Older binaries,
|
|
43
|
+
and builds compiled without `network`, explain the gap and leave every
|
|
44
|
+
socket closed.
|
|
45
|
+
- Sharing stays a terminal command. The extension never starts it. Request
|
|
46
|
+
listens only while that call runs, with the relay off, discovery off unless
|
|
47
|
+
asked, and router port mapping off unless asked.
|
|
48
|
+
|
|
49
|
+
## Previously in 0.1.5
|
|
50
|
+
|
|
51
|
+
- Pi 1.2.0 compatibility: custom-triggered turns expire old memory without
|
|
52
|
+
treating background notifications as user prompts.
|
|
53
|
+
- Integration tests exercise Pi's real loader, event runner, model-message
|
|
54
|
+
conversion, and session saving with the Kimetsu 2.8.0 CLI in isolated fixtures.
|
|
55
|
+
- Refreshed development dependencies and removed the source-map-js advisory.
|
|
56
|
+
|
|
57
|
+
## Previously in 0.1.4
|
|
37
58
|
|
|
38
59
|
- Fresh memory per task: older raw injections are excluded from model requests
|
|
39
60
|
and new summaries, including after corrections, queued user messages, and
|
|
@@ -69,8 +90,10 @@ Install current Pi (Node.js 22.19 or newer):
|
|
|
69
90
|
npm install -g @earendil-works/pi-coding-agent
|
|
70
91
|
```
|
|
71
92
|
|
|
72
|
-
The `kimetsu` binary must be on `PATH`. Kimetsu 2.
|
|
73
|
-
|
|
93
|
+
The `kimetsu` binary must be on `PATH`. Memory hooks work on Kimetsu 2.7.0 and
|
|
94
|
+
newer. This package targets Kimetsu 2.11.0 for the peer network; the extension
|
|
95
|
+
checks `kimetsu --version` and skips network commands on anything older.
|
|
96
|
+
Install Kimetsu with:
|
|
74
97
|
|
|
75
98
|
```sh
|
|
76
99
|
npm install -g kimetsu-ai
|
|
@@ -109,7 +132,7 @@ pi update npm:kimetsu-pi
|
|
|
109
132
|
For a pinned installation, select this version explicitly:
|
|
110
133
|
|
|
111
134
|
```sh
|
|
112
|
-
pi install npm:kimetsu-pi@0.
|
|
135
|
+
pi install npm:kimetsu-pi@0.2.0
|
|
113
136
|
```
|
|
114
137
|
|
|
115
138
|
Then restart Pi or run `/reload`. Pi package updates and Kimetsu CLI updates are
|
|
@@ -151,6 +174,59 @@ missing evidence, correct existing memories, and cite only memories that
|
|
|
151
174
|
actually helped. Run `kimetsu brain status` from the project directory to inspect
|
|
152
175
|
the brain. Automatic model-based distillation requires configuration.
|
|
153
176
|
|
|
177
|
+
## Peer network
|
|
178
|
+
|
|
179
|
+
Kimetsu 2.11.0 adds a peer memory network to release and npm builds. Pi has no
|
|
180
|
+
MCP, so this package shells out to the local `kimetsu` binary. Nothing listens
|
|
181
|
+
until `share` or `request` runs. The relay stays off. Packs are signed and
|
|
182
|
+
scrubbed, and only the repo mount is exchanged. An unknown signer is
|
|
183
|
+
quarantined and stays out of retrieval.
|
|
184
|
+
|
|
185
|
+
Create a network on one machine, then join it on another. These commands write
|
|
186
|
+
keys and do not open a socket:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
kimetsu network invite
|
|
190
|
+
kimetsu network join <code>
|
|
191
|
+
kimetsu network status
|
|
192
|
+
kimetsu network leave
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`invite` prints a join code. The code does not include the admin key. `status`
|
|
196
|
+
reports whether this machine has joined and always leaves listening off.
|
|
197
|
+
|
|
198
|
+
To send memories, the user runs this in a terminal from the project directory
|
|
199
|
+
and leaves it running until the other machine has fetched the pack:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
kimetsu network share
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
That process listens until Ctrl-C. It prints a `peer:` line
|
|
206
|
+
(`endpoint-id@host:port`). The extension does not run `share` for you.
|
|
207
|
+
`/kimetsu-network-share` prints the same instructions and does not open a
|
|
208
|
+
socket. Add `--map-ports` only when you want UPnP or NAT-PMP to open a UDP
|
|
209
|
+
port. That stays off by default.
|
|
210
|
+
|
|
211
|
+
On the machine that should receive the pack, either ask Pi to call
|
|
212
|
+
`kimetsu_network_request` or run:
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
/kimetsu-network-request <endpoint-id>@host:port
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The same fetch from a shell, with discovery and port mapping left off:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
kimetsu network request --json --no-mdns --peer <endpoint-id>@host:port
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`kimetsu_network_status` is the read-only check. Both tools no-op, with an
|
|
225
|
+
explanation, when `kimetsu --version` is older than 2.11.0 or when
|
|
226
|
+
`kimetsu network status --json` reports `"compiled": false`. A request is
|
|
227
|
+
killed if it runs longer than 60 seconds or if Pi cancels the call, so the
|
|
228
|
+
socket does not outlive the command.
|
|
229
|
+
|
|
154
230
|
## Development
|
|
155
231
|
|
|
156
232
|
```sh
|
|
@@ -160,11 +236,14 @@ npm run typecheck
|
|
|
160
236
|
```
|
|
161
237
|
|
|
162
238
|
To run the optional CLI integration test, set `KIMETSU_TEST_BINARY` to the
|
|
163
|
-
absolute path of a Kimetsu
|
|
239
|
+
absolute path of a Kimetsu executable named `kimetsu` (`kimetsu.exe` on
|
|
164
240
|
Windows), then run `npm run test:integration`. This command fails if the binary
|
|
165
241
|
is missing. It creates an isolated temporary brain, uses
|
|
166
242
|
lexical retrieval without model calls, and verifies correction, context
|
|
167
|
-
filtering, citation, and session persistence.
|
|
243
|
+
filtering, citation, and session persistence. It also runs the network status
|
|
244
|
+
and request tools. A binary older than 2.11.0 must degrade without listening.
|
|
245
|
+
A 2.11.0 or newer binary must report `listening: false` from
|
|
246
|
+
`kimetsu network status --json`.
|
|
168
247
|
|
|
169
248
|
The fixture anchors a new Git repository and gives subprocesses temporary homes,
|
|
170
249
|
caches, and user-brain paths. It removes inherited Git/Kimetsu overrides and
|
|
@@ -172,7 +251,10 @@ credentials before initialization. Never run integration commands against your
|
|
|
172
251
|
normal project or home brain.
|
|
173
252
|
|
|
174
253
|
CI requires the real CLI test on Linux, macOS, and Windows using checksum-pinned
|
|
175
|
-
Kimetsu 2.8.0 binaries
|
|
254
|
+
Kimetsu 2.8.0 binaries (the latest published release) and the minimum supported
|
|
255
|
+
Node version. Those binaries have no `network` command, so CI covers the
|
|
256
|
+
degradation path. Repin the workflow assets to 2.11.0 after that release is
|
|
257
|
+
published if the capable path should run in CI. The `required checks`
|
|
176
258
|
gate also requires typechecking, tests, the dependency audit, and package dry-run.
|
|
177
259
|
Release tags must match the package version and point to a commit on `main` with
|
|
178
260
|
successful CI. Wait for that commit's CI before tagging; a release started too
|
package/extensions/kimetsu.ts
CHANGED
|
@@ -10,12 +10,20 @@
|
|
|
10
10
|
// the hook payload goes in on stdin, the `additionalContext` block comes back
|
|
11
11
|
// on stdout, and Pi carries it into the turn as a context message.
|
|
12
12
|
//
|
|
13
|
-
// Every failure mode is a silent no-op: a missing binary,
|
|
14
|
-
// crash, unparseable output. Kimetsu is a sidecar — it must
|
|
13
|
+
// Every failure mode of a lifecycle hook is a silent no-op: a missing binary,
|
|
14
|
+
// a hung binary, a crash, unparseable output. Kimetsu is a sidecar — it must
|
|
15
|
+
// never break Pi.
|
|
16
|
+
//
|
|
17
|
+
// The peer network is separate. Pi has no MCP, so `kimetsu_network_status`
|
|
18
|
+
// and `kimetsu_network_request` shell out to the local binary. They require
|
|
19
|
+
// Kimetsu 2.11.0 and a build with the `network` feature. Anything older
|
|
20
|
+
// degrades with an explanation. Sharing is never started from here: it
|
|
21
|
+
// listens until the user stops it, and that stays a terminal command.
|
|
15
22
|
|
|
16
23
|
import { spawn } from "node:child_process";
|
|
17
24
|
import { randomUUID } from "node:crypto";
|
|
18
25
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { Type } from "typebox";
|
|
19
27
|
|
|
20
28
|
/** Interactive hooks must not leave a turn waiting on a hung binary. */
|
|
21
29
|
const EXEC_TIMEOUT_MS = 10000;
|
|
@@ -151,6 +159,374 @@ function workspaceArgs(ctx: any): string[] {
|
|
|
151
159
|
return typeof cwd === "string" && cwd.trim() !== "" ? ["--workspace", cwd] : [];
|
|
152
160
|
}
|
|
153
161
|
|
|
162
|
+
/** First Kimetsu release that ships `kimetsu network` in release builds. */
|
|
163
|
+
const NETWORK_MIN_VERSION: readonly [number, number, number] = [2, 11, 0];
|
|
164
|
+
|
|
165
|
+
/** `request` exchanges announcements for up to 20s, then transfers one pack. */
|
|
166
|
+
const NETWORK_REQUEST_TIMEOUT_MS = 60_000;
|
|
167
|
+
|
|
168
|
+
const SHARE_INSTRUCTIONS = [
|
|
169
|
+
"Sharing stays off until you run it yourself. In this project directory, run: kimetsu network share",
|
|
170
|
+
"That command listens until you press Ctrl-C. The relay stays off. Only this repo's memories are sent, after they are signed and scrubbed. Do not add --map-ports unless you want the router to open a UDP port. Nothing is listening now.",
|
|
171
|
+
].join("\n");
|
|
172
|
+
|
|
173
|
+
type Capture = {
|
|
174
|
+
stdout: string;
|
|
175
|
+
stderr: string;
|
|
176
|
+
code: number | null;
|
|
177
|
+
timedOut: boolean;
|
|
178
|
+
aborted: boolean;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
type StatusDetails = {
|
|
182
|
+
available: boolean;
|
|
183
|
+
compiled: boolean | null;
|
|
184
|
+
joined: boolean;
|
|
185
|
+
listening: boolean;
|
|
186
|
+
group: string | null;
|
|
187
|
+
role: string | null;
|
|
188
|
+
generation: number | null;
|
|
189
|
+
endpoint_id: string | null;
|
|
190
|
+
message: string;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
type RequestDetails = {
|
|
194
|
+
available: boolean;
|
|
195
|
+
listening: boolean;
|
|
196
|
+
verified: boolean;
|
|
197
|
+
imported: number;
|
|
198
|
+
deduped: number;
|
|
199
|
+
quarantined: number;
|
|
200
|
+
superseded: number;
|
|
201
|
+
digest: string | null;
|
|
202
|
+
message: string;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
type ToolResult<T> = {
|
|
206
|
+
content: [{ type: "text"; text: string }];
|
|
207
|
+
details: T;
|
|
208
|
+
structuredContent: T;
|
|
209
|
+
isError?: boolean;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Run `kimetsu <args>` and keep stdout, stderr, and the exit code.
|
|
214
|
+
* stdin is ignored. A timeout or abort kills the child so a request
|
|
215
|
+
* cannot keep a socket open after Pi has moved on.
|
|
216
|
+
*/
|
|
217
|
+
function kimetsuCapture(args: string[], timeoutMs: number, signal?: AbortSignal): Promise<Capture> {
|
|
218
|
+
if (signal?.aborted) {
|
|
219
|
+
return Promise.resolve({ stdout: "", stderr: "", code: null, timedOut: false, aborted: true });
|
|
220
|
+
}
|
|
221
|
+
return new Promise((resolve) => {
|
|
222
|
+
let settled = false;
|
|
223
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
224
|
+
let stdout = "";
|
|
225
|
+
let stderr = "";
|
|
226
|
+
let timedOut = false;
|
|
227
|
+
let aborted = false;
|
|
228
|
+
const finish = (code: number | null) => {
|
|
229
|
+
if (settled) return;
|
|
230
|
+
settled = true;
|
|
231
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
232
|
+
if (signal !== undefined) signal.removeEventListener("abort", onAbort);
|
|
233
|
+
resolve({ stdout, stderr, code, timedOut, aborted });
|
|
234
|
+
};
|
|
235
|
+
const onAbort = () => {
|
|
236
|
+
aborted = true;
|
|
237
|
+
child.kill();
|
|
238
|
+
finish(null);
|
|
239
|
+
};
|
|
240
|
+
let child: ReturnType<typeof spawn>;
|
|
241
|
+
try {
|
|
242
|
+
child = spawn("kimetsu", args, {
|
|
243
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
244
|
+
shell: false,
|
|
245
|
+
windowsHide: true,
|
|
246
|
+
});
|
|
247
|
+
timer = setTimeout(() => {
|
|
248
|
+
timedOut = true;
|
|
249
|
+
child.kill();
|
|
250
|
+
finish(null);
|
|
251
|
+
}, timeoutMs);
|
|
252
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
253
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
254
|
+
child.stdout?.setEncoding("utf8");
|
|
255
|
+
child.stderr?.setEncoding("utf8");
|
|
256
|
+
child.stdout?.on("data", (chunk: string) => { stdout += chunk; });
|
|
257
|
+
child.stderr?.on("data", (chunk: string) => { stderr += chunk; });
|
|
258
|
+
child.stdout?.on("error", () => {});
|
|
259
|
+
child.stderr?.on("error", () => {});
|
|
260
|
+
child.on("error", () => finish(null));
|
|
261
|
+
child.on("close", (code) => finish(typeof code === "number" ? code : 0));
|
|
262
|
+
} catch {
|
|
263
|
+
finish(null);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function parseVersion(output: string): { numbers: [number, number, number]; label: string } | undefined {
|
|
269
|
+
const match = output.match(/(\d+)\.(\d+)\.(\d+)(\s*\([^)]*\))?/);
|
|
270
|
+
if (match === null) return undefined;
|
|
271
|
+
return {
|
|
272
|
+
numbers: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
273
|
+
label: `${match[1]}.${match[2]}.${match[3]}${match[4] ?? ""}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function versionAtLeast(version: readonly [number, number, number], min: readonly [number, number, number]): boolean {
|
|
278
|
+
for (let index = 0; index < 3; index += 1) {
|
|
279
|
+
if (version[index] !== min[index]) return version[index] > min[index];
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function parseLastJson(stdout: string): Record<string, unknown> | undefined {
|
|
285
|
+
const lines = stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "").reverse();
|
|
286
|
+
for (const line of lines) {
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(line);
|
|
289
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
290
|
+
} catch {
|
|
291
|
+
// Keep looking at earlier lines.
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function textResult<T extends { message: string }>(details: T, isError = false): ToolResult<T> {
|
|
298
|
+
return {
|
|
299
|
+
content: [{ type: "text", text: details.message }],
|
|
300
|
+
details,
|
|
301
|
+
structuredContent: details,
|
|
302
|
+
...(isError ? { isError: true } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function emptyStatus(message: string, compiled: boolean | null = null): StatusDetails {
|
|
307
|
+
return {
|
|
308
|
+
available: false,
|
|
309
|
+
compiled,
|
|
310
|
+
joined: false,
|
|
311
|
+
listening: false,
|
|
312
|
+
group: null,
|
|
313
|
+
role: null,
|
|
314
|
+
generation: null,
|
|
315
|
+
endpoint_id: null,
|
|
316
|
+
message,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function emptyRequest(message: string): RequestDetails {
|
|
321
|
+
return {
|
|
322
|
+
available: false,
|
|
323
|
+
listening: false,
|
|
324
|
+
verified: false,
|
|
325
|
+
imported: 0,
|
|
326
|
+
deduped: 0,
|
|
327
|
+
quarantined: 0,
|
|
328
|
+
superseded: 0,
|
|
329
|
+
digest: null,
|
|
330
|
+
message,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function commandFailure(captured: Capture, fallback: string): string {
|
|
335
|
+
const text = `${captured.stderr}\n${captured.stdout}`
|
|
336
|
+
.split("\n")
|
|
337
|
+
.map((line) => line.trim())
|
|
338
|
+
.filter((line) => line !== "")
|
|
339
|
+
.join(" ");
|
|
340
|
+
if (text === "") return fallback;
|
|
341
|
+
if (/nothing is listening|nothing was contacted/i.test(text)) return text;
|
|
342
|
+
return `${text} Nothing is listening.`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function normalizePeer(peer: string): string | undefined {
|
|
346
|
+
const value = peer.trim();
|
|
347
|
+
const at = value.indexOf("@");
|
|
348
|
+
if (at <= 0 || at !== value.lastIndexOf("@") || at === value.length - 1) return undefined;
|
|
349
|
+
if (/\s/.test(value)) return undefined;
|
|
350
|
+
const id = value.slice(0, at);
|
|
351
|
+
const address = value.slice(at + 1);
|
|
352
|
+
if (id.startsWith("-") || address.startsWith("-")) return undefined;
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function asNullableString(value: unknown): string | null {
|
|
357
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function asCount(value: unknown): number {
|
|
361
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
type Gate =
|
|
365
|
+
| { ok: true; status: StatusDetails }
|
|
366
|
+
| { ok: false; status: StatusDetails };
|
|
367
|
+
|
|
368
|
+
/** Version gate, then `network status --json`. Status never opens a socket. */
|
|
369
|
+
async function networkGate(signal?: AbortSignal): Promise<Gate> {
|
|
370
|
+
const version = await kimetsuCapture(["--version"], EXEC_TIMEOUT_MS, signal);
|
|
371
|
+
if (version.aborted) {
|
|
372
|
+
return { ok: false, status: emptyStatus("The network check was cancelled. Nothing is listening.") };
|
|
373
|
+
}
|
|
374
|
+
if (version.timedOut) {
|
|
375
|
+
return { ok: false, status: emptyStatus("Kimetsu did not respond and was stopped. Nothing is listening.") };
|
|
376
|
+
}
|
|
377
|
+
if (version.code === null) {
|
|
378
|
+
return { ok: false, status: emptyStatus("Kimetsu is not on PATH. Nothing is listening.") };
|
|
379
|
+
}
|
|
380
|
+
const parsed = parseVersion(`${version.stdout}\n${version.stderr}`);
|
|
381
|
+
if (parsed !== undefined && !versionAtLeast(parsed.numbers, NETWORK_MIN_VERSION)) {
|
|
382
|
+
return {
|
|
383
|
+
ok: false,
|
|
384
|
+
status: emptyStatus(
|
|
385
|
+
`Peer network needs Kimetsu ${NETWORK_MIN_VERSION.join(".")} or newer. This binary is ${parsed.label}. Nothing is listening.`,
|
|
386
|
+
),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (version.code !== 0 && parsed === undefined) {
|
|
390
|
+
return { ok: false, status: emptyStatus(commandFailure(version, "Kimetsu is not on PATH. Nothing is listening.")) };
|
|
391
|
+
}
|
|
392
|
+
const status = await kimetsuCapture(["network", "status", "--json"], EXEC_TIMEOUT_MS, signal);
|
|
393
|
+
if (status.aborted) {
|
|
394
|
+
return { ok: false, status: emptyStatus("The network check was cancelled. Nothing is listening.") };
|
|
395
|
+
}
|
|
396
|
+
if (status.timedOut) {
|
|
397
|
+
return { ok: false, status: emptyStatus("Kimetsu did not respond and was stopped. Nothing is listening.") };
|
|
398
|
+
}
|
|
399
|
+
if (status.code === null) {
|
|
400
|
+
return { ok: false, status: emptyStatus("Kimetsu is not on PATH. Nothing is listening.") };
|
|
401
|
+
}
|
|
402
|
+
if (status.code !== 0) {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
status: emptyStatus(commandFailure(status, "Kimetsu network status failed. Nothing is listening.")),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const body = parseLastJson(status.stdout);
|
|
409
|
+
if (body === undefined) {
|
|
410
|
+
return { ok: false, status: emptyStatus("Kimetsu network status returned nothing readable. Nothing is listening.") };
|
|
411
|
+
}
|
|
412
|
+
if (body.compiled !== true) {
|
|
413
|
+
return { ok: false, status: emptyStatus("Peer network is not included in this build. Nothing is listening.", false) };
|
|
414
|
+
}
|
|
415
|
+
const details: StatusDetails = {
|
|
416
|
+
available: true,
|
|
417
|
+
compiled: true,
|
|
418
|
+
joined: body.joined === true,
|
|
419
|
+
listening: body.listening === true,
|
|
420
|
+
group: asNullableString(body.group),
|
|
421
|
+
role: asNullableString(body.role),
|
|
422
|
+
generation: typeof body.generation === "number" && Number.isFinite(body.generation) ? body.generation : null,
|
|
423
|
+
endpoint_id: asNullableString(body.endpoint_id),
|
|
424
|
+
message: "",
|
|
425
|
+
};
|
|
426
|
+
if (!details.joined) {
|
|
427
|
+
details.message = "Peer network: not joined. Nothing is listening.";
|
|
428
|
+
} else {
|
|
429
|
+
const endpoint = details.endpoint_id === null ? "" : ` Endpoint ${details.endpoint_id}.`;
|
|
430
|
+
details.message = `Peer network: joined "${details.group ?? "network"}" (${details.role ?? "member"}).${endpoint} Nothing is listening.`;
|
|
431
|
+
}
|
|
432
|
+
return { ok: true, status: details };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function networkStatus(signal?: AbortSignal): Promise<ToolResult<StatusDetails>> {
|
|
436
|
+
const gate = await networkGate(signal);
|
|
437
|
+
return textResult(gate.status, false);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function networkRequest(
|
|
441
|
+
params: { peer?: string; discover?: boolean; map_ports?: boolean },
|
|
442
|
+
ctx: { cwd?: string },
|
|
443
|
+
signal?: AbortSignal,
|
|
444
|
+
): Promise<ToolResult<RequestDetails>> {
|
|
445
|
+
const discover = params.discover === true;
|
|
446
|
+
const mapPorts = params.map_ports === true;
|
|
447
|
+
if (typeof params.peer === "string" && normalizePeer(params.peer) === undefined) {
|
|
448
|
+
return textResult(emptyRequest(
|
|
449
|
+
"Peer should look like <endpoint-id>@host:port, the line `kimetsu network share` prints. Nothing was contacted.",
|
|
450
|
+
), true);
|
|
451
|
+
}
|
|
452
|
+
const peer = typeof params.peer === "string" ? normalizePeer(params.peer) : undefined;
|
|
453
|
+
if (peer === undefined && !discover) {
|
|
454
|
+
return textResult(emptyRequest(
|
|
455
|
+
"Pass peer (endpoint-id@host:port) or set discover to true. Nothing was contacted.",
|
|
456
|
+
), true);
|
|
457
|
+
}
|
|
458
|
+
const gate = await networkGate(signal);
|
|
459
|
+
if (!gate.ok) return textResult(emptyRequest(gate.status.message), true);
|
|
460
|
+
const args = ["network", "request", "--json"];
|
|
461
|
+
if (!discover) args.push("--no-mdns");
|
|
462
|
+
args.push(...workspaceArgs(ctx));
|
|
463
|
+
if (peer !== undefined) args.push("--peer", peer);
|
|
464
|
+
if (mapPorts) args.push("--map-ports");
|
|
465
|
+
const captured = await kimetsuCapture(args, NETWORK_REQUEST_TIMEOUT_MS, signal);
|
|
466
|
+
if (captured.aborted) {
|
|
467
|
+
return textResult(emptyRequest("The peer request was cancelled and the socket was closed. Nothing is listening."), true);
|
|
468
|
+
}
|
|
469
|
+
if (captured.timedOut) {
|
|
470
|
+
return textResult(emptyRequest("The peer request timed out and the socket was closed. Nothing is listening."), true);
|
|
471
|
+
}
|
|
472
|
+
if (captured.code === null) {
|
|
473
|
+
return textResult(emptyRequest("Kimetsu is not on PATH. Nothing is listening."), true);
|
|
474
|
+
}
|
|
475
|
+
if (captured.code !== 0) {
|
|
476
|
+
return textResult(emptyRequest(commandFailure(captured, "Kimetsu network request failed. Nothing is listening.")), true);
|
|
477
|
+
}
|
|
478
|
+
const body = parseLastJson(captured.stdout);
|
|
479
|
+
if (body === undefined || body.verified !== true) {
|
|
480
|
+
return textResult(emptyRequest(commandFailure(captured, "Kimetsu network request returned nothing readable. Nothing is listening.")), true);
|
|
481
|
+
}
|
|
482
|
+
const quarantined = asCount(body.quarantined);
|
|
483
|
+
let message = `Verified the signature. Imported ${asCount(body.imported)}, already had ${asCount(body.deduped)}, quarantined ${quarantined}, superseded ${asCount(body.superseded)}. Nothing is listening now.`;
|
|
484
|
+
if (quarantined > 0) {
|
|
485
|
+
message += " Quarantined memories stay out of retrieval until you accept them.";
|
|
486
|
+
}
|
|
487
|
+
const details: RequestDetails = {
|
|
488
|
+
available: true,
|
|
489
|
+
listening: body.listening === true,
|
|
490
|
+
verified: true,
|
|
491
|
+
imported: asCount(body.imported),
|
|
492
|
+
deduped: asCount(body.deduped),
|
|
493
|
+
quarantined,
|
|
494
|
+
superseded: asCount(body.superseded),
|
|
495
|
+
digest: asNullableString(body.digest),
|
|
496
|
+
message,
|
|
497
|
+
};
|
|
498
|
+
return textResult(details, false);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const networkNamespace = {
|
|
502
|
+
name: "kimetsu-network",
|
|
503
|
+
description: "Peer memory network. Nothing listens until a request runs. Sharing is a terminal command.",
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const statusOutput = Type.Object({
|
|
507
|
+
available: Type.Boolean(),
|
|
508
|
+
compiled: Type.Union([Type.Boolean(), Type.Null()]),
|
|
509
|
+
joined: Type.Boolean(),
|
|
510
|
+
listening: Type.Boolean(),
|
|
511
|
+
group: Type.Union([Type.String(), Type.Null()]),
|
|
512
|
+
role: Type.Union([Type.String(), Type.Null()]),
|
|
513
|
+
generation: Type.Union([Type.Number(), Type.Null()]),
|
|
514
|
+
endpoint_id: Type.Union([Type.String(), Type.Null()]),
|
|
515
|
+
message: Type.String(),
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const requestOutput = Type.Object({
|
|
519
|
+
available: Type.Boolean(),
|
|
520
|
+
listening: Type.Boolean(),
|
|
521
|
+
verified: Type.Boolean(),
|
|
522
|
+
imported: Type.Number(),
|
|
523
|
+
deduped: Type.Number(),
|
|
524
|
+
quarantined: Type.Number(),
|
|
525
|
+
superseded: Type.Number(),
|
|
526
|
+
digest: Type.Union([Type.String(), Type.Null()]),
|
|
527
|
+
message: Type.String(),
|
|
528
|
+
});
|
|
529
|
+
|
|
154
530
|
export default function (pi: ExtensionAPI) {
|
|
155
531
|
// Pi persists injected messages, including display:false messages. Only the
|
|
156
532
|
// current task's injection belongs in future model calls. A unique marker
|
|
@@ -172,12 +548,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
172
548
|
// --warm-on-first-prompt folds the repo digest and episodic resume into the
|
|
173
549
|
// first turn of each session.
|
|
174
550
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
551
|
+
// Pi 1.x also starts turns from custom messages. Those are not user
|
|
552
|
+
// prompts: expire earlier evidence without retrieving for an empty task.
|
|
553
|
+
// Checking the prompt field also preserves pre-1.x user events.
|
|
554
|
+
activeContext = undefined;
|
|
555
|
+
if (!("prompt" in event) || typeof event.prompt !== "string") return;
|
|
175
556
|
const request = { id: randomUUID(), sessionId: sessionIdOf(ctx) };
|
|
176
557
|
// Expire the last task immediately, even if retrieval is empty or fails.
|
|
177
558
|
activeContext = request;
|
|
178
559
|
const payload = JSON.stringify({
|
|
179
560
|
session_id: request.sessionId,
|
|
180
|
-
prompt:
|
|
561
|
+
prompt: event.prompt,
|
|
181
562
|
});
|
|
182
563
|
const stdout = await kimetsuRun(
|
|
183
564
|
["brain", "context-hook", "--warm-on-first-prompt", ...workspaceArgs(ctx)],
|
|
@@ -267,4 +648,88 @@ export default function (pi: ExtensionAPI) {
|
|
|
267
648
|
SESSION_SAVE_TIMEOUT_MS,
|
|
268
649
|
);
|
|
269
650
|
});
|
|
651
|
+
|
|
652
|
+
// Registered after the lifecycle hooks. Neither tool runs during a turn
|
|
653
|
+
// unless the model or the user calls it. Share is a command, not a tool,
|
|
654
|
+
// and that command does not spawn the binary.
|
|
655
|
+
pi.registerTool({
|
|
656
|
+
name: "kimetsu_network_status",
|
|
657
|
+
label: "Kimetsu network status",
|
|
658
|
+
description: "Whether this machine has joined a Kimetsu peer network. Does not listen, discover peers, or contact a relay. Sharing stays a terminal command: kimetsu network share.",
|
|
659
|
+
promptSnippet: "Check Kimetsu peer-network membership without listening",
|
|
660
|
+
promptGuidelines: [
|
|
661
|
+
"Does not listen, discover peers, or contact a relay.",
|
|
662
|
+
"Never run `kimetsu network share` unless the user explicitly asks in this turn. It listens until Ctrl-C; tell them to run it in a terminal.",
|
|
663
|
+
],
|
|
664
|
+
parameters: Type.Object({}),
|
|
665
|
+
outputSchema: statusOutput,
|
|
666
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
667
|
+
namespace: networkNamespace,
|
|
668
|
+
async execute(_toolCallId, _params, signal) {
|
|
669
|
+
return networkStatus(signal);
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
pi.registerTool({
|
|
674
|
+
name: "kimetsu_network_request",
|
|
675
|
+
label: "Kimetsu network request",
|
|
676
|
+
description: "Fetch repo memories a peer is sharing. Verifies the pack signature before any write. Refuses packs that are not scoped to repo. Does nothing and opens no socket when this machine has not joined. Router port mapping stays off unless map_ports is true.",
|
|
677
|
+
promptSnippet: "Fetch a signed repo memory pack from a Kimetsu peer",
|
|
678
|
+
promptGuidelines: [
|
|
679
|
+
"Call only when the user asks to fetch memories from a peer.",
|
|
680
|
+
"Nothing listens until this call, and the socket closes when it finishes, times out, or Pi cancels it. The relay stays off.",
|
|
681
|
+
"Packs are signed and scrubbed and must be the repo mount. An unknown signer is quarantined and stays out of retrieval.",
|
|
682
|
+
"Leave discover false, and do not set map_ports, unless the user explicitly asks.",
|
|
683
|
+
"Never run `kimetsu network share` from the agent. Tell the user to run it in a terminal; it listens until they press Ctrl-C.",
|
|
684
|
+
],
|
|
685
|
+
parameters: Type.Object({
|
|
686
|
+
peer: Type.Optional(Type.String({
|
|
687
|
+
description: "endpoint-id@host:port from kimetsu network share",
|
|
688
|
+
})),
|
|
689
|
+
discover: Type.Optional(Type.Boolean({
|
|
690
|
+
description: "Look for roster peers on the local network. Default false.",
|
|
691
|
+
})),
|
|
692
|
+
map_ports: Type.Optional(Type.Boolean({
|
|
693
|
+
description: "Ask the router to open a UDP port. Default false.",
|
|
694
|
+
})),
|
|
695
|
+
}),
|
|
696
|
+
outputSchema: requestOutput,
|
|
697
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
698
|
+
executionMode: "sequential",
|
|
699
|
+
namespace: networkNamespace,
|
|
700
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
701
|
+
return networkRequest(params, ctx, signal);
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
pi.registerCommand("kimetsu-network-status", {
|
|
706
|
+
description: "Show whether this machine has joined a Kimetsu peer network. Does not listen.",
|
|
707
|
+
handler: async (_args, ctx) => {
|
|
708
|
+
const result = await networkStatus(ctx.signal);
|
|
709
|
+
ctx.ui.notify(result.content[0].text, result.details.available ? "info" : "warning");
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
pi.registerCommand("kimetsu-network-request", {
|
|
714
|
+
description: "Fetch repo memories a peer is sharing. Listens only while the fetch runs.",
|
|
715
|
+
handler: async (args, ctx) => {
|
|
716
|
+
const peer = args.trim();
|
|
717
|
+
if (peer === "") {
|
|
718
|
+
ctx.ui.notify(
|
|
719
|
+
"Pass the peer line from kimetsu network share: /kimetsu-network-request <endpoint-id>@host:port",
|
|
720
|
+
"warning",
|
|
721
|
+
);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const result = await networkRequest({ peer }, ctx, ctx.signal);
|
|
725
|
+
ctx.ui.notify(result.content[0].text, result.isError ? "error" : "info");
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
pi.registerCommand("kimetsu-network-share", {
|
|
730
|
+
description: "Show the terminal command that shares repo memories. Does not open a socket.",
|
|
731
|
+
handler: async (_args, ctx) => {
|
|
732
|
+
ctx.ui.notify(SHARE_INSTRUCTIONS, "info");
|
|
733
|
+
},
|
|
734
|
+
});
|
|
270
735
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimetsu-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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": [
|
|
6
6
|
"pi-package",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"extensions/",
|
|
34
34
|
"skills/",
|
|
35
35
|
"README.md",
|
|
36
|
+
"CHANGELOG.md",
|
|
36
37
|
"LICENSE"
|
|
37
38
|
],
|
|
38
39
|
"engines": {
|
|
@@ -44,12 +45,15 @@
|
|
|
44
45
|
"test:integration": "node scripts/test-integration.mjs"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
|
-
"@earendil-works/pi-coding-agent": "
|
|
48
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
48
49
|
"@types/node": "^26.0.0",
|
|
49
50
|
"typescript": "^7.0.0",
|
|
50
|
-
"vitest": "5.0.
|
|
51
|
+
"vitest": "5.0.3"
|
|
51
52
|
},
|
|
52
53
|
"peerDependencies": {
|
|
53
54
|
"@earendil-works/pi-coding-agent": "*"
|
|
55
|
+
},
|
|
56
|
+
"overrides": {
|
|
57
|
+
"undici": "8.12.4"
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kimetsu-brain
|
|
3
|
-
description: Use when Pi tasks benefit from prior session knowledge, durable lessons, memory corrections,
|
|
3
|
+
description: Use when Pi tasks benefit from prior session knowledge, durable lessons, memory corrections, feedback on helpful memories, or the peer memory network.
|
|
4
4
|
---
|
|
5
5
|
Kimetsu is a persistent memory sidecar accessed through the `kimetsu` CLI.
|
|
6
6
|
Run commands from the relevant project directory. If the binary is unavailable,
|
|
@@ -62,3 +62,38 @@ repeat credit for the same use.
|
|
|
62
62
|
`kimetsu brain status` reports initialization, accepted memories, and pending
|
|
63
63
|
proposals. Automatic session saving is separate from these deliberate actions;
|
|
64
64
|
model-based lesson distillation requires a configured distiller.
|
|
65
|
+
|
|
66
|
+
## Peer network
|
|
67
|
+
|
|
68
|
+
Kimetsu 2.11.0 adds a peer memory network. Check `kimetsu --version` before
|
|
69
|
+
using it. If the binary is older, or `kimetsu network status --json` reports
|
|
70
|
+
`"compiled": false`, say so and continue the task. Nothing listens until
|
|
71
|
+
`share` or `request` runs. The relay stays off. Packs are signed and scrubbed,
|
|
72
|
+
and only the repo mount is exchanged.
|
|
73
|
+
|
|
74
|
+
`kimetsu_network_status` and `kimetsu network status` report membership. They
|
|
75
|
+
do not discover peers or contact a relay.
|
|
76
|
+
|
|
77
|
+
Fetch a pack only when the user asks. Prefer `kimetsu_network_request` with
|
|
78
|
+
the peer line printed by `kimetsu network share` (`endpoint-id@host:port`).
|
|
79
|
+
That tool passes `--no-mdns` unless discovery was requested, passes
|
|
80
|
+
`--map-ports` only when the user asks the router to open a UDP port, and
|
|
81
|
+
closes the socket when the fetch finishes, times out, or Pi cancels it. The
|
|
82
|
+
signature is checked before any write. An unknown signer is quarantined and
|
|
83
|
+
stays out of retrieval. A pack that is not scoped to the repo is refused.
|
|
84
|
+
|
|
85
|
+
Do not run `kimetsu network share` unless the user explicitly asks to share
|
|
86
|
+
memories in this turn. Even then, do not start it in the background or from a
|
|
87
|
+
tool. Tell the user to run `kimetsu network share` in a terminal. It listens
|
|
88
|
+
until they press Ctrl-C. `/kimetsu-network-share` prints that command and does
|
|
89
|
+
not open a socket.
|
|
90
|
+
|
|
91
|
+
Membership commands also stay quiet until the user asks. They do not listen:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
kimetsu network invite
|
|
95
|
+
kimetsu network join <code>
|
|
96
|
+
kimetsu network leave
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`invite` and `join` only save keys. `leave` forgets them.
|