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 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
@@ -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]) < 12)
94
- throw new Error('Pinote MCP requires note 0.12.0+');
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]) < 12) throw new Error('Pinote MCP requires note 0.12.0+');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-note",
3
- "version": "0.40.1",
3
+ "version": "0.41.0",
4
4
  "description": "Persistent pinote tasks, Markdown handoffs, and task status for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
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
- await session.start(branch, ctx.sessionManager.getSessionId() || randomUUID(), carried);
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.