@paigy/mcp 0.40.1 → 0.40.3

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.
@@ -10,7 +10,7 @@ import {
10
10
  saveToken,
11
11
  sleep,
12
12
  startE2ee
13
- } from "./chunk-4UZAPBKK.js";
13
+ } from "./chunk-6QQHTUKJ.js";
14
14
 
15
15
  // src/identity.ts
16
16
  var CLIENT_LABELS = {
@@ -36,7 +36,22 @@ function mcpInputSchema(s) {
36
36
  }
37
37
  var StartContactSchema = z.object({
38
38
  goalIds: z.tuple([z.string().uuid()]),
39
- ask: z.string().trim().min(1).max(1e4),
39
+ // THE ASK IS THE QUESTION, not a bulletin with a question at the end (2026-09-16).
40
+ //
41
+ // A DecisionNeed is settled only by an answer in the shape THIS ask declares. Bundle news,
42
+ // findings and a decision into one, and the person answers whichever part engaged them —
43
+ // which settles nothing, leaves the need open, and gets them asked again. On call 619c5a92
44
+ // one ask carried a briefing, a side question ("the ring should show the agent's name now —
45
+ // what did you actually see?") and a one-of-four about how far an erase reaches. The owner
46
+ // answered the side question. The need stayed open; the card asked again. Of the 45 call
47
+ // asks carrying a decision, 27 are over 600 characters.
48
+ //
49
+ // Sending them separately costs nothing, because a CALL contact joins the call already
50
+ // happening (`goal/store.ts`, JOIN BEFORE MINTING) — several contacts on one Goal arrive as
51
+ // one call, planned as its turns, and each keeps its own settleable need.
52
+ ask: z.string().trim().min(1).max(1e4).describe(
53
+ "The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
54
+ ),
40
55
  waiting: z.enum(["none", "hard"]).default("none"),
41
56
  channel: z.enum(["notification", "call"]).default("notification"),
42
57
  options: z.array(z.object({ label: z.string().trim().min(1).max(1e3), image: z.string().url().optional(), html: z.string().max(16384).optional() }).strict()).min(2).max(6).optional(),
@@ -48,12 +63,25 @@ var CONTACT_DESCRIPTION = "Contact the user about exactly one existing Goal: pas
48
63
  var CreateGoalSchema = z2.object({
49
64
  outcome: z2.string().trim().min(1).max(1e4),
50
65
  ownerParticipant: z2.string().trim().min(1).optional(),
51
- idempotencyKey: z2.string().trim().min(1).max(200)
66
+ idempotencyKey: z2.string().trim().min(1).max(200),
67
+ /** A past conversation this Goal should be read against — History's "new session from this"
68
+ * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
69
+ * reference only: the owner reads it through `get_thread`, which does its own scoping, and
70
+ * the writer refuses a thread belonging to another account. */
71
+ contextThreadId: z2.string().uuid().optional(),
72
+ /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
73
+ * didn't get created as a child goal of the voice UI goal, which is how it should've
74
+ * worked"). It could not have been: this door took no parent, so the only route was
75
+ * create -> claim -> `update_goal`, three calls with a lease in the middle, and every agent
76
+ * took the short one. The hierarchy has been modelled since Goals existed and had been used
77
+ * ZERO times in 2,031 of them. Absent, the server judges it against the caller's open Goals
78
+ * (`apps/api/src/goal/intake.ts`). The writer refuses a Goal belonging to another account. */
79
+ parentGoalId: z2.string().uuid().optional()
52
80
  });
53
81
  var CreateGoalToolSchema = CreateGoalSchema.extend({
54
82
  idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
55
83
  }).strict();
56
- var CREATE_GOAL_DESCRIPTION = "Create a durable root Goal for an outcome. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
84
+ var CREATE_GOAL_DESCRIPTION = "Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
57
85
  var UpdateGoalSchema = z2.object({
58
86
  revision: z2.number().int().positive(),
59
87
  changes: z2.object({
@@ -99,6 +127,21 @@ var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
99
127
  function serverInstructions(_opts) {
100
128
  return "On startup and after a wake, call check_replies for the open Deliveries addressed to you, and claim_goal for your runnable or review-pending Goal and its durable Entries and accepted decisions. get_goal rereads the same evidence without consuming it. Contact only for an existing Goal. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. On stdio, Calls hold one bounded window; on hosted MCP, Calls return after one read; continue with contact({deliveryId}) to reread that exact Delivery. Evidence can repeat: reads do not acknowledge or hide it. Use update_goal to report progress and explicitly acknowledge review. Never infer ringing from an open Call Delivery. Soft waiting, multipart contact, and re-presentation are unsupported. In an interactive session, ask the user directly; use Paigy when they asked for it or are away.";
101
129
  }
130
+ function entryWords(entry) {
131
+ const content = entry.content;
132
+ if (content && "sealed" in content) return "";
133
+ const plain = content?.plain;
134
+ if (typeof plain === "string") return plain;
135
+ if (plain && typeof plain === "object") {
136
+ const text = plain.text;
137
+ if (typeof text === "string") return text;
138
+ const title = plain.title;
139
+ const description = plain.description;
140
+ const parts = [title, ...Array.isArray(description) ? description : []].filter((v) => typeof v === "string");
141
+ if (parts.length) return parts.join("\n\n");
142
+ }
143
+ return entry.sources.map((source) => source.text).join("\n");
144
+ }
102
145
  var ContextSchema = z3.object({
103
146
  title: z3.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
104
147
  description: z3.array(z3.string().min(1)).describe(
@@ -329,16 +372,6 @@ var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
329
372
  });
330
373
  var NotifyStatusSchema = z3.enum(["pending", "answered", "ignored"]);
331
374
  var AgentStateSchema = z3.enum(["idle", "in_progress", "completed", "needs_input"]);
332
- var SetTaskStateSchema = z3.object({
333
- state: z3.enum(["in_progress", "completed", "needs_input"])
334
- });
335
- var SetWorkStateSchema = z3.object({
336
- workId: z3.string().uuid().optional(),
337
- notificationId: z3.string().optional(),
338
- state: SetTaskStateSchema.shape.state
339
- }).refine((value) => Number(Boolean(value.workId)) + Number(Boolean(value.notificationId)) === 1, {
340
- message: "exactly one of workId or notificationId is required"
341
- });
342
375
  var TurnSchema = z3.object({
343
376
  prompt: z3.string(),
344
377
  reply: z3.string()
@@ -468,108 +501,6 @@ var AwaitItemSchema = z3.discriminatedUnion("type", [
468
501
  })
469
502
  ]);
470
503
  var CallbackTriggerSchema = z3.enum(["on_done", "on_blocked", "scheduled"]);
471
- var ScheduleCallbackSchema = z3.object({
472
- parentId: z3.string().describe("The thread to call back on (from a prior contact / reply / request)."),
473
- goalId: z3.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
474
- trigger: CallbackTriggerSchema,
475
- dueInSeconds: z3.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
476
- note: z3.string().optional().describe("What to tell the user when you follow up.")
477
- });
478
- var PendingRepliesSchema = z3.object({
479
- replies: z3.array(
480
- z3.object({
481
- parentId: z3.string(),
482
- notificationId: z3.string(),
483
- workId: z3.string().uuid().optional(),
484
- decisionId: z3.string().uuid().optional(),
485
- answer: UserAnswerSchema,
486
- /** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
487
- * pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
488
- * plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
489
- sealed: z3.lazy(() => SealedAnswerSchema).optional(),
490
- /** The call record rendered for THIS agent: the raw words the shaped answer was
491
- * mapped from, filtered to its own claims. See AwaitItemSchema's reply variant. */
492
- transcript: z3.string().optional(),
493
- /** Coverage report (#396): which declared `points` this answer addressed. */
494
- covered: z3.array(z3.string()).optional()
495
- })
496
- ),
497
- pending: z3.array(
498
- z3.object({ parentId: z3.string(), notificationId: z3.string(), createdAt: z3.string() })
499
- ),
500
- /** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
501
- * sees for this session's identity. From inside a session there was no way to find out —
502
- * `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
503
- * agent that cannot tell which agent it is cannot tell whether work addressed to
504
- * "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
505
- you: z3.object({ name: z3.string(), device: z3.string().nullable(), tokenId: z3.string() }).optional(),
506
- /** User-initiated requests addressed to this agent; act on them and reply via
507
- * contact on the same parentId. Keeps reappearing until you call
508
- * set_work_state on its workId (or notificationId once while bootstrapping). */
509
- requests: z3.array(
510
- z3.object({
511
- parentId: z3.string(),
512
- notificationId: z3.string(),
513
- workId: z3.string().uuid().optional(),
514
- text: z3.string(),
515
- createdAt: z3.string(),
516
- /** The user seeded this request with a past conversation — call get_thread on it
517
- * FIRST and treat the transcript as prior context (#57/#251). */
518
- contextParentId: z3.string().optional(),
519
- /** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
520
- * the account — the name here — which has not been seen since it landed, so nobody
521
- * came for it. Handed to you because you are the session that is here. Take it like
522
- * any request (set_work_state claims it, reply with contact on its parentId), and say
523
- * whose it was, because the user chose that agent on purpose. */
524
- stranded: z3.string().optional()
525
- })
526
- ),
527
- /** Durable outcomes currently owned by this agent. This is the Work-native queue; flat
528
- * notification lists remain during migration so old clients keep their existing view. */
529
- work: z3.array(z3.object({
530
- workId: z3.string().uuid(),
531
- parentId: z3.string().optional(),
532
- objective: z3.string().nullable(),
533
- state: z3.enum(["active", "blocked", "waiting_external"]),
534
- blockedOn: z3.array(z3.string().uuid()),
535
- updatedAt: z3.string()
536
- })).optional(),
537
- /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
538
- * if blocked, or at a time that has passed). Re-surfaced every sweep until you
539
- * fulfill one by calling contact on its parentId. */
540
- /** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
541
- * reach. Same array the contact/await replies carry — one queue, every carrier. */
542
- also: z3.array(RideAlongSchema).optional(),
543
- owedCallbacks: z3.array(
544
- z3.object({ parentId: z3.string(), trigger: CallbackTriggerSchema, note: z3.string() })
545
- ),
546
- /** Work (either direction) you reported in_progress a while ago and never reported
547
- * completed — likely left half-done by this session or a prior one that crashed or
548
- * went idle. Report a real state (set_work_state) or continue the work. */
549
- stalled: z3.array(
550
- z3.object({ parentId: z3.string(), notificationId: z3.string(), title: z3.string().nullable(), startedAt: z3.string() })
551
- ),
552
- /** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
553
- * thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
554
- * a thread's `items` into a single turn rather than interleaving threads. `busy` = the
555
- * thread already has a turn in progress (younger than the stall cutoff); let it finish and
556
- * ride the next turn. `items` are that thread's replies/requests in arrival order; the
557
- * full payload for each is in the flat `replies`/`requests` arrays (matched by
558
- * notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
559
- threads: z3.array(
560
- z3.object({
561
- parentId: z3.string(),
562
- busy: z3.boolean(),
563
- items: z3.array(
564
- z3.object({
565
- kind: z3.enum(["reply", "request"]),
566
- notificationId: z3.string(),
567
- at: z3.string()
568
- })
569
- )
570
- })
571
- )
572
- });
573
504
  var NotifyResponseSchema = z3.object({
574
505
  notificationId: z3.string(),
575
506
  workId: z3.string().uuid().optional(),
@@ -797,7 +728,21 @@ var InboxItemSchema = z3.object({
797
728
  /** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
798
729
  * sender the phone replies to). Sourced server-side from this item's pairing credential.
799
730
  * Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
800
- agentX25519: z3.string().optional()
731
+ agentX25519: z3.string().optional(),
732
+ /** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
733
+ * that Delivery's kind, the request Entry, the Goals it answers for, the exact DecisionNeed (none
734
+ * for a request that asks nothing), whether its content is sealed, and that Goal's state. The
735
+ * answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
736
+ * here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
737
+ communication: z3.object({
738
+ deliveryId: z3.string(),
739
+ kind: z3.enum(["notification", "call"]),
740
+ entryId: z3.string(),
741
+ goalIds: z3.array(z3.string()),
742
+ decisionNeedId: z3.string().optional(),
743
+ sealed: z3.boolean(),
744
+ goalState: z3.string().optional()
745
+ }).optional()
801
746
  });
802
747
  var SnoozeRequestSchema = z3.object({
803
748
  requestId: z3.string(),
@@ -1033,26 +978,54 @@ var CreateRequestSchema = z3.object({
1033
978
  * the agent reads it via get_thread. Must belong to the requesting user. */
1034
979
  contextParentId: z3.string().optional()
1035
980
  });
1036
- var HandoffSchema = z3.object({
1037
- /** Move this existing outcome to `target` without reminting it. Requires `target`. */
1038
- workId: z3.string().uuid().optional(),
1039
- /** Land the note on an existing thread; omitted mints a fresh one. */
1040
- parentId: z3.string().uuid().optional(),
1041
- /** One-line headline of the working context handed off. */
1042
- title: z3.string().min(1),
1043
- /** The brief — standalone notes the successor reads (what was done, what's left, links). */
1044
- notes: z3.array(z3.string().min(1)).min(1),
1045
- /** A sibling connection to dispatch directly to (token id or agent nickname). Same-account
1046
- * only; omit to leave the thread for the user to hand off in the app. */
1047
- target: z3.string().optional(),
1048
- /** Write the note as a RECAP (kind:'recap', #617): a summary turn that supersedes the
1049
- * thread's earlier turns for rehydration — get_thread returns the latest recap + only
1050
- * the turns after it. Handoff-to-a-successor and handoff-to-yourself-later are the
1051
- * same primitive; a recap is one whose audience includes you. */
1052
- recap: z3.boolean().optional()
1053
- }).refine((value) => !value.workId || Boolean(value.target), {
1054
- message: "target is required when handing off Work",
1055
- path: ["target"]
981
+ var QueueQuestionSchema = z3.object({
982
+ /** The decision need's id — what an answer is accepted against. */
983
+ id: z3.string(),
984
+ /** The words that were asked, from the request Entry that asked them. */
985
+ question: z3.string(),
986
+ /** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
987
+ * for a need whose request Entry is carried by no interactive Delivery, which nothing
988
+ * can answer. */
989
+ deliveryId: z3.string().nullable().default(null),
990
+ /** The Entry the ruling is about. */
991
+ aboutId: z3.string().nullable().default(null),
992
+ /** Empty for a free-text question. */
993
+ options: z3.array(OptionSchema).default([]),
994
+ select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
995
+ askedAt: z3.string(),
996
+ /** Null while the question is open — which is how the page tells the two apart. */
997
+ answeredAt: z3.string().nullable().default(null),
998
+ /** The ruling in the person's own words, from the contribution that replied — not the
999
+ * option id, which is not something anyone reads back. Null while it is open, and null
1000
+ * for a settled question whose reply carried nothing readable. */
1001
+ answer: z3.string().nullable().default(null)
1002
+ });
1003
+ var QueueItemSchema = z3.object({
1004
+ id: z3.string(),
1005
+ /** One-line headline — the first sentence of the outcome. */
1006
+ title: z3.string(),
1007
+ /** The outcome in full, verbatim: the person's own words are what an assignee sees. */
1008
+ intent: z3.string(),
1009
+ /** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
1010
+ state: z3.string(),
1011
+ /** Who holds it (a participant ref); null when nobody does yet. */
1012
+ assignee: z3.string().nullable().default(null),
1013
+ /** What the agent last said it was doing; null if it has said nothing. */
1014
+ progress: z3.string().nullable().default(null),
1015
+ reviewPending: z3.boolean().default(false),
1016
+ dueAt: z3.string().nullable().default(null),
1017
+ /** The Goal this one was opened under; null at the root. */
1018
+ parentGoalId: z3.string().nullable().default(null),
1019
+ /** Goals opened under this one — only those the same list holds. */
1020
+ childGoalIds: z3.array(z3.string()).default([]),
1021
+ /** Goals this one waits on (start or finish gates). */
1022
+ dependencyGoalIds: z3.array(z3.string()).default([]),
1023
+ /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
1024
+ blocked: z3.boolean().default(false),
1025
+ /** Every decision need on it, open or settled — the page decides which to show. */
1026
+ questions: z3.array(QueueQuestionSchema).default([]),
1027
+ createdAt: z3.string(),
1028
+ updatedAt: z3.string().nullable().default(null)
1056
1029
  });
1057
1030
  var NoteSourceSchema = z3.enum(["app", "call"]);
1058
1031
  var NoteStatusSchema = z3.enum(["open", "assigned", "in_progress", "done"]);
@@ -1181,8 +1154,6 @@ var AcceptTriageResultSchema = z3.object({
1181
1154
  failed: z3.array(z3.object({ noteId: z3.string(), reason: z3.string() }))
1182
1155
  });
1183
1156
  var DeliveryModeSchema = z3.enum(["poll", "self_hosted"]);
1184
- var WAKE_EVENT = "wake";
1185
- var wakeChannel = (tokenId) => `wake:${tokenId}`;
1186
1157
  var RegisterDeliverySchema = z3.object({ mode: DeliveryModeSchema });
1187
1158
  var OAuthStartSchema = z3.object({
1188
1159
  provider: z3.enum(["cma"]),
@@ -1191,8 +1162,9 @@ var OAuthStartSchema = z3.object({
1191
1162
  var DeliveryConfigSchema = z3.object({
1192
1163
  tokenId: z3.string(),
1193
1164
  mode: DeliveryModeSchema,
1194
- /** null when the server has no SUPABASE_ANON_KEY set — the listener then falls
1195
- * back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
1165
+ /** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
1166
+ * (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
1167
+ * carries credentials; only a `poll` registration can come back with null here. */
1196
1168
  realtime: z3.object({ url: z3.string(), anonKey: z3.string() }).nullable()
1197
1169
  });
1198
1170
  var StatusSchema = z3.object({
@@ -1383,6 +1355,5 @@ export {
1383
1355
  mcpInputSchema,
1384
1356
  AGENT_TOOLS,
1385
1357
  serverInstructions,
1386
- WAKE_EVENT,
1387
- wakeChannel
1358
+ entryWords
1388
1359
  };
package/dist/enable.js CHANGED
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  PAIGY_TOOL_IDS,
4
4
  enablePaigyTools
5
- } from "./chunk-TLASZUJD.js";
6
- import "./chunk-3QUIB42M.js";
7
- import "./chunk-4UZAPBKK.js";
5
+ } from "./chunk-4JDG67EF.js";
6
+ import "./chunk-V3VVCML7.js";
7
+ import "./chunk-6QQHTUKJ.js";
8
8
 
9
9
  // src/enable.ts
10
10
  function main() {
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  resolvePairing,
6
6
  startPairing,
7
7
  suggestedAgentName
8
- } from "./chunk-CXABA27N.js";
8
+ } from "./chunk-TNQOFCZC.js";
9
9
  import {
10
10
  ENABLE_COMMAND,
11
11
  OnboardSchema,
@@ -16,8 +16,8 @@ import {
16
16
  autoConfigureClients,
17
17
  claudeInstallHint,
18
18
  paigyToolsAllowlisted
19
- } from "./chunk-TLASZUJD.js";
20
- import "./chunk-3QUIB42M.js";
19
+ } from "./chunk-4JDG67EF.js";
20
+ import "./chunk-V3VVCML7.js";
21
21
  import {
22
22
  clearSurface,
23
23
  writeSurface
@@ -38,7 +38,7 @@ import {
38
38
  setIdentity,
39
39
  slotName,
40
40
  whoAmI
41
- } from "./chunk-4UZAPBKK.js";
41
+ } from "./chunk-6QQHTUKJ.js";
42
42
 
43
43
  // src/index.ts
44
44
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
package/dist/listen.js CHANGED
@@ -1,18 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- WAKE_EVENT,
4
- wakeChannel
5
- } from "./chunk-3QUIB42M.js";
3
+ entryWords
4
+ } from "./chunk-V3VVCML7.js";
6
5
  import {
7
- ackReply,
8
- authToken,
9
- decodeReply,
10
- registerDelivery,
11
- sweepLeased
12
- } from "./chunk-4UZAPBKK.js";
6
+ checkReplies,
7
+ getGoal,
8
+ subscribeWake
9
+ } from "./chunk-6QQHTUKJ.js";
13
10
 
14
11
  // src/listen.ts
15
- import { createClient } from "@supabase/supabase-js";
16
12
  import { spawn } from "child_process";
17
13
  import { realpathSync } from "fs";
18
14
  import { fileURLToPath } from "url";
@@ -24,7 +20,7 @@ import { homedir } from "os";
24
20
  import { join } from "path";
25
21
  var LABEL = "ai.paigy.listen";
26
22
  function frozenEnv() {
27
- const keys = ["PAIGY_BACKEND_URL", "PAIGY_ON_WAKE", "PAIGY_SUPABASE_URL", "PAIGY_SUPABASE_ANON_KEY"];
23
+ const keys = ["PAIGY_BACKEND_URL", "PAIGY_ON_WAKE"];
28
24
  return Object.fromEntries(keys.flatMap((k) => process.env[k] ? [[k, process.env[k]]] : []));
29
25
  }
30
26
  function renderLaunchd(nodePath, scriptPath, env) {
@@ -114,114 +110,49 @@ function uninstallService() {
114
110
  return unit;
115
111
  }
116
112
 
117
- // src/message.ts
118
- function answerText(answer) {
119
- switch (answer.kind) {
120
- case "text":
121
- return answer.text;
122
- case "option":
123
- return answer.label ?? answer.optionId;
124
- case "multi":
125
- case "ranked":
126
- return (answer.labels ?? answer.optionIds).join(", ");
127
- case "confirm":
128
- return answer.approved ? "approved" : "declined";
129
- case "clarify":
130
- return answer.chunks.join(" ");
131
- case "turns":
132
- return answer.turns.map((turn) => turn.reply).join(" ");
133
- case "precedent":
134
- return answer.answer;
135
- case "ignored":
136
- return "";
137
- }
138
- }
139
-
140
113
  // src/listen.ts
141
114
  var emit = (obj) => void process.stdout.write(JSON.stringify(obj) + "\n");
142
- function launchEnv(work, reason) {
143
- const env = { PAIGY_WORK: JSON.stringify(work), PAIGY_EVENT: reason };
115
+ function saidIn(delivery) {
116
+ return delivery.entries.filter((entry) => entry.kind === "contribution").map((entry) => entryWords(entry)).filter(Boolean).join("\n");
117
+ }
118
+ function launchEnv(waiting, reason, goalId) {
119
+ const env = { PAIGY_WORK: JSON.stringify(waiting), PAIGY_EVENT: reason };
144
120
  const put = (key, value) => {
145
121
  if (value) env[key] = value;
146
122
  };
147
- const lead = work.threads?.find((t) => !t.busy)?.items[0]?.notificationId;
148
- const reply = lead ? work.replies.find((r) => r.notificationId === lead) : work.replies[0];
149
- const request = lead ? work.requests.find((r) => r.notificationId === lead) : work.requests[0];
150
- if (reply) {
151
- put("PAIGY_THREAD_ID", reply.parentId);
152
- put("PAIGY_PARENT_ID", reply.parentId);
153
- put("PAIGY_NOTIFICATION_ID", reply.notificationId);
154
- put("PAIGY_TEXT", answerText(reply.answer));
155
- return env;
156
- }
157
- if (request) {
158
- put("PAIGY_THREAD_ID", request.parentId);
159
- put("PAIGY_PARENT_ID", request.parentId);
160
- put("PAIGY_NOTIFICATION_ID", request.notificationId);
161
- put("PAIGY_CONTEXT_THREAD_ID", request.contextParentId);
162
- put("PAIGY_TEXT", request.text);
163
- return env;
164
- }
165
- const callback = work.owedCallbacks[0];
166
- if (callback) {
167
- put("PAIGY_THREAD_ID", callback.parentId);
168
- put("PAIGY_PARENT_ID", callback.parentId);
169
- put("PAIGY_TEXT", callback.note);
170
- }
123
+ const named = goalId ? waiting.deliveries.find((d) => d.goalIds.includes(goalId)) : void 0;
124
+ const lead = named ?? waiting.deliveries[0];
125
+ if (!lead) return env;
126
+ put("PAIGY_THREAD_ID", lead.threadId);
127
+ put("PAIGY_PARENT_ID", lead.threadId);
128
+ put("PAIGY_DELIVERY_ID", lead.deliveryId);
129
+ put("PAIGY_GOAL_ID", waiting.goal?.goalId ?? lead.goalIds[0]);
130
+ put("PAIGY_CONTEXT_THREAD_ID", waiting.goal?.contextThreadId);
131
+ put("PAIGY_TEXT", saidIn(lead) || lead.message);
171
132
  return env;
172
133
  }
173
- var SWEEP_PASSES = 10;
174
- async function sweep(reason) {
134
+ async function sweep(reason, goalId) {
175
135
  try {
176
136
  const cmd = process.env.PAIGY_ON_WAKE;
177
- const token = authToken() ?? "";
178
- for (let pass = 0; pass < SWEEP_PASSES; pass += 1) {
179
- const raw = await sweepLeased();
180
- const leased = raw.replies;
181
- const work = {
182
- ...raw,
183
- work: raw.work ?? [],
184
- replies: leased.map((r) => {
185
- const { type: _type, ...reply } = decodeReply({ ...r, type: "reply" });
186
- return reply;
187
- })
188
- };
189
- emit({ type: "sweep", reason, ...work });
190
- const inbound = work.replies.length > 0 || work.requests.length > 0;
191
- const reengage = work.owedCallbacks.length > 0 && (reason === "boot" || reason.includes("callback"));
192
- if (cmd && (inbound || reengage)) {
193
- spawn(cmd, { shell: true, stdio: "inherit", env: { ...process.env, ...launchEnv(work, reason) } });
194
- }
195
- if (cmd) for (const r of leased) await ackReply({ ...r, type: "reply" }, token);
196
- if (leased.length === 0) return;
137
+ const { deliveries } = await checkReplies();
138
+ const lead = (goalId ? deliveries.find((d) => d.goalIds.includes(goalId)) : void 0) ?? deliveries[0];
139
+ const leadGoalId = lead?.goalIds[0];
140
+ const goal = leadGoalId ? await getGoal(leadGoalId).then((g) => ({ goalId: leadGoalId, contextThreadId: g.contextThreadId, progress: g.progress })).catch(() => null) : null;
141
+ const waiting = { deliveries, goal };
142
+ emit({ type: "sweep", reason, ...goalId ? { about: goalId } : {}, ...waiting });
143
+ if (cmd && deliveries.length) {
144
+ spawn(cmd, { shell: true, stdio: "inherit", env: { ...process.env, ...launchEnv(waiting, reason, goalId) } });
197
145
  }
198
146
  } catch (e) {
199
147
  emit({ type: "error", reason, message: e.message });
200
148
  }
201
149
  }
202
150
  async function main() {
203
- const cfg = await registerDelivery("self_hosted");
204
- const url = cfg.realtime?.url ?? process.env.PAIGY_SUPABASE_URL;
205
- const anonKey = cfg.realtime?.anonKey ?? process.env.PAIGY_SUPABASE_ANON_KEY;
206
- if (!url || !anonKey) {
207
- throw new Error(
208
- "No Realtime credentials \u2014 set SUPABASE_ANON_KEY on the server, or PAIGY_SUPABASE_URL + PAIGY_SUPABASE_ANON_KEY locally."
209
- );
210
- }
211
- const channel = wakeChannel(cfg.tokenId);
212
- emit({ type: "listening", channel });
151
+ const sub = await subscribeWake((nudge) => void sweep(`wake:${nudge.kind ?? "?"}`, nudge.parentId));
152
+ emit({ type: "listening", channel: sub.channel });
213
153
  await sweep("boot");
214
- const sb = createClient(url, anonKey, { auth: { persistSession: false } });
215
- sb.channel(channel).on(
216
- "broadcast",
217
- { event: WAKE_EVENT },
218
- (msg) => void sweep(`wake:${msg.payload?.kind ?? "?"}`)
219
- ).subscribe();
220
154
  const shutdown = async () => {
221
- try {
222
- await registerDelivery("poll");
223
- } catch {
224
- }
155
+ await sub.close();
225
156
  process.exit(0);
226
157
  };
227
158
  process.on("SIGINT", () => void shutdown());
@@ -251,5 +182,6 @@ if (isEntry()) {
251
182
  }
252
183
  export {
253
184
  launchEnv,
185
+ saidIn,
254
186
  sweep
255
187
  };
package/dist/onboard.js CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  resolvePairing,
4
4
  startPairing,
5
5
  suggestedAgentName
6
- } from "./chunk-CXABA27N.js";
6
+ } from "./chunk-TNQOFCZC.js";
7
7
  import {
8
8
  autoConfigureClients,
9
9
  claudeInstallHint,
10
10
  openBrowser
11
- } from "./chunk-TLASZUJD.js";
12
- import "./chunk-3QUIB42M.js";
11
+ } from "./chunk-4JDG67EF.js";
12
+ import "./chunk-V3VVCML7.js";
13
13
  import {
14
14
  AGENT_NAME,
15
15
  TOKEN_PATH,
@@ -20,7 +20,7 @@ import {
20
20
  saveToken,
21
21
  setIdentity,
22
22
  whoAmI
23
- } from "./chunk-4UZAPBKK.js";
23
+ } from "./chunk-6QQHTUKJ.js";
24
24
 
25
25
  // src/onboard.ts
26
26
  function reportRegistered() {
package/dist/slot.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AGENT_NAME
4
- } from "./chunk-4UZAPBKK.js";
4
+ } from "./chunk-6QQHTUKJ.js";
5
5
 
6
6
  // src/slot.ts
7
7
  process.stdout.write(AGENT_NAME);
@@ -9,7 +9,7 @@ import {
9
9
  readToken,
10
10
  sessionSlot,
11
11
  slotName
12
- } from "./chunk-4UZAPBKK.js";
12
+ } from "./chunk-6QQHTUKJ.js";
13
13
 
14
14
  // src/statusline.ts
15
15
  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.40.1",
3
+ "version": "0.40.3",
4
4
  "description": "Paigy MCP server — the AI agent harness that calls you. Lets an agent notify a user and await their reply.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,28 +24,28 @@
24
24
  "url": "git+https://github.com/mauurda/paigy.git",
25
25
  "directory": "apps/mcp"
26
26
  },
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "dev": "tsup --watch",
30
+ "typecheck": "tsc --noEmit",
31
+ "test": "vitest run",
32
+ "prepublishOnly": "pnpm --filter @paigy/schema build && pnpm --filter @paigy/crypto build && pnpm --filter @paigy/sdk build && pnpm build"
33
+ },
27
34
  "dependencies": {
28
35
  "@modelcontextprotocol/sdk": "^1.0.4",
29
- "@supabase/supabase-js": "^2.47.10",
30
36
  "qrcode-generator": "^2.0.4",
31
37
  "undici": "^6.27.0",
32
38
  "zod": "^3.24.1",
33
39
  "zod-to-json-schema": "^3.24.1"
34
40
  },
35
41
  "devDependencies": {
42
+ "@paigy/crypto": "workspace:*",
43
+ "@paigy/schema": "workspace:*",
44
+ "@paigy/sdk": "workspace:*",
36
45
  "@types/node": "^22.0.0",
37
46
  "@types/qrcode-generator": "^1.0.6",
38
47
  "tsup": "^8.3.5",
39
48
  "typescript": "^5.7.2",
40
- "vitest": "^2.1.8",
41
- "@paigy/crypto": "0.0.0",
42
- "@paigy/schema": "0.0.0",
43
- "@paigy/sdk": "0.2.1"
44
- },
45
- "scripts": {
46
- "build": "tsup",
47
- "dev": "tsup --watch",
48
- "typecheck": "tsc --noEmit",
49
- "test": "vitest run"
49
+ "vitest": "^2.1.8"
50
50
  }
51
- }
51
+ }