@paigy/mcp 0.10.4 → 0.12.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/dist/{chunk-UWM2Q4DJ.js → chunk-IFBWVH2X.js} +8 -14
- package/dist/index.js +7 -13
- package/dist/listen.js +1 -1
- package/package.json +1 -1
|
@@ -50,6 +50,9 @@ var NotifyRequestSchema = z.object({
|
|
|
50
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
|
+
),
|
|
54
|
+
blocking: z.boolean().default(false).describe(
|
|
55
|
+
"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."
|
|
53
56
|
)
|
|
54
57
|
}).superRefine((r, ctx) => {
|
|
55
58
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
@@ -103,22 +106,14 @@ var ScheduleCallbackSchema = z.object({
|
|
|
103
106
|
});
|
|
104
107
|
var PendingRepliesSchema = z.object({
|
|
105
108
|
replies: z.array(
|
|
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
|
-
})
|
|
109
|
+
z.object({ threadId: z.string(), notificationId: z.string(), answer: UserAnswerSchema })
|
|
116
110
|
),
|
|
117
111
|
pending: z.array(
|
|
118
112
|
z.object({ threadId: z.string(), notificationId: z.string(), createdAt: z.string() })
|
|
119
113
|
),
|
|
120
114
|
/** User-initiated requests addressed to this agent; act on them and reply via
|
|
121
|
-
* notify_user on the same threadId.
|
|
115
|
+
* notify_user on the same threadId. Keeps reappearing until you call
|
|
116
|
+
* set_task_state on its notificationId. */
|
|
122
117
|
requests: z.array(
|
|
123
118
|
z.object({ threadId: z.string(), notificationId: z.string(), text: z.string(), createdAt: z.string() })
|
|
124
119
|
),
|
|
@@ -400,10 +395,9 @@ async function awaitReply(notificationId, opts = {}) {
|
|
|
400
395
|
await doSleep(intervalMs);
|
|
401
396
|
}
|
|
402
397
|
}
|
|
403
|
-
async function checkReplies(
|
|
398
|
+
async function checkReplies() {
|
|
404
399
|
const token = loadToken();
|
|
405
|
-
const
|
|
406
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending${qs}`, {
|
|
400
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
|
|
407
401
|
headers: { authorization: `Bearer ${token}` }
|
|
408
402
|
}));
|
|
409
403
|
if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
scheduleCallback,
|
|
9
9
|
setTaskState,
|
|
10
10
|
submitNotification
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-IFBWVH2X.js";
|
|
12
12
|
import {
|
|
13
13
|
deleteToken,
|
|
14
14
|
openBrowser,
|
|
@@ -61,11 +61,6 @@ function json(s) {
|
|
|
61
61
|
var AwaitReplySchema = z.object({
|
|
62
62
|
notificationId: z.string().describe("The notificationId returned by notify_user \u2014 waits for the user's reply to THIS notification only.")
|
|
63
63
|
});
|
|
64
|
-
var CheckRepliesSchema = z.object({
|
|
65
|
-
includeRecent: z.boolean().optional().describe(
|
|
66
|
-
"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."
|
|
67
|
-
)
|
|
68
|
-
});
|
|
69
64
|
var PairSchema = z.object({
|
|
70
65
|
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.")
|
|
71
66
|
});
|
|
@@ -110,22 +105,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
110
105
|
},
|
|
111
106
|
{
|
|
112
107
|
name: "notify_user",
|
|
113
|
-
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.",
|
|
108
|
+
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). This is the premier use case for Paigy: getting UNBLOCKED so you can keep working, not just reporting that you're stuck. The user started a big task and went to do something else \u2014 the cardinal failure is going idle on one small decision and silently waiting to be checked on, so they come back to find you never actually progressed. Judge urgency by how much WORK IS BLOCKED behind this decision (a lot of dependent downstream work \u2192 call, even if it's a single question), not by how many things happen to be pending \u2014 one blocking decision outweighs five independent low-stakes ones sitting in the inbox. Set `blocking: true` whenever that's the case, independent of `urgency` \u2014 it's what lets Paigy escalate this to a real call later on its own if it goes unanswered, even if you sent it at a lower urgency. Don't set it for things you could work around, defer, or where other useful work exists meanwhile. 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.",
|
|
114
109
|
inputSchema: json(NotifyRequestSchema)
|
|
115
110
|
},
|
|
116
111
|
{
|
|
117
112
|
name: "await_reply",
|
|
118
|
-
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).",
|
|
113
|
+
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' } (timed out this window, no answer yet). On idle, if this is genuinely still blocking you and you have nothing else useful to do meanwhile, just call await_reply again immediately \u2014 keep looping. This is how you actually deliver on the point of calling: the user steps away for a while and comes back to find you'd already continued the moment they answered, not idle waiting to be checked on. Don't give up after one window. Only stop looping to do other work (and check back later), or after an unreasonably long stretch (tens of minutes to hours) worth telling the user about instead. 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).",
|
|
119
114
|
inputSchema: json(AwaitReplySchema)
|
|
120
115
|
},
|
|
121
116
|
{
|
|
122
117
|
name: "check_replies",
|
|
123
|
-
description: "The catch-up sweep for everything
|
|
124
|
-
inputSchema: json(
|
|
118
|
+
description: "The catch-up sweep for everything outstanding \u2014 a PURE read, takes no arguments, safe to call as often as you like: nothing here is consumed by reading it. Returns `replies` (answers to notifications you sent), your still-pending notifications, and `requests` \u2014 requests the user started toward you (each { notificationId, threadId, text }). Each keeps reappearing on every call until you actually engage with it: call set_task_state on its notificationId, which is what claims/acknowledges it \u2014 a human-initiated reply or request must never be silently dropped just because you read the list without acting. Use check_replies when booting up / starting a session, or when you've been waiting a long time on something else. 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. Also returns `stalled`: work (either direction) you reported in_progress via set_task_state a while ago and never reported completed \u2014 likely left half-done by this session or a prior one that crashed or went idle. For each, either continue the work and report a real state, or investigate why it stalled.",
|
|
119
|
+
inputSchema: json(z.object({}))
|
|
125
120
|
},
|
|
126
121
|
{
|
|
127
122
|
name: "set_task_state",
|
|
128
|
-
description: "Report progress on the follow-up work behind ANY notification you own \u2014 a user-initiated request (from check_replies), or your OWN notify_user question once await_reply/check_replies returns its answer and you start acting on it. Pass that notificationId. States: 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). Calling this reliably is what lets a future session's check_replies surface `stalled` work you (or a crashed/idle prior session) left at in_progress without ever reporting completed.",
|
|
123
|
+
description: "Report progress on the follow-up work behind ANY notification you own \u2014 a user-initiated request (from check_replies), or your OWN notify_user question once await_reply/check_replies returns its answer and you start acting on it. Pass that notificationId. THIS is what actually claims/acknowledges a reply or request \u2014 check_replies is a pure read that never consumes anything on its own, so call this as soon as you start engaging with something it returned; otherwise that same item just keeps reappearing forever. States: 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). Calling this reliably is also what lets a future session's check_replies surface `stalled` work you (or a crashed/idle prior session) left at in_progress without ever reporting completed.",
|
|
129
124
|
inputSchema: json(SetTaskStateToolSchema)
|
|
130
125
|
},
|
|
131
126
|
{
|
|
@@ -226,8 +221,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
226
221
|
return { content: [{ type: "text", text: JSON.stringify(item) }] };
|
|
227
222
|
}
|
|
228
223
|
case "check_replies": {
|
|
229
|
-
const
|
|
230
|
-
const result = await checkReplies(includeRecent);
|
|
224
|
+
const result = await checkReplies();
|
|
231
225
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
232
226
|
}
|
|
233
227
|
case "set_task_state": {
|
package/dist/listen.js
CHANGED