pi-note 0.28.0 → 0.29.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/exit-confirm.ts DELETED
@@ -1,127 +0,0 @@
1
- import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { truncateToWidth } from "@earendil-works/pi-tui";
3
- import { realpath } from "node:fs/promises";
4
- import { homedir } from "node:os";
5
- import { isAbsolute, join } from "node:path";
6
- import { stripVTControlCharacters } from "node:util";
7
-
8
- type Task = { id: number; text: string; updated_at: string; state: string };
9
- type Current = { task: Task | null; database?: string };
10
- const complete = "Complete task and exit";
11
- const leave = "Exit without completing";
12
- const clean = (value: string) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f-\x9f]/gu, " ");
13
- function task(value: unknown): Task | null {
14
- if (value === null) return null;
15
- const item = value as Task | undefined;
16
- if (!item || !Number.isSafeInteger(item.id) || item.id < 1 || typeof item.text !== "string" ||
17
- typeof item.updated_at !== "string" || !item.updated_at ||
18
- !["active", "in_progress", "done"].includes(item.state)) throw new Error("Invalid Pinote task response");
19
- return item;
20
- }
21
- function same(left: Task | null, right: Task): boolean {
22
- return left?.id === right.id && left.updated_at === right.updated_at && left.text === right.text && left.state === right.state;
23
- }
24
-
25
- // Optional MCP companion: deliberately absent from package.json's default extensions.
26
- export default function (pi: ExtensionAPI) {
27
- let invalidate: (() => void) | undefined;
28
- let stop: (() => void) | undefined;
29
- const readCurrent = async (): Promise<Current> => {
30
- const replies: { handled?: boolean; error?: string; result?: Promise<unknown> }[] = [];
31
- pi.events.emit("gateway:query-service-tool", {
32
- service: "pinote", tool: "get_current",
33
- respond: (reply: typeof replies[number]) => {
34
- if (reply.handled) { replies.push(reply); void reply.result?.catch(() => {}); }
35
- },
36
- });
37
- if (replies.length !== 1 || replies[0].error || !replies[0].result) {
38
- throw new Error(replies[0]?.error || "Pinote gateway service unavailable; use /quit to exit without completing");
39
- }
40
- const result = await replies[0].result as {
41
- isError?: boolean; content?: { type: string; text?: string }[]; _meta?: Record<string, unknown>;
42
- };
43
- if (result.isError) throw new Error(result.content?.find((item) => item.type === "text")?.text || "Pinote query failed");
44
- const raw = result.content?.find((item) => item.type === "text")?.text;
45
- if (raw === undefined) throw new Error("Missing Pinote task response");
46
- const value = task(JSON.parse(raw));
47
- if (value && !["active", "in_progress"].includes(value.state)) throw new Error("Pinote task is no longer active");
48
- const database = result._meta?.["pinote/database"];
49
- return { task: value, database: typeof database === "string" ? database : undefined };
50
- };
51
- pi.on("session_start", (_event, ctx) => {
52
- if (ctx.mode !== "tui" || !ctx.hasUI) return;
53
- const previous = ctx.ui.getEditorComponent();
54
- let alive = true, pending = false, epoch = 0;
55
- let controller: AbortController | undefined;
56
- invalidate = () => { epoch++; controller?.abort(); };
57
- const exit = async () => {
58
- if (pending || !alive) return;
59
- pending = true;
60
- const generation = epoch;
61
- const operation = controller = new AbortController();
62
- const current = () => alive && generation === epoch && !operation.signal.aborted &&
63
- ctx.ui.getEditorText().length === 0;
64
- const shutdown = () => { if (!ctx.isIdle()) ctx.abort(); ctx.shutdown(); };
65
- const run = async (args: string[]) => {
66
- if (!current() || !ctx.isIdle()) throw new Error("Pinote exit cancelled: session changed or became busy");
67
- const result = await pi.exec(process.env.PINOTE_NOTE_COMMAND || "note", args, { timeout: 10_000, signal: operation.signal });
68
- if (result.killed || result.code !== 0) throw new Error(clean(result.stderr || result.stdout || "Pinote CLI failed or timed out"));
69
- return result.stdout;
70
- };
71
- try {
72
- const selected = await readCurrent();
73
- if (!current()) return;
74
- if (!selected.task) { shutdown(); return; }
75
- const approved = selected.task;
76
- const title = truncateToWidth(clean(approved.text.split("\n", 1)[0]), 70);
77
- const choice = await ctx.ui.select(`Pinote · ${title}`, [complete, leave, "Cancel"], { signal: operation.signal });
78
- if (!current()) return;
79
- if (choice === leave) { shutdown(); return; }
80
- if (choice !== complete) return;
81
- if (!ctx.isIdle()) throw new Error("Wait for Pi to become idle before completing a task");
82
- const dataHome = process.env.XDG_DATA_HOME;
83
- const localDatabase = join(dataHome && isAbsolute(dataHome) ? dataHome : join(homedir(), ".local/share"), "pinote/notes.db");
84
- if (!selected.database || !isAbsolute(selected.database) ||
85
- await realpath(selected.database) !== await realpath(localDatabase)) {
86
- throw new Error("Pinote database mismatch: update the MCP server and use the same XDG_DATA_HOME in Pi and the service");
87
- }
88
- const version = (await run(["--version"])).trim();
89
- const match = /^pinote (\d+)\.(\d+)\.(\d+)$/.exec(version);
90
- if (!match || Number(match[1]) === 0 && Number(match[2]) < 12) throw new Error("Pinote exit confirmation requires note 0.12.0+");
91
- const local = task(JSON.parse(await run(["--no-notify", "agent", "get", String(approved.id)])));
92
- const fresh = await readCurrent();
93
- if (!same(local, approved) || !same(fresh.task, approved) || fresh.database !== selected.database) {
94
- throw new Error("Pinote task changed; inspect it before retrying");
95
- }
96
- const done = task(JSON.parse(await run(["--no-notify", "agent", "done", String(approved.id), "--expected-updated-at", approved.updated_at])));
97
- if (done?.id !== approved.id || done.state !== "done") throw new Error("Unexpected completion response; inspect the task before retrying");
98
- if (current() && !(await readCurrent()).task && current()) shutdown();
99
- else if (current()) ctx.ui.notify("Task completed; Pi stayed open because the selection changed", "info");
100
- } catch (error) {
101
- if (alive && generation === epoch) ctx.ui.notify(`Pinote: ${clean(String(error))}. Pi stayed open; inspect the task before retrying. /quit exits without completing.`, "error");
102
- } finally { pending = false; }
103
- };
104
- const factory: NonNullable<ReturnType<typeof ctx.ui.getEditorComponent>> = (tui, theme, keybindings) => {
105
- const editor = previous ? previous(tui, theme, keybindings) : new CustomEditor(tui, theme, keybindings);
106
- const handleInput = editor.handleInput.bind(editor);
107
- editor.handleInput = (data) => {
108
- if (keybindings.matches(data, "app.exit") && editor.getText().length === 0) { void exit(); return; }
109
- handleInput(data);
110
- };
111
- return editor;
112
- };
113
- ctx.ui.setEditorComponent(factory);
114
- stop = () => {
115
- alive = false;
116
- invalidate?.();
117
- if (ctx.ui.getEditorComponent() === factory) ctx.ui.setEditorComponent(previous);
118
- };
119
- });
120
- pi.on("session_shutdown", () => { stop?.(); stop = invalidate = undefined; });
121
- // Retire in-flight dialogs/reads before navigation or a new agent run can act.
122
- const interrupted = () => { invalidate?.(); };
123
- pi.on("session_before_switch", interrupted);
124
- pi.on("session_before_fork", interrupted);
125
- pi.on("session_before_tree", interrupted);
126
- pi.on("agent_start", interrupted);
127
- }