pi-note 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -12
- package/index.ts +31 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# pi-note
|
|
2
2
|
|
|
3
3
|
Persistent [pinote](https://github.com/kvidzibo/pinote) tasks and Markdown handoffs
|
|
4
|
-
in Pi: selected-task footer status, `/pi-note`, and agent read/update
|
|
4
|
+
in Pi: selected-task footer status, `/pi-note`, and agent current-task read/update,
|
|
5
|
+
add, and tag-listing tools.
|
|
5
6
|
The extension and Python app share a repository but install separately.
|
|
6
7
|
|
|
7
8
|
## Install
|
|
8
9
|
|
|
9
|
-
Requires Node.js 22.19+ and **pinote 0.3.0+** (`note` on PATH). Add and tag need **pinote 0.4.0+**.
|
|
10
|
+
Requires Node.js 22.19+ and **pinote 0.3.0+** (`note` on PATH). Add and tag listing need **pinote 0.4.0+**.
|
|
10
11
|
From this checkout, install the extension with `pi install ./pi`, then run
|
|
11
12
|
`/reload` in interactive Pi. Use `/pi-note-setup` if the CLI is missing, or the
|
|
12
13
|
suggested `/pi-note-upgrade` if it is older than the bundled CLI. The planned npm package name is
|
|
@@ -56,8 +57,8 @@ standalone `settings/pi/extensions/pinote.ts` entry if installed; load only one
|
|
|
56
57
|
## Use
|
|
57
58
|
|
|
58
59
|
`/pi-note` picks a task, starts it, and adds a short task-ID prompt to the editor:
|
|
59
|
-
“Read task #121 with
|
|
60
|
-
with
|
|
60
|
+
“Read the current task (#121) with pinote_get_current and work on it. Ask only if
|
|
61
|
+
blocked. Save progress with pinote_update_current.” The agent fetches current task text and saved handoff fields
|
|
61
62
|
when you submit; they are not copied into the draft. Existing drafts are preserved;
|
|
62
63
|
nothing is submitted automatically.
|
|
63
64
|
With a selected task it offers **Continue**, **Done**, and **Switch task**.
|
|
@@ -82,16 +83,20 @@ command is not this session memory.
|
|
|
82
83
|
|
|
83
84
|
Agent tools:
|
|
84
85
|
|
|
85
|
-
- `
|
|
86
|
-
|
|
87
|
-
- `
|
|
88
|
-
`set` label/value pairs or `remove` labels. Updates
|
|
89
|
-
|
|
86
|
+
- `pinote_get_current`: read this session's selected task, including its revision.
|
|
87
|
+
Takes no arguments; returns null when this session has no selected task.
|
|
88
|
+
- `pinote_update_current`: supply `expected_updated_at` from that read and arbitrary
|
|
89
|
+
`set` label/value pairs or `remove` labels. Updates the selected task only, with no
|
|
90
|
+
ID argument. Updates merge fields, never replace the task text or tag. Fails if
|
|
91
|
+
no task is selected or the selection changes during the operation. If another
|
|
92
|
+
process changed the task, read again before retrying.
|
|
90
93
|
- `pinote_tags`: list saved tag names.
|
|
91
94
|
- `pinote_add`: create an active task. Optional `tag`. `select: true` starts and
|
|
92
95
|
remembers it for this session only; use that only after the user agrees.
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
|
|
97
|
+
Version 0.7.0 replaces `pinote_get`/`pinote_update` with these current-task tools
|
|
98
|
+
and removes `pinote_tag`, without aliases. Agents cannot read/update tasks by ID
|
|
99
|
+
or retag existing tasks; use `/pi-note` to select an existing task.
|
|
95
100
|
|
|
96
101
|
When the user gives work and no task is selected, the agent proposes one note as
|
|
97
102
|
`[tag] text` and asks before creating it. No continues without a note. Yes calls
|
|
@@ -142,7 +147,7 @@ off the footer unless named in `Bar`.
|
|
|
142
147
|
|
|
143
148
|
### PR merge watcher
|
|
144
149
|
|
|
145
|
-
Set `PR` with `
|
|
150
|
+
Set `PR` with `pinote_update_current` to one `https://github.com/owner/repo/pull/123` URL
|
|
146
151
|
or Markdown link. The footer adds a clickable, link-coloured **PR #123**, without
|
|
147
152
|
status text. Other hosts, multiple links, and prose are not watched.
|
|
148
153
|
|
package/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { bundledCLIVersion, cliMenu, detectCLI, setupCLI, setupHint, versionAtLe
|
|
|
10
10
|
const handoffGuidance = "Keep agent notes to at most three short bullets total: outcome, blocker, next action, only when relevant. Replace stale notes; omit narration, repeated task text, and routine test logs. Keep a GitHub pull request in the PR field. To show another footer field, set its Markdown value and append its label to Bar, one label per line. Links in that text are clickable. Do not list PR in Bar. Remove the field and its Bar line to drop it. Notes stay off the footer unless named in Bar.";
|
|
11
11
|
const createGuidance = [
|
|
12
12
|
"When the user gives work and no pinote task is selected, propose one note as `[tag] text` and ask before creating it. On no, continue without a note. On yes, call pinote_add with select true so it becomes this session's active task. Never add or select without a yes. If a task is already selected, do not replace it unless the user asks to switch.",
|
|
13
|
-
"Reuse a pinote_tags name when it fits. One tag; case-sensitive, trimmed, at most 64 characters.
|
|
13
|
+
"Reuse a pinote_tags name when it fits. One tag; case-sensitive, trimmed, at most 64 characters. Tags are set when creating tasks with pinote_add; agent tools cannot retag existing tasks. Do not use pinote_update_current for tags or task text.",
|
|
14
14
|
];
|
|
15
15
|
type Task = {
|
|
16
16
|
id: number;
|
|
@@ -24,7 +24,6 @@ type Task = {
|
|
|
24
24
|
type Summary = Pick<Task, "id" | "text" | "state" | "tag">;
|
|
25
25
|
// The standard Pi footer accepts text, so ship a portable terminal glyph, not a theme icon.
|
|
26
26
|
const noteIcon = readFileSync(new URL("./icons/note.txt", import.meta.url), "utf8").trim();
|
|
27
|
-
const idSchema = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
28
27
|
const compatible = "Incompatible note CLI response. Install pinote 0.3.0+ and check note on PATH.";
|
|
29
28
|
const validId = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) > 0;
|
|
30
29
|
const record = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -118,6 +117,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
118
117
|
};
|
|
119
118
|
const writeTask = async (
|
|
120
119
|
ctx: ExtensionContext, signal: AbortSignal | undefined, argv: string[], id?: number, minimum = "0.3.0",
|
|
120
|
+
canRun = () => true,
|
|
121
121
|
) => {
|
|
122
122
|
if (pending) throw new Error("A pinote operation is already open. Retry after it finishes.");
|
|
123
123
|
const operation = Symbol();
|
|
@@ -125,7 +125,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
125
125
|
const generation = epoch;
|
|
126
126
|
refreshSerial++;
|
|
127
127
|
try {
|
|
128
|
-
return requiredTask(await run(argv, signal, () => alive && generation === epoch, minimum), id);
|
|
128
|
+
return requiredTask(await run(argv, signal, () => alive && generation === epoch && canRun(), minimum), id);
|
|
129
129
|
} finally {
|
|
130
130
|
if (pending === operation) pending = undefined;
|
|
131
131
|
if (alive && generation === epoch) await refresh(ctx);
|
|
@@ -162,7 +162,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
162
162
|
};
|
|
163
163
|
const selected = async (_ctx: ExtensionContext, signal?: AbortSignal) => {
|
|
164
164
|
const generation = epoch;
|
|
165
|
-
const
|
|
165
|
+
const branch = branchEpoch;
|
|
166
|
+
const canUse = () => alive && generation === epoch && branch === branchEpoch;
|
|
166
167
|
await ensureCLI(signal, canUse);
|
|
167
168
|
return currentTask(signal, canUse);
|
|
168
169
|
};
|
|
@@ -348,7 +349,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
348
349
|
const chosen = await startTask(id, canAct);
|
|
349
350
|
if (!chosen || !canAct()) return;
|
|
350
351
|
const draft = ctx.ui.getEditorText();
|
|
351
|
-
const handoff = `Read task #${chosen.id} with
|
|
352
|
+
const handoff = `Read the current task (#${chosen.id}) with pinote_get_current and work on it. Ask only if blocked. Save progress with pinote_update_current.`;
|
|
352
353
|
ctx.ui.setEditorText(draft ? `${draft}\n\n${handoff}` : handoff);
|
|
353
354
|
ctx.ui.notify(`Pinote #${chosen.id} is in progress. Task added to input.`, "info");
|
|
354
355
|
} catch (error) {
|
|
@@ -361,38 +362,44 @@ export default function (pi: ExtensionAPI) {
|
|
|
361
362
|
});
|
|
362
363
|
|
|
363
364
|
pi.registerTool({
|
|
364
|
-
name: "
|
|
365
|
-
label: "Pinote get",
|
|
365
|
+
name: "pinote_get_current",
|
|
366
|
+
label: "Pinote get current",
|
|
366
367
|
promptGuidelines: createGuidance,
|
|
367
|
-
description: "Read
|
|
368
|
-
parameters: Type.Object({
|
|
369
|
-
async execute(_toolCallId,
|
|
370
|
-
|
|
371
|
-
const result = params.id === undefined
|
|
372
|
-
? await selected(ctx, signal)
|
|
373
|
-
: requiredTask(await run(["agent", "get", String(params.id)], signal), params.id);
|
|
374
|
-
return toolResult(result);
|
|
368
|
+
description: "Read this session's current pinote task and its Markdown agent fields/revision. Returns null when none is selected. No ID argument. Use the returned updated_at for pinote_update_current.",
|
|
369
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
370
|
+
async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
|
|
371
|
+
return toolResult(await selected(ctx, signal));
|
|
375
372
|
},
|
|
376
373
|
});
|
|
377
374
|
pi.registerTool({
|
|
378
|
-
name: "
|
|
379
|
-
label: "Pinote update",
|
|
375
|
+
name: "pinote_update_current",
|
|
376
|
+
label: "Pinote update current",
|
|
380
377
|
promptGuidelines: [handoffGuidance],
|
|
381
|
-
description: "Patch arbitrary agent handoff fields on
|
|
378
|
+
description: "Patch arbitrary agent handoff fields on this session's current pinote task. No ID argument. Read first with pinote_get_current; pass its updated_at as expected_updated_at. Fails when no task is selected or the selection changes during the operation. set merges label/value pairs without replacing other fields or task text; values are Markdown, e.g. PR: [Fix #42](https://github.com/org/repo/pull/42). Set PR to one GitHub pull-request URL or Markdown link to show PR #N in the footer and watch for merge confirmation in interactive Pi. Set Bar to newline-separated field labels to show those Markdown fields in the footer; links in the text are clickable. remove deletes named fields. A stale revision fails; read again before retrying. Does not complete the task or change its tag.",
|
|
382
379
|
parameters: Type.Object({
|
|
383
|
-
id: idSchema,
|
|
384
380
|
expected_updated_at: Type.String({ minLength: 1 }),
|
|
385
381
|
set: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
386
382
|
remove: Type.Optional(Type.Array(Type.String())),
|
|
387
|
-
}),
|
|
383
|
+
}, { additionalProperties: false }),
|
|
388
384
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
389
|
-
if (!
|
|
390
|
-
throw new Error("
|
|
385
|
+
if (!text(params.expected_updated_at) || !params.expected_updated_at.trim()) {
|
|
386
|
+
throw new Error("expected_updated_at is required; read the current task first.");
|
|
391
387
|
}
|
|
392
|
-
const
|
|
388
|
+
const generation = epoch;
|
|
389
|
+
const branch = branchEpoch;
|
|
390
|
+
const id = selectedId;
|
|
391
|
+
const canRun = () => alive && generation === epoch && branch === branchEpoch && selectedId === id;
|
|
392
|
+
const current = await selected(ctx, signal);
|
|
393
|
+
if (!canRun()) throw new Error("Pinote operation cancelled: the session or selected task changed.");
|
|
394
|
+
if (!current) throw new Error("No selected pinote task. Select one with /pi-note first.");
|
|
395
|
+
const argv = ["agent", "update", String(current.id), "--expected-updated-at", params.expected_updated_at,
|
|
393
396
|
"--set-json", JSON.stringify(params.set ?? {})];
|
|
394
397
|
for (const label of params.remove ?? []) argv.push(`--remove=${label}`);
|
|
395
|
-
|
|
398
|
+
const updated = await writeTask(ctx, signal, argv, current.id, "0.3.0", canRun);
|
|
399
|
+
if (!canRun()) {
|
|
400
|
+
throw new Error("Pinote operation cancelled: the session or selected task changed. The write may have committed; read the current task before retrying.");
|
|
401
|
+
}
|
|
402
|
+
return toolResult(updated);
|
|
396
403
|
},
|
|
397
404
|
});
|
|
398
405
|
pi.registerTool({
|
|
@@ -444,25 +451,4 @@ export default function (pi: ExtensionAPI) {
|
|
|
444
451
|
}
|
|
445
452
|
},
|
|
446
453
|
});
|
|
447
|
-
pi.registerTool({
|
|
448
|
-
name: "pinote_tag",
|
|
449
|
-
label: "Pinote tag",
|
|
450
|
-
description: "Set or clear the tag on an active or in-progress pinote task. Read first with pinote_get and pass updated_at as expected_updated_at. Provide tag or clear true, not both. A stale revision fails without writing. Does not change task text.",
|
|
451
|
-
parameters: Type.Object({
|
|
452
|
-
id: idSchema,
|
|
453
|
-
expected_updated_at: Type.String({ minLength: 1 }),
|
|
454
|
-
tag: Type.Optional(Type.String({ minLength: 1 })),
|
|
455
|
-
clear: Type.Optional(Type.Boolean()),
|
|
456
|
-
}),
|
|
457
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
458
|
-
if (!validId(params.id) || !text(params.expected_updated_at) || !params.expected_updated_at.trim()) {
|
|
459
|
-
throw new Error("id and expected_updated_at are required; read the task first.");
|
|
460
|
-
}
|
|
461
|
-
if ((params.tag !== undefined) === (params.clear === true)) throw new Error("Provide tag or clear true, not both.");
|
|
462
|
-
if (params.tag !== undefined && (!text(params.tag) || !params.tag.trim())) throw new Error("tag must be a non-empty name.");
|
|
463
|
-
const argv = ["agent", "tag", String(params.id), "--expected-updated-at", params.expected_updated_at];
|
|
464
|
-
argv.push(params.clear === true ? "--clear" : `--tag=${params.tag}`);
|
|
465
|
-
return toolResult(await writeTask(ctx, signal, argv, params.id, "0.4.0"));
|
|
466
|
-
},
|
|
467
|
-
});
|
|
468
454
|
}
|