@paigy/mcp 0.17.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  AGENT_NAME
3
- } from "./chunk-4LQAS5ZB.js";
3
+ } from "./chunk-RMTTO6BI.js";
4
4
 
5
5
  // src/clients.ts
6
6
  import { execFile } from "child_process";
@@ -0,0 +1,633 @@
1
+ // ../../packages/schema/dist/index.js
2
+ import { z } from "zod";
3
+ var ContextSchema = z.object({
4
+ title: z.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
5
+ 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.")
6
+ });
7
+ var ParticipantSchema = z.object({
8
+ kind: z.enum(["human", "agent"]),
9
+ id: z.string()
10
+ });
11
+ var TransformSchema = z.enum([
12
+ "structure",
13
+ // shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
14
+ "request_more",
15
+ // clarify / follow-ups / uncovered points; escalate inbox→call — {kind:'clarify'}, escalate, blocking
16
+ "redirect",
17
+ // seed / hand off a thread to a new recipient — handoff, "new session from this"
18
+ "break_down",
19
+ // one bundle → many sub-asks — checklist fan-out, `points`
20
+ "coalesce",
21
+ // many bundles → one — morning triage (#347), threading-supersede, digest
22
+ "organize",
23
+ // group related bundles onto one thread — threading (`threadId`), parent/clarify links
24
+ "summarize"
25
+ // reduce volume, keep decision value — 30-turn cap, spoken briefing
26
+ ]);
27
+ var OptionSchema = z.object({
28
+ id: z.string(),
29
+ label: z.string(),
30
+ // .describe() flows into the MCP notify_user JSON schema (zodToJsonSchema), so
31
+ // the constraints below are what an agent reads when deciding to use these.
32
+ html: z.string().max(16384).describe(
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."
34
+ ).optional(),
35
+ image: z.string().url().describe(
36
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
37
+ ).optional()
38
+ });
39
+ var VisualSchema = z.object({
40
+ url: z.string().url(),
41
+ label: z.string().optional()
42
+ });
43
+ var NotifyLevelSchema = z.enum(["inbox", "push", "banner", "call"]);
44
+ var SelectShapeSchema = z.enum(["one", "many", "rank", "confirm", "text"]);
45
+ var ReceiptEventSchema = z.enum([
46
+ "delivered",
47
+ // the bundle reached the recipient at some level
48
+ "seen",
49
+ // the recipient opened it
50
+ "answered",
51
+ // the recipient replied
52
+ "escalated",
53
+ // re-reached at a higher level (re-ring / promote)
54
+ "coalesced",
55
+ // merged into another live claim
56
+ "expired",
57
+ // deadline passed unanswered
58
+ "woke",
59
+ // the agent was woken for an owed obligation (callback)
60
+ "gave_up"
61
+ // the budget was spent — stopped re-engaging
62
+ ]);
63
+ var AttentionSchema = z.object({
64
+ urgency: NotifyLevelSchema,
65
+ /** The required answer shape, or null for a plain notify that asks nothing back. */
66
+ select: SelectShapeSchema.nullable(),
67
+ /** Coverage contract (#396) — points the answer must address; null = none declared. */
68
+ points: z.array(z.string()).nullable(),
69
+ /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
70
+ blocking: z.boolean(),
71
+ /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
72
+ * slice wires it; optional so today's rows/callers project cleanly. */
73
+ deadline: z.string().datetime().nullable().optional()
74
+ });
75
+ var NotifyRequestSchema = z.object({
76
+ /** Plaintext message content. Present on the plaintext path (today's shape);
77
+ * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
78
+ * superRefine at the bottom enforces exactly one of the two. */
79
+ context: ContextSchema.optional(),
80
+ options: z.array(OptionSchema.omit({ id: true })).min(1).optional().describe(
81
+ "The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
82
+ ),
83
+ points: z.array(z.string().min(1)).optional().describe(
84
+ "The distinct things you need answered, each a short phrase \u2014 on a call the broker keeps the conversation going until each is addressed, and the reply reports which were covered, so a half-answer is never silently returned as final. Omit for single-part asks."
85
+ ),
86
+ visuals: z.array(VisualSchema).optional().describe(
87
+ "Images attached to the message itself \u2014 context for the whole question (a screenshot, a chart). For a preview on one selectable choice, use that option's `html`/`image` instead."
88
+ ),
89
+ /** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
90
+ repo: z.string().optional(),
91
+ /** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
92
+ branch: z.string().optional(),
93
+ /** Continue an existing conversation; omitted = start a new thread. */
94
+ threadId: z.string().uuid().optional(),
95
+ urgency: NotifyLevelSchema.default("inbox").describe(
96
+ "The level you're requesting \u2014 the user's account permissions + session mode can lower it. 'inbox' (default) = sits silently in the inbox for the user to get to. 'push' = a quiet passive push (lands in Notification Center, no sound) \u2014 a gentle heads-up. 'banner' = a time-sensitive banner/lock-screen push with sound (a 'paige') they tap to open \u2014 use when you need them soon-ish but it's not worth ringing them. 'call' = rings the user's phone now (a CallKit voice call) \u2014 use only when you genuinely need them in the moment (blocked and waiting, time-sensitive). context.title is what they see on the banner/ring, so make it specific."
97
+ ),
98
+ /** The request this one was spawned from, for a clarification. */
99
+ parentId: z.string().optional(),
100
+ /** E2EE (text lane): when the pairing is E2EE, the sealed replacements for the
101
+ * plaintext content fields, keyed by field name. FINALIZED wire shape (was
102
+ * provisional in the storage PR): a per-field map `{ context?, options?,
103
+ * visuals? }` of opaque Envelopes — one seal per present content field, so a
104
+ * message with no options/visuals seals only `context`. It COEXISTS with the
105
+ * plaintext fields by mutual exclusion: the superRefine below requires that
106
+ * when `envelope` is present the plaintext `context`/`options`/`visuals` are
107
+ * ABSENT (and vice-versa), so a row is either fully plaintext or fully sealed —
108
+ * never a readable half. The server persists this OPAQUELY into
109
+ * notifications.envelope and relays it blindly; it never decrypts. Absent =
110
+ * today's plaintext path (context/options/visuals carry the cleartext).
111
+ * z.lazy because EnvelopeSchema is declared further down (E2EE section). */
112
+ envelope: z.object({
113
+ context: z.lazy(() => EnvelopeSchema).optional(),
114
+ options: z.lazy(() => EnvelopeSchema).optional(),
115
+ visuals: z.lazy(() => EnvelopeSchema).optional()
116
+ }).optional(),
117
+ select: SelectShapeSchema.optional().describe(
118
+ "How the user answers \u2014 required on the fully-shaped form, pick the shape that fits the question: 'one' = pick one option, 'many' = pick several, 'rank' = pick & order (each needs `options`); 'confirm' = yes/no or approve/deny; 'text' = free-form reply only (status updates, open questions). 'confirm' and 'text' take no options. Omit only when sending the simplified `ask` form \u2014 the broker picks the shape."
119
+ ),
120
+ /** The simplified form (#395): instead of shaping the notification yourself
121
+ * (context/select/options/urgency), state what you need to learn and why it
122
+ * matters now — the broker derives the optimal shape and channel. Mutually
123
+ * exclusive with `context` (and never sent alongside `envelope`: E2EE pairings
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.'
127
+ ),
128
+ needs: z.array(z.string().min(1)).optional().describe(
129
+ "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."
130
+ ),
131
+ urgencyHint: z.enum(["whenever", "soon", "now"]).optional().describe(
132
+ "With `ask` only: how urgently you need the answer \u2014 'whenever' (inbox), 'soon' (worth a heads-up), 'now' (you're blocked this minute). A hint, not a command: the user's settings still have the final word."
133
+ ),
134
+ confirmStyle: z.enum(["yesno", "approve"]).default("yesno").describe(
135
+ "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
136
+ ),
137
+ blocking: z.boolean().default(false).describe(
138
+ "Set true when real downstream work is stuck behind this specific decision \u2014 you can't make meaningful progress until it's answered. This is the real signal for how urgently the user should be reached; it's what the premier use case (an agent that stays unblocked instead of going idle) depends on. Independent of `urgency`: a `banner`-level question can still be `blocking` (something IS stuck, just not time-critical enough to ring for immediately) \u2014 if it goes unanswered a while, Paigy escalates it to a real call using this flag rather than guessing from how many other things happen to be pending. Leave false for anything you could work around, defer, or where other useful work exists meanwhile."
139
+ )
140
+ }).superRefine((r, ctx) => {
141
+ const sealed = !!r.envelope;
142
+ if (sealed) {
143
+ if (!r.envelope?.context)
144
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["envelope", "context"], message: "sealed request must include envelope.context" });
145
+ for (const f of ["context", "options", "visuals"]) {
146
+ if (r[f] !== void 0)
147
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [f], message: `E2EE request must not carry plaintext ${f} \u2014 it's sealed in envelope.${f}` });
148
+ }
149
+ if (r.points !== void 0)
150
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["points"], message: "E2EE request must not carry plaintext points" });
151
+ if (r.ask !== void 0)
152
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["ask"], message: "E2EE request must not carry a plaintext ask \u2014 derive the shape agent-side and seal it" });
153
+ if (r.needs !== void 0)
154
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["needs"], message: "E2EE request must not carry plaintext needs" });
155
+ return;
156
+ }
157
+ if (r.ask !== void 0) {
158
+ for (const f of ["context", "options", "select", "points", "visuals"]) {
159
+ if (r[f] !== void 0)
160
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives it (use \`needs\` for multi-part asks)` });
161
+ }
162
+ return;
163
+ }
164
+ if (r.needs !== void 0 || r.urgencyHint !== void 0)
165
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
166
+ if (!r.context)
167
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
168
+ if (!r.select)
169
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
170
+ const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
171
+ if (needsOptions && !r.options?.length)
172
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
173
+ if (!needsOptions && r.options?.length)
174
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
175
+ });
176
+ var NotifyStatusSchema = z.enum(["pending", "answered", "ignored"]);
177
+ var AgentStateSchema = z.enum(["idle", "in_progress", "completed", "needs_input"]);
178
+ var SetTaskStateSchema = z.object({
179
+ state: z.enum(["in_progress", "completed", "needs_input"])
180
+ });
181
+ var TurnSchema = z.object({
182
+ prompt: z.string(),
183
+ reply: z.string()
184
+ });
185
+ var UserAnswerSchema = z.discriminatedUnion("kind", [
186
+ z.object({ kind: z.literal("option"), optionId: z.string() }),
187
+ z.object({ kind: z.literal("text"), text: z.string() }),
188
+ z.object({ kind: z.literal("ignored") }),
189
+ z.object({ kind: z.literal("multi"), optionIds: z.array(z.string()) }),
190
+ z.object({ kind: z.literal("ranked"), optionIds: z.array(z.string()) }),
191
+ z.object({ kind: z.literal("clarify"), chunks: z.array(z.string()).min(1) }),
192
+ z.object({ kind: z.literal("confirm"), approved: z.boolean() }),
193
+ z.object({ kind: z.literal("turns"), turns: z.array(TurnSchema).min(1) })
194
+ ]);
195
+ var IntentSchema = z.object({
196
+ kind: z.enum(["defer", "delegate", "channel"]),
197
+ detail: z.string(),
198
+ /** Landed defer (#397): the MCP parses common spoken forms ("in 20 minutes",
199
+ * "after lunch") against the agent machine's clock — the user's — and attaches
200
+ * the seconds, ready to pass straight to schedule_callback. Absent when the
201
+ * detail didn't parse (the agent interprets it) or the kind isn't defer. */
202
+ dueInSeconds: z.number().int().positive().optional()
203
+ });
204
+ var AwaitItemSchema = z.discriminatedUnion("type", [
205
+ z.object({
206
+ type: z.literal("reply"),
207
+ threadId: z.string(),
208
+ notificationId: z.string(),
209
+ answer: UserAnswerSchema,
210
+ /** E2EE: present when the answer is sealed. The server relays the opaque answer
211
+ * envelope + the plaintext `ignored` status hint; the receiving agent OPENS it
212
+ * with its device key and re-derives the real UserAnswer, treating a mismatch
213
+ * against `ignored` as tampering. Absent = today's plaintext answer (in `answer`).
214
+ * On a sealed reply the plaintext `answer` is a placeholder (kind reflects only
215
+ * the `ignored` bit) — never the real content, which stays sealed. */
216
+ sealed: z.lazy(() => SealedAnswerSchema).optional(),
217
+ /** Broker extras (#381), present when the answer was mapped from a call: next
218
+ * steps the user attached ("call me after lunch" → defer — act on it via
219
+ * schedule_callback) and the raw words the shaped answer was mapped from. */
220
+ intents: z.array(IntentSchema).optional(),
221
+ transcript: z.string().optional(),
222
+ /** Coverage report (#396), when the ask declared `points`: which of them this
223
+ * answer addressed. Missing points = re-ask or proceed knowingly partial. */
224
+ covered: z.array(z.string()).optional()
225
+ }),
226
+ z.object({
227
+ type: z.literal("remind"),
228
+ threadId: z.string(),
229
+ notificationId: z.string(),
230
+ remindAt: z.string().datetime({ offset: true }),
231
+ /** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
232
+ remindInSeconds: z.number()
233
+ }),
234
+ z.object({ type: z.literal("idle") })
235
+ ]);
236
+ var CallbackTriggerSchema = z.enum(["on_done", "on_blocked", "scheduled"]);
237
+ var ScheduleCallbackSchema = z.object({
238
+ threadId: z.string().describe("The thread to call back on (from a prior notify_user / reply / request)."),
239
+ trigger: CallbackTriggerSchema,
240
+ dueInSeconds: z.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
241
+ note: z.string().optional().describe("What to tell the user when you follow up.")
242
+ });
243
+ var PendingRepliesSchema = z.object({
244
+ replies: z.array(
245
+ z.object({
246
+ threadId: z.string(),
247
+ notificationId: z.string(),
248
+ answer: UserAnswerSchema,
249
+ /** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
250
+ * pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
251
+ * plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
252
+ sealed: z.lazy(() => SealedAnswerSchema).optional(),
253
+ /** Broker extras (#381): next steps the user attached to a call-mapped answer
254
+ * ("call me after lunch" → defer — act on it via schedule_callback) and the
255
+ * raw words the shaped answer was mapped from. */
256
+ intents: z.array(IntentSchema).optional(),
257
+ transcript: z.string().optional(),
258
+ /** Coverage report (#396): which declared `points` this answer addressed. */
259
+ covered: z.array(z.string()).optional()
260
+ })
261
+ ),
262
+ pending: z.array(
263
+ z.object({ threadId: z.string(), notificationId: z.string(), createdAt: z.string() })
264
+ ),
265
+ /** User-initiated requests addressed to this agent; act on them and reply via
266
+ * notify_user on the same threadId. Keeps reappearing until you call
267
+ * set_task_state on its notificationId. */
268
+ requests: z.array(
269
+ z.object({
270
+ threadId: z.string(),
271
+ notificationId: z.string(),
272
+ text: z.string(),
273
+ createdAt: z.string(),
274
+ /** The user seeded this request with a past conversation — call get_thread on it
275
+ * FIRST and treat the transcript as prior context (#57/#251). */
276
+ contextThreadId: z.string().optional()
277
+ })
278
+ ),
279
+ /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
280
+ * if blocked, or at a time that has passed). Re-surfaced every sweep until you
281
+ * fulfill one by calling notify_user on its threadId. */
282
+ owedCallbacks: z.array(
283
+ z.object({ threadId: z.string(), trigger: CallbackTriggerSchema, note: z.string() })
284
+ ),
285
+ /** Work (either direction) you reported in_progress a while ago and never reported
286
+ * completed — likely left half-done by this session or a prior one that crashed or
287
+ * went idle. Report a real state (set_task_state) or continue the work. */
288
+ stalled: z.array(
289
+ z.object({ threadId: z.string(), notificationId: z.string(), title: z.string().nullable(), startedAt: z.string() })
290
+ )
291
+ });
292
+ var NotifyResponseSchema = z.object({
293
+ notificationId: z.string(),
294
+ status: NotifyStatusSchema,
295
+ createdAt: z.string().datetime(),
296
+ answer: UserAnswerSchema.optional(),
297
+ answeredAt: z.string().datetime().optional()
298
+ });
299
+ var UserResponseSchema = z.object({
300
+ requestId: z.string(),
301
+ answer: UserAnswerSchema,
302
+ answeredAt: z.string().datetime(),
303
+ intents: z.array(IntentSchema).optional(),
304
+ transcript: z.string().optional(),
305
+ /** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the item was
306
+ * E2EE-sealed. Present → the server persists it opaquely and branches status on `ignored`;
307
+ * the plaintext `answer` is a placeholder (`{ kind: "ignored" }`) the server ignores for a
308
+ * sealed row. Absent = today's plaintext answer, unchanged. */
309
+ sealed: z.lazy(() => SealedAnswerSchema).optional(),
310
+ /** Coverage report (#396): which of the ask's declared `points` were addressed. */
311
+ covered: z.array(z.string()).optional()
312
+ });
313
+ var VoiceKeySchema = z.enum(["rachel", "george", "jessica", "brian", "lily"]);
314
+ var InboxItemSchema = z.object({
315
+ id: z.string(),
316
+ /** The conversation thread + connection this item lives on. Present on the replied
317
+ * detail — they power History's "Continue" / "New session from this" (#57/#251). */
318
+ threadId: z.string().optional(),
319
+ tokenId: z.string().optional(),
320
+ status: NotifyStatusSchema,
321
+ context: ContextSchema,
322
+ options: z.array(OptionSchema).optional(),
323
+ /** The ask's declared coverage points (#396), when the agent sent them. */
324
+ points: z.array(z.string()).optional(),
325
+ /** Natural spoken questions for the points (#417 follow-up), phrased at ring time
326
+ * and index-aligned — the call bot prefers these over the raw shorthand. */
327
+ pointsSpeak: z.array(z.string()).optional(),
328
+ /** On a replied detail (#397): the next steps the user attached to the answer
329
+ * ("call back after lunch") — shown so they can see the commitment was captured. */
330
+ intents: z.array(IntentSchema).optional(),
331
+ visuals: z.array(VisualSchema).optional(),
332
+ agent: z.string(),
333
+ nickname: z.string(),
334
+ /** The pairing's assigned voice (#462); absent = the default voice. */
335
+ voice: VoiceKeySchema.optional(),
336
+ repo: z.string().optional(),
337
+ branch: z.string().optional(),
338
+ createdAt: z.string().datetime(),
339
+ snoozedUntil: z.string().datetime().optional(),
340
+ agentState: AgentStateSchema.default("idle"),
341
+ /** Whose action the item is waiting on: "you" = an agent asked you (the default,
342
+ * every agent→user notification); "agent" = you sent a request and it's awaiting the
343
+ * agent (held in the inbox until the agent replies on the thread). */
344
+ turn: z.enum(["you", "agent"]).default("you"),
345
+ /** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
346
+ * the agent (provider-agnostic; set server-side). Absent = no hard error, though the
347
+ * client may still flag a stall by age. Drives the inbox error badge + Retry. */
348
+ error: z.string().optional(),
349
+ parentId: z.string().optional(),
350
+ select: z.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
351
+ confirmStyle: z.enum(["yesno", "approve"]).default("yesno").describe(
352
+ "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
353
+ ),
354
+ /** Real downstream work is stuck behind this one — set by the agent, independent of
355
+ * urgency (see the main README's "premier use case" + notify/states.md). Drives the
356
+ * inbox's blocking badge and the extra confirm step before dismissing it. */
357
+ blocking: z.boolean().default(false),
358
+ /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
359
+ answer: UserAnswerSchema.optional(),
360
+ /** E2EE (text lane): the sealed content this item carries when the pairing is E2EE, in
361
+ * place of the plaintext `context`/`options`/`visuals` (which the operator would
362
+ * otherwise read). A per-field map of opaque Envelopes — the SAME shape as
363
+ * NotifyRequest.envelope. The native app decrypts it locally via openNotification;
364
+ * web / an un-enrolled device can't and renders a locked placeholder. Absent = today's
365
+ * plaintext item (context carries the cleartext), so plaintext items are unchanged. */
366
+ envelope: z.object({
367
+ context: z.lazy(() => EnvelopeSchema).optional(),
368
+ options: z.lazy(() => EnvelopeSchema).optional(),
369
+ visuals: z.lazy(() => EnvelopeSchema).optional()
370
+ }).optional(),
371
+ /** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
372
+ * sender the phone replies to). Sourced server-side from this item's pairing credential.
373
+ * Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
374
+ agentX25519: z.string().optional()
375
+ });
376
+ var SnoozeRequestSchema = z.object({
377
+ requestId: z.string(),
378
+ until: z.string().datetime()
379
+ });
380
+ var PushTokenSchema = z.object({
381
+ voipToken: z.string().min(1).optional(),
382
+ alertToken: z.string().min(1).optional(),
383
+ fcmToken: z.string().min(1).optional(),
384
+ platform: z.enum(["ios", "android"])
385
+ });
386
+ var MissedCallSchema = z.enum([
387
+ "retry_10m",
388
+ "retry_30m",
389
+ "retry_60m",
390
+ "backoff_gentle",
391
+ "backoff_standard",
392
+ "backoff_aggressive",
393
+ "inbox",
394
+ "dismiss"
395
+ ]);
396
+ var BrokerTuningSchema = z.object({
397
+ /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
398
+ ackVerbosity: z.enum(["normal", "none"]).optional(),
399
+ /** How readily the mapper asks its one clarification: 'low' = only when truly
400
+ * uninterpretable, 'high' = whenever not fully certain. */
401
+ clarifyEagerness: z.enum(["low", "normal", "high"]).optional(),
402
+ /** The user's own shorthand: when they say `say`, they mean `mean`. */
403
+ phrasebook: z.array(z.object({ say: z.string().min(1).max(60), mean: z.string().min(1).max(120) })).max(24).optional()
404
+ });
405
+ var UserSettingsSchema = z.object({
406
+ permissions: z.object({
407
+ call: z.boolean(),
408
+ banner: z.boolean(),
409
+ push: z.boolean()
410
+ }),
411
+ sessionMode: z.enum(["default", "all_calls", "silent"]),
412
+ silentPush: z.boolean(),
413
+ autoCallback: z.boolean(),
414
+ /** Opt-in (default false) to using your content to improve Paigy and train models. */
415
+ improveConsent: z.boolean(),
416
+ missedCall: MissedCallSchema.default("backoff_standard"),
417
+ /** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
418
+ * (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
419
+ * synthesizes and transcribes locally — no audio or spoken text leaves it.
420
+ * Optional, NOT defaulted: a stale client PATCHing the full settings object
421
+ * must not silently reset this privacy choice. Absent = leave unchanged on
422
+ * write, 'hosted' on read (see store.ts). */
423
+ voiceMode: z.enum(["hosted", "on_device"]).optional(),
424
+ /** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
425
+ * content is sealed end-to-end between the local agent and the phone. Like
426
+ * voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
427
+ * settings object without it can't silently flip the account's E2EE state.
428
+ * Absent = leave unchanged on write, 'off' on read (see store.ts). The demo
429
+ * account is plaintext by construction and refuses any non-'off' value. */
430
+ e2eeMode: z.enum(["off", "on"]).optional(),
431
+ /** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
432
+ * clobber guard as voiceMode: absent = leave unchanged on write. */
433
+ broker: BrokerTuningSchema.optional()
434
+ });
435
+ var HistoryItemSchema = z.object({
436
+ id: z.string(),
437
+ threadId: z.string(),
438
+ /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
439
+ initiator: z.enum(["user", "agent"]),
440
+ title: z.string(),
441
+ /** The agent on the other end (nickname). */
442
+ agent: z.string(),
443
+ createdAt: z.string(),
444
+ /** When the agent fetched your request (user→agent only). */
445
+ agentAckedAt: z.string().nullable(),
446
+ /** When you answered the agent's notification (agent→user only). */
447
+ humanAckedAt: z.string().nullable()
448
+ });
449
+ var ConnectionSummarySchema = z.object({
450
+ /** The connection = the agent's token id (used to address a request). */
451
+ id: z.string(),
452
+ agent: z.string(),
453
+ device: z.string().nullable(),
454
+ nickname: z.string(),
455
+ /** The pairing's assigned voice (#462); null = the default voice. */
456
+ voice: VoiceKeySchema.nullable(),
457
+ createdAt: z.string().datetime(),
458
+ /** Most recent notification on this connection, either direction. Null = no contact yet.
459
+ * Drives the agents-page recency grouping (Today / This week / …). */
460
+ lastContactAt: z.string().datetime().nullable(),
461
+ /** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
462
+ * false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
463
+ managed: z.boolean()
464
+ });
465
+ var CreateRequestSchema = z.object({
466
+ /** The connection (token id) to send to, from GET /api/tokens. */
467
+ tokenId: z.string(),
468
+ /** The user's message to the agent. */
469
+ text: z.string().min(1),
470
+ /** Land the request on an existing conversation thread (History → "Continue")
471
+ * instead of minting a fresh one. Must belong to the requesting user. */
472
+ threadId: z.string().optional(),
473
+ /** Point the agent at a past conversation (possibly with a different agent) as
474
+ * starting context (History → "New session from this"). A reference, not a copy —
475
+ * the agent reads it via get_thread. Must belong to the requesting user. */
476
+ contextThreadId: z.string().optional()
477
+ });
478
+ var HandoffSchema = z.object({
479
+ /** Land the note on an existing thread; omitted mints a fresh one. */
480
+ threadId: z.string().uuid().optional(),
481
+ /** One-line headline of the working context handed off. */
482
+ title: z.string().min(1),
483
+ /** The brief — standalone notes the successor reads (what was done, what's left, links). */
484
+ notes: z.array(z.string().min(1)).min(1),
485
+ /** A sibling connection to dispatch directly to (token id or agent nickname). Same-account
486
+ * only; omit to leave the thread for the user to hand off in the app. */
487
+ target: z.string().optional()
488
+ });
489
+ var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
490
+ var WAKE_EVENT = "wake";
491
+ var wakeChannel = (tokenId) => `wake:${tokenId}`;
492
+ var RegisterDeliverySchema = z.object({ mode: DeliveryModeSchema });
493
+ var OAuthStartSchema = z.object({
494
+ provider: z.enum(["cma"]),
495
+ returnTo: z.string().min(1)
496
+ });
497
+ var DeliveryConfigSchema = z.object({
498
+ tokenId: z.string(),
499
+ mode: DeliveryModeSchema,
500
+ /** null when the server has no SUPABASE_ANON_KEY set — the listener then falls
501
+ * back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
502
+ realtime: z.object({ url: z.string(), anonKey: z.string() }).nullable()
503
+ });
504
+ var StatusSchema = z.object({
505
+ agent: z.string(),
506
+ nickname: z.string(),
507
+ sessionMode: z.enum(["default", "all_calls", "silent"]),
508
+ /** A phone is registered for push/ring (any push token on the account). */
509
+ phone: z.boolean()
510
+ });
511
+ var EnvelopeRecipientSchema = z.object({
512
+ keyId: z.string(),
513
+ epk: z.string(),
514
+ wnonce: z.string(),
515
+ wrap: z.string()
516
+ });
517
+ var EnvelopeHeaderSchema = z.object({
518
+ field: z.enum(["context", "options", "visuals", "answer"]),
519
+ kind: z.string(),
520
+ senderRole: z.enum(["agent", "user"]),
521
+ recipientKeyIds: z.array(z.string()),
522
+ seq: z.number().int().nonnegative()
523
+ });
524
+ var EnvelopeSchema = z.object({
525
+ v: z.literal(1),
526
+ alg: z.literal("x25519-xsalsa20poly1305"),
527
+ msgId: z.string(),
528
+ hdr: EnvelopeHeaderSchema,
529
+ recipients: z.array(EnvelopeRecipientSchema).min(1),
530
+ nonce: z.string(),
531
+ ct: z.string()
532
+ });
533
+ var SealedAnswerSchema = z.object({
534
+ ignored: z.boolean(),
535
+ envelope: EnvelopeSchema
536
+ });
537
+ var DeviceCredentialSchema = z.object({
538
+ deviceId: z.string(),
539
+ kind: z.enum(["phone", "web", "agent"]),
540
+ x25519Pub: z.string(),
541
+ ed25519Pub: z.string(),
542
+ sig: z.string()
543
+ });
544
+ var DeviceRosterSchema = z.object({
545
+ uikPub: z.string(),
546
+ devices: z.array(DeviceCredentialSchema)
547
+ });
548
+ var WakeNudgeSchema = z.object({
549
+ kind: z.enum(["reply", "request", "callback"]),
550
+ notificationId: z.string().optional(),
551
+ threadId: z.string()
552
+ });
553
+ var DeviceAgentTokenSchema = z.object({
554
+ token: z.string(),
555
+ deviceId: z.string(),
556
+ agentName: z.string(),
557
+ /** User-chosen session label; defaults to `<agent> <device>`. */
558
+ nickname: z.string(),
559
+ createdAt: z.string().datetime()
560
+ });
561
+ var PairingStatusSchema = z.enum(["pending", "approved", "denied", "expired"]);
562
+ var PairingRevealSchema = z.object({
563
+ x25519: z.string(),
564
+ ed25519: z.string(),
565
+ nonce: z.string()
566
+ });
567
+ var DeviceCodeRequestSchema = z.object({
568
+ agent: z.union([z.string(), z.object({ name: z.string().optional() })]).optional(),
569
+ device: z.string().optional(),
570
+ proto: z.string().optional(),
571
+ // compiled-in tag, e.g. "paigy-pair-v2|sas=24"
572
+ commitment: z.string().optional()
573
+ // agent's Ca
574
+ });
575
+ var DeviceCodeSchema = z.object({
576
+ device_code: z.string(),
577
+ user_code: z.string(),
578
+ verification_uri: z.string().url(),
579
+ verification_uri_complete: z.string().url(),
580
+ interval: z.number(),
581
+ expires_in: z.number()
582
+ });
583
+ var DeviceInfoSchema = z.object({
584
+ code: z.string(),
585
+ agent: z.string(),
586
+ device: z.string().nullable(),
587
+ status: PairingStatusSchema,
588
+ proto: z.string().nullable().optional(),
589
+ agent_commitment: z.string().nullable().optional(),
590
+ // Ca
591
+ agent_reveal: PairingRevealSchema.nullable().optional(),
592
+ // present once the agent reveals
593
+ agent_credential: DeviceCredentialSchema.nullable().optional(),
594
+ // the phone's UIK-signed cred over the agent's keys — the agent fetches it here to finalizeE2ee
595
+ // The phone's reveal + identity key, exposed here so a GATED agent (e2ee_mode='on', token
596
+ // withheld until e2ee=true) can compute the SAS WITHOUT a token. Public keys — same safety
597
+ // class as agent_reveal above. Present only once the phone reveals; null otherwise.
598
+ phone_reveal: PairingRevealSchema.nullable().optional(),
599
+ uik_pub: z.string().nullable().optional()
600
+ });
601
+ var DeviceTokenRequestSchema = z.object({
602
+ device_code: z.string(),
603
+ reveal: PairingRevealSchema.optional()
604
+ // agent's reveal {x25519, ed25519, nonce}
605
+ });
606
+ var DeviceTokenSchema = z.object({
607
+ access_token: z.string(),
608
+ nickname: z.string(),
609
+ agent: z.string(),
610
+ device: z.string().nullable(),
611
+ phone_reveal: PairingRevealSchema.nullable().optional(),
612
+ // present once the phone reveals
613
+ uik_pub: z.string().nullable().optional()
614
+ });
615
+ var DeviceCommitRequestSchema = z.object({
616
+ code: z.string(),
617
+ commitment: z.string()
618
+ // Cb
619
+ });
620
+ var DevicePeerCommitSchema = z.object({
621
+ commitment: z.string().nullable()
622
+ });
623
+ var SupportRequestSchema = z.object({
624
+ email: z.string().email().max(320),
625
+ message: z.string().trim().min(1).max(5e3),
626
+ name: z.string().trim().max(120).optional()
627
+ });
628
+
629
+ export {
630
+ HandoffSchema,
631
+ WAKE_EVENT,
632
+ wakeChannel
633
+ };