@paigy/mcp 0.5.0 → 0.7.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.
@@ -0,0 +1,362 @@
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 OptionSchema = z.object({
8
+ id: z.string(),
9
+ label: z.string(),
10
+ // .describe() flows into the MCP notify_user JSON schema (zodToJsonSchema), so
11
+ // the constraints below are what an agent reads when deciding to use these.
12
+ html: z.string().max(16384).describe(
13
+ "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."
14
+ ).optional(),
15
+ image: z.string().url().describe(
16
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
17
+ ).optional()
18
+ });
19
+ var VisualSchema = z.object({
20
+ url: z.string().url(),
21
+ label: z.string().optional()
22
+ });
23
+ var AgentSchema = z.object({
24
+ name: z.string()
25
+ });
26
+ var NotifyLevelSchema = z.enum(["inbox", "push", "banner", "call"]);
27
+ var NotifyRequestSchema = z.object({
28
+ context: ContextSchema,
29
+ options: z.array(OptionSchema).optional(),
30
+ visuals: z.array(VisualSchema).optional(),
31
+ agent: AgentSchema,
32
+ /** Git repo the agent is working in ("owner/name"), when applicable. */
33
+ repo: z.string().optional(),
34
+ /** Git branch the agent is on, when applicable. */
35
+ branch: z.string().optional(),
36
+ /** Continue an existing conversation; omitted = start a new thread. */
37
+ threadId: z.string().uuid().optional(),
38
+ createdAt: z.string().datetime(),
39
+ urgency: NotifyLevelSchema.default("inbox").describe(
40
+ "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."
41
+ ),
42
+ /** The request this one was spawned from, for a clarification. */
43
+ parentId: z.string().optional(),
44
+ /** How the user answers the options: pick one (default), pick several, pick & order, or yes/no confirm. */
45
+ select: z.enum(["one", "many", "rank", "confirm"]).default("one"),
46
+ confirmStyle: z.enum(["yesno", "approve"]).default("yesno").describe(
47
+ "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
48
+ )
49
+ });
50
+ var NotifyStatusSchema = z.enum(["pending", "answered", "ignored"]);
51
+ var AgentStateSchema = z.enum(["idle", "in_progress", "completed", "needs_input"]);
52
+ var SetTaskStateSchema = z.object({
53
+ state: z.enum(["in_progress", "completed", "needs_input"])
54
+ });
55
+ var TurnSchema = z.object({
56
+ prompt: z.string(),
57
+ reply: z.string()
58
+ });
59
+ var UserAnswerSchema = z.discriminatedUnion("kind", [
60
+ z.object({ kind: z.literal("option"), optionId: z.string() }),
61
+ z.object({ kind: z.literal("text"), text: z.string() }),
62
+ z.object({ kind: z.literal("ignored") }),
63
+ z.object({ kind: z.literal("multi"), optionIds: z.array(z.string()) }),
64
+ z.object({ kind: z.literal("ranked"), optionIds: z.array(z.string()) }),
65
+ z.object({ kind: z.literal("clarify"), chunks: z.array(z.string()).min(1) }),
66
+ z.object({ kind: z.literal("confirm"), approved: z.boolean() }),
67
+ z.object({ kind: z.literal("turns"), turns: z.array(TurnSchema).min(1) })
68
+ ]);
69
+ var AwaitItemSchema = z.discriminatedUnion("type", [
70
+ z.object({
71
+ type: z.literal("reply"),
72
+ threadId: z.string(),
73
+ notificationId: z.string(),
74
+ answer: UserAnswerSchema
75
+ }),
76
+ z.object({
77
+ type: z.literal("remind"),
78
+ threadId: z.string(),
79
+ notificationId: z.string(),
80
+ remindAt: z.string().datetime({ offset: true }),
81
+ /** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
82
+ remindInSeconds: z.number()
83
+ }),
84
+ z.object({ type: z.literal("idle") })
85
+ ]);
86
+ var CallbackTriggerSchema = z.enum(["on_done", "on_blocked", "scheduled"]);
87
+ var ScheduleCallbackSchema = z.object({
88
+ threadId: z.string().describe("The thread to call back on (from a prior notify_user / reply / request)."),
89
+ trigger: CallbackTriggerSchema,
90
+ dueInSeconds: z.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
91
+ note: z.string().optional().describe("What to tell the user when you follow up.")
92
+ });
93
+ var PendingRepliesSchema = z.object({
94
+ replies: z.array(
95
+ z.object({ threadId: z.string(), notificationId: z.string(), answer: UserAnswerSchema })
96
+ ),
97
+ pending: z.array(
98
+ z.object({ threadId: z.string(), notificationId: z.string(), createdAt: z.string() })
99
+ ),
100
+ /** User-initiated requests addressed to this agent; act on them and reply via
101
+ * notify_user on the same threadId. Each is returned once (claimed on read). */
102
+ requests: z.array(
103
+ z.object({ threadId: z.string(), notificationId: z.string(), text: z.string(), createdAt: z.string() })
104
+ ),
105
+ /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
106
+ * if blocked, or at a time that has passed). Re-surfaced every sweep until you
107
+ * fulfill one by calling notify_user on its threadId. */
108
+ owedCallbacks: z.array(
109
+ z.object({ threadId: z.string(), trigger: CallbackTriggerSchema, note: z.string() })
110
+ )
111
+ });
112
+ var NotifyResponseSchema = z.object({
113
+ id: z.string(),
114
+ status: NotifyStatusSchema,
115
+ createdAt: z.string().datetime(),
116
+ answer: UserAnswerSchema.optional(),
117
+ answeredAt: z.string().datetime().optional()
118
+ });
119
+ var UserResponseSchema = z.object({
120
+ requestId: z.string(),
121
+ answer: UserAnswerSchema,
122
+ answeredAt: z.string().datetime()
123
+ });
124
+ var InboxItemSchema = z.object({
125
+ id: z.string(),
126
+ status: NotifyStatusSchema,
127
+ context: ContextSchema,
128
+ options: z.array(OptionSchema).optional(),
129
+ visuals: z.array(VisualSchema).optional(),
130
+ agent: z.string(),
131
+ nickname: z.string(),
132
+ repo: z.string().optional(),
133
+ branch: z.string().optional(),
134
+ createdAt: z.string().datetime(),
135
+ snoozedUntil: z.string().datetime().optional(),
136
+ agentState: AgentStateSchema.default("idle"),
137
+ parentId: z.string().optional(),
138
+ select: z.enum(["one", "many", "rank", "confirm"]).default("one"),
139
+ confirmStyle: z.enum(["yesno", "approve"]).default("yesno").describe(
140
+ "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
141
+ ),
142
+ /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
143
+ answer: UserAnswerSchema.optional()
144
+ });
145
+ var SnoozeRequestSchema = z.object({
146
+ requestId: z.string(),
147
+ until: z.string().datetime()
148
+ });
149
+ var PushTokenSchema = z.object({
150
+ voipToken: z.string().min(1).optional(),
151
+ alertToken: z.string().min(1).optional(),
152
+ platform: z.literal("ios")
153
+ });
154
+ var UserSettingsSchema = z.object({
155
+ permissions: z.object({
156
+ call: z.boolean(),
157
+ banner: z.boolean(),
158
+ push: z.boolean()
159
+ }),
160
+ sessionMode: z.enum(["default", "all_calls", "silent"]),
161
+ silentPush: z.boolean(),
162
+ autoCallback: z.boolean()
163
+ });
164
+ var HistoryItemSchema = z.object({
165
+ id: z.string(),
166
+ threadId: z.string(),
167
+ /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
168
+ initiator: z.enum(["user", "agent"]),
169
+ title: z.string(),
170
+ /** The agent on the other end (nickname). */
171
+ agent: z.string(),
172
+ createdAt: z.string(),
173
+ /** When the agent fetched your request (user→agent only). */
174
+ agentAckedAt: z.string().nullable(),
175
+ /** When you answered the agent's notification (agent→user only). */
176
+ humanAckedAt: z.string().nullable()
177
+ });
178
+ var ConnectionSummarySchema = z.object({
179
+ /** The connection = the agent's token id (used to address a request). */
180
+ id: z.string(),
181
+ agent: z.string(),
182
+ device: z.string().nullable(),
183
+ nickname: z.string(),
184
+ createdAt: z.string().datetime()
185
+ });
186
+ var CreateRequestSchema = z.object({
187
+ /** The connection (token id) to send to, from GET /api/tokens. */
188
+ tokenId: z.string(),
189
+ /** The user's message to the agent. */
190
+ text: z.string().min(1)
191
+ });
192
+ var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
193
+ var WAKE_EVENT = "wake";
194
+ var wakeChannel = (tokenId) => `wake:${tokenId}`;
195
+ var RegisterDeliverySchema = z.object({ mode: DeliveryModeSchema });
196
+ var OAuthStartSchema = z.object({
197
+ provider: z.enum(["cma"]),
198
+ returnTo: z.string().min(1)
199
+ });
200
+ var SetProviderSchema = z.object({
201
+ provider: z.enum(["cma"]),
202
+ /** Provider-side agent id (e.g. CMA `agent_…`). */
203
+ agentRef: z.string().min(1),
204
+ /** Provider-side environment id (e.g. CMA `env_…`). */
205
+ environmentId: z.string().min(1),
206
+ /** The delegated API key Paigy holds (in Vault) to spawn sessions on the user's behalf. */
207
+ apiKey: z.string().min(1)
208
+ });
209
+ var DeliveryConfigSchema = z.object({
210
+ tokenId: z.string(),
211
+ mode: DeliveryModeSchema,
212
+ /** null when the server has no SUPABASE_ANON_KEY set — the listener then falls
213
+ * back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
214
+ realtime: z.object({ url: z.string(), anonKey: z.string() }).nullable()
215
+ });
216
+ var WakeNudgeSchema = z.object({
217
+ kind: z.enum(["reply", "request", "callback"]),
218
+ notificationId: z.string().optional(),
219
+ threadId: z.string()
220
+ });
221
+ var DeviceAgentTokenSchema = z.object({
222
+ token: z.string(),
223
+ deviceId: z.string(),
224
+ agentName: z.string(),
225
+ /** User-chosen session label; defaults to `<agent> <device>`. */
226
+ nickname: z.string(),
227
+ createdAt: z.string().datetime()
228
+ });
229
+ var PairingStatusSchema = z.enum(["pending", "approved", "denied", "expired"]);
230
+ var DeviceCodeSchema = z.object({
231
+ device_code: z.string(),
232
+ user_code: z.string(),
233
+ verification_uri: z.string().url(),
234
+ verification_uri_complete: z.string().url(),
235
+ interval: z.number(),
236
+ expires_in: z.number()
237
+ });
238
+ var DeviceInfoSchema = z.object({
239
+ code: z.string(),
240
+ agent: z.string(),
241
+ device: z.string().nullable(),
242
+ status: PairingStatusSchema
243
+ });
244
+ var DeviceTokenSchema = z.object({
245
+ access_token: z.string(),
246
+ nickname: z.string(),
247
+ agent: z.string(),
248
+ device: z.string().nullable()
249
+ });
250
+
251
+ // src/client.ts
252
+ import { existsSync, readFileSync } from "fs";
253
+ import { homedir } from "os";
254
+ import { join } from "path";
255
+ var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
256
+ var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
257
+ function loadToken() {
258
+ if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
259
+ if (existsSync(TOKEN_PATH)) {
260
+ try {
261
+ const raw = readFileSync(TOKEN_PATH, "utf8");
262
+ const parsed = JSON.parse(raw);
263
+ if (parsed.access_token) return parsed.access_token;
264
+ } catch {
265
+ }
266
+ }
267
+ return "";
268
+ }
269
+ var ONBOARD_MSG = "Not paired with Paigy yet \u2014 call the `pair` tool to connect this agent (it returns an approval link to show the user), then retry. Manual fallback: `npx -y @paigy/mcp paigy-mcp-onboard`.";
270
+ function ensureAuthed(res) {
271
+ if (res.status === 401) throw new Error(ONBOARD_MSG);
272
+ return res;
273
+ }
274
+ async function submitNotification(req) {
275
+ const token = loadToken();
276
+ const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/notify`, {
277
+ method: "POST",
278
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
279
+ body: JSON.stringify(req)
280
+ }));
281
+ if (!res.ok) throw new Error(`notify failed: ${res.status} ${await res.text()}`);
282
+ return await res.json();
283
+ }
284
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
285
+ async function awaitReply(notificationId, opts = {}) {
286
+ const intervalMs = opts.intervalMs ?? 5e3;
287
+ const windowMs = opts.windowMs ?? 3e5;
288
+ const doSleep = opts.sleep ?? sleep;
289
+ const now = opts.now ?? Date.now;
290
+ const token = loadToken();
291
+ const start = now();
292
+ while (true) {
293
+ const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`, {
294
+ headers: { authorization: `Bearer ${token}` }
295
+ }));
296
+ if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
297
+ const item = await res.json();
298
+ if (item.type !== "idle") return item;
299
+ if (now() - start >= windowMs) return { type: "idle" };
300
+ await doSleep(intervalMs);
301
+ }
302
+ }
303
+ async function checkReplies() {
304
+ const token = loadToken();
305
+ const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/pending`, {
306
+ headers: { authorization: `Bearer ${token}` }
307
+ }));
308
+ if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
309
+ return await res.json();
310
+ }
311
+ async function setTaskState(id, state) {
312
+ const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/notify/${id}/state`, {
313
+ method: "PATCH",
314
+ headers: { "content-type": "application/json", authorization: `Bearer ${loadToken()}` },
315
+ body: JSON.stringify({ state })
316
+ }));
317
+ if (!res.ok) throw new Error(`set_task_state failed: ${res.status} ${await res.text()}`);
318
+ return await res.json();
319
+ }
320
+ async function registerDelivery(mode) {
321
+ const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/delivery`, {
322
+ method: "POST",
323
+ headers: { "content-type": "application/json", authorization: `Bearer ${loadToken()}` },
324
+ body: JSON.stringify({ mode })
325
+ }));
326
+ if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
327
+ return await res.json();
328
+ }
329
+ async function scheduleCallback(req) {
330
+ const token = loadToken();
331
+ const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/callback`, {
332
+ method: "POST",
333
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
334
+ body: JSON.stringify(req)
335
+ }));
336
+ if (!res.ok) throw new Error(`schedule_callback failed: ${res.status} ${await res.text()}`);
337
+ return await res.json();
338
+ }
339
+ async function pollAnswer(id) {
340
+ const token = loadToken();
341
+ const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/poll/${id}`, {
342
+ headers: { authorization: `Bearer ${token}` }
343
+ }));
344
+ if (!res.ok) throw new Error(`poll failed: ${res.status} ${await res.text()}`);
345
+ return await res.json();
346
+ }
347
+
348
+ export {
349
+ NotifyRequestSchema,
350
+ SetTaskStateSchema,
351
+ ScheduleCallbackSchema,
352
+ DeliveryModeSchema,
353
+ WAKE_EVENT,
354
+ wakeChannel,
355
+ submitNotification,
356
+ awaitReply,
357
+ checkReplies,
358
+ setTaskState,
359
+ registerDelivery,
360
+ scheduleCallback,
361
+ pollAnswer
362
+ };
@@ -1,6 +1,6 @@
1
1
  // src/device.ts
2
2
  import { execFile } from "child_process";
3
- import { mkdirSync, writeFileSync } from "fs";
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
4
4
  import { homedir, platform } from "os";
5
5
  import { join } from "path";
6
6
  var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
@@ -17,6 +17,28 @@ function saveToken(token) {
17
17
  writeFileSync(TOKEN_PATH, JSON.stringify(token, null, 2) + "\n", { mode: 384 });
18
18
  }
19
19
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
20
+ function readToken() {
21
+ if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
22
+ if (existsSync(TOKEN_PATH)) {
23
+ try {
24
+ return JSON.parse(readFileSync(TOKEN_PATH, "utf8")).access_token ?? "";
25
+ } catch {
26
+ }
27
+ }
28
+ return "";
29
+ }
30
+ function deleteToken() {
31
+ if (!existsSync(TOKEN_PATH)) return false;
32
+ rmSync(TOKEN_PATH);
33
+ return true;
34
+ }
35
+ async function revokeToken(token) {
36
+ const res = await fetch(`${BACKEND_URL}/api/device/revoke`, {
37
+ method: "POST",
38
+ headers: { authorization: `Bearer ${token}` }
39
+ });
40
+ return res.ok;
41
+ }
20
42
  async function requestCode(agent = AGENT_NAME) {
21
43
  const res = await fetch(`${BACKEND_URL}/api/device/code`, {
22
44
  method: "POST",
@@ -47,6 +69,9 @@ export {
47
69
  openBrowser,
48
70
  saveToken,
49
71
  sleep,
72
+ readToken,
73
+ deleteToken,
74
+ revokeToken,
50
75
  requestCode,
51
76
  pollToken
52
77
  };
package/dist/index.js CHANGED
@@ -1,11 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ DeliveryModeSchema,
4
+ NotifyRequestSchema,
5
+ ScheduleCallbackSchema,
6
+ SetTaskStateSchema,
7
+ awaitReply,
8
+ checkReplies,
9
+ pollAnswer,
10
+ registerDelivery,
11
+ scheduleCallback,
12
+ setTaskState,
13
+ submitNotification
14
+ } from "./chunk-4MRCUW5I.js";
15
+ import {
16
+ deleteToken,
3
17
  openBrowser,
4
18
  pollToken,
19
+ readToken,
5
20
  requestCode,
21
+ revokeToken,
6
22
  saveToken,
7
23
  sleep
8
- } from "./chunk-BSUXW66R.js";
24
+ } from "./chunk-XXBSXCOC.js";
9
25
 
10
26
  // src/index.ts
11
27
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -15,256 +31,27 @@ import {
15
31
  ListToolsRequestSchema
16
32
  } from "@modelcontextprotocol/sdk/types.js";
17
33
  import { execSync } from "child_process";
18
-
19
- // ../../packages/schema/dist/index.js
20
34
  import { z } from "zod";
21
- var ContextSchema = z.object({
22
- title: z.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
23
- 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.")
24
- });
25
- var OptionSchema = z.object({
26
- id: z.string(),
27
- label: z.string(),
28
- // .describe() flows into the MCP notify_user JSON schema (zodToJsonSchema), so
29
- // the constraints below are what an agent reads when deciding to use these.
30
- html: z.string().max(16384).describe(
31
- "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."
32
- ).optional(),
33
- image: z.string().url().describe(
34
- "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
35
- ).optional()
36
- });
37
- var VisualSchema = z.object({
38
- url: z.string().url(),
39
- label: z.string().optional()
40
- });
41
- var AgentSchema = z.object({
42
- name: z.string()
43
- });
44
- var NotifyRequestSchema = z.object({
45
- context: ContextSchema,
46
- options: z.array(OptionSchema).optional(),
47
- visuals: z.array(VisualSchema).optional(),
48
- agent: AgentSchema,
49
- /** Git repo the agent is working in ("owner/name"), when applicable. */
50
- repo: z.string().optional(),
51
- /** Git branch the agent is on, when applicable. */
52
- branch: z.string().optional(),
53
- /** Continue an existing conversation; omitted = start a new thread. */
54
- threadId: z.string().uuid().optional(),
55
- createdAt: z.string().datetime(),
56
- urgency: z.enum(["inbox", "call"]).default("inbox").describe(
57
- "'inbox' (default) = sits silently in the inbox for the user to get to. '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 becomes what they see on the ring, so make it specific."
58
- ),
59
- /** The request this one was spawned from, for a clarification. */
60
- parentId: z.string().optional(),
61
- /** How the user answers the options: pick one (default), pick several, or pick & order. */
62
- select: z.enum(["one", "many", "rank"]).default("one")
63
- });
64
- var NotifyStatusSchema = z.enum(["pending", "answered", "ignored"]);
65
- var AgentStateSchema = z.enum(["idle", "in_progress", "completed", "needs_input"]);
66
- var SetTaskStateSchema = z.object({
67
- state: z.enum(["in_progress", "completed", "needs_input"])
68
- });
69
- var UserAnswerSchema = z.discriminatedUnion("kind", [
70
- z.object({ kind: z.literal("option"), optionId: z.string() }),
71
- z.object({ kind: z.literal("text"), text: z.string() }),
72
- z.object({ kind: z.literal("ignored") }),
73
- z.object({ kind: z.literal("multi"), optionIds: z.array(z.string()) }),
74
- z.object({ kind: z.literal("ranked"), optionIds: z.array(z.string()) }),
75
- z.object({ kind: z.literal("clarify"), chunks: z.array(z.string()).min(1) })
76
- ]);
77
- var AwaitItemSchema = z.discriminatedUnion("type", [
78
- z.object({
79
- type: z.literal("reply"),
80
- threadId: z.string(),
81
- notificationId: z.string(),
82
- answer: UserAnswerSchema
83
- }),
84
- z.object({
85
- type: z.literal("remind"),
86
- threadId: z.string(),
87
- notificationId: z.string(),
88
- remindAt: z.string().datetime({ offset: true }),
89
- /** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
90
- remindInSeconds: z.number()
91
- }),
92
- z.object({ type: z.literal("idle") })
93
- ]);
94
- var PendingRepliesSchema = z.object({
95
- replies: z.array(
96
- z.object({ threadId: z.string(), notificationId: z.string(), answer: UserAnswerSchema })
97
- ),
98
- pending: z.array(
99
- z.object({ threadId: z.string(), notificationId: z.string(), createdAt: z.string() })
100
- )
101
- });
102
- var NotifyResponseSchema = z.object({
103
- id: z.string(),
104
- status: NotifyStatusSchema,
105
- createdAt: z.string().datetime(),
106
- answer: UserAnswerSchema.optional(),
107
- answeredAt: z.string().datetime().optional()
108
- });
109
- var UserResponseSchema = z.object({
110
- requestId: z.string(),
111
- answer: UserAnswerSchema,
112
- answeredAt: z.string().datetime()
113
- });
114
- var InboxItemSchema = z.object({
115
- id: z.string(),
116
- status: NotifyStatusSchema,
117
- context: ContextSchema,
118
- options: z.array(OptionSchema).optional(),
119
- visuals: z.array(VisualSchema).optional(),
120
- agent: z.string(),
121
- nickname: z.string(),
122
- repo: z.string().optional(),
123
- branch: z.string().optional(),
124
- createdAt: z.string().datetime(),
125
- snoozedUntil: z.string().datetime().optional(),
126
- agentState: AgentStateSchema.default("idle"),
127
- parentId: z.string().optional(),
128
- select: z.enum(["one", "many", "rank"]).default("one")
129
- });
130
- var SnoozeRequestSchema = z.object({
131
- requestId: z.string(),
132
- until: z.string().datetime()
133
- });
134
- var PushTokenSchema = z.object({
135
- voipToken: z.string().min(1),
136
- platform: z.enum(["ios"])
137
- });
138
- var UserSettingsSchema = z.object({
139
- callsEnabled: z.boolean()
140
- });
141
- var DeviceAgentTokenSchema = z.object({
142
- token: z.string(),
143
- deviceId: z.string(),
144
- agentName: z.string(),
145
- /** User-chosen session label; defaults to `<agent> <device>`. */
146
- nickname: z.string(),
147
- createdAt: z.string().datetime()
148
- });
149
- var PairingStatusSchema = z.enum(["pending", "approved", "denied", "expired"]);
150
- var DeviceCodeSchema = z.object({
151
- device_code: z.string(),
152
- user_code: z.string(),
153
- verification_uri: z.string().url(),
154
- verification_uri_complete: z.string().url(),
155
- interval: z.number(),
156
- expires_in: z.number()
157
- });
158
- var DeviceInfoSchema = z.object({
159
- code: z.string(),
160
- agent: z.string(),
161
- device: z.string().nullable(),
162
- status: PairingStatusSchema
163
- });
164
- var DeviceTokenSchema = z.object({
165
- access_token: z.string(),
166
- nickname: z.string(),
167
- agent: z.string(),
168
- device: z.string().nullable()
169
- });
170
-
171
- // src/index.ts
172
- import { z as z2 } from "zod";
173
35
  import { zodToJsonSchema } from "zod-to-json-schema";
174
-
175
- // src/client.ts
176
- import { existsSync, readFileSync } from "fs";
177
- import { homedir } from "os";
178
- import { join } from "path";
179
- var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
180
- var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
181
- function loadToken() {
182
- if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
183
- if (existsSync(TOKEN_PATH)) {
184
- try {
185
- const raw = readFileSync(TOKEN_PATH, "utf8");
186
- const parsed = JSON.parse(raw);
187
- if (parsed.access_token) return parsed.access_token;
188
- } catch {
189
- }
190
- }
191
- return "";
192
- }
193
- var ONBOARD_MSG = "Not paired with Paigy yet \u2014 call the `pair` tool to connect this agent (it returns an approval link to show the user), then retry. Manual fallback: `npx -y @paigy/mcp paigy-mcp-onboard`.";
194
- function ensureAuthed(res) {
195
- if (res.status === 401) throw new Error(ONBOARD_MSG);
196
- return res;
197
- }
198
- async function submitNotification(req) {
199
- const token = loadToken();
200
- const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/notify`, {
201
- method: "POST",
202
- headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
203
- body: JSON.stringify(req)
204
- }));
205
- if (!res.ok) throw new Error(`notify failed: ${res.status} ${await res.text()}`);
206
- return await res.json();
207
- }
208
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
209
- async function awaitReply(notificationId, opts = {}) {
210
- const intervalMs = opts.intervalMs ?? 5e3;
211
- const windowMs = opts.windowMs ?? 3e5;
212
- const doSleep = opts.sleep ?? sleep2;
213
- const now = opts.now ?? Date.now;
214
- const token = loadToken();
215
- const start = now();
216
- while (true) {
217
- const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`, {
218
- headers: { authorization: `Bearer ${token}` }
219
- }));
220
- if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
221
- const item = await res.json();
222
- if (item.type !== "idle") return item;
223
- if (now() - start >= windowMs) return { type: "idle" };
224
- await doSleep(intervalMs);
225
- }
226
- }
227
- async function checkReplies() {
228
- const token = loadToken();
229
- const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/pending`, {
230
- headers: { authorization: `Bearer ${token}` }
231
- }));
232
- if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
233
- return await res.json();
234
- }
235
- async function setTaskState(id, state) {
236
- const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/notify/${id}/state`, {
237
- method: "PATCH",
238
- headers: { "content-type": "application/json", authorization: `Bearer ${loadToken()}` },
239
- body: JSON.stringify({ state })
240
- }));
241
- if (!res.ok) throw new Error(`set_task_state failed: ${res.status} ${await res.text()}`);
242
- return await res.json();
243
- }
244
- async function pollAnswer(id) {
245
- const token = loadToken();
246
- const res = ensureAuthed(await fetch(`${BACKEND_URL}/api/poll/${id}`, {
247
- headers: { authorization: `Bearer ${token}` }
248
- }));
249
- if (!res.ok) throw new Error(`poll failed: ${res.status} ${await res.text()}`);
250
- return await res.json();
251
- }
252
-
253
- // src/index.ts
254
- var PollAnswerSchema = z2.object({
255
- id: z2.string().describe("Request id returned by notify_user.")
36
+ var PollAnswerSchema = z.object({
37
+ id: z.string().describe("Request id returned by notify_user.")
256
38
  });
257
- var AwaitReplySchema = z2.object({
258
- notificationId: z2.string().describe("The id returned by notify_user \u2014 waits for the user's reply to THIS notification only.")
39
+ var AwaitReplySchema = z.object({
40
+ notificationId: z.string().describe("The id returned by notify_user \u2014 waits for the user's reply to THIS notification only.")
259
41
  });
260
- var CheckRepliesSchema = z2.object({});
261
- var PairSchema = z2.object({
262
- device_code: z2.string().optional().describe("Omit to start pairing (returns an approval link to show the user). Pass the device_code from that first call to finish, once the user has approved.")
42
+ var CheckRepliesSchema = z.object({});
43
+ var PairSchema = z.object({
44
+ device_code: z.string().optional().describe("Omit to start pairing (returns an approval link to show the user). Pass the device_code from that first call to finish, once the user has approved.")
263
45
  });
264
- var SetTaskStateToolSchema = z2.object({
265
- notificationId: z2.string(),
46
+ var SetTaskStateToolSchema = z.object({
47
+ notificationId: z.string(),
266
48
  state: SetTaskStateSchema.shape.state
267
49
  });
50
+ var RegisterDeliveryToolSchema = z.object({
51
+ mode: DeliveryModeSchema.describe(
52
+ "'self_hosted' = the API pushes the moment work lands (requires `npx -y @paigy/mcp paigy-listen` running); 'poll' = the default, you catch up via check_replies/await_reply."
53
+ )
54
+ });
268
55
  function detectGit() {
269
56
  const run = (cmd) => {
270
57
  try {
@@ -285,7 +72,7 @@ var server = new Server(
285
72
  { name: "paigy", version: "0.0.0" },
286
73
  {
287
74
  capabilities: { tools: {} },
288
- instructions: "On startup, call check_replies once to pick up any replies or pending work you missed while away. To wait for the answer to something you just asked, call await_reply with that notification's id \u2014 it's scoped to that one request, so it never returns replies meant for other requests. Use check_replies again only when re-booting or after waiting a long time on something else. Never end a turn that still needs the user without notify_user + await_reply. If a reply comes back as { kind: 'clarify', chunks: [...] }, the user wants more detail on those chunks \u2014 respond by calling notify_user again with the SAME threadId and an expanded description covering them."
75
+ instructions: "On startup, call check_replies once to pick up any replies or pending work you missed while away. To wait for the answer to something you just asked, call await_reply with that notification's id \u2014 it's scoped to that one request, so it never returns replies meant for other requests. Use check_replies again only when re-booting or after waiting a long time on something else. Never end a turn that still needs the user without notify_user + await_reply. When you need a decision or input, PREFER a structured answer (select:'confirm', 'one', 'many', or 'rank') over an open-ended free-text ask \u2014 structured options save the user a tap, and they can always add free text on top. If a reply comes back as { kind: 'clarify', chunks: [...] }, the user wants more detail on those chunks \u2014 respond by calling notify_user again with the SAME threadId and an expanded description covering them. When urgency is 'call', remember the title + description are spoken aloud \u2014 write them short and conversational, and name things instead of using IDs (e.g. 'the pull request about the agents page', not 'PR #235'). When the user asks you to follow up later \u2014 when you're done, if you're blocked, or at a set time \u2014 record it with schedule_callback so you don't drop it if you go idle."
289
76
  }
290
77
  );
291
78
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -295,9 +82,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
295
82
  description: "Pair this agent with the user's Paigy account (one-time) \u2014 required before notify_user/await_reply work. Two steps: (1) call with NO args to start; it opens the user's browser and returns { verification_uri_complete, user_code, device_code } \u2014 show the user the URL + user_code and ask them to approve. (2) call again passing that device_code to finish; it waits for approval and saves the token. If it returns { status:'pending' }, the user hasn't approved yet \u2014 call again with the same device_code to keep waiting.",
296
83
  inputSchema: zodToJsonSchema(PairSchema, { target: "openApi3" })
297
84
  },
85
+ {
86
+ name: "unpair",
87
+ description: "Log out / unpair this agent from the user's Paigy account: revokes the token server-side (it stops working everywhere) and deletes the local ~/.paigy/token.json. Takes no arguments. After this, notify_user/await_reply won't work until the user pairs again with the pair tool.",
88
+ inputSchema: zodToJsonSchema(z.object({}), { target: "openApi3" })
89
+ },
298
90
  {
299
91
  name: "notify_user",
300
- description: "Notify the user via Paigy and get a request id to poll for their answer. Provide context.title (a specific, non-empty one-line headline \u2014 this is what the user sees first, and what shows on the ring for a call) and context.description (an array of standalone, non-empty detail chunks the user can selectively ask you to expand). Set `urgency`: 'inbox' (default) drops it silently in their inbox; 'call' rings their phone now as a voice call \u2014 use 'call' only when you genuinely need them in the moment (blocked/waiting, time-sensitive), not for routine FYIs. Plus optional options (answerable choices; each may carry a sandboxed `html` or an `image` preview for a visual 'pick one' \u2014 e.g. show layout/UI alternatives) and visuals. Use `select` to control how the user answers options: 'one' (default) = pick a single option; 'many' = multi-select, user checks any subset \u2192 answer arrives as {kind:'multi', optionIds:[...]}; 'rank' = select and order, user taps options in preferred order \u2192 answer arrives as {kind:'ranked', optionIds:[...]} (ordered by choice). If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail on those chunks \u2014 respond via notify_user with the SAME threadId and an expanded description. Pass `threadId` from a prior notify_user result or an await_reply reply to continue that conversation thread; omit it to start a new one.",
92
+ description: "Notify the user via Paigy and get a request id to poll for their answer. Provide context.title (a specific, non-empty one-line headline \u2014 this is what the user sees first, and what shows on the ring for a call) and context.description (an array of standalone, non-empty detail chunks the user can selectively ask you to expand). Set `urgency`: 'inbox' (default) drops it silently in their inbox; 'push' is a quiet passive notification (no sound); 'banner' sends a time-sensitive banner/lock-screen push (a 'paige') they tap to open \u2014 for when you need them soon-ish but not enough to ring them; 'call' rings their phone now as a voice call \u2014 only when you genuinely need them in the moment (blocked/waiting, time-sensitive). ON A CALL, your title + description are READ ALOUD by a voice \u2014 write them to be HEARD, not read: keep it short and conversational, front-load the ask, and refer to things BY NAME, not by ID or code (say 'the pull request about the agents page', not 'PR #235'; 'the login-bug ticket', not 'ABC-1234'). Spell out only what's natural to say out loud. DEFAULT to a structured answer shape \u2014 structured options save the user a tap, and they can ALWAYS add free text on top of any structured paige, so you lose nothing by structuring it. Pick the answer shape with `select` \u2014 DON'T leave a decision as open-ended free text: a yes/no or approve/deny \u2192 select:'confirm' (set confirmStyle:'approve' for Approve/Deny); it's answerable right from the banner and arrives as {kind:'confirm', approved:boolean}. Pick one of several \u2192 options + select:'one' (default) \u2192 {kind:'option', optionId}. Pick several \u2192 select:'many' \u2192 {kind:'multi', optionIds:[...]}. Rank/order a subset \u2192 select:'rank', user taps in preferred order \u2192 {kind:'ranked', optionIds:[...]}. For visual choices give each option a sandboxed `html` or an `image` preview (e.g. layout/UI alternatives). Only leave options off (pure free text / voice) when the answer genuinely can't be structured. Plus optional visuals. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail on those chunks \u2014 respond via notify_user with the SAME threadId and an expanded description. Pass `threadId` from a prior notify_user result or an await_reply reply to continue that conversation thread; omit it to start a new one. To follow up on a call (e.g. the user asked you to 'call me back when it's done'), reuse the threadId from that call's reply so it threads as the same conversation.",
301
93
  inputSchema: zodToJsonSchema(NotifyRequestSchema, { target: "openApi3" })
302
94
  },
303
95
  {
@@ -307,18 +99,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
307
99
  },
308
100
  {
309
101
  name: "await_reply",
310
- description: "Wait for the user's reply to THE specific notification you sent for the current request (pass its id from notify_user). This is how you wait for your answer in-context. Polls ~5 min; returns { type:'reply', answer } when they respond, { type:'remind', remindInSeconds } on snooze (ScheduleWakeup then await_reply again), or { type:'idle' }. Scoped to that one notification \u2014 it NEVER returns replies meant for other notifications/requests, so concurrent requests don't cross.",
102
+ description: "Wait for the user's reply to THE specific notification you sent for the current request (pass its id from notify_user). This is how you wait for your answer in-context. Polls ~5 min; returns { type:'reply', answer } when they respond, { type:'remind', remindInSeconds } on snooze (ScheduleWakeup then await_reply again), or { type:'idle' }. Scoped to that one notification \u2014 it NEVER returns replies meant for other notifications/requests, so concurrent requests don't cross. A CALL answer can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction and any later turns as their follow-up (e.g. the end-of-call 'call me back when it's done / I have a blocking question' reply). If they asked for a callback, re-engage in the SAME thread (notify_user with the reply's threadId) when the task is done or you hit a blocker \u2014 urgency:'call' for a blocker, 'banner'/'push'/'inbox' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing).",
311
103
  inputSchema: zodToJsonSchema(AwaitReplySchema, { target: "openApi3" })
312
104
  },
313
105
  {
314
106
  name: "check_replies",
315
- description: "The catch-up router for everything NOT tied to your current request: returns replies the user sent that you haven't seen yet (now marked seen) plus still-pending notifications you sent. Use it when booting up / starting a session, or when you've been waiting a long time on something else, to discover acks or work you're unaware of. To wait on a request you just sent, use await_reply instead.",
107
+ description: "The catch-up router for everything NOT tied to your current request: returns replies the user sent that you haven't seen yet (now marked seen), still-pending notifications you sent, and `requests` \u2014 new requests the user started toward you (each { notificationId, threadId, text }, returned once). Act on each request and reply with notify_user on the SAME threadId. Use it when booting up / starting a session, or when you've been waiting a long time on something else, to discover acks or work you're unaware of. To wait on a request you just sent, use await_reply instead. Also returns owedCallbacks: callbacks now due that you promised \u2014 fulfill each with notify_user on its threadId.",
316
108
  inputSchema: zodToJsonSchema(CheckRepliesSchema, { target: "openApi3" })
317
109
  },
318
110
  {
319
111
  name: "set_task_state",
320
112
  description: "Report progress on a request you received: in_progress (you started working), completed (done), or needs_input (you need more from the user \u2014 usually paired with a notify_user carrying parentId = this request's id).",
321
113
  inputSchema: zodToJsonSchema(SetTaskStateToolSchema, { target: "openApi3" })
114
+ },
115
+ {
116
+ name: "register_delivery",
117
+ description: "Choose how Paigy reaches you when a reply or new request lands. 'self_hosted' makes Paigy PUSH it instantly over a realtime channel instead of you polling \u2014 but it only helps if a `paigy listen` daemon is running (npx -y @paigy/mcp paigy-listen), which the user runs once. 'poll' (default) keeps the catch-up model (check_replies / await_reply). Either way no work is ever lost: a missed push is reconciled by your next check_replies sweep.",
118
+ inputSchema: zodToJsonSchema(RegisterDeliveryToolSchema, { target: "openApi3" })
119
+ },
120
+ {
121
+ name: "schedule_callback",
122
+ description: "Promise the user a follow-up you'll keep even if you go idle. Use it when they ask you to report back: trigger 'on_done' (when you finish \u2014 fires when you call set_task_state completed), 'on_blocked' (if you hit a blocker \u2014 fires on set_task_state needs_input), or 'scheduled' with dueInSeconds (e.g. 'remind me in 10 min'). Pass the threadId of the conversation and a short note. Fulfill it by calling notify_user on that threadId; check_replies re-lists due callbacks until you do.",
123
+ inputSchema: zodToJsonSchema(ScheduleCallbackSchema, { target: "openApi3" })
322
124
  }
323
125
  ]
324
126
  }));
@@ -344,7 +146,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
344
146
  };
345
147
  }
346
148
  const start = Date.now();
347
- const capMs = 24e4;
149
+ const capMs = 3e4;
348
150
  while (Date.now() - start < capMs) {
349
151
  const token = await pollToken(device_code);
350
152
  if (token) {
@@ -356,7 +158,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
356
158
  }]
357
159
  };
358
160
  }
359
- await sleep(5e3);
161
+ await sleep(2e3);
360
162
  }
361
163
  return {
362
164
  content: [{
@@ -364,7 +166,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
364
166
  text: JSON.stringify({
365
167
  status: "pending",
366
168
  device_code,
367
- message: "Still awaiting approval. Call pair again with this device_code to keep waiting."
169
+ message: "Still awaiting approval after ~30s. Call pair again with this device_code to keep waiting."
170
+ })
171
+ }]
172
+ };
173
+ }
174
+ case "unpair": {
175
+ const token = readToken();
176
+ let revoked = false;
177
+ if (token) {
178
+ try {
179
+ revoked = await revokeToken(token);
180
+ } catch {
181
+ revoked = false;
182
+ }
183
+ }
184
+ const removed = deleteToken();
185
+ return {
186
+ content: [{
187
+ type: "text",
188
+ text: JSON.stringify({
189
+ ok: true,
190
+ revoked,
191
+ removed,
192
+ message: token ? `Unpaired. Server token ${revoked ? "revoked" : "revoke not confirmed"}; local token ${removed ? "deleted" : "was already absent"}.` : "No token found \u2014 already unpaired."
368
193
  })
369
194
  }]
370
195
  };
@@ -400,6 +225,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
400
225
  const result = await setTaskState(notificationId, state);
401
226
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
402
227
  }
228
+ case "register_delivery": {
229
+ const { mode } = RegisterDeliveryToolSchema.parse(request.params.arguments);
230
+ const result = await registerDelivery(mode);
231
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
232
+ }
233
+ case "schedule_callback": {
234
+ const result = await scheduleCallback(ScheduleCallbackSchema.parse(request.params.arguments));
235
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
236
+ }
403
237
  default:
404
238
  throw new Error(`Unknown tool: ${request.params.name}`);
405
239
  }
package/dist/listen.js ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ WAKE_EVENT,
4
+ checkReplies,
5
+ registerDelivery,
6
+ wakeChannel
7
+ } from "./chunk-4MRCUW5I.js";
8
+
9
+ // src/listen.ts
10
+ import { createClient } from "@supabase/supabase-js";
11
+ import { spawn } from "child_process";
12
+ var emit = (obj) => void process.stdout.write(JSON.stringify(obj) + "\n");
13
+ async function sweep(reason) {
14
+ try {
15
+ const work = await checkReplies();
16
+ emit({ type: "sweep", reason, ...work });
17
+ const cmd = process.env.PAIGY_ON_WAKE;
18
+ if (cmd && (work.replies.length > 0 || work.requests.length > 0 || work.owedCallbacks.length > 0)) {
19
+ spawn(cmd, { shell: true, stdio: "inherit" });
20
+ }
21
+ } catch (e) {
22
+ emit({ type: "error", reason, message: e.message });
23
+ }
24
+ }
25
+ async function main() {
26
+ const cfg = await registerDelivery("self_hosted");
27
+ const url = cfg.realtime?.url ?? process.env.PAIGY_SUPABASE_URL;
28
+ const anonKey = cfg.realtime?.anonKey ?? process.env.PAIGY_SUPABASE_ANON_KEY;
29
+ if (!url || !anonKey) {
30
+ throw new Error(
31
+ "No Realtime credentials \u2014 set SUPABASE_ANON_KEY on the server, or PAIGY_SUPABASE_URL + PAIGY_SUPABASE_ANON_KEY locally."
32
+ );
33
+ }
34
+ const channel = wakeChannel(cfg.tokenId);
35
+ emit({ type: "listening", channel });
36
+ await sweep("boot");
37
+ const sb = createClient(url, anonKey, { auth: { persistSession: false } });
38
+ sb.channel(channel).on(
39
+ "broadcast",
40
+ { event: WAKE_EVENT },
41
+ (msg) => void sweep(`wake:${msg.payload?.kind ?? "?"}`)
42
+ ).subscribe();
43
+ const shutdown = async () => {
44
+ try {
45
+ await registerDelivery("poll");
46
+ } catch {
47
+ }
48
+ process.exit(0);
49
+ };
50
+ process.on("SIGINT", () => void shutdown());
51
+ process.on("SIGTERM", () => void shutdown());
52
+ }
53
+ if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/^file:\/\//, ""))) {
54
+ main().catch((e) => {
55
+ emit({ type: "fatal", message: e.message });
56
+ process.exit(1);
57
+ });
58
+ }
59
+ export {
60
+ sweep
61
+ };
package/dist/onboard.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  requestCode,
8
8
  saveToken,
9
9
  sleep
10
- } from "./chunk-BSUXW66R.js";
10
+ } from "./chunk-XXBSXCOC.js";
11
11
 
12
12
  // src/onboard.ts
13
13
  async function main() {
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@paigy/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.7.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",
7
7
  "bin": {
8
8
  "mcp": "./dist/index.js",
9
9
  "paigy-mcp": "./dist/index.js",
10
- "paigy-mcp-onboard": "./dist/onboard.js"
10
+ "paigy-mcp-onboard": "./dist/onboard.js",
11
+ "paigy-listen": "./dist/listen.js"
11
12
  },
12
13
  "files": [
13
14
  "dist"
@@ -20,22 +21,24 @@
20
21
  "url": "git+https://github.com/mauurda/paigy.git",
21
22
  "directory": "apps/mcp"
22
23
  },
24
+ "scripts": {
25
+ "build": "tsup src/index.ts src/onboard.ts src/listen.ts --format esm --clean",
26
+ "dev": "tsup src/index.ts src/onboard.ts src/listen.ts --format esm --watch",
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "vitest run",
29
+ "prepublishOnly": "pnpm --filter @paigy/schema build && pnpm build"
30
+ },
23
31
  "dependencies": {
24
32
  "@modelcontextprotocol/sdk": "^1.0.4",
33
+ "@supabase/supabase-js": "^2.47.10",
25
34
  "zod": "^3.24.1",
26
35
  "zod-to-json-schema": "^3.24.1"
27
36
  },
28
37
  "devDependencies": {
38
+ "@paigy/schema": "workspace:*",
29
39
  "@types/node": "^22.0.0",
30
40
  "tsup": "^8.3.5",
31
41
  "typescript": "^5.7.2",
32
- "vitest": "^2.1.8",
33
- "@paigy/schema": "0.0.0"
34
- },
35
- "scripts": {
36
- "build": "tsup src/index.ts src/onboard.ts --format esm --clean",
37
- "dev": "tsup src/index.ts src/onboard.ts --format esm --watch",
38
- "typecheck": "tsc --noEmit",
39
- "test": "vitest run"
42
+ "vitest": "^2.1.8"
40
43
  }
41
44
  }