pi-note 0.40.1 → 0.41.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 +9 -0
- package/dist-mcp/mcp/core.js +8 -7
- package/mcp/README.md +2 -1
- package/mcp/core.ts +8 -7
- package/package.json +1 -1
- package/remote/session.ts +4 -2
- package/remote.ts +2 -1
package/README.md
CHANGED
|
@@ -107,6 +107,15 @@ Unrelated fresh launches start unselected.
|
|
|
107
107
|
Changing endpoints starts a separate selection namespace. Unsaved Pi sessions
|
|
108
108
|
cannot guarantee selection persistence.
|
|
109
109
|
|
|
110
|
+
With pi-note 0.41.0+ on both client and service, and note 0.25.0+, selecting a task
|
|
111
|
+
(including accepting a suggestion, creating one, or **Start in Kitty…**) appends
|
|
112
|
+
the actual Pi session ID to its persistent `pi-sessions` array, without duplicates.
|
|
113
|
+
Task reads and injected agent context include the array so later agents can locate
|
|
114
|
+
previous sessions on the machine that saved them. Reads, previews, reload/resume,
|
|
115
|
+
and carried selections do not append IDs; select the task in a new session to record it.
|
|
116
|
+
IDs are metadata only: transcripts are not uploaded, and unsaved/deleted sessions
|
|
117
|
+
may no longer be available. The array is separate from editable handoff fields.
|
|
118
|
+
|
|
110
119
|
The GTK app's **Start in Kitty…** action (note 0.13.0+) chooses a working directory
|
|
111
120
|
and opens a named tab with this task selected, without submitting a prompt.
|
|
112
121
|
In note 0.16.0+, it uses the tag's default folder when available, otherwise keeps
|
package/dist-mcp/mcp/core.js
CHANGED
|
@@ -34,7 +34,7 @@ const ProposalInput = z.union([
|
|
|
34
34
|
ExistingProposal,
|
|
35
35
|
]);
|
|
36
36
|
const Proposal = z.union([NewProposal.extend({ id: z.string().uuid() }), ExistingProposal.extend({ id: z.string().uuid() })]);
|
|
37
|
-
const Context = z.object({ conversationId: z.string().min(1).max(256), contextId: z.string().min(1).max(256).optional(), fieldReceipts: z.array(z.string().uuid()).max(4096).optional(), state: z.object({
|
|
37
|
+
const Context = z.object({ conversationId: z.string().min(1).max(256), piSessionId: z.string().max(256).regex(/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/).optional(), contextId: z.string().min(1).max(256).optional(), fieldReceipts: z.array(z.string().uuid()).max(4096).optional(), state: z.object({
|
|
38
38
|
proposal: Proposal.nullable().optional(),
|
|
39
39
|
launch: z.object({ task_id: TaskId, updated_at: z.string().min(1).max(128) }).strict().optional(),
|
|
40
40
|
selectedId: z.number().int().positive().safe().nullable().optional(), screen: z.enum(['tasks', 'settings', 'field']).optional(),
|
|
@@ -90,8 +90,8 @@ async function note(args, signal) {
|
|
|
90
90
|
const options = { signal, timeout: 10_000, maxBuffer: 4 * 1024 * 1024 };
|
|
91
91
|
const version = (await exec(noteCommand, ['--version'], options)).stdout.trim();
|
|
92
92
|
const match = /^pinote (\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
93
|
-
if (!match || Number(match[1]) === 0 && Number(match[2]) <
|
|
94
|
-
throw new Error('Pinote
|
|
93
|
+
if (!match || Number(match[1]) === 0 && Number(match[2]) < 25)
|
|
94
|
+
throw new Error('Pinote service requires note 0.25.0+');
|
|
95
95
|
return (await exec(noteCommand, ['--no-notify', ...args], options)).stdout;
|
|
96
96
|
}
|
|
97
97
|
async function selected(state, signal) {
|
|
@@ -363,7 +363,7 @@ async function agentContext(state, signal, automation) {
|
|
|
363
363
|
const guidance = agentGuidance();
|
|
364
364
|
const config = task ? settings().config : null;
|
|
365
365
|
const data = task
|
|
366
|
-
? `Selected task (complete text, revision and handoff notes):\n${JSON.stringify({ id: task.id, text: task.text, tag: task.tag, state: task.state, updated_at: task.updated_at, agent_notes: task.agent_notes })}\nEffective handoff field definitions:\n${JSON.stringify({ ...config, fields: fields(config, task.agent_notes) })}`
|
|
366
|
+
? `Selected task (complete text, revision and handoff notes):\n${JSON.stringify({ id: task.id, text: task.text, tag: task.tag, state: task.state, updated_at: task.updated_at, agent_notes: task.agent_notes, 'pi-sessions': task['pi-sessions'] })}\nEffective handoff field definitions:\n${JSON.stringify({ ...config, fields: fields(config, task.agent_notes) })}`
|
|
367
367
|
: `No task is selected (including unavailable or completed selected targets).\nSaved tags:\n${await note(['agent', 'tags'], signal)}\nPending suggestion (not selected):\n${JSON.stringify(proposal ? { ...proposal, ...('task_id' in proposal ? { text: proposedTask.text, tag: proposedTask.tag } : {}) } : null)}`;
|
|
368
368
|
const statuses = task ? automation?.statuses(task) ?? [] : [];
|
|
369
369
|
let text = guidance ? `${guidance}\n${data}` : data;
|
|
@@ -421,6 +421,7 @@ export function createPinoteServer(options = {}) {
|
|
|
421
421
|
const caller = context(request.params._meta);
|
|
422
422
|
const state = caller.state;
|
|
423
423
|
const owner = caller.contextId ?? caller.conversationId;
|
|
424
|
+
const sessionArgs = caller.piSessionId ? ['--pi-session-id', caller.piSessionId] : [];
|
|
424
425
|
const before = await snapshot(state, extra.signal, options.fieldAutomation, owner, caller.fieldReceipts);
|
|
425
426
|
const scoped = scopedActions(request.params._meta);
|
|
426
427
|
const { action, value } = request.params;
|
|
@@ -440,7 +441,7 @@ export function createPinoteServer(options = {}) {
|
|
|
440
441
|
next.proposal = null;
|
|
441
442
|
if (action === 'proposal:accept') {
|
|
442
443
|
const task = 'task_id' in proposal ? before.proposedTask : JSON.parse(await note(['agent', 'add', `--text=${proposal.text}`, ...(proposal.tag ? [`--tag=${proposal.tag}`] : [])], extra.signal));
|
|
443
|
-
await note(['agent', 'start', String(task.id), '--expected-updated-at', task.updated_at], extra.signal);
|
|
444
|
+
await note(['agent', 'start', String(task.id), '--expected-updated-at', task.updated_at, ...sessionArgs], extra.signal);
|
|
444
445
|
next.selectedId = task.id;
|
|
445
446
|
}
|
|
446
447
|
}
|
|
@@ -470,7 +471,7 @@ export function createPinoteServer(options = {}) {
|
|
|
470
471
|
staleAction();
|
|
471
472
|
if (next.proposal)
|
|
472
473
|
await consume(next.proposal.id);
|
|
473
|
-
await note(['agent', 'start', String(id), '--expected-updated-at', target.updated_at], extra.signal);
|
|
474
|
+
await note(['agent', 'start', String(id), '--expected-updated-at', target.updated_at, ...sessionArgs], extra.signal);
|
|
474
475
|
next.selectedId = id;
|
|
475
476
|
next.proposal = null;
|
|
476
477
|
}
|
|
@@ -479,7 +480,7 @@ export function createPinoteServer(options = {}) {
|
|
|
479
480
|
await consume(next.proposal.id);
|
|
480
481
|
next.proposal = null;
|
|
481
482
|
const task = JSON.parse(await note(['agent', 'add', `--text=${z.string().min(1).parse(value)}`], extra.signal));
|
|
482
|
-
await note(['agent', 'start', String(task.id), '--expected-updated-at', task.updated_at], extra.signal);
|
|
483
|
+
await note(['agent', 'start', String(task.id), '--expected-updated-at', task.updated_at, ...sessionArgs], extra.signal);
|
|
483
484
|
next.selectedId = task.id;
|
|
484
485
|
}
|
|
485
486
|
else if (action === 'done') {
|
package/mcp/README.md
CHANGED
|
@@ -7,7 +7,8 @@ resources, notifications and its native terminal UI. No browser UI is served.
|
|
|
7
7
|
## Build and run
|
|
8
8
|
|
|
9
9
|
On the machine holding your notes, requires **Node.js 22.19+**, **Python 3.11+**
|
|
10
|
-
and the checkout's `note` CLI
|
|
10
|
+
and the checkout's `note` CLI **0.25.0+** (schema 8). Upgrade all installations
|
|
11
|
+
sharing the database before starting the service:
|
|
11
12
|
|
|
12
13
|
```sh
|
|
13
14
|
uv sync --locked
|
package/mcp/core.ts
CHANGED
|
@@ -36,14 +36,14 @@ const ProposalInput = z.union([
|
|
|
36
36
|
ExistingProposal,
|
|
37
37
|
]);
|
|
38
38
|
const Proposal = z.union([NewProposal.extend({ id: z.string().uuid() }), ExistingProposal.extend({ id: z.string().uuid() })]);
|
|
39
|
-
const Context = z.object({ conversationId: z.string().min(1).max(256), contextId: z.string().min(1).max(256).optional(), fieldReceipts: z.array(z.string().uuid()).max(4096).optional(), state: z.object({
|
|
39
|
+
const Context = z.object({ conversationId: z.string().min(1).max(256), piSessionId: z.string().max(256).regex(/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/).optional(), contextId: z.string().min(1).max(256).optional(), fieldReceipts: z.array(z.string().uuid()).max(4096).optional(), state: z.object({
|
|
40
40
|
proposal: Proposal.nullable().optional(),
|
|
41
41
|
launch: z.object({ task_id: TaskId, updated_at: z.string().min(1).max(128) }).strict().optional(),
|
|
42
42
|
selectedId: z.number().int().positive().safe().nullable().optional(), screen: z.enum(['tasks', 'settings', 'field']).optional(),
|
|
43
43
|
field: z.string().refine((value) => [...value].length <= 64, 'Field name exceeds 64 code points').optional(), search: z.string().max(256).optional(),
|
|
44
44
|
}).default({}) });
|
|
45
45
|
type State = z.infer<typeof Context>['state'];
|
|
46
|
-
type Task = { id: number; text: string; state: string; tag: string | null; updated_at: string; agent_notes: Record<string, string
|
|
46
|
+
type Task = { id: number; text: string; state: string; tag: string | null; updated_at: string; agent_notes: Record<string, string>; 'pi-sessions': string[] };
|
|
47
47
|
type Scalar = string | number | boolean | null;
|
|
48
48
|
type Control = { id: string; label: string; revision?: string; kind: 'action' | 'text' | 'number' | 'toggle' | 'select'; value?: Scalar; icon?: string; description?: string; multiline?: boolean; confirm?: string; options?: { label: string; value: Scalar }[] };
|
|
49
49
|
const Action = z.object({ method: z.literal('native-ui/action'), params: z.object({
|
|
@@ -81,7 +81,7 @@ async function note(args: string[], signal: AbortSignal): Promise<string> {
|
|
|
81
81
|
const options = { signal, timeout: 10_000, maxBuffer: 4 * 1024 * 1024 };
|
|
82
82
|
const version = (await exec(noteCommand, ['--version'], options)).stdout.trim();
|
|
83
83
|
const match = /^pinote (\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
84
|
-
if (!match || Number(match[1]) === 0 && Number(match[2]) <
|
|
84
|
+
if (!match || Number(match[1]) === 0 && Number(match[2]) < 25) throw new Error('Pinote service requires note 0.25.0+');
|
|
85
85
|
return (await exec(noteCommand, ['--no-notify', ...args], options)).stdout;
|
|
86
86
|
}
|
|
87
87
|
async function selected(state: State, signal: AbortSignal): Promise<Task | null> {
|
|
@@ -306,7 +306,7 @@ async function agentContext(state: State, signal: AbortSignal, automation?: Fiel
|
|
|
306
306
|
const guidance = agentGuidance();
|
|
307
307
|
const config = task ? settings().config : null;
|
|
308
308
|
const data = task
|
|
309
|
-
? `Selected task (complete text, revision and handoff notes):\n${JSON.stringify({ id: task.id, text: task.text, tag: task.tag, state: task.state, updated_at: task.updated_at, agent_notes: task.agent_notes })}\nEffective handoff field definitions:\n${JSON.stringify({ ...config, fields: fields(config!, task.agent_notes) })}`
|
|
309
|
+
? `Selected task (complete text, revision and handoff notes):\n${JSON.stringify({ id: task.id, text: task.text, tag: task.tag, state: task.state, updated_at: task.updated_at, agent_notes: task.agent_notes, 'pi-sessions': task['pi-sessions'] })}\nEffective handoff field definitions:\n${JSON.stringify({ ...config, fields: fields(config!, task.agent_notes) })}`
|
|
310
310
|
: `No task is selected (including unavailable or completed selected targets).\nSaved tags:\n${await note(['agent', 'tags'], signal)}\nPending suggestion (not selected):\n${JSON.stringify(proposal ? { ...proposal, ...('task_id' in proposal ? { text: proposedTask!.text, tag: proposedTask!.tag } : {}) } : null)}`;
|
|
311
311
|
const statuses = task ? automation?.statuses(task) ?? [] : [];
|
|
312
312
|
let text = guidance ? `${guidance}\n${data}` : data;
|
|
@@ -358,6 +358,7 @@ server.setRequestHandler(Action, (request, extra) => serial(async () => {
|
|
|
358
358
|
const caller = context(request.params._meta);
|
|
359
359
|
const state = caller.state;
|
|
360
360
|
const owner = caller.contextId ?? caller.conversationId;
|
|
361
|
+
const sessionArgs = caller.piSessionId ? ['--pi-session-id', caller.piSessionId] : [];
|
|
361
362
|
const before = await snapshot(state, extra.signal, options.fieldAutomation, owner, caller.fieldReceipts);
|
|
362
363
|
const scoped = scopedActions(request.params._meta);
|
|
363
364
|
const { action, value } = request.params;
|
|
@@ -374,7 +375,7 @@ server.setRequestHandler(Action, (request, extra) => serial(async () => {
|
|
|
374
375
|
next.proposal = null;
|
|
375
376
|
if (action === 'proposal:accept') {
|
|
376
377
|
const task: Task = 'task_id' in proposal ? before.proposedTask! : JSON.parse(await note(['agent', 'add', `--text=${proposal.text}`, ...(proposal.tag ? [`--tag=${proposal.tag}`] : [])], extra.signal));
|
|
377
|
-
await note(['agent', 'start', String(task.id), '--expected-updated-at', task.updated_at], extra.signal); next.selectedId = task.id;
|
|
378
|
+
await note(['agent', 'start', String(task.id), '--expected-updated-at', task.updated_at, ...sessionArgs], extra.signal); next.selectedId = task.id;
|
|
378
379
|
}
|
|
379
380
|
} else if (action.startsWith('screen:')) next.screen = action.slice(7) as State['screen'];
|
|
380
381
|
else if (action.startsWith('field:')) { next.screen = 'field'; next.field = action.slice(6); }
|
|
@@ -390,12 +391,12 @@ server.setRequestHandler(Action, (request, extra) => serial(async () => {
|
|
|
390
391
|
const target: Task = JSON.parse(await note(['agent', 'get', String(id)], extra.signal));
|
|
391
392
|
if (target.text !== candidate.text || target.state !== candidate.state || target.tag !== candidate.tag || state.launch && target.updated_at !== state.launch.updated_at) staleAction();
|
|
392
393
|
if (next.proposal) await consume(next.proposal.id);
|
|
393
|
-
await note(['agent', 'start', String(id), '--expected-updated-at', target.updated_at], extra.signal); next.selectedId = id; next.proposal = null;
|
|
394
|
+
await note(['agent', 'start', String(id), '--expected-updated-at', target.updated_at, ...sessionArgs], extra.signal); next.selectedId = id; next.proposal = null;
|
|
394
395
|
} else if (action === 'add') {
|
|
395
396
|
if (next.proposal) await consume(next.proposal.id);
|
|
396
397
|
next.proposal = null;
|
|
397
398
|
const task: Task = JSON.parse(await note(['agent', 'add', `--text=${z.string().min(1).parse(value)}`], extra.signal));
|
|
398
|
-
await note(['agent', 'start', String(task.id), '--expected-updated-at', task.updated_at], extra.signal); next.selectedId = task.id;
|
|
399
|
+
await note(['agent', 'start', String(task.id), '--expected-updated-at', task.updated_at, ...sessionArgs], extra.signal); next.selectedId = task.id;
|
|
399
400
|
} else if (action === 'done') {
|
|
400
401
|
await note(['agent', 'done', String(before.task!.id), '--expected-updated-at', before.task!.updated_at], extra.signal);
|
|
401
402
|
next.selectedId = null;
|
package/package.json
CHANGED
package/remote/session.ts
CHANGED
|
@@ -31,6 +31,7 @@ export class RemoteSession {
|
|
|
31
31
|
private refreshing = false;
|
|
32
32
|
private active = false;
|
|
33
33
|
private conversationId = '';
|
|
34
|
+
private piSessionId?: string;
|
|
34
35
|
private saved?: SavedState;
|
|
35
36
|
view?: NativeView;
|
|
36
37
|
fullscreen = false;
|
|
@@ -46,10 +47,11 @@ export class RemoteSession {
|
|
|
46
47
|
get signal(): AbortSignal { return this.lifetime.signal; }
|
|
47
48
|
snapshot(): SavedState | undefined { return this.saved && structuredClone(this.saved); }
|
|
48
49
|
current(): () => boolean { const epoch = this.epoch; return () => this.active && epoch === this.epoch; }
|
|
49
|
-
async start(branch: readonly BranchEntry[], conversationId: string, carried?: SavedState): Promise<void> {
|
|
50
|
+
async start(branch: readonly BranchEntry[], conversationId: string, carried?: SavedState, piSessionId?: string): Promise<void> {
|
|
50
51
|
await this.stop();
|
|
51
52
|
this.lifetime = new AbortController(); this.active = true;
|
|
52
53
|
this.conversationId = conversationId;
|
|
54
|
+
this.piSessionId = piSessionId;
|
|
53
55
|
this.saved = carried?.scope === this.config.scope ? structuredClone(carried) : restoreState(branch, this.config.scope);
|
|
54
56
|
this.persist(this.saved);
|
|
55
57
|
await this.refresh(true);
|
|
@@ -64,7 +66,7 @@ export class RemoteSession {
|
|
|
64
66
|
private meta(): Record<string, unknown> {
|
|
65
67
|
if (!this.saved) throw new RemoteFailure();
|
|
66
68
|
return {
|
|
67
|
-
[UI_CONTEXT]: { conversationId: this.conversationId, contextId: this.saved.contextId, state: this.saved.state, fieldReceipts: this.saved.fieldReceipts ?? [] },
|
|
69
|
+
[UI_CONTEXT]: { conversationId: this.conversationId, piSessionId: this.piSessionId, contextId: this.saved.contextId, state: this.saved.state, fieldReceipts: this.saved.fieldReceipts ?? [] },
|
|
68
70
|
[UI_FEATURES]: { actionRevisions: true, fieldEvents: true, fieldWatchers: true, footerStyles: true, footerDoubleActions: true, footerSecondaryActions: true, footerNativeActions: this.fullscreen },
|
|
69
71
|
};
|
|
70
72
|
}
|
package/remote.ts
CHANGED
|
@@ -89,7 +89,8 @@ export default function pinoteRemote(pi: ExtensionAPI): void {
|
|
|
89
89
|
ctx.ui.notify('Pinote launch ignored: invalid request or nonempty/resumed session. Existing selection is unchanged.', 'warning');
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
const piSessionId = ctx.sessionManager.getSessionId();
|
|
93
|
+
await session.start(branch, piSessionId || randomUUID(), carried, piSessionId || undefined);
|
|
93
94
|
if (launch) {
|
|
94
95
|
const view = session.view!;
|
|
95
96
|
// The service exposes this control only for the exact GUI-displayed revision.
|