@paigy/mcp 0.8.5 → 0.10.2
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 +6 -4
- package/dist/{chunk-FDJFLJHR.js → chunk-5EY3Q5DX.js} +72 -29
- package/dist/index.js +17 -44
- package/dist/listen.js +13 -2
- package/dist/statusline.js +11 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,11 +92,13 @@ Then pair: `npx -p @paigy/mcp@latest paigy-mcp-onboard`.
|
|
|
92
92
|
|
|
93
93
|
## Tools
|
|
94
94
|
|
|
95
|
-
- **`
|
|
96
|
-
- **`
|
|
97
|
-
- **`
|
|
98
|
-
- **`
|
|
95
|
+
- **`pair`** — pair this agent with the user's Paigy account (one-time). No args to start (opens the approval link); pass the returned `device_code` to finish.
|
|
96
|
+
- **`unpair`** — log this agent out of the user's Paigy account; revokes the token server-side and deletes the local one.
|
|
97
|
+
- **`notify_user`** — notify the user (context: title + description chunks; a required `select` answer shape — `one`/`many`/`rank` take `options`, `confirm`/`text` don't; plus `visuals`, `urgency`, and `parentId` for a clarification). Options carry no ids — the backend assigns them by position ("1", "2", …) and answers reference those; each option can carry a sandboxed `html` or `image` preview for visual "pick one" decisions. Returns `{ notificationId, threadId }`. If a reply comes back as `{kind:'clarify', chunks:[...]}`, respond via notify_user with the SAME threadId and an expanded description.
|
|
98
|
+
- **`await_reply`** — wait for the user's reply to a specific notification (pass its notificationId). Scoped: will not return replies meant for other notifications. Returns `reply` / `remind` / `idle`.
|
|
99
|
+
- **`check_replies`** — catch-up sweep: returns replies you haven't consumed yet (now marked seen) plus still-pending notifications, new user-initiated requests, and owed callbacks.
|
|
99
100
|
- **`set_task_state`** — report progress on a request: `in_progress` / `completed` / `needs_input`.
|
|
101
|
+
- **`schedule_callback`** — promise the user a follow-up (`on_done` / `on_blocked` / `scheduled`) so it's not dropped if you go idle.
|
|
100
102
|
|
|
101
103
|
## Configuration
|
|
102
104
|
|
|
@@ -25,32 +25,38 @@ var VisualSchema = z.object({
|
|
|
25
25
|
url: z.string().url(),
|
|
26
26
|
label: z.string().optional()
|
|
27
27
|
});
|
|
28
|
-
var AgentSchema = z.object({
|
|
29
|
-
name: z.string()
|
|
30
|
-
});
|
|
31
28
|
var NotifyLevelSchema = z.enum(["inbox", "push", "banner", "call"]);
|
|
32
29
|
var NotifyRequestSchema = z.object({
|
|
33
30
|
context: ContextSchema,
|
|
34
|
-
options: z.array(OptionSchema).optional()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
options: z.array(OptionSchema.omit({ id: true })).min(1).optional().describe(
|
|
32
|
+
"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)."
|
|
33
|
+
),
|
|
34
|
+
visuals: z.array(VisualSchema).optional().describe(
|
|
35
|
+
"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."
|
|
36
|
+
),
|
|
37
|
+
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
38
38
|
repo: z.string().optional(),
|
|
39
|
-
/** Git branch the agent is on
|
|
39
|
+
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
40
40
|
branch: z.string().optional(),
|
|
41
41
|
/** Continue an existing conversation; omitted = start a new thread. */
|
|
42
42
|
threadId: z.string().uuid().optional(),
|
|
43
|
-
createdAt: z.string().datetime(),
|
|
44
43
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
45
44
|
"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."
|
|
46
45
|
),
|
|
47
46
|
/** The request this one was spawned from, for a clarification. */
|
|
48
47
|
parentId: z.string().optional(),
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
select: z.enum(["one", "many", "rank", "confirm", "text"]).describe(
|
|
49
|
+
"How the user answers \u2014 required, 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."
|
|
50
|
+
),
|
|
51
51
|
confirmStyle: z.enum(["yesno", "approve"]).default("yesno").describe(
|
|
52
52
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
53
53
|
)
|
|
54
|
+
}).superRefine((r, ctx) => {
|
|
55
|
+
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
56
|
+
if (needsOptions && !r.options?.length)
|
|
57
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
58
|
+
if (!needsOptions && r.options?.length)
|
|
59
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
54
60
|
});
|
|
55
61
|
var NotifyStatusSchema = z.enum(["pending", "answered", "ignored"]);
|
|
56
62
|
var AgentStateSchema = z.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
@@ -97,7 +103,16 @@ var ScheduleCallbackSchema = z.object({
|
|
|
97
103
|
});
|
|
98
104
|
var PendingRepliesSchema = z.object({
|
|
99
105
|
replies: z.array(
|
|
100
|
-
z.object({
|
|
106
|
+
z.object({
|
|
107
|
+
threadId: z.string(),
|
|
108
|
+
notificationId: z.string(),
|
|
109
|
+
answer: UserAnswerSchema,
|
|
110
|
+
/** True if this reply was already claimed on a prior check_replies/await_reply —
|
|
111
|
+
* re-surfaced because includeRecent recovered it (e.g. after an unexpected
|
|
112
|
+
* restart). You may have already acted on it; check before repeating a
|
|
113
|
+
* side-effecting response. */
|
|
114
|
+
redelivered: z.boolean().optional()
|
|
115
|
+
})
|
|
101
116
|
),
|
|
102
117
|
pending: z.array(
|
|
103
118
|
z.object({ threadId: z.string(), notificationId: z.string(), createdAt: z.string() })
|
|
@@ -115,7 +130,7 @@ var PendingRepliesSchema = z.object({
|
|
|
115
130
|
)
|
|
116
131
|
});
|
|
117
132
|
var NotifyResponseSchema = z.object({
|
|
118
|
-
|
|
133
|
+
notificationId: z.string(),
|
|
119
134
|
status: NotifyStatusSchema,
|
|
120
135
|
createdAt: z.string().datetime(),
|
|
121
136
|
answer: UserAnswerSchema.optional(),
|
|
@@ -148,7 +163,7 @@ var InboxItemSchema = z.object({
|
|
|
148
163
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
149
164
|
error: z.string().optional(),
|
|
150
165
|
parentId: z.string().optional(),
|
|
151
|
-
select: z.enum(["one", "many", "rank", "confirm"]).default("one"),
|
|
166
|
+
select: z.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
152
167
|
confirmStyle: z.enum(["yesno", "approve"]).default("yesno").describe(
|
|
153
168
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
154
169
|
),
|
|
@@ -251,6 +266,43 @@ var StatusSchema = z.object({
|
|
|
251
266
|
/** A phone is registered for push/ring (any push token on the account). */
|
|
252
267
|
phone: z.boolean()
|
|
253
268
|
});
|
|
269
|
+
var EnvelopeRecipientSchema = z.object({
|
|
270
|
+
keyId: z.string(),
|
|
271
|
+
epk: z.string(),
|
|
272
|
+
wnonce: z.string(),
|
|
273
|
+
wrap: z.string()
|
|
274
|
+
});
|
|
275
|
+
var EnvelopeHeaderSchema = z.object({
|
|
276
|
+
field: z.enum(["context", "options", "visuals", "answer"]),
|
|
277
|
+
kind: z.string(),
|
|
278
|
+
senderRole: z.enum(["agent", "user"]),
|
|
279
|
+
recipientKeyIds: z.array(z.string()),
|
|
280
|
+
seq: z.number().int().nonnegative()
|
|
281
|
+
});
|
|
282
|
+
var EnvelopeSchema = z.object({
|
|
283
|
+
v: z.literal(1),
|
|
284
|
+
alg: z.literal("x25519-xsalsa20poly1305"),
|
|
285
|
+
msgId: z.string(),
|
|
286
|
+
hdr: EnvelopeHeaderSchema,
|
|
287
|
+
recipients: z.array(EnvelopeRecipientSchema).min(1),
|
|
288
|
+
nonce: z.string(),
|
|
289
|
+
ct: z.string()
|
|
290
|
+
});
|
|
291
|
+
var SealedAnswerSchema = z.object({
|
|
292
|
+
ignored: z.boolean(),
|
|
293
|
+
envelope: EnvelopeSchema
|
|
294
|
+
});
|
|
295
|
+
var DeviceCredentialSchema = z.object({
|
|
296
|
+
deviceId: z.string(),
|
|
297
|
+
kind: z.enum(["phone", "web", "agent"]),
|
|
298
|
+
x25519Pub: z.string(),
|
|
299
|
+
ed25519Pub: z.string(),
|
|
300
|
+
sig: z.string()
|
|
301
|
+
});
|
|
302
|
+
var DeviceRosterSchema = z.object({
|
|
303
|
+
uikPub: z.string(),
|
|
304
|
+
devices: z.array(DeviceCredentialSchema)
|
|
305
|
+
});
|
|
254
306
|
var WakeNudgeSchema = z.object({
|
|
255
307
|
kind: z.enum(["reply", "request", "callback"]),
|
|
256
308
|
notificationId: z.string().optional(),
|
|
@@ -342,16 +394,17 @@ async function awaitReply(notificationId, opts = {}) {
|
|
|
342
394
|
await doSleep(intervalMs);
|
|
343
395
|
}
|
|
344
396
|
}
|
|
345
|
-
async function checkReplies() {
|
|
397
|
+
async function checkReplies(includeRecent = false) {
|
|
346
398
|
const token = loadToken();
|
|
347
|
-
const
|
|
399
|
+
const qs = includeRecent ? "?includeRecent=true" : "";
|
|
400
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending${qs}`, {
|
|
348
401
|
headers: { authorization: `Bearer ${token}` }
|
|
349
402
|
}));
|
|
350
403
|
if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
|
|
351
404
|
return await res.json();
|
|
352
405
|
}
|
|
353
|
-
async function setTaskState(
|
|
354
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify/${
|
|
406
|
+
async function setTaskState(notificationId, state) {
|
|
407
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify/${notificationId}/state`, {
|
|
355
408
|
method: "PATCH",
|
|
356
409
|
headers: { "content-type": "application/json", authorization: `Bearer ${loadToken()}` },
|
|
357
410
|
body: JSON.stringify({ state })
|
|
@@ -378,20 +431,11 @@ async function scheduleCallback(req) {
|
|
|
378
431
|
if (!res.ok) throw new Error(`schedule_callback failed: ${res.status} ${await res.text()}`);
|
|
379
432
|
return await res.json();
|
|
380
433
|
}
|
|
381
|
-
async function pollAnswer(id) {
|
|
382
|
-
const token = loadToken();
|
|
383
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/poll/${id}`, {
|
|
384
|
-
headers: { authorization: `Bearer ${token}` }
|
|
385
|
-
}));
|
|
386
|
-
if (!res.ok) throw new Error(`poll failed: ${res.status} ${await res.text()}`);
|
|
387
|
-
return await res.json();
|
|
388
|
-
}
|
|
389
434
|
|
|
390
435
|
export {
|
|
391
436
|
NotifyRequestSchema,
|
|
392
437
|
SetTaskStateSchema,
|
|
393
438
|
ScheduleCallbackSchema,
|
|
394
|
-
DeliveryModeSchema,
|
|
395
439
|
WAKE_EVENT,
|
|
396
440
|
wakeChannel,
|
|
397
441
|
submitNotification,
|
|
@@ -399,6 +443,5 @@ export {
|
|
|
399
443
|
checkReplies,
|
|
400
444
|
setTaskState,
|
|
401
445
|
registerDelivery,
|
|
402
|
-
scheduleCallback
|
|
403
|
-
pollAnswer
|
|
446
|
+
scheduleCallback
|
|
404
447
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
DeliveryModeSchema,
|
|
4
3
|
NotifyRequestSchema,
|
|
5
4
|
ScheduleCallbackSchema,
|
|
6
5
|
SetTaskStateSchema,
|
|
7
6
|
awaitReply,
|
|
8
7
|
checkReplies,
|
|
9
|
-
pollAnswer,
|
|
10
|
-
registerDelivery,
|
|
11
8
|
scheduleCallback,
|
|
12
9
|
setTaskState,
|
|
13
10
|
submitNotification
|
|
14
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-5EY3Q5DX.js";
|
|
15
12
|
import {
|
|
16
13
|
deleteToken,
|
|
17
14
|
openBrowser,
|
|
@@ -60,13 +57,14 @@ function json(s) {
|
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
// src/index.ts
|
|
63
|
-
var PollAnswerSchema = z.object({
|
|
64
|
-
id: z.string().describe("Request id returned by notify_user.")
|
|
65
|
-
});
|
|
66
60
|
var AwaitReplySchema = z.object({
|
|
67
|
-
notificationId: z.string().describe("The
|
|
61
|
+
notificationId: z.string().describe("The notificationId returned by notify_user \u2014 waits for the user's reply to THIS notification only.")
|
|
62
|
+
});
|
|
63
|
+
var CheckRepliesSchema = z.object({
|
|
64
|
+
includeRecent: z.boolean().optional().describe(
|
|
65
|
+
"Set true only if you just unexpectedly restarted/reconnected and might have already claimed a reply without acting on it (that reply won't show up again otherwise \u2014 claims are one-time). Re-includes replies from the last ~10 min, marked redelivered:true so you can tell they may be duplicates. Leave false/omitted for normal use."
|
|
66
|
+
)
|
|
68
67
|
});
|
|
69
|
-
var CheckRepliesSchema = z.object({});
|
|
70
68
|
var PairSchema = z.object({
|
|
71
69
|
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.")
|
|
72
70
|
});
|
|
@@ -74,11 +72,6 @@ var SetTaskStateToolSchema = z.object({
|
|
|
74
72
|
notificationId: z.string(),
|
|
75
73
|
state: SetTaskStateSchema.shape.state
|
|
76
74
|
});
|
|
77
|
-
var RegisterDeliveryToolSchema = z.object({
|
|
78
|
-
mode: DeliveryModeSchema.describe(
|
|
79
|
-
"'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."
|
|
80
|
-
)
|
|
81
|
-
});
|
|
82
75
|
function detectGit() {
|
|
83
76
|
const run = (cmd) => {
|
|
84
77
|
try {
|
|
@@ -99,14 +92,14 @@ var server = new Server(
|
|
|
99
92
|
{ name: "paigy", version: "0.0.0" },
|
|
100
93
|
{
|
|
101
94
|
capabilities: { tools: {} },
|
|
102
|
-
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
|
|
95
|
+
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 notificationId \u2014 it's scoped to that one notification, so it never returns replies meant for other notifications. Use check_replies again only when re-booting or after waiting a long time on something else. Never end a turn that still needs the user without notify_user + await_reply. When you need a decision or input, MATCH the answer shape to the question \u2014 don't default everything to free text, and don't reflexively make everything yes/no. Pick the best tool for the job: yes/no \u2192 select:'confirm'; approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve'; pick one of several \u2192 options + select:'one'; pick several / a subset \u2192 options + select:'many'; rank or prioritize \u2192 options + select:'rank'. Reserve select:'text' (free-form reply only) for plain updates and answers that genuinely can't be structured (the user can always add free text on top of any shape). On a { kind: 'clarify' } reply, see notify_user's own description for how to respond. When urgency is 'call', remember the title + description are spoken aloud \u2014 write them short and conversational, and name things instead of using IDs (e.g. 'the pull request about the agents page', not 'PR #235'). When the user asks you to follow up later \u2014 when you're done, if you're blocked, or at a set time \u2014 record it with schedule_callback so you don't drop it if you go idle. If you're about to start a genuinely long-running or blocking piece of work \u2014 one where the user would otherwise sit and wait \u2014 mention ONCE, in passing, that you can reach them when it's done or if you hit a blocker, instead of them needing to babysit the terminal. Don't offer this for quick tasks, and don't repeat the offer if they've already said yes or no earlier in the conversation. Escalate silence, don't just wait on it: if you notified at a lower urgency (inbox/push/banner) for something that's genuinely blocking real progress, call await_reply up to twice (~5 min each, ~10 min total) \u2014 if it's still idle after that AND the item is genuinely blocking, send a fresh notify_user on the SAME threadId at urgency:'call'. Skip this for anything that isn't truly blocking; a normal question can just sit in the inbox."
|
|
103
96
|
}
|
|
104
97
|
);
|
|
105
98
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
106
99
|
tools: [
|
|
107
100
|
{
|
|
108
101
|
name: "pair",
|
|
109
|
-
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
|
|
102
|
+
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 attempts to open the user's browser and returns { verification_uri_complete, user_code, device_code }. That attempt can silently fail in headless/remote environments (no browser to open) \u2014 always show the user verification_uri_complete AND user_code regardless of whether it opened, so they can go there manually and enter the code themselves if needed; 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.",
|
|
110
103
|
inputSchema: json(PairSchema)
|
|
111
104
|
},
|
|
112
105
|
{
|
|
@@ -116,34 +109,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
116
109
|
},
|
|
117
110
|
{
|
|
118
111
|
name: "notify_user",
|
|
119
|
-
description: "Notify the user via Paigy
|
|
112
|
+
description: "Notify the user via Paigy. Returns { notificationId, threadId } \u2014 pass notificationId to await_reply for the answer, threadId to notify_user to continue the conversation. 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. MATCH the answer shape to the question \u2014 `select` is required; pick the best tool for the job, not always yes/no. The user can ALWAYS add free text on top of any shape, so structuring loses nothing. Choose `select`: yes/no \u2192 select:'confirm' \u2192 {kind:'confirm', approved:boolean}. Approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve' \u2192 {kind:'confirm', approved:boolean}. Both are answerable right from the banner \u2014 no need to open the app. Pick one of several \u2192 options + select:'one' \u2192 {kind:'option', optionId}. Pick several / a subset \u2192 options + select:'many' \u2192 {kind:'multi', optionIds:[...]}. Rank or prioritize \u2192 options + select:'rank', user taps in preferred order \u2192 {kind:'ranked', optionIds:[...]}. One/many/rank/text all need the user to open the app to answer \u2014 only confirm is answerable straight from the banner. Options carry no ids \u2014 they're assigned by position ('1', '2', \u2026), and the answer's optionId(s) are those positions. For visual choices give each option a sandboxed `html` or an `image` preview (e.g. layout/UI alternatives); use `visuals` for images that set context for the whole question. select:'text' = free-form reply only (plain updates, or answers that genuinely can't be structured). 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.",
|
|
120
113
|
inputSchema: json(NotifyRequestSchema)
|
|
121
114
|
},
|
|
122
|
-
{
|
|
123
|
-
name: "poll_answer",
|
|
124
|
-
description: "Fetch the user's answer to a previous notify_user request. Returns status pending | answered | ignored, with the answer once present. Errors if the id is unknown or expired.",
|
|
125
|
-
inputSchema: json(PollAnswerSchema)
|
|
126
|
-
},
|
|
127
115
|
{
|
|
128
116
|
name: "await_reply",
|
|
129
|
-
description: "Wait for the user's reply to
|
|
117
|
+
description: "Wait for the user's reply to a specific notification you sent (pass the notificationId 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, so concurrent notify_user calls don't cross. A CALL answer can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a blocking question'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (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).",
|
|
130
118
|
inputSchema: json(AwaitReplySchema)
|
|
131
119
|
},
|
|
132
120
|
{
|
|
133
121
|
name: "check_replies",
|
|
134
|
-
description: "The catch-up
|
|
122
|
+
description: "The catch-up sweep for everything you haven't consumed yet: returns answers to notifications you sent that you haven't seen (now marked seen), your still-pending notifications, 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 an answer to a notify_user call you just made, use await_reply instead. Also returns owedCallbacks: callbacks now due that you promised \u2014 fulfill each with notify_user on its threadId. Pass includeRecent:true only right after an unexpected restart, to recover a reply you may have claimed but not acted on.",
|
|
135
123
|
inputSchema: json(CheckRepliesSchema)
|
|
136
124
|
},
|
|
137
125
|
{
|
|
138
126
|
name: "set_task_state",
|
|
139
|
-
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 =
|
|
127
|
+
description: "Report progress on a user-initiated request you received (from check_replies): 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 = the same notificationId you're reporting on).",
|
|
140
128
|
inputSchema: json(SetTaskStateToolSchema)
|
|
141
129
|
},
|
|
142
|
-
{
|
|
143
|
-
name: "register_delivery",
|
|
144
|
-
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.",
|
|
145
|
-
inputSchema: json(RegisterDeliveryToolSchema)
|
|
146
|
-
},
|
|
147
130
|
{
|
|
148
131
|
name: "schedule_callback",
|
|
149
132
|
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.",
|
|
@@ -173,7 +156,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
173
156
|
};
|
|
174
157
|
}
|
|
175
158
|
const start = Date.now();
|
|
176
|
-
const capMs =
|
|
159
|
+
const capMs = 9e4;
|
|
177
160
|
while (Date.now() - start < capMs) {
|
|
178
161
|
const token = await pollToken(device_code);
|
|
179
162
|
if (token) {
|
|
@@ -193,7 +176,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
193
176
|
text: JSON.stringify({
|
|
194
177
|
status: "pending",
|
|
195
178
|
device_code,
|
|
196
|
-
message: "Still awaiting approval after ~
|
|
179
|
+
message: "Still awaiting approval after ~90s. Call pair again with this device_code to keep waiting."
|
|
197
180
|
})
|
|
198
181
|
}]
|
|
199
182
|
};
|
|
@@ -232,19 +215,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
232
215
|
const result = await submitNotification(enriched);
|
|
233
216
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
234
217
|
}
|
|
235
|
-
case "poll_answer": {
|
|
236
|
-
const { id } = PollAnswerSchema.parse(request.params.arguments);
|
|
237
|
-
const result = await pollAnswer(id);
|
|
238
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
239
|
-
}
|
|
240
218
|
case "await_reply": {
|
|
241
219
|
const { notificationId } = AwaitReplySchema.parse(request.params.arguments);
|
|
242
220
|
const item = await awaitReply(notificationId);
|
|
243
221
|
return { content: [{ type: "text", text: JSON.stringify(item) }] };
|
|
244
222
|
}
|
|
245
223
|
case "check_replies": {
|
|
246
|
-
CheckRepliesSchema.parse(request.params.arguments ?? {});
|
|
247
|
-
const result = await checkReplies();
|
|
224
|
+
const { includeRecent } = CheckRepliesSchema.parse(request.params.arguments ?? {});
|
|
225
|
+
const result = await checkReplies(includeRecent);
|
|
248
226
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
249
227
|
}
|
|
250
228
|
case "set_task_state": {
|
|
@@ -252,11 +230,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
252
230
|
const result = await setTaskState(notificationId, state);
|
|
253
231
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
254
232
|
}
|
|
255
|
-
case "register_delivery": {
|
|
256
|
-
const { mode } = RegisterDeliveryToolSchema.parse(request.params.arguments);
|
|
257
|
-
const result = await registerDelivery(mode);
|
|
258
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
259
|
-
}
|
|
260
233
|
case "schedule_callback": {
|
|
261
234
|
const result = await scheduleCallback(ScheduleCallbackSchema.parse(request.params.arguments));
|
|
262
235
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
package/dist/listen.js
CHANGED
|
@@ -4,12 +4,14 @@ import {
|
|
|
4
4
|
checkReplies,
|
|
5
5
|
registerDelivery,
|
|
6
6
|
wakeChannel
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-5EY3Q5DX.js";
|
|
8
8
|
import "./chunk-GU7C5H6L.js";
|
|
9
9
|
|
|
10
10
|
// src/listen.ts
|
|
11
11
|
import { createClient } from "@supabase/supabase-js";
|
|
12
12
|
import { spawn } from "child_process";
|
|
13
|
+
import { realpathSync } from "fs";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
13
15
|
var emit = (obj) => void process.stdout.write(JSON.stringify(obj) + "\n");
|
|
14
16
|
async function sweep(reason) {
|
|
15
17
|
try {
|
|
@@ -51,7 +53,16 @@ async function main() {
|
|
|
51
53
|
process.on("SIGINT", () => void shutdown());
|
|
52
54
|
process.on("SIGTERM", () => void shutdown());
|
|
53
55
|
}
|
|
54
|
-
|
|
56
|
+
function isEntry() {
|
|
57
|
+
const arg = process.argv[1];
|
|
58
|
+
if (!arg) return false;
|
|
59
|
+
try {
|
|
60
|
+
return realpathSync(arg) === fileURLToPath(import.meta.url);
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (isEntry()) {
|
|
55
66
|
main().catch((e) => {
|
|
56
67
|
emit({ type: "fatal", message: e.message });
|
|
57
68
|
process.exit(1);
|
package/dist/statusline.js
CHANGED
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
} from "./chunk-GU7C5H6L.js";
|
|
8
8
|
|
|
9
9
|
// src/statusline.ts
|
|
10
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import { join } from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
13
14
|
var DIR = join(homedir(), ".paigy");
|
|
14
15
|
var CACHE = join(DIR, "status.json");
|
|
15
16
|
var TTL_MS = 6e4;
|
|
@@ -57,9 +58,16 @@ async function main() {
|
|
|
57
58
|
return cached ? render(cached.status) : "paigy: \u2026";
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
function isEntry() {
|
|
62
|
+
const arg = process.argv[1];
|
|
63
|
+
if (!arg) return false;
|
|
64
|
+
try {
|
|
65
|
+
return realpathSync(arg) === fileURLToPath(import.meta.url);
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
62
69
|
}
|
|
70
|
+
if (isEntry()) void main().then((line) => console.log(line));
|
|
63
71
|
export {
|
|
64
72
|
main,
|
|
65
73
|
render
|