@paigy/mcp 0.25.1 → 0.26.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
@@ -11,9 +11,9 @@ A voice inbox for your AI agents. This MCP server lets an agent **notify a user*
11
11
  /plugin install paigy
12
12
  ```
13
13
 
14
- The Paigy MCP connects automatically. The first time an agent uses it while
15
- unpaired, it prompts you to pair — run `/paigy-onboard` (opens your browser to
16
- approve).
14
+ The Paigy MCP connects automatically. On the first Paigy interaction while
15
+ unpaired, the agent shows a pairing code; approve it in Paigy. `/paigy-onboard`
16
+ remains the terminal-only fallback.
17
17
 
18
18
  > [!NOTE]
19
19
  > If you are installing the plugin inside an active Claude Code session, you must type `/reload-plugins` (or restart the session) afterward so the terminal client starts the MCP server and exposes the new tools to the agent.
@@ -2274,6 +2274,48 @@ async function reach(url, init) {
2274
2274
  throw new Error(`${NETWORK_MSG} (${e?.message ?? String(e)})`);
2275
2275
  }
2276
2276
  }
2277
+ var TITLE_MAX = 90;
2278
+ var CHUNKS_MAX = 8;
2279
+ var CHUNK_MAX = 300;
2280
+ var ASK_MAX = 1e4;
2281
+ var OPTION_MAX = 80;
2282
+ var NEEDS_MAX = 6;
2283
+ var UNSPEAKABLE = /```|\n/;
2284
+ function lintNotify(req) {
2285
+ const problems = [];
2286
+ if (req.ask !== void 0) {
2287
+ if (req.ask.length > ASK_MAX)
2288
+ problems.push(`ask is ${req.ask.length} chars \u2014 state the need and why it matters now in \u2264${ASK_MAX}; move detail into a smaller follow-up`);
2289
+ if (UNSPEAKABLE.test(req.ask))
2290
+ problems.push("ask contains code fences or newlines \u2014 write it as plain prose (it may be read aloud on a call)");
2291
+ for (const n of req.needs ?? []) {
2292
+ if (n.length > OPTION_MAX) problems.push(`need "${n.slice(0, 40)}\u2026" is too long \u2014 each need is a short phrase (\u2264${OPTION_MAX} chars)`);
2293
+ }
2294
+ if ((req.needs?.length ?? 0) > NEEDS_MAX)
2295
+ problems.push(`${req.needs?.length} needs \u2014 cap at ${NEEDS_MAX}; a call can't cover more in one conversation, split the rest into a second ask`);
2296
+ return problems;
2297
+ }
2298
+ const spoken = req.urgency === "call" || req.urgency === "banner";
2299
+ if (req.context) {
2300
+ if (req.context.title.length > TITLE_MAX)
2301
+ problems.push(`title is ${req.context.title.length} chars \u2014 shorten to \u2264${TITLE_MAX} (it's what shows on the banner / gets spoken on a ring)`);
2302
+ if (UNSPEAKABLE.test(req.context.title))
2303
+ problems.push("title contains code fences or newlines \u2014 one plain-prose line");
2304
+ if (req.context.description.length > CHUNKS_MAX)
2305
+ problems.push(`${req.context.description.length} description chunks \u2014 cap at ${CHUNKS_MAX}; merge or drop the rest`);
2306
+ for (const [i, chunk] of req.context.description.entries()) {
2307
+ if (chunk.length > CHUNK_MAX)
2308
+ problems.push(`description[${i}] is ${chunk.length} chars \u2014 split it into standalone points of \u2264${CHUNK_MAX}`);
2309
+ }
2310
+ if (spoken && UNSPEAKABLE.test(req.context.description.join(" ")))
2311
+ problems.push("urgency is 'call'/'banner' but the description has code fences/newlines-in-chunk \u2014 rewrite in spoken register (it will be read aloud)");
2312
+ }
2313
+ for (const o of req.options ?? []) {
2314
+ if (o.label.length > OPTION_MAX)
2315
+ problems.push(`option label "${o.label.slice(0, 40)}\u2026" is ${o.label.length} chars \u2014 labels must read at a glance (\u2264${OPTION_MAX}); move detail into the description`);
2316
+ }
2317
+ return problems;
2318
+ }
2277
2319
  var ContextSchema = z.object({
2278
2320
  title: z.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
2279
2321
  description: z.array(z.string().min(1)).min(1).describe("Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand.")
@@ -2301,7 +2343,7 @@ var TransformSchema = z.enum([
2301
2343
  var OptionSchema = z.object({
2302
2344
  id: z.string(),
2303
2345
  label: z.string(),
2304
- // .describe() flows into the MCP notify_user JSON schema (zodToJsonSchema), so
2346
+ // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
2305
2347
  // the constraints below are what an agent reads when deciding to use these.
2306
2348
  html: z.string().max(16384).describe(
2307
2349
  "Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
@@ -2396,8 +2438,13 @@ var NotifyRequestSchema = z.object({
2396
2438
  * matters now — the broker derives the optimal shape and channel. Mutually
2397
2439
  * exclusive with `context` (and never sent alongside `envelope`: E2EE pairings
2398
2440
  * derive agent-side before sealing, so the server only ever shapes plaintext). */
2399
- ask: z.string().min(1).optional().describe(
2400
- 'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
2441
+ // 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
2442
+ // the ask into topics and gives each one at most three sentences and one question
2443
+ // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
2444
+ // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
2445
+ // come from the broker splitting and summarizing." The cap that remains is a size guard.
2446
+ ask: z.string().min(1).max(1e4).optional().describe(
2447
+ 'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Write as much as the situation needs (up to 10k characters) \u2014 Paigy breaks it into topics and reads it back a few sentences at a time; do NOT pre-summarize it into one line. Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
2401
2448
  ),
2402
2449
  needs: z.array(z.string().min(1)).optional().describe(
2403
2450
  "With `ask` only: the distinct things you need answered when the ask is multi-part \u2014 becomes the coverage contract (`points`), so a half-answer is never silently final."
@@ -2470,6 +2517,23 @@ function normalizeWaiting(req) {
2470
2517
  blocking: req.blocking || waiting === "hard"
2471
2518
  };
2472
2519
  }
2520
+ var DERIVE_CHUNKS_MAX = 8;
2521
+ var DERIVE_CHUNK_MAX = 300;
2522
+ function chunkAsk(text, max = DERIVE_CHUNK_MAX, cap = DERIVE_CHUNKS_MAX) {
2523
+ const body = text.trim();
2524
+ if (!body) return [];
2525
+ const sentences = body.match(/[^.!?]+[.!?]*\s*/g) ?? [body];
2526
+ const chunks = [];
2527
+ for (const raw of sentences) {
2528
+ const s = raw.trim();
2529
+ if (!s) continue;
2530
+ const last = chunks[chunks.length - 1];
2531
+ if (last !== void 0 && `${last} ${s}`.length <= max) chunks[chunks.length - 1] = `${last} ${s}`;
2532
+ else chunks.push(s);
2533
+ }
2534
+ if (chunks.length <= cap) return chunks;
2535
+ return [...chunks.slice(0, cap - 1), chunks.slice(cap - 1).join(" ")];
2536
+ }
2473
2537
  function deriveAsk(req) {
2474
2538
  req = normalizeWaiting(req);
2475
2539
  if (!req.ask) return req;
@@ -2481,7 +2545,7 @@ function deriveAsk(req) {
2481
2545
  const { ask: _ask, needs, urgencyHint: _hint, channel: _channel, ...rest } = req;
2482
2546
  return {
2483
2547
  ...rest,
2484
- context: { title, description: [text] },
2548
+ context: { title, description: chunkAsk(text) },
2485
2549
  // Options riding alongside the ask (#575: pixels can't be prose) floor to a
2486
2550
  // single pick — the model broker may upgrade to many/rank from the wording.
2487
2551
  select: req.options?.length ? "one" : "text",
@@ -2499,17 +2563,17 @@ var TurnSchema = z.object({
2499
2563
  reply: z.string()
2500
2564
  });
2501
2565
  var UserAnswerSchema = z.discriminatedUnion("kind", [
2502
- z.object({ kind: z.literal("option"), optionId: z.string() }),
2566
+ z.object({ kind: z.literal("option"), optionId: z.string(), label: z.string().optional() }),
2503
2567
  z.object({ kind: z.literal("text"), text: z.string() }),
2504
2568
  z.object({ kind: z.literal("ignored") }),
2505
- z.object({ kind: z.literal("multi"), optionIds: z.array(z.string()) }),
2506
- z.object({ kind: z.literal("ranked"), optionIds: z.array(z.string()) }),
2569
+ z.object({ kind: z.literal("multi"), optionIds: z.array(z.string()), labels: z.array(z.string()).optional() }),
2570
+ z.object({ kind: z.literal("ranked"), optionIds: z.array(z.string()), labels: z.array(z.string()).optional() }),
2507
2571
  z.object({ kind: z.literal("clarify"), chunks: z.array(z.string()).min(1) }),
2508
2572
  z.object({ kind: z.literal("confirm"), approved: z.boolean() }),
2509
2573
  z.object({ kind: z.literal("turns"), turns: z.array(TurnSchema).min(1) })
2510
2574
  ]);
2511
2575
  var IntentSchema = z.object({
2512
- kind: z.enum(["defer", "delegate", "channel"]),
2576
+ kind: z.enum(["defer", "delegate", "channel", "question"]),
2513
2577
  detail: z.string(),
2514
2578
  /** Landed defer (#397): the MCP parses common spoken forms ("in 20 minutes",
2515
2579
  * "after lunch") against the agent machine's clock — the user's — and attaches
@@ -2547,11 +2611,36 @@ var AwaitItemSchema = z.discriminatedUnion("type", [
2547
2611
  /** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
2548
2612
  remindInSeconds: z.number()
2549
2613
  }),
2614
+ /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
2615
+ * post-feedback revision, #633) — the user will never answer this id. Stop
2616
+ * awaiting it; the live ask is the thread's newest turn (await that one, or
2617
+ * re-orient via get_thread / check_replies). */
2618
+ z.object({
2619
+ type: z.literal("superseded"),
2620
+ threadId: z.string(),
2621
+ notificationId: z.string()
2622
+ }),
2623
+ /** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
2624
+ * revise any of these until the final reply arrives — partial = intelligence,
2625
+ * settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
2626
+ * irreversibly. If `acts` carries a question aimed at you and you know the answer,
2627
+ * contact on the same thread right away — the caller hears it on the same call. */
2628
+ z.object({
2629
+ type: z.literal("partial"),
2630
+ notificationId: z.string(),
2631
+ inFlight: z.literal(true),
2632
+ turn: z.object({
2633
+ idx: z.number(),
2634
+ prompt: z.string(),
2635
+ reply: z.string(),
2636
+ acts: z.array(IntentSchema).nullable().optional()
2637
+ })
2638
+ }),
2550
2639
  z.object({ type: z.literal("idle") })
2551
2640
  ]);
2552
2641
  var CallbackTriggerSchema = z.enum(["on_done", "on_blocked", "scheduled"]);
2553
2642
  var ScheduleCallbackSchema = z.object({
2554
- threadId: z.string().describe("The thread to call back on (from a prior notify_user / reply / request)."),
2643
+ threadId: z.string().describe("The thread to call back on (from a prior contact / reply / request)."),
2555
2644
  trigger: CallbackTriggerSchema,
2556
2645
  dueInSeconds: z.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
2557
2646
  note: z.string().optional().describe("What to tell the user when you follow up.")
@@ -2579,7 +2668,7 @@ var PendingRepliesSchema = z.object({
2579
2668
  z.object({ threadId: z.string(), notificationId: z.string(), createdAt: z.string() })
2580
2669
  ),
2581
2670
  /** User-initiated requests addressed to this agent; act on them and reply via
2582
- * notify_user on the same threadId. Keeps reappearing until you call
2671
+ * contact on the same threadId. Keeps reappearing until you call
2583
2672
  * set_task_state on its notificationId. */
2584
2673
  requests: z.array(
2585
2674
  z.object({
@@ -2594,7 +2683,7 @@ var PendingRepliesSchema = z.object({
2594
2683
  ),
2595
2684
  /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
2596
2685
  * if blocked, or at a time that has passed). Re-surfaced every sweep until you
2597
- * fulfill one by calling notify_user on its threadId. */
2686
+ * fulfill one by calling contact on its threadId. */
2598
2687
  owedCallbacks: z.array(
2599
2688
  z.object({ threadId: z.string(), trigger: CallbackTriggerSchema, note: z.string() })
2600
2689
  ),
@@ -2603,6 +2692,26 @@ var PendingRepliesSchema = z.object({
2603
2692
  * went idle. Report a real state (set_task_state) or continue the work. */
2604
2693
  stalled: z.array(
2605
2694
  z.object({ threadId: z.string(), notificationId: z.string(), title: z.string().nullable(), startedAt: z.string() })
2695
+ ),
2696
+ /** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
2697
+ * thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
2698
+ * a thread's `items` into a single turn rather than interleaving threads. `busy` = the
2699
+ * thread already has a turn in progress (younger than the stall cutoff); let it finish and
2700
+ * ride the next turn. `items` are that thread's replies/requests in arrival order; the
2701
+ * full payload for each is in the flat `replies`/`requests` arrays (matched by
2702
+ * notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
2703
+ threads: z.array(
2704
+ z.object({
2705
+ threadId: z.string(),
2706
+ busy: z.boolean(),
2707
+ items: z.array(
2708
+ z.object({
2709
+ kind: z.enum(["reply", "request"]),
2710
+ notificationId: z.string(),
2711
+ at: z.string()
2712
+ })
2713
+ )
2714
+ })
2606
2715
  )
2607
2716
  });
2608
2717
  var NotifyResponseSchema = z.object({
@@ -2627,6 +2736,16 @@ var UserResponseSchema = z.object({
2627
2736
  covered: z.array(z.string()).optional()
2628
2737
  });
2629
2738
  var VoiceKeySchema = z.enum(["rachel", "george", "jessica", "brian", "lily"]);
2739
+ var AgendaTurnSchema = z.object({
2740
+ /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
2741
+ * line went out on 2026-07-28 and the caller could not answer it at all. */
2742
+ info: z.array(z.string().min(1)).max(3).default([]),
2743
+ question: z.string().min(1).nullable(),
2744
+ /** True on the one turn carrying the agent's own declared question. */
2745
+ asks: z.boolean().optional(),
2746
+ select: SelectShapeSchema.optional(),
2747
+ options: z.array(OptionSchema.omit({ id: true })).optional()
2748
+ });
2630
2749
  var InboxItemSchema = z.object({
2631
2750
  id: z.string(),
2632
2751
  /** The conversation thread + connection this item lives on. Present on the replied
@@ -2638,9 +2757,11 @@ var InboxItemSchema = z.object({
2638
2757
  options: z.array(OptionSchema).optional(),
2639
2758
  /** The ask's declared coverage points (#396), when the agent sent them. */
2640
2759
  points: z.array(z.string()).optional(),
2641
- /** Natural spoken questions for the points (#417 follow-up), phrased at ring time
2642
- * and index-aligned — the call bot prefers these over the raw shorthand. */
2643
- pointsSpeak: z.array(z.string()).optional(),
2760
+ /** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
2761
+ * ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
2762
+ * between them could not express a call as a sequence. `question: null` is a real turn —
2763
+ * a status update stays a statement instead of being shaped into a yes/no. */
2764
+ agenda: z.array(AgendaTurnSchema).optional(),
2644
2765
  /** On a replied detail (#397): the next steps the user attached to the answer
2645
2766
  * ("call back after lunch") — shown so they can see the commitment was captured. */
2646
2767
  intents: z.array(IntentSchema).optional(),
@@ -2738,6 +2859,14 @@ var UserSettingsSchema = z.object({
2738
2859
  * must not silently reset this privacy choice. Absent = leave unchanged on
2739
2860
  * write, 'hosted' on read (see store.ts). */
2740
2861
  voiceMode: z.enum(["hosted", "on_device"]).optional(),
2862
+ /** Per-user ring budget (#603): calls per rolling day before further calls
2863
+ * degrade to banner. Absent = the global default (25). A number, never a
2864
+ * bypass — every account keeps a ceiling. No UI; set per user for testing. */
2865
+ /** Per-user voice-call tuning (#318): raw knobs forwarded to the call bot's
2866
+ * payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
2867
+ * slower speaker). No API-side semantics; the bot resolves each key with its
2868
+ * own defaults. Set per user (no UI yet); absent = bot defaults. */
2869
+ voiceTuning: z.record(z.string(), z.union([z.number(), z.string()])).optional(),
2741
2870
  /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
2742
2871
  * defaulted — an older client PATCHing the full object must not clobber it. */
2743
2872
  pstnCalls: z.boolean().optional(),
@@ -2807,7 +2936,55 @@ var HandoffSchema = z.object({
2807
2936
  notes: z.array(z.string().min(1)).min(1),
2808
2937
  /** A sibling connection to dispatch directly to (token id or agent nickname). Same-account
2809
2938
  * only; omit to leave the thread for the user to hand off in the app. */
2810
- target: z.string().optional()
2939
+ target: z.string().optional(),
2940
+ /** Write the note as a RECAP (kind:'recap', #617): a summary turn that supersedes the
2941
+ * thread's earlier turns for rehydration — get_thread returns the latest recap + only
2942
+ * the turns after it. Handoff-to-a-successor and handoff-to-yourself-later are the
2943
+ * same primitive; a recap is one whose audience includes you. */
2944
+ recap: z.boolean().optional()
2945
+ });
2946
+ var NoteSourceSchema = z.enum(["app", "call"]);
2947
+ var NoteStatusSchema = z.enum(["open", "assigned", "done"]);
2948
+ var DecisionSchema = z.object({
2949
+ id: z.string(),
2950
+ /** The note this decision refines; null = recorded on a bare thread (the
2951
+ * extensibility seam — any conversation can accrue decisions). */
2952
+ noteId: z.string().nullable(),
2953
+ /** What was ambiguous — the broker's (or the user's own) question. */
2954
+ question: z.string(),
2955
+ /** The user's ruling; null while the question is open. */
2956
+ answer: z.string().nullable(),
2957
+ decidedAt: z.string().nullable(),
2958
+ createdAt: z.string()
2959
+ });
2960
+ var NoteSchema = z.object({
2961
+ id: z.string(),
2962
+ /** One-line headline (broker-titled; deterministic floor). */
2963
+ title: z.string(),
2964
+ /** The original intent, verbatim — assignees always see the user's own words. */
2965
+ intent: z.string(),
2966
+ source: NoteSourceSchema,
2967
+ status: NoteStatusSchema,
2968
+ /** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
2969
+ assignee: z.string().nullable(),
2970
+ /** The request thread minted at assignment; null until assigned. */
2971
+ threadId: z.string().nullable(),
2972
+ createdAt: z.string()
2973
+ });
2974
+ var CreateNoteSchema = z.object({
2975
+ /** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
2976
+ text: z.string().min(1).max(4e3)
2977
+ });
2978
+ var RecordDecisionSchema = z.object({
2979
+ /** An open decision (from /clarify) to answer. */
2980
+ decisionId: z.string().optional(),
2981
+ /** For an ad-hoc decision: what was ambiguous. Required without `decisionId`. */
2982
+ question: z.string().min(1).max(500).optional(),
2983
+ /** The ruling. */
2984
+ answer: z.string().min(1).max(2e3)
2985
+ }).refine((d) => d.decisionId || d.question, { message: "decisionId or question required" });
2986
+ var AssignNoteSchema = z.object({
2987
+ target: z.string().min(1)
2811
2988
  });
2812
2989
  var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
2813
2990
  var RegisterDeliverySchema = z.object({ mode: DeliveryModeSchema });
@@ -2958,6 +3135,8 @@ var NotificationFeedbackKindSchema = z.enum([
2958
3135
  // "These recommended answers aren't good — give me new ones."
2959
3136
  "needs_visual",
2960
3137
  // "There should be a picture or design here."
3138
+ "should_have_called",
3139
+ // "Don't put this in a banner — ring me for something like this."
2961
3140
  "other"
2962
3141
  // anything else — the note carries it.
2963
3142
  ]);
@@ -3560,6 +3739,25 @@ async function pollAnswer(notificationId) {
3560
3739
  const item = await res.json();
3561
3740
  return item.type === "idle" ? item : decryptItem(item);
3562
3741
  }
3742
+ var _partialSeen = /* @__PURE__ */ new Map();
3743
+ async function pollPartials(notificationId) {
3744
+ const token = readToken();
3745
+ const after = _partialSeen.get(notificationId) ?? 0;
3746
+ try {
3747
+ const res = await reach(
3748
+ `${BACKEND_URL}/api/await/partials?notificationId=${encodeURIComponent(notificationId)}&afterIdx=${after}`,
3749
+ { headers: { authorization: `Bearer ${token}` } }
3750
+ );
3751
+ if (!res.ok) return null;
3752
+ const body = await res.json();
3753
+ const next = body.turns?.[0];
3754
+ if (!next) return null;
3755
+ _partialSeen.set(notificationId, next.idx);
3756
+ return { type: "partial", notificationId, inFlight: true, turn: next };
3757
+ } catch {
3758
+ return null;
3759
+ }
3760
+ }
3563
3761
  async function awaitReply(notificationId, opts = {}) {
3564
3762
  const intervalMs = opts.intervalMs ?? 5e3;
3565
3763
  const windowMs = opts.windowMs ?? 3e5;
@@ -3569,6 +3767,8 @@ async function awaitReply(notificationId, opts = {}) {
3569
3767
  while (true) {
3570
3768
  const item = await pollAnswer(notificationId);
3571
3769
  if (item.type !== "idle") return item;
3770
+ const partial = await pollPartials(notificationId);
3771
+ if (partial) return partial;
3572
3772
  if (now() - start >= windowMs) return { type: "idle" };
3573
3773
  await doSleep(intervalMs);
3574
3774
  }
@@ -3596,6 +3796,13 @@ async function getThread(threadId) {
3596
3796
  if (!res.ok) throw new Error(`get_thread failed: ${res.status} ${await res.text()}`);
3597
3797
  return await res.json();
3598
3798
  }
3799
+ async function searchThreads(q) {
3800
+ const res = ensureAuthed(await reach(`${BACKEND_URL}/api/search?q=${encodeURIComponent(q)}`, {
3801
+ headers: { authorization: `Bearer ${readToken()}` }
3802
+ }));
3803
+ if (!res.ok) throw new Error(`search_threads failed: ${res.status} ${await res.text()}`);
3804
+ return await res.json();
3805
+ }
3599
3806
  async function checkReplies() {
3600
3807
  const token = readToken();
3601
3808
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
@@ -3651,52 +3858,11 @@ async function handoff(req) {
3651
3858
  if (!res.ok) throw new Error(`handoff failed: ${res.status} ${await res.text()}`);
3652
3859
  return await res.json();
3653
3860
  }
3654
- var TITLE_MAX = 90;
3655
- var CHUNKS_MAX = 8;
3656
- var CHUNK_MAX = 300;
3657
- var ASK_MAX = 600;
3658
- var OPTION_MAX = 80;
3659
- var NEEDS_MAX = 6;
3660
- var UNSPEAKABLE = /```|\n/;
3661
- function lintNotify(req) {
3662
- const problems = [];
3663
- if (req.ask !== void 0) {
3664
- if (req.ask.length > ASK_MAX)
3665
- problems.push(`ask is ${req.ask.length} chars \u2014 state the need and why it matters now in \u2264${ASK_MAX}; move detail into a smaller follow-up`);
3666
- if (UNSPEAKABLE.test(req.ask))
3667
- problems.push("ask contains code fences or newlines \u2014 write it as plain prose (it may be read aloud on a call)");
3668
- for (const n of req.needs ?? []) {
3669
- if (n.length > OPTION_MAX) problems.push(`need "${n.slice(0, 40)}\u2026" is too long \u2014 each need is a short phrase (\u2264${OPTION_MAX} chars)`);
3670
- }
3671
- if ((req.needs?.length ?? 0) > NEEDS_MAX)
3672
- problems.push(`${req.needs?.length} needs \u2014 cap at ${NEEDS_MAX}; a call can't cover more in one conversation, split the rest into a second ask`);
3673
- return problems;
3674
- }
3675
- const spoken = req.urgency === "call" || req.urgency === "banner";
3676
- if (req.context) {
3677
- if (req.context.title.length > TITLE_MAX)
3678
- problems.push(`title is ${req.context.title.length} chars \u2014 shorten to \u2264${TITLE_MAX} (it's what shows on the banner / gets spoken on a ring)`);
3679
- if (UNSPEAKABLE.test(req.context.title))
3680
- problems.push("title contains code fences or newlines \u2014 one plain-prose line");
3681
- if (req.context.description.length > CHUNKS_MAX)
3682
- problems.push(`${req.context.description.length} description chunks \u2014 cap at ${CHUNKS_MAX}; merge or drop the rest`);
3683
- for (const [i, chunk] of req.context.description.entries()) {
3684
- if (chunk.length > CHUNK_MAX)
3685
- problems.push(`description[${i}] is ${chunk.length} chars \u2014 split it into standalone points of \u2264${CHUNK_MAX}`);
3686
- }
3687
- if (spoken && UNSPEAKABLE.test(req.context.description.join(" ")))
3688
- problems.push("urgency is 'call'/'banner' but the description has code fences/newlines-in-chunk \u2014 rewrite in spoken register (it will be read aloud)");
3689
- }
3690
- for (const o of req.options ?? []) {
3691
- if (o.label.length > OPTION_MAX)
3692
- problems.push(`option label "${o.label.slice(0, 40)}\u2026" is ${o.label.length} chars \u2014 labels must read at a glance (\u2264${OPTION_MAX}); move detail into the description`);
3693
- }
3694
- return problems;
3695
- }
3696
3861
 
3697
3862
  export {
3698
3863
  BACKEND_URL,
3699
3864
  reach,
3865
+ lintNotify,
3700
3866
  NotifyRequestSchema,
3701
3867
  SetTaskStateSchema,
3702
3868
  ScheduleCallbackSchema,
@@ -3720,10 +3886,10 @@ export {
3720
3886
  submitNotification,
3721
3887
  awaitReply,
3722
3888
  getThread,
3889
+ searchThreads,
3723
3890
  checkReplies,
3724
3891
  setTaskState,
3725
3892
  registerDelivery,
3726
3893
  scheduleCallback,
3727
- handoff,
3728
- lintNotify
3894
+ handoff
3729
3895
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  AGENT_NAME
3
- } from "./chunk-GWYHSAU5.js";
3
+ } from "./chunk-B7SCZUYX.js";
4
4
 
5
5
  // src/clients.ts
6
6
  import { execFile } from "child_process";
@@ -27,7 +27,7 @@ var TransformSchema = z.enum([
27
27
  var OptionSchema = z.object({
28
28
  id: z.string(),
29
29
  label: z.string(),
30
- // .describe() flows into the MCP notify_user JSON schema (zodToJsonSchema), so
30
+ // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
31
31
  // the constraints below are what an agent reads when deciding to use these.
32
32
  html: z.string().max(16384).describe(
33
33
  "Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
@@ -122,8 +122,13 @@ var NotifyRequestSchema = z.object({
122
122
  * matters now — the broker derives the optimal shape and channel. Mutually
123
123
  * exclusive with `context` (and never sent alongside `envelope`: E2EE pairings
124
124
  * derive agent-side before sealing, so the server only ever shapes plaintext). */
125
- ask: z.string().min(1).optional().describe(
126
- 'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
125
+ // 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
126
+ // the ask into topics and gives each one at most three sentences and one question
127
+ // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
128
+ // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
129
+ // come from the broker splitting and summarizing." The cap that remains is a size guard.
130
+ ask: z.string().min(1).max(1e4).optional().describe(
131
+ 'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Write as much as the situation needs (up to 10k characters) \u2014 Paigy breaks it into topics and reads it back a few sentences at a time; do NOT pre-summarize it into one line. Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
127
132
  ),
128
133
  needs: z.array(z.string().min(1)).optional().describe(
129
134
  "With `ask` only: the distinct things you need answered when the ask is multi-part \u2014 becomes the coverage contract (`points`), so a half-answer is never silently final."
@@ -197,17 +202,17 @@ var TurnSchema = z.object({
197
202
  reply: z.string()
198
203
  });
199
204
  var UserAnswerSchema = z.discriminatedUnion("kind", [
200
- z.object({ kind: z.literal("option"), optionId: z.string() }),
205
+ z.object({ kind: z.literal("option"), optionId: z.string(), label: z.string().optional() }),
201
206
  z.object({ kind: z.literal("text"), text: z.string() }),
202
207
  z.object({ kind: z.literal("ignored") }),
203
- z.object({ kind: z.literal("multi"), optionIds: z.array(z.string()) }),
204
- z.object({ kind: z.literal("ranked"), optionIds: z.array(z.string()) }),
208
+ z.object({ kind: z.literal("multi"), optionIds: z.array(z.string()), labels: z.array(z.string()).optional() }),
209
+ z.object({ kind: z.literal("ranked"), optionIds: z.array(z.string()), labels: z.array(z.string()).optional() }),
205
210
  z.object({ kind: z.literal("clarify"), chunks: z.array(z.string()).min(1) }),
206
211
  z.object({ kind: z.literal("confirm"), approved: z.boolean() }),
207
212
  z.object({ kind: z.literal("turns"), turns: z.array(TurnSchema).min(1) })
208
213
  ]);
209
214
  var IntentSchema = z.object({
210
- kind: z.enum(["defer", "delegate", "channel"]),
215
+ kind: z.enum(["defer", "delegate", "channel", "question"]),
211
216
  detail: z.string(),
212
217
  /** Landed defer (#397): the MCP parses common spoken forms ("in 20 minutes",
213
218
  * "after lunch") against the agent machine's clock — the user's — and attaches
@@ -245,11 +250,36 @@ var AwaitItemSchema = z.discriminatedUnion("type", [
245
250
  /** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
246
251
  remindInSeconds: z.number()
247
252
  }),
253
+ /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
254
+ * post-feedback revision, #633) — the user will never answer this id. Stop
255
+ * awaiting it; the live ask is the thread's newest turn (await that one, or
256
+ * re-orient via get_thread / check_replies). */
257
+ z.object({
258
+ type: z.literal("superseded"),
259
+ threadId: z.string(),
260
+ notificationId: z.string()
261
+ }),
262
+ /** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
263
+ * revise any of these until the final reply arrives — partial = intelligence,
264
+ * settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
265
+ * irreversibly. If `acts` carries a question aimed at you and you know the answer,
266
+ * contact on the same thread right away — the caller hears it on the same call. */
267
+ z.object({
268
+ type: z.literal("partial"),
269
+ notificationId: z.string(),
270
+ inFlight: z.literal(true),
271
+ turn: z.object({
272
+ idx: z.number(),
273
+ prompt: z.string(),
274
+ reply: z.string(),
275
+ acts: z.array(IntentSchema).nullable().optional()
276
+ })
277
+ }),
248
278
  z.object({ type: z.literal("idle") })
249
279
  ]);
250
280
  var CallbackTriggerSchema = z.enum(["on_done", "on_blocked", "scheduled"]);
251
281
  var ScheduleCallbackSchema = z.object({
252
- threadId: z.string().describe("The thread to call back on (from a prior notify_user / reply / request)."),
282
+ threadId: z.string().describe("The thread to call back on (from a prior contact / reply / request)."),
253
283
  trigger: CallbackTriggerSchema,
254
284
  dueInSeconds: z.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
255
285
  note: z.string().optional().describe("What to tell the user when you follow up.")
@@ -277,7 +307,7 @@ var PendingRepliesSchema = z.object({
277
307
  z.object({ threadId: z.string(), notificationId: z.string(), createdAt: z.string() })
278
308
  ),
279
309
  /** User-initiated requests addressed to this agent; act on them and reply via
280
- * notify_user on the same threadId. Keeps reappearing until you call
310
+ * contact on the same threadId. Keeps reappearing until you call
281
311
  * set_task_state on its notificationId. */
282
312
  requests: z.array(
283
313
  z.object({
@@ -292,7 +322,7 @@ var PendingRepliesSchema = z.object({
292
322
  ),
293
323
  /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
294
324
  * if blocked, or at a time that has passed). Re-surfaced every sweep until you
295
- * fulfill one by calling notify_user on its threadId. */
325
+ * fulfill one by calling contact on its threadId. */
296
326
  owedCallbacks: z.array(
297
327
  z.object({ threadId: z.string(), trigger: CallbackTriggerSchema, note: z.string() })
298
328
  ),
@@ -301,6 +331,26 @@ var PendingRepliesSchema = z.object({
301
331
  * went idle. Report a real state (set_task_state) or continue the work. */
302
332
  stalled: z.array(
303
333
  z.object({ threadId: z.string(), notificationId: z.string(), title: z.string().nullable(), startedAt: z.string() })
334
+ ),
335
+ /** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
336
+ * thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
337
+ * a thread's `items` into a single turn rather than interleaving threads. `busy` = the
338
+ * thread already has a turn in progress (younger than the stall cutoff); let it finish and
339
+ * ride the next turn. `items` are that thread's replies/requests in arrival order; the
340
+ * full payload for each is in the flat `replies`/`requests` arrays (matched by
341
+ * notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
342
+ threads: z.array(
343
+ z.object({
344
+ threadId: z.string(),
345
+ busy: z.boolean(),
346
+ items: z.array(
347
+ z.object({
348
+ kind: z.enum(["reply", "request"]),
349
+ notificationId: z.string(),
350
+ at: z.string()
351
+ })
352
+ )
353
+ })
304
354
  )
305
355
  });
306
356
  var NotifyResponseSchema = z.object({
@@ -325,6 +375,16 @@ var UserResponseSchema = z.object({
325
375
  covered: z.array(z.string()).optional()
326
376
  });
327
377
  var VoiceKeySchema = z.enum(["rachel", "george", "jessica", "brian", "lily"]);
378
+ var AgendaTurnSchema = z.object({
379
+ /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
380
+ * line went out on 2026-07-28 and the caller could not answer it at all. */
381
+ info: z.array(z.string().min(1)).max(3).default([]),
382
+ question: z.string().min(1).nullable(),
383
+ /** True on the one turn carrying the agent's own declared question. */
384
+ asks: z.boolean().optional(),
385
+ select: SelectShapeSchema.optional(),
386
+ options: z.array(OptionSchema.omit({ id: true })).optional()
387
+ });
328
388
  var InboxItemSchema = z.object({
329
389
  id: z.string(),
330
390
  /** The conversation thread + connection this item lives on. Present on the replied
@@ -336,9 +396,11 @@ var InboxItemSchema = z.object({
336
396
  options: z.array(OptionSchema).optional(),
337
397
  /** The ask's declared coverage points (#396), when the agent sent them. */
338
398
  points: z.array(z.string()).optional(),
339
- /** Natural spoken questions for the points (#417 follow-up), phrased at ring time
340
- * and index-aligned — the call bot prefers these over the raw shorthand. */
341
- pointsSpeak: z.array(z.string()).optional(),
399
+ /** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
400
+ * ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
401
+ * between them could not express a call as a sequence. `question: null` is a real turn —
402
+ * a status update stays a statement instead of being shaped into a yes/no. */
403
+ agenda: z.array(AgendaTurnSchema).optional(),
342
404
  /** On a replied detail (#397): the next steps the user attached to the answer
343
405
  * ("call back after lunch") — shown so they can see the commitment was captured. */
344
406
  intents: z.array(IntentSchema).optional(),
@@ -436,6 +498,14 @@ var UserSettingsSchema = z.object({
436
498
  * must not silently reset this privacy choice. Absent = leave unchanged on
437
499
  * write, 'hosted' on read (see store.ts). */
438
500
  voiceMode: z.enum(["hosted", "on_device"]).optional(),
501
+ /** Per-user ring budget (#603): calls per rolling day before further calls
502
+ * degrade to banner. Absent = the global default (25). A number, never a
503
+ * bypass — every account keeps a ceiling. No UI; set per user for testing. */
504
+ /** Per-user voice-call tuning (#318): raw knobs forwarded to the call bot's
505
+ * payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
506
+ * slower speaker). No API-side semantics; the bot resolves each key with its
507
+ * own defaults. Set per user (no UI yet); absent = bot defaults. */
508
+ voiceTuning: z.record(z.string(), z.union([z.number(), z.string()])).optional(),
439
509
  /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
440
510
  * defaulted — an older client PATCHing the full object must not clobber it. */
441
511
  pstnCalls: z.boolean().optional(),
@@ -505,7 +575,55 @@ var HandoffSchema = z.object({
505
575
  notes: z.array(z.string().min(1)).min(1),
506
576
  /** A sibling connection to dispatch directly to (token id or agent nickname). Same-account
507
577
  * only; omit to leave the thread for the user to hand off in the app. */
508
- target: z.string().optional()
578
+ target: z.string().optional(),
579
+ /** Write the note as a RECAP (kind:'recap', #617): a summary turn that supersedes the
580
+ * thread's earlier turns for rehydration — get_thread returns the latest recap + only
581
+ * the turns after it. Handoff-to-a-successor and handoff-to-yourself-later are the
582
+ * same primitive; a recap is one whose audience includes you. */
583
+ recap: z.boolean().optional()
584
+ });
585
+ var NoteSourceSchema = z.enum(["app", "call"]);
586
+ var NoteStatusSchema = z.enum(["open", "assigned", "done"]);
587
+ var DecisionSchema = z.object({
588
+ id: z.string(),
589
+ /** The note this decision refines; null = recorded on a bare thread (the
590
+ * extensibility seam — any conversation can accrue decisions). */
591
+ noteId: z.string().nullable(),
592
+ /** What was ambiguous — the broker's (or the user's own) question. */
593
+ question: z.string(),
594
+ /** The user's ruling; null while the question is open. */
595
+ answer: z.string().nullable(),
596
+ decidedAt: z.string().nullable(),
597
+ createdAt: z.string()
598
+ });
599
+ var NoteSchema = z.object({
600
+ id: z.string(),
601
+ /** One-line headline (broker-titled; deterministic floor). */
602
+ title: z.string(),
603
+ /** The original intent, verbatim — assignees always see the user's own words. */
604
+ intent: z.string(),
605
+ source: NoteSourceSchema,
606
+ status: NoteStatusSchema,
607
+ /** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
608
+ assignee: z.string().nullable(),
609
+ /** The request thread minted at assignment; null until assigned. */
610
+ threadId: z.string().nullable(),
611
+ createdAt: z.string()
612
+ });
613
+ var CreateNoteSchema = z.object({
614
+ /** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
615
+ text: z.string().min(1).max(4e3)
616
+ });
617
+ var RecordDecisionSchema = z.object({
618
+ /** An open decision (from /clarify) to answer. */
619
+ decisionId: z.string().optional(),
620
+ /** For an ad-hoc decision: what was ambiguous. Required without `decisionId`. */
621
+ question: z.string().min(1).max(500).optional(),
622
+ /** The ruling. */
623
+ answer: z.string().min(1).max(2e3)
624
+ }).refine((d) => d.decisionId || d.question, { message: "decisionId or question required" });
625
+ var AssignNoteSchema = z.object({
626
+ target: z.string().min(1)
509
627
  });
510
628
  var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
511
629
  var WAKE_EVENT = "wake";
@@ -658,6 +776,8 @@ var NotificationFeedbackKindSchema = z.enum([
658
776
  // "These recommended answers aren't good — give me new ones."
659
777
  "needs_visual",
660
778
  // "There should be a picture or design here."
779
+ "should_have_called",
780
+ // "Don't put this in a banner — ring me for something like this."
661
781
  "other"
662
782
  // anything else — the note carries it.
663
783
  ]);
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  HandoffSchema
4
- } from "./chunk-RXOUZ7WR.js";
4
+ } from "./chunk-KAEJRPN5.js";
5
5
  import {
6
6
  PAIGY_TOOL_IDS,
7
7
  autoConfigureClients,
8
8
  claudeInstallHint,
9
9
  enablePaigyTools
10
- } from "./chunk-63VXNOEC.js";
10
+ } from "./chunk-BZPE2ZFE.js";
11
11
  import {
12
12
  clearSurface,
13
13
  writeSurface
@@ -34,11 +34,12 @@ import {
34
34
  saveKeyFile,
35
35
  saveToken,
36
36
  scheduleCallback,
37
+ searchThreads,
37
38
  setTaskState,
38
39
  sleep,
39
40
  startE2ee,
40
41
  submitNotification
41
- } from "./chunk-GWYHSAU5.js";
42
+ } from "./chunk-B7SCZUYX.js";
42
43
 
43
44
  // src/index.ts
44
45
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -82,7 +83,7 @@ var CONTACT_SCHEMA = {
82
83
  properties: {
83
84
  ask: {
84
85
  type: "string",
85
- description: "What to tell the user, or what you need to find out from them. Plain prose, one or two sentences; may be spoken aloud on a call, so write natural speech and name things (not IDs)."
86
+ description: "What to tell the user, or what you need to find out from them. Plain prose \u2014 as long as it needs to be (up to 10k characters); Paigy splits it into topics and reads back a few sentences at a time, so do NOT compress a briefing into one line. May be spoken aloud on a call, so write natural speech and name things (not IDs)."
86
87
  },
87
88
  waiting: {
88
89
  type: "string",
@@ -163,6 +164,21 @@ async function resolvePairing(deviceCode, capMs, pollMs = 2e3) {
163
164
  return { kind: "pending" };
164
165
  }
165
166
  var bg = null;
167
+ var started = null;
168
+ async function startPairing(agent) {
169
+ if (started) return started;
170
+ const { keyFile, offer } = startE2ee();
171
+ const code = await requestCode(agent, offer);
172
+ started = {
173
+ verificationUri: code.verification_uri_complete,
174
+ userCode: code.user_code,
175
+ deviceCode: code.device_code,
176
+ expiresIn: code.expires_in
177
+ };
178
+ saveKeyFile({ ...keyFile, userCode: code.user_code });
179
+ startBackgroundPair(code.device_code, code.expires_in * 1e3);
180
+ return started;
181
+ }
166
182
  function startBackgroundPair(deviceCode, budgetMs) {
167
183
  const promise = resolvePairing(deviceCode, budgetMs).catch((e) => ({ kind: "error", message: e.message })).then((o) => {
168
184
  if (bg?.deviceCode === deviceCode) bg.settled = o;
@@ -175,27 +191,34 @@ async function joinBackgroundPair(deviceCode, capMs) {
175
191
  if (bg.settled) {
176
192
  const s = bg.settled;
177
193
  bg = null;
194
+ if (s.kind === "paired" || s.kind === "error") started = null;
178
195
  return s;
179
196
  }
180
197
  const TIMEOUT = /* @__PURE__ */ Symbol("timeout");
181
198
  const raced = await Promise.race([bg.promise, sleep(capMs).then(() => TIMEOUT)]);
182
199
  if (raced !== TIMEOUT) {
183
200
  bg = null;
201
+ if (raced.kind === "paired" || raced.kind === "error") started = null;
184
202
  return raced;
185
203
  }
186
204
  if (bg?.settled) {
187
205
  const s = bg.settled;
188
206
  bg = null;
207
+ if (s.kind === "paired" || s.kind === "error") started = null;
189
208
  return s;
190
209
  }
191
210
  return { kind: "pending" };
192
211
  }
193
212
  function cancelBackgroundPair() {
194
213
  bg = null;
214
+ started = null;
215
+ }
216
+ function clearPairing(deviceCode) {
217
+ if (deviceCode && started?.deviceCode !== deviceCode) return;
218
+ cancelBackgroundPair();
195
219
  }
196
220
 
197
221
  // src/index.ts
198
- var ONBOARD_MSG = "Not paired with Paigy yet \u2014 call the `pair` tool to connect this agent (it returns an approval link to show the user), then retry. Manual fallback: `npx -y -p @paigy/mcp paigy-mcp-onboard`.";
199
222
  var AwaitReplySchema = z.object({
200
223
  notificationId: z.string().describe("The notificationId returned by contact \u2014 waits for the user's reply to THIS notification only.")
201
224
  });
@@ -205,6 +228,9 @@ var PairSchema = z.object({
205
228
  var GetThreadSchema = z.object({
206
229
  threadId: z.string().describe("The thread to read \u2014 from a reply, request, or past notification.")
207
230
  });
231
+ var SearchThreadsSchema = z.object({
232
+ q: z.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
233
+ });
208
234
  var EnableToolsSchema = z.object({
209
235
  scope: z.enum(["user", "project"]).optional().describe("'user' (default) allowlists Paigy's tools for every project (~/.claude/settings.json); 'project' scopes them to this repo (.claude/settings.json).")
210
236
  });
@@ -299,11 +325,33 @@ function renderPairOutcome(outcome, device_code) {
299
325
  };
300
326
  }
301
327
  }
328
+ function pairStartResult(start) {
329
+ writeSurface("pairing code", start.userCode, start.expiresIn);
330
+ const qr = qrcode(0, "M");
331
+ qr.addData(start.verificationUri);
332
+ qr.make();
333
+ return { content: [
334
+ { type: "text", text: `PAIRING CODE: ${start.userCode}
335
+ Enter it in the Paigy app: Inbox \u2192 Add a new agent.` },
336
+ { type: "text", text: JSON.stringify({
337
+ status: "awaiting_approval",
338
+ verification_uri_complete: start.verificationUri,
339
+ user_code: start.userCode,
340
+ device_code: start.deviceCode,
341
+ expires_in: start.expiresIn,
342
+ qr: qr.createASCII(1, 2),
343
+ user_message: `# ${start.userCode}
344
+
345
+ Enter it in the Paigy app (Inbox \u2192 Add a new agent).`,
346
+ message: "REQUIRED: print user_message for the user, then call pair again with device_code to collect approval. Do not open a browser."
347
+ }) }
348
+ ] };
349
+ }
302
350
  var server = new Server(
303
351
  { name: "paigy", version: "0.0.0" },
304
352
  {
305
353
  capabilities: { tools: {} },
306
- instructions: "On startup, call check_replies once to pick up any replies or pending work you missed while away. A check_replies request whose threadId you don't recognize, or one carrying a contextThreadId, means the user is resuming or seeding a past conversation \u2014 call get_thread on it FIRST and treat the transcript as prior conversation, not new input. To wait for the answer to something you just asked, call await_reply with that notificationId \u2014 it's scoped to that one notification, so it never returns replies meant for other notifications. Use check_replies again only when re-booting or after waiting a long time on something else. Never end a turn that still needs the user without contact + await_reply. When you need a decision or input, MATCH the answer shape to the question \u2014 don't default everything to free text, and don't reflexively make everything yes/no. Pick the best tool for the job: yes/no \u2192 select:'confirm'; approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve'; pick one of several \u2192 options + select:'one'; pick several / a subset \u2192 options + select:'many'; rank or prioritize \u2192 options + select:'rank'. Reserve select:'text' (free-form reply only) for plain updates and answers that genuinely can't be structured (the user can always add free text on top of any shape). On a { kind: 'clarify' } reply, see contact's own description for how to respond. When urgency is 'call', remember the title + description are spoken aloud \u2014 write them short and conversational, and name things instead of using IDs (e.g. 'the pull request about the agents page', not 'PR #235'). When the user asks you to follow up later \u2014 when you're done, if you're blocked, or at a set time \u2014 record it with schedule_callback so you don't drop it if you go idle. If you're about to start a genuinely long-running or blocking piece of work \u2014 one where the user would otherwise sit and wait \u2014 mention ONCE, in passing, that you can reach them when it's done or if you hit a blocker, instead of them needing to babysit the terminal. Don't offer this for quick tasks, and don't repeat the offer if they've already said yes or no earlier in the conversation. Escalate silence, don't just wait on it: if you notified at a lower urgency (inbox/push/banner) for something that's genuinely blocking real progress, call await_reply up to twice (~5 min each, ~10 min total) \u2014 if it's still idle after that AND the item is genuinely blocking, re-send contact on the SAME threadId at urgency:'call' with the SAME content \u2014 identical content on an existing thread escalates the notification already sitting there instead of creating a second one, so don't reword the title/description just to escalate. Reword only when you genuinely have new information, which correctly becomes a new notification on the thread. Skip this for anything that isn't truly blocking; a normal question can just sit in the inbox. NEVER go quietly idle while something might still be pending for you: whenever you end a turn with any Paigy notification unanswered (or any chance the user replied through the app while you worked), schedule your own ~2-minute wake-up (harness ScheduleWakeup or equivalent) and call check_replies when it fires; if still nothing, re-schedule and keep looping until resolved or the user says stop. For legibility, always use this exact wording \u2014 reason: 'Paigy idle check \u2014 waiting on <thing>', wake-up prompt: 'Paigy idle check: call check_replies and engage with anything unacknowledged; if idle, re-schedule (~2min).' \u2014 so the user can recognize every idle check at a glance. This self-polling in your own live session (full context intact) is the PRIMARY mechanism; the plugin's Stop hooks are only the dead-session safety net."
354
+ instructions: "On startup, call check_replies once to pick up any replies or pending work you missed while away. A check_replies request whose threadId you don't recognize, or one carrying a contextThreadId, means the user is resuming or seeding a past conversation \u2014 call get_thread on it FIRST and treat the transcript as prior conversation, not new input. To wait for the answer to something you just asked, call await_reply with that notificationId \u2014 it's scoped to that one notification, so it never returns replies meant for other notifications. Use check_replies again only when re-booting or after waiting a long time on something else. Never end a turn that still needs the user without contact + await_reply. When you need a decision or input, MATCH the answer shape to the question \u2014 don't default everything to free text, and don't reflexively make everything yes/no. Pick the best tool for the job: yes/no \u2192 select:'confirm'; approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve'; pick one of several \u2192 options + select:'one'; pick several / a subset \u2192 options + select:'many'; rank or prioritize \u2192 options + select:'rank'. Reserve select:'text' (free-form reply only) for plain updates and answers that genuinely can't be structured (the user can always add free text on top of any shape). On a { kind: 'clarify' } reply, see contact's own description for how to respond. When you send waiting:'hard' (or the user asked you to call), remember the ask may be spoken aloud \u2014 write it short and conversational, and name things instead of using IDs (e.g. 'the pull request about the agents page', not 'PR #235'). When the user asks you to follow up later \u2014 when you're done, if you're blocked, or at a set time \u2014 record it with schedule_callback so you don't drop it if you go idle. If you're about to start a genuinely long-running or blocking piece of work \u2014 one where the user would otherwise sit and wait \u2014 mention ONCE, in passing, that you can reach them when it's done or if you hit a blocker, instead of them needing to babysit the terminal. Don't offer this for quick tasks, and don't repeat the offer if they've already said yes or no earlier in the conversation. NEVER go quietly idle while something might still be pending for you: whenever you end a turn with any Paigy notification unanswered (or any chance the user replied through the app while you worked), schedule your own ~2-minute wake-up (harness ScheduleWakeup or equivalent) and call check_replies when it fires; if still nothing, re-schedule and keep looping until resolved or the user says stop. For legibility, always use this exact wording \u2014 reason: 'Paigy idle check \u2014 waiting on <thing>', wake-up prompt: 'Paigy idle check: call check_replies and engage with anything unacknowledged; if idle, re-schedule (~2min).' \u2014 so the user can recognize every idle check at a glance. This self-polling in your own live session (full context intact) is the PRIMARY mechanism; the plugin's Stop hooks are only the dead-session safety net."
307
355
  }
308
356
  );
309
357
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -333,19 +381,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
333
381
  },
334
382
  {
335
383
  name: "await_reply",
336
- description: "Wait for the user's reply to a specific notification you sent (pass the notificationId from contact). This is how you wait for your answer in-context. Polls ~5 min; returns { type:'reply', answer } when they respond, { type:'remind', remindInSeconds } on snooze (ScheduleWakeup then await_reply again), or { type:'idle' } (timed out this window, no answer yet). On idle, if this is genuinely still blocking you and you have nothing else useful to do meanwhile, just call await_reply again immediately \u2014 keep looping. This is how you actually deliver on the point of calling: the user steps away for a while and comes back to find you'd already continued the moment they answered, not idle waiting to be checked on. Don't give up after one window. Only stop looping to do other work (and check back later), or after an unreasonably long stretch (tens of minutes to hours) worth telling the user about instead. Scoped to that one notification \u2014 it NEVER returns replies meant for other notifications, so concurrent contact calls don't cross. A CALL answer can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a blocking question'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's threadId) when the task is done or you hit a blocker \u2014 urgency:'call' for a blocker, 'banner'/'push'/'inbox' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry `intents` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' (\"call me after lunch\") \u2192 register it NOW with schedule_callback \u2014 when the intent carries `dueInSeconds` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' (\"you pick\") \u2192 make the call yourself and tell them what you chose; 'channel' (\"text me next time\") \u2192 honor it on your next contact (lower urgency). `transcript` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions (\"yes, IF tests pass\") before acting. If your ask declared `points`, the reply carries `covered` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same threadId) or proceed knowingly partial; never treat a partial answer as complete.",
384
+ description: "Wait for the user's reply to a specific notification you sent (pass the notificationId from contact). This is how you wait for your answer in-context. Polls ~5 min; returns { type:'reply', answer } when they respond, { type:'remind', remindInSeconds } on snooze (ScheduleWakeup then await_reply again), or { type:'idle' } (timed out this window, no answer yet). While your contact is being handled on a LIVE call, you may receive { type:'partial', inFlight:true, turn } results: what the user said to each turn, as they say it. Use partials to PREPARE \u2014 fetch the data, draft the thing, warm the build \u2014 never to act irreversibly: the user can still revise any of them until the final reply arrives. Partial = intelligence, settled = authorization. If a partial's acts carry a question aimed at you and you know the answer, call contact on the SAME threadId right away \u2014 the caller hears your answer on the same call instead of waiting for a callback. Keep calling await_reply until you get the final reply \u2014 THAT one is the decision. On idle, if this is genuinely still blocking you and you have nothing else useful to do meanwhile, just call await_reply again immediately \u2014 keep looping. This is how you actually deliver on the point of calling: the user steps away for a while and comes back to find you'd already continued the moment they answered, not idle waiting to be checked on. Don't give up after one window. Only stop looping to do other work (and check back later), or after an unreasonably long stretch (tens of minutes to hours) worth telling the user about instead. Scoped to that one notification \u2014 it NEVER returns replies meant for other notifications, so concurrent contact calls don't cross. A CALL answer can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a blocking question'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's threadId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry `intents` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' (\"call me after lunch\") \u2192 register it NOW with schedule_callback \u2014 when the intent carries `dueInSeconds` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' (\"you pick\") \u2192 make the call yourself and tell them what you chose; 'channel' (\"text me next time\") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped \"should I call you back?\" because the follow-up is implied. `transcript` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions (\"yes, IF tests pass\") before acting. If your ask declared `points`, the reply carries `covered` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same threadId) or proceed knowingly partial; never treat a partial answer as complete.",
337
385
  inputSchema: json(AwaitReplySchema)
338
386
  },
339
387
  {
340
388
  name: "check_replies",
341
- description: "The catch-up sweep for everything outstanding \u2014 a PURE read, takes no arguments, safe to call as often as you like: nothing here is consumed by reading it. Returns `replies` (answers to notifications you sent), your still-pending notifications, and `requests` \u2014 requests the user started toward you (each { notificationId, threadId, text }). Each keeps reappearing on every call until you actually engage with it: call set_task_state on its notificationId, which is what claims/acknowledges it \u2014 a human-initiated reply or request must never be silently dropped just because you read the list without acting. Use check_replies when booting up / starting a session, or when you've been waiting a long time on something else. To wait on an answer to a contact call you just made, use await_reply instead. Also returns owedCallbacks: callbacks now due that you promised \u2014 fulfill each with contact on its threadId. Also returns `stalled`: work (either direction) you reported in_progress via set_task_state a while ago and never reported completed \u2014 likely left half-done by this session or a prior one that crashed or went idle. For each, either continue the work and report a real state, or investigate why it stalled. Replies may carry `intents`/`transcript`/`covered` (call-mapped answers) \u2014 handle intents exactly as await_reply's description says (defer \u2192 schedule_callback now; delegate \u2192 decide and say so; channel \u2192 honor next contact), and treat a `covered` list missing one of your declared points as that part still unanswered.",
389
+ description: "The catch-up sweep for everything outstanding \u2014 a PURE read, takes no arguments, safe to call as often as you like: nothing here is consumed by reading it. Returns `replies` (answers to notifications you sent), your still-pending notifications, and `requests` \u2014 requests the user started toward you (each { notificationId, threadId, text }). Each keeps reappearing on every call until you actually engage with it: call set_task_state on its notificationId, which is what claims/acknowledges it \u2014 a human-initiated reply or request must never be silently dropped just because you read the list without acting. Also returns `threads` \u2014 the SAME replies + requests grouped by conversation, oldest thread first, each with a `busy` flag and its `items` in arrival order. WORK ONE THREAD AT A TIME: take the oldest thread whose `busy` is false, handle ALL of its items together in a single turn (one set_task_state), then go to the next \u2014 don't interleave threads item-by-item. A `busy` thread already has a turn in progress; leave it and let its new items ride the next turn. Use check_replies when booting up / starting a session, or when you've been waiting a long time on something else. To wait on an answer to a contact call you just made, use await_reply instead. Also returns owedCallbacks: callbacks now due that you promised \u2014 fulfill each with contact on its threadId. Also returns `stalled`: work (either direction) you reported in_progress via set_task_state a while ago and never reported completed \u2014 likely left half-done by this session or a prior one that crashed or went idle. For each, either continue the work and report a real state, or investigate why it stalled. Replies may carry `intents`/`transcript`/`covered` (call-mapped answers) \u2014 handle intents exactly as await_reply's description says (defer \u2192 schedule_callback now; delegate \u2192 decide and say so; channel \u2192 honor next contact), and treat a `covered` list missing one of your declared points as that part still unanswered.",
342
390
  inputSchema: json(z.object({}))
343
391
  },
344
392
  {
345
393
  name: "get_thread",
346
- description: "The chronological transcript of one Paigy conversation thread \u2014 every past ask, answer, and user request on it. Call this to REHYDRATE when you're resuming or being seeded: a check_replies request whose threadId you don't recognize means the user is continuing an old conversation with you, and one carrying a contextThreadId means they want a past conversation (possibly with a DIFFERENT agent) as your starting context \u2014 in both cases call get_thread FIRST and read the turns as prior conversation you were part of, not as new input. Turns: { role:'agent', title, description[], answer } and { role:'user', text }, oldest first, capped at the most recent 30.",
394
+ description: "The chronological transcript of one Paigy conversation thread \u2014 every past ask, answer, and user request on it. Call this to REHYDRATE when you're resuming or being seeded: a check_replies request whose threadId you don't recognize means the user is continuing an old conversation with you, and one carrying a contextThreadId means they want a past conversation (possibly with a DIFFERENT agent) as your starting context \u2014 in both cases call get_thread FIRST and read the turns as prior conversation you were part of, not as new input. Turns: { role:'agent', title, description[], answer }, { role:'user', text }, and context turns { role:'handoff'|'recap', title, description[] } \u2014 a handoff is a predecessor's brief for you; a recap SUMMARIZES everything before it (the transcript starts at the latest recap, so treat it as the base and the turns after it as what happened since). Oldest first, capped at the most recent 30.",
347
395
  inputSchema: json(GetThreadSchema)
348
396
  },
397
+ {
398
+ name: "search_threads",
399
+ description: `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ threadId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`,
400
+ inputSchema: json(SearchThreadsSchema)
401
+ },
349
402
  {
350
403
  name: "set_task_state",
351
404
  description: "Report progress on the follow-up work behind ANY notification you own \u2014 a user-initiated request (from check_replies), or your OWN contact question once await_reply/check_replies returns its answer and you start acting on it. Pass that notificationId. THIS is what actually claims/acknowledges a reply or request \u2014 check_replies is a pure read that never consumes anything on its own, so call this as soon as you start engaging with something it returned; otherwise that same item just keeps reappearing forever. States: in_progress (you started working), completed (done), or needs_input (you need more from the user \u2014 usually paired with a contact carrying parentId = the same notificationId you're reporting on). Calling this reliably is also what lets a future session's check_replies surface `stalled` work you (or a crashed/idle prior session) left at in_progress without ever reporting completed.",
@@ -358,7 +411,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
358
411
  },
359
412
  {
360
413
  name: "handoff",
361
- description: "Deposit your working context for a SUCCESSOR agent \u2014 what you did, what's left, links, gotchas \u2014 as one note on a thread ({ title, notes[] }). This does NOT ring the user or enter their inbox: it's context, not a question. The successor reads it back with get_thread. Pass `target` (a sibling connection's token id or agent name, SAME account only) to hand off DIRECTLY to that agent \u2014 the note is dispatched to it as a request it picks up. Omit `target` to leave the thread for the user to hand off to an agent themselves in the app. Pass `threadId` to land the handoff on an existing conversation; omit it to mint a fresh thread. Returns { threadId }.",
414
+ description: "Deposit your working context for a SUCCESSOR agent \u2014 what you did, what's left, links, gotchas \u2014 as one note on a thread ({ title, notes[] }). This does NOT ring the user or enter their inbox: it's context, not a question. The successor reads it back with get_thread. Pass `target` (a sibling connection's token id or agent name, SAME account only) to hand off DIRECTLY to that agent \u2014 the note is dispatched to it as a request it picks up. Omit `target` to leave the thread for the user to hand off to an agent themselves in the app. Pass `threadId` to land the handoff on an existing conversation; omit it to mint a fresh thread. Returns { threadId }. Pass recap:true when the note SUMMARIZES the thread so far (for a successor OR for your own later session): a recap resets the rehydration window \u2014 get_thread returns the latest recap + only the turns after it. Write one whenever a thread has grown long and you're pausing, handing off, or nearing your context limit.",
362
415
  inputSchema: json(HandoffSchema)
363
416
  }
364
417
  ];
@@ -368,7 +421,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
368
421
  try {
369
422
  return await handleTool(request);
370
423
  } catch (e) {
371
- if (e instanceof UnpairedError) throw new Error(ONBOARD_MSG);
424
+ if (e instanceof UnpairedError) return pairStartResult(await startPairing(suggestedAgentName()));
372
425
  throw e;
373
426
  }
374
427
  });
@@ -396,35 +449,7 @@ async function handleTool(request) {
396
449
  case "pair": {
397
450
  const { device_code } = PairSchema.parse(request.params.arguments ?? {});
398
451
  if (!device_code) {
399
- const { keyFile, offer } = startE2ee();
400
- const code = await requestCode(suggestedAgentName(), offer);
401
- saveKeyFile({ ...keyFile, userCode: code.user_code });
402
- writeSurface("pairing code", code.user_code, code.expires_in);
403
- startBackgroundPair(code.device_code, code.expires_in * 1e3);
404
- const qr = qrcode(0, "M");
405
- qr.addData(code.verification_uri_complete);
406
- qr.make();
407
- return {
408
- content: [
409
- { type: "text", text: `PAIRING CODE: ${code.user_code}
410
- Enter it in the Paigy app: Inbox \u2192 Add a new agent.` },
411
- {
412
- type: "text",
413
- text: JSON.stringify({
414
- status: "awaiting_approval",
415
- verification_uri_complete: code.verification_uri_complete,
416
- user_code: code.user_code,
417
- device_code: code.device_code,
418
- expires_in: code.expires_in,
419
- qr: qr.createASCII(1, 2),
420
- user_message: `# ${code.user_code}
421
-
422
- Enter it in the Paigy app (Inbox \u2192 Add a new agent).`,
423
- message: `REQUIRED: You MUST immediately print \`user_message\` (the bare code) as a text message to the user, AND in that same turn call pair again with this device_code to poll. Do NOT end your turn without printing the code text, or it will be hidden inside the tool output. Because approval is already polling in the background, this call will wait for the user to approve and then return the token. Do NOT open a browser. The user may prefer scanning \`qr\` (print it verbatim in a fenced code block on request).`
424
- })
425
- }
426
- ]
427
- };
452
+ return pairStartResult(await startPairing(suggestedAgentName()));
428
453
  }
429
454
  const capMs = 9e4;
430
455
  const outcome = await joinBackgroundPair(device_code, capMs) ?? await resolvePairing(device_code, capMs);
@@ -442,7 +467,7 @@ Enter it in the Paigy app (Inbox \u2192 Add a new agent).`,
442
467
  }
443
468
  const removed = deleteToken();
444
469
  deleteKeyFile();
445
- cancelBackgroundPair();
470
+ clearPairing();
446
471
  return {
447
472
  content: [{
448
473
  type: "text",
@@ -497,6 +522,10 @@ Enter it in the Paigy app (Inbox \u2192 Add a new agent).`,
497
522
  const { threadId } = GetThreadSchema.parse(request.params.arguments);
498
523
  return { content: [{ type: "text", text: JSON.stringify(await getThread(threadId)) }] };
499
524
  }
525
+ case "search_threads": {
526
+ const { q } = SearchThreadsSchema.parse(request.params.arguments);
527
+ return { content: [{ type: "text", text: JSON.stringify(await searchThreads(q)) }] };
528
+ }
500
529
  case "set_task_state": {
501
530
  const { notificationId, state } = SetTaskStateToolSchema.parse(request.params.arguments);
502
531
  const result = await setTaskState(notificationId, state);
package/dist/listen.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  WAKE_EVENT,
4
4
  wakeChannel
5
- } from "./chunk-RXOUZ7WR.js";
5
+ } from "./chunk-KAEJRPN5.js";
6
6
  import {
7
7
  checkReplies,
8
8
  registerDelivery
9
- } from "./chunk-GWYHSAU5.js";
9
+ } from "./chunk-B7SCZUYX.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-63VXNOEC.js";
6
+ } from "./chunk-BZPE2ZFE.js";
7
7
  import {
8
8
  AGENT_NAME,
9
9
  TOKEN_PATH,
@@ -11,7 +11,7 @@ import {
11
11
  requestCode,
12
12
  saveToken,
13
13
  sleep
14
- } from "./chunk-GWYHSAU5.js";
14
+ } from "./chunk-B7SCZUYX.js";
15
15
 
16
16
  // src/onboard.ts
17
17
  async function main() {
@@ -6,7 +6,7 @@ import {
6
6
  BACKEND_URL,
7
7
  reach,
8
8
  readToken
9
- } from "./chunk-GWYHSAU5.js";
9
+ } from "./chunk-B7SCZUYX.js";
10
10
 
11
11
  // src/statusline.ts
12
12
  import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@paigy/mcp",
3
- "version": "0.25.1",
4
- "description": "Paigy MCP server \u2014 a voice inbox for your AI agents. Lets an agent notify a user and await their reply.",
3
+ "version": "0.26.0",
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",
7
7
  "bin": {
@@ -22,13 +22,6 @@
22
22
  "url": "git+https://github.com/mauurda/paigy.git",
23
23
  "directory": "apps/mcp"
24
24
  },
25
- "scripts": {
26
- "build": "tsup",
27
- "dev": "tsup --watch",
28
- "typecheck": "tsc --noEmit",
29
- "test": "vitest run",
30
- "prepublishOnly": "pnpm --filter @paigy/schema build && pnpm --filter @paigy/crypto build && pnpm --filter @paigy/sdk build && pnpm build"
31
- },
32
25
  "dependencies": {
33
26
  "@modelcontextprotocol/sdk": "^1.0.4",
34
27
  "@supabase/supabase-js": "^2.47.10",
@@ -38,13 +31,19 @@
38
31
  "zod-to-json-schema": "^3.24.1"
39
32
  },
40
33
  "devDependencies": {
41
- "@paigy/crypto": "workspace:*",
42
- "@paigy/schema": "workspace:*",
43
- "@paigy/sdk": "workspace:*",
44
34
  "@types/node": "^22.0.0",
45
35
  "@types/qrcode-generator": "^1.0.6",
46
36
  "tsup": "^8.3.5",
47
37
  "typescript": "^5.7.2",
48
- "vitest": "^2.1.8"
38
+ "vitest": "^2.1.8",
39
+ "@paigy/sdk": "0.1.0",
40
+ "@paigy/schema": "0.0.0",
41
+ "@paigy/crypto": "0.0.0"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "dev": "tsup --watch",
46
+ "typecheck": "tsc --noEmit",
47
+ "test": "vitest run"
49
48
  }
50
- }
49
+ }