@paigy/mcp 0.30.0 → 0.31.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.
@@ -2490,6 +2490,14 @@ var NotifyRequestSchema = z.object({
2490
2490
  waiting: z.enum(["none", "soft", "hard"]).optional().describe(
2491
2491
  "With `ask`: what happens to your work while you wait. 'none' = you're just informing the user. 'soft' = you'd like an answer but can keep working. 'hard' = you are stopped until they answer (reaches them urgently and escalates to a real phone call if unanswered). Replaces urgencyHint + blocking \u2014 send this one field."
2492
2492
  ),
2493
+ /** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
2494
+ * interrupted. Opt-in per claim, because the fail-open is a minute-granularity cron —
2495
+ * holding by default would charge every quiet claim that minute before any agent could
2496
+ * correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
2497
+ * and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
2498
+ confirm: z.boolean().optional().describe(
2499
+ "Hold this one so you can correct the plan before the user is interrupted. The response comes back with `held: true` and the plan; POST the confirm route to release it (with options/visuals/urgency corrections, or nothing at all). If you never do, it is announced anyway a couple of minutes later. Ignored when waiting is 'hard'."
2500
+ ),
2493
2501
  /** #575: a RELAY of the user's explicitly stated preference, never the agent's
2494
2502
  * choice. Outranks waiting in both directions: 'call' rings even for a
2495
2503
  * waiting:'none' "call me when it's done"; 'message' never rings even for
@@ -2776,6 +2784,25 @@ var NotifyResponseSchema = z.object({
2776
2784
  answer: UserAnswerSchema.optional(),
2777
2785
  answeredAt: z.string().datetime().optional()
2778
2786
  });
2787
+ var NotifyPlanUnitSchema = z.object({
2788
+ notificationId: z.string(),
2789
+ /** The unit's own heading, so the agent can tell which of its paragraphs this became. */
2790
+ title: z.string(),
2791
+ /** How loudly this unit was arbitrated to arrive — per unit, which is the point of units. */
2792
+ level: NotifyLevelSchema,
2793
+ /** Answered from something the user already decided: nobody is interrupted, and a trail card
2794
+ * says so. The agent should not wait on this one. */
2795
+ settled: z.literal(true).optional(),
2796
+ /** What this unit would need to be answerable and does not carry (#894). A PROPOSAL to the
2797
+ * agent — nothing here changed the ask, and ignoring it costs nothing. */
2798
+ needs: z.array(z.enum(["options", "visuals"])).optional()
2799
+ });
2800
+ var NotifyPlanSchema = z.object({
2801
+ units: z.array(NotifyPlanUnitSchema),
2802
+ /** Which unit is this arrival's ONE interruption (units-design.md D23). Absent means nobody
2803
+ * was interrupted — every unit was either settled or quiet enough to sit in the inbox. */
2804
+ speaks: z.string().optional()
2805
+ });
2779
2806
  var UserResponseSchema = z.object({
2780
2807
  requestId: z.string(),
2781
2808
  answer: UserAnswerSchema,
@@ -2830,6 +2857,20 @@ var InboxItemSchema = z.object({
2830
2857
  /** The conversation thread + connection this item lives on. Present on the replied
2831
2858
  * detail — they power History's "Continue" / "New session from this" (#57/#251). */
2832
2859
  parentId: z.string().optional(),
2860
+ /** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
2861
+ * arrival and its units are N rows of it, so this — not `parentId` — is what makes a
2862
+ * multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
2863
+ * accumulates every message an agent ever sent, so grouping by it renders a day of
2864
+ * unrelated updates as a single "12-part request". Absent on rows written before the
2865
+ * `asks` table, and on anything that never went through `notify` — both fall back to the
2866
+ * thread, which is what the client did for all rows until now. */
2867
+ askId: z.string().optional(),
2868
+ /** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
2869
+ * shares one `created_at` to the microsecond, so without it the author's order is
2870
+ * unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
2871
+ * (live 2026-08-10, D35). The API already orders by it; this lets a reader that
2872
+ * re-sorts (grouping, filtering) put an arrival back in the order it was written. */
2873
+ seq: z.number().int().optional(),
2833
2874
  tokenId: z.string().optional(),
2834
2875
  status: NotifyStatusSchema,
2835
2876
  context: ContextSchema,
@@ -3279,6 +3320,11 @@ var DeviceTokenSchema = z.object({
3279
3320
  /** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
3280
3321
  * that survives a rename. Cached by the host's identity beat. */
3281
3322
  token_id: z.string().nullable().optional(),
3323
+ /** WHERE this identity works — the folder a wake should land it in. Written by the host
3324
+ * at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
3325
+ * landed in the FIRST granted workspace and the agent rediscovered its own repo from
3326
+ * the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
3327
+ workspace: z.string().nullable().optional(),
3282
3328
  phone_reveal: PairingRevealSchema.nullable().optional(),
3283
3329
  // present once the phone reveals
3284
3330
  uik_pub: z.string().nullable().optional()
@@ -4067,6 +4113,14 @@ async function setIdentity(patch, opts = {}) {
4067
4113
  if (!res.ok) throw new Error(`set_identity failed: ${res.status} ${await res.text()}`);
4068
4114
  return await res.json();
4069
4115
  }
4116
+ async function heartbeat(runtime, opts = {}) {
4117
+ const res = ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
4118
+ method: "POST",
4119
+ headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
4120
+ ...runtime !== void 0 ? { body: JSON.stringify({ runtime }) } : {}
4121
+ }));
4122
+ if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
4123
+ }
4070
4124
  async function setTaskState(notificationId, state, opts = {}) {
4071
4125
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify/${notificationId}/state`, {
4072
4126
  method: "PATCH",
@@ -4143,6 +4197,7 @@ export {
4143
4197
  hatch,
4144
4198
  whoAmI,
4145
4199
  setIdentity,
4200
+ heartbeat,
4146
4201
  setTaskState,
4147
4202
  registerDelivery,
4148
4203
  scheduleCallback,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  AGENT_NAME
3
- } from "./chunk-3QSGCROD.js";
3
+ } from "./chunk-2WPQJYJH.js";
4
4
 
5
5
  // src/clients.ts
6
6
  import { execFile } from "child_process";
@@ -151,6 +151,14 @@ var NotifyRequestSchema = z.object({
151
151
  waiting: z.enum(["none", "soft", "hard"]).optional().describe(
152
152
  "With `ask`: what happens to your work while you wait. 'none' = you're just informing the user. 'soft' = you'd like an answer but can keep working. 'hard' = you are stopped until they answer (reaches them urgently and escalates to a real phone call if unanswered). Replaces urgencyHint + blocking \u2014 send this one field."
153
153
  ),
154
+ /** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
155
+ * interrupted. Opt-in per claim, because the fail-open is a minute-granularity cron —
156
+ * holding by default would charge every quiet claim that minute before any agent could
157
+ * correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
158
+ * and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
159
+ confirm: z.boolean().optional().describe(
160
+ "Hold this one so you can correct the plan before the user is interrupted. The response comes back with `held: true` and the plan; POST the confirm route to release it (with options/visuals/urgency corrections, or nothing at all). If you never do, it is announced anyway a couple of minutes later. Ignored when waiting is 'hard'."
161
+ ),
154
162
  /** #575: a RELAY of the user's explicitly stated preference, never the agent's
155
163
  * choice. Outranks waiting in both directions: 'call' rings even for a
156
164
  * waiting:'none' "call me when it's done"; 'message' never rings even for
@@ -382,6 +390,25 @@ var NotifyResponseSchema = z.object({
382
390
  answer: UserAnswerSchema.optional(),
383
391
  answeredAt: z.string().datetime().optional()
384
392
  });
393
+ var NotifyPlanUnitSchema = z.object({
394
+ notificationId: z.string(),
395
+ /** The unit's own heading, so the agent can tell which of its paragraphs this became. */
396
+ title: z.string(),
397
+ /** How loudly this unit was arbitrated to arrive — per unit, which is the point of units. */
398
+ level: NotifyLevelSchema,
399
+ /** Answered from something the user already decided: nobody is interrupted, and a trail card
400
+ * says so. The agent should not wait on this one. */
401
+ settled: z.literal(true).optional(),
402
+ /** What this unit would need to be answerable and does not carry (#894). A PROPOSAL to the
403
+ * agent — nothing here changed the ask, and ignoring it costs nothing. */
404
+ needs: z.array(z.enum(["options", "visuals"])).optional()
405
+ });
406
+ var NotifyPlanSchema = z.object({
407
+ units: z.array(NotifyPlanUnitSchema),
408
+ /** Which unit is this arrival's ONE interruption (units-design.md D23). Absent means nobody
409
+ * was interrupted — every unit was either settled or quiet enough to sit in the inbox. */
410
+ speaks: z.string().optional()
411
+ });
385
412
  var UserResponseSchema = z.object({
386
413
  requestId: z.string(),
387
414
  answer: UserAnswerSchema,
@@ -436,6 +463,20 @@ var InboxItemSchema = z.object({
436
463
  /** The conversation thread + connection this item lives on. Present on the replied
437
464
  * detail — they power History's "Continue" / "New session from this" (#57/#251). */
438
465
  parentId: z.string().optional(),
466
+ /** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
467
+ * arrival and its units are N rows of it, so this — not `parentId` — is what makes a
468
+ * multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
469
+ * accumulates every message an agent ever sent, so grouping by it renders a day of
470
+ * unrelated updates as a single "12-part request". Absent on rows written before the
471
+ * `asks` table, and on anything that never went through `notify` — both fall back to the
472
+ * thread, which is what the client did for all rows until now. */
473
+ askId: z.string().optional(),
474
+ /** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
475
+ * shares one `created_at` to the microsecond, so without it the author's order is
476
+ * unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
477
+ * (live 2026-08-10, D35). The API already orders by it; this lets a reader that
478
+ * re-sorts (grouping, filtering) put an arrival back in the order it was written. */
479
+ seq: z.number().int().optional(),
439
480
  tokenId: z.string().optional(),
440
481
  status: NotifyStatusSchema,
441
482
  context: ContextSchema,
@@ -887,6 +928,11 @@ var DeviceTokenSchema = z.object({
887
928
  /** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
888
929
  * that survives a rename. Cached by the host's identity beat. */
889
930
  token_id: z.string().nullable().optional(),
931
+ /** WHERE this identity works — the folder a wake should land it in. Written by the host
932
+ * at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
933
+ * landed in the FIRST granted workspace and the agent rediscovered its own repo from
934
+ * the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
935
+ workspace: z.string().nullable().optional(),
890
936
  phone_reveal: PairingRevealSchema.nullable().optional(),
891
937
  // present once the phone reveals
892
938
  uik_pub: z.string().nullable().optional()
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  HandoffSchema
4
- } from "./chunk-KMPAFUT6.js";
4
+ } from "./chunk-PYORUJ7Q.js";
5
5
  import {
6
6
  PAIGY_TOOL_IDS,
7
7
  autoConfigureClients,
8
8
  claudeInstallHint,
9
9
  enablePaigyTools,
10
10
  paigyToolsAllowlisted
11
- } from "./chunk-57N3NFYG.js";
11
+ } from "./chunk-ENE65M5Y.js";
12
12
  import {
13
13
  clearSurface,
14
14
  writeSurface
@@ -29,6 +29,7 @@ import {
29
29
  getThread,
30
30
  handoff,
31
31
  hatch,
32
+ heartbeat,
32
33
  lintNotify,
33
34
  listSlots,
34
35
  normalizeSpeech,
@@ -49,7 +50,7 @@ import {
49
50
  startE2ee,
50
51
  submitNotification,
51
52
  whoAmI
52
- } from "./chunk-3QSGCROD.js";
53
+ } from "./chunk-2WPQJYJH.js";
53
54
 
54
55
  // src/index.ts
55
56
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -128,7 +129,7 @@ var CONTACT_SCHEMA = {
128
129
  },
129
130
  required: ["ask"]
130
131
  };
131
- var CONTACT_DESCRIPTION = "Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in `ask`, say what happens to your work while you wait in `waiting`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId } \u2014 pass notificationId to await_reply for the answer, parentId to a later contact to continue the conversation. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use `needs` for a genuinely multi-part NEW ask.";
132
+ var CONTACT_DESCRIPTION = "Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in `ask`, say what happens to your work while you wait in `waiting`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId } \u2014 pass notificationId to await_reply for the answer, parentId to a later contact to continue the conversation. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take (\"Rings again 10 min, 30 min and 2 hr after the missed call, then leaves it in your inbox\"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use `needs` for a genuinely multi-part NEW ask.";
132
133
  var ONBOARD_DESCRIPTION = "Get this agent talking to Paigy \u2014 call it FIRST, before contact/await_reply, and any time you're unsure who you are. One call, and it does whatever the situation needs: NOT SET UP \u2192 hatches an identity instantly if this machine holds a device credential (the user ran the Paigy desktop app or harness), otherwise starts the code ceremony; ALREADY SET UP \u2192 returns your current identity and offers the two things left to decide, renaming it or unpairing; TOKEN NO LONGER VALID \u2192 says so, then re-pairs. Pass { name, voice } to choose who you are when hatching, or to RENAME yourself when already set up (voices: rachel, george, jessica, brian, lily). Safe to call any time: idempotent, and it never writes settings \u2014 the tool-allowlist state it reports is read-only. If it returns a `user_code`, print it to the user immediately and call onboard again with the `device_code`. If it returns `enable_prompt`, ask the user that question and call `enable_tools` on yes.";
133
134
 
134
135
  // src/pairing.ts
@@ -687,5 +688,21 @@ async function handleTool(request, signal) {
687
688
  throw new Error(`Unknown tool: ${request.params.name}`);
688
689
  }
689
690
  }
691
+ var beatDown = false;
692
+ async function beat() {
693
+ try {
694
+ const token = readToken();
695
+ if (!token) return;
696
+ await heartbeat(void 0, { token });
697
+ if (beatDown) console.error("paigy: presence restored \u2014 the live dot is honest again");
698
+ beatDown = false;
699
+ } catch (err) {
700
+ if (err instanceof UnpairedError) return;
701
+ if (!beatDown) console.error(`paigy: presence beat failed \u2014 the phone may show this agent offline (${String(err)})`);
702
+ beatDown = true;
703
+ }
704
+ }
705
+ void beat();
706
+ setInterval(() => void beat(), 6e4).unref();
690
707
  var transport = new StdioServerTransport();
691
708
  await server.connect(transport);
package/dist/listen.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  WAKE_EVENT,
4
4
  wakeChannel
5
- } from "./chunk-KMPAFUT6.js";
5
+ } from "./chunk-PYORUJ7Q.js";
6
6
  import {
7
7
  checkReplies,
8
8
  registerDelivery
9
- } from "./chunk-3QSGCROD.js";
9
+ } from "./chunk-2WPQJYJH.js";
10
10
 
11
11
  // src/listen.ts
12
12
  import { createClient } from "@supabase/supabase-js";
package/dist/onboard.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  autoConfigureClients,
4
4
  claudeInstallHint,
5
5
  openBrowser
6
- } from "./chunk-57N3NFYG.js";
6
+ } from "./chunk-ENE65M5Y.js";
7
7
  import {
8
8
  AGENT_NAME,
9
9
  TOKEN_PATH,
@@ -17,7 +17,7 @@ import {
17
17
  setIdentity,
18
18
  sleep,
19
19
  whoAmI
20
- } from "./chunk-3QSGCROD.js";
20
+ } from "./chunk-2WPQJYJH.js";
21
21
 
22
22
  // src/onboard.ts
23
23
  async function main() {
@@ -6,7 +6,7 @@ import {
6
6
  BACKEND_URL,
7
7
  reach,
8
8
  readToken
9
- } from "./chunk-3QSGCROD.js";
9
+ } from "./chunk-2WPQJYJH.js";
10
10
 
11
11
  // src/statusline.ts
12
12
  import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/mcp",
3
- "version": "0.30.0",
3
+ "version": "0.31.1",
4
4
  "description": "Paigy MCP server — a voice inbox for your AI agents. Lets an agent notify a user and await their reply.",
5
5
  "license": "MIT",
6
6
  "type": "module",