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