@paigy/mcp 0.25.2 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -8
- package/dist/{chunk-LUY5MXDM.js → chunk-47E77CDG.js} +99 -7
- package/dist/{chunk-FEAIZ6DA.js → chunk-QXPLQKIM.js} +1 -1
- package/dist/{chunk-2374V6WK.js → chunk-VCEFF2VA.js} +152 -10
- package/dist/index.js +38 -13
- package/dist/listen.js +53 -3
- package/dist/onboard.js +2 -2
- package/dist/statusline.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -141,22 +141,51 @@ wake channel and sweeps on every nudge. Keep it alive past the terminal with
|
|
|
141
141
|
removes it).
|
|
142
142
|
|
|
143
143
|
To launch an agent — any agent, not just Claude — when work arrives, set
|
|
144
|
-
`PAIGY_ON_WAKE` to a command before `--install`. The
|
|
145
|
-
work
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
`PAIGY_ON_WAKE` to a command before `--install`. The sweep has already claimed the
|
|
145
|
+
work, so the launcher reads it from the environment rather than calling
|
|
146
|
+
`check_replies` again:
|
|
147
|
+
|
|
148
|
+
| Variable | What it holds |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| `PAIGY_WORK` | the whole swept payload as JSON (`replies`, `requests`, `owedCallbacks`, `stalled`, `threads`) |
|
|
151
|
+
| `PAIGY_EVENT` | the wake that caused this run — `boot`, `wake:reply`, `cron:callback`… |
|
|
152
|
+
| `PAIGY_THREAD_ID` | the thread to continue on |
|
|
153
|
+
| `PAIGY_NOTIFICATION_ID` | the notification being answered or acted on (unset for an owed callback) |
|
|
154
|
+
| `PAIGY_CONTEXT_THREAD_ID` | a past conversation the user seeded this with — `get_thread` it **first** |
|
|
155
|
+
| `PAIGY_TEXT` | what the user said (or the callback note you owe them), in prose |
|
|
156
|
+
|
|
157
|
+
Hand `$PAIGY_WORK` to a harness that can read JSON and decide for itself; use the
|
|
158
|
+
scalars for a plain shell launcher that shouldn't need `jq`. They describe **one**
|
|
159
|
+
item — the oldest thread without a turn already in flight — because the queue rail's
|
|
160
|
+
contract is one thread at a time. An absent fact is unset rather than empty, so
|
|
161
|
+
`${PAIGY_CONTEXT_THREAD_ID:-}` distinguishes "no seed" from "seeded with nothing".
|
|
149
162
|
|
|
150
163
|
```sh
|
|
151
|
-
# Claude Code:
|
|
164
|
+
# Claude Code — hand it everything and let it plan:
|
|
152
165
|
PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — rehydrate threads you do not recognize via get_thread first."' \
|
|
153
166
|
npx -y -p @paigy/mcp paigy-listen --install
|
|
154
167
|
|
|
155
|
-
# Codex (or any CLI harness)
|
|
156
|
-
PAIGY_ON_WAKE='codex exec "
|
|
168
|
+
# Codex (or any CLI harness) — the scalars are enough for a one-liner:
|
|
169
|
+
PAIGY_ON_WAKE='codex exec "Continue Paigy thread $PAIGY_THREAD_ID. The user said: $PAIGY_TEXT. Call get_thread on it first if you do not recognize it, then reply with contact."' \
|
|
157
170
|
npx -y -p @paigy/mcp paigy-listen --install
|
|
158
171
|
```
|
|
159
172
|
|
|
173
|
+
`$PAIGY_TEXT` is whatever the user said, expanded inside a shell command — keep it
|
|
174
|
+
quoted, as above. For anything longer than a one-liner, point `PAIGY_ON_WAKE` at a
|
|
175
|
+
script and branch on `$PAIGY_EVENT` there:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
#!/bin/sh
|
|
179
|
+
# ~/.paigy/on-wake.sh — chmod +x, then PAIGY_ON_WAKE=~/.paigy/on-wake.sh
|
|
180
|
+
seed=""
|
|
181
|
+
[ -n "${PAIGY_CONTEXT_THREAD_ID:-}" ] && seed="It continues thread $PAIGY_CONTEXT_THREAD_ID — get_thread that first."
|
|
182
|
+
|
|
183
|
+
case "$PAIGY_EVENT" in
|
|
184
|
+
cron:callback*) codex exec "You owe the user a callback on thread $PAIGY_THREAD_ID: $PAIGY_TEXT. Deliver it with contact." ;;
|
|
185
|
+
*) codex exec "Paigy thread $PAIGY_THREAD_ID. The user said: $PAIGY_TEXT. $seed Reply with contact when done." ;;
|
|
186
|
+
esac
|
|
187
|
+
```
|
|
188
|
+
|
|
160
189
|
## Statusline (Claude Code)
|
|
161
190
|
|
|
162
191
|
`paigy-statusline` prints a one-line connection status — the account's session
|
|
@@ -122,8 +122,13 @@ var NotifyRequestSchema = z.object({
|
|
|
122
122
|
* matters now — the broker derives the optimal shape and channel. Mutually
|
|
123
123
|
* exclusive with `context` (and never sent alongside `envelope`: E2EE pairings
|
|
124
124
|
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
126
|
+
// the ask into topics and gives each one at most three sentences and one question
|
|
127
|
+
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
128
|
+
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
129
|
+
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
130
|
+
ask: z.string().min(1).max(1e4).optional().describe(
|
|
131
|
+
'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Write as much as the situation needs (up to 10k characters) \u2014 Paigy breaks it into topics and reads it back a few sentences at a time; do NOT pre-summarize it into one line. Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
|
|
127
132
|
),
|
|
128
133
|
needs: z.array(z.string().min(1)).optional().describe(
|
|
129
134
|
"With `ask` only: the distinct things you need answered when the ask is multi-part \u2014 becomes the coverage contract (`points`), so a half-answer is never silently final."
|
|
@@ -207,7 +212,11 @@ var UserAnswerSchema = z.discriminatedUnion("kind", [
|
|
|
207
212
|
z.object({ kind: z.literal("turns"), turns: z.array(TurnSchema).min(1) })
|
|
208
213
|
]);
|
|
209
214
|
var IntentSchema = z.object({
|
|
210
|
-
|
|
215
|
+
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
216
|
+
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
217
|
+
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
218
|
+
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
219
|
+
kind: z.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command"]),
|
|
211
220
|
detail: z.string(),
|
|
212
221
|
/** Landed defer (#397): the MCP parses common spoken forms ("in 20 minutes",
|
|
213
222
|
* "after lunch") against the agent machine's clock — the user's — and attaches
|
|
@@ -254,6 +263,22 @@ var AwaitItemSchema = z.discriminatedUnion("type", [
|
|
|
254
263
|
threadId: z.string(),
|
|
255
264
|
notificationId: z.string()
|
|
256
265
|
}),
|
|
266
|
+
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
267
|
+
* revise any of these until the final reply arrives — partial = intelligence,
|
|
268
|
+
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
269
|
+
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
270
|
+
* contact on the same thread right away — the caller hears it on the same call. */
|
|
271
|
+
z.object({
|
|
272
|
+
type: z.literal("partial"),
|
|
273
|
+
notificationId: z.string(),
|
|
274
|
+
inFlight: z.literal(true),
|
|
275
|
+
turn: z.object({
|
|
276
|
+
idx: z.number(),
|
|
277
|
+
prompt: z.string(),
|
|
278
|
+
reply: z.string(),
|
|
279
|
+
acts: z.array(IntentSchema).nullable().optional()
|
|
280
|
+
})
|
|
281
|
+
}),
|
|
257
282
|
z.object({ type: z.literal("idle") })
|
|
258
283
|
]);
|
|
259
284
|
var CallbackTriggerSchema = z.enum(["on_done", "on_blocked", "scheduled"]);
|
|
@@ -354,6 +379,27 @@ var UserResponseSchema = z.object({
|
|
|
354
379
|
covered: z.array(z.string()).optional()
|
|
355
380
|
});
|
|
356
381
|
var VoiceKeySchema = z.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
382
|
+
var AgendaTurnSchema = z.object({
|
|
383
|
+
/** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
|
|
384
|
+
* line went out on 2026-07-28 and the caller could not answer it at all. */
|
|
385
|
+
info: z.array(z.string().min(1)).max(3).default([]),
|
|
386
|
+
question: z.string().min(1).nullable(),
|
|
387
|
+
/** True on the one turn carrying the agent's own declared question. */
|
|
388
|
+
asks: z.boolean().optional(),
|
|
389
|
+
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
390
|
+
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
391
|
+
* which route nothing. */
|
|
392
|
+
claimId: z.string().optional(),
|
|
393
|
+
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
394
|
+
voice: z.string().optional(),
|
|
395
|
+
select: SelectShapeSchema.optional(),
|
|
396
|
+
options: z.array(OptionSchema.omit({ id: true })).optional(),
|
|
397
|
+
/** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
|
|
398
|
+
* caller already answered this claim in an earlier utterance, quoted here VERBATIM —
|
|
399
|
+
* the bot speaks the turn's short confirmation, posts these words as the claim's
|
|
400
|
+
* answer, and never re-asks. Grounded at parse time: an invented settle is #796. */
|
|
401
|
+
settle: z.string().optional()
|
|
402
|
+
});
|
|
357
403
|
var InboxItemSchema = z.object({
|
|
358
404
|
id: z.string(),
|
|
359
405
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
@@ -365,9 +411,11 @@ var InboxItemSchema = z.object({
|
|
|
365
411
|
options: z.array(OptionSchema).optional(),
|
|
366
412
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
367
413
|
points: z.array(z.string()).optional(),
|
|
368
|
-
/**
|
|
369
|
-
*
|
|
370
|
-
|
|
414
|
+
/** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
|
|
415
|
+
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
416
|
+
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
417
|
+
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
418
|
+
agenda: z.array(AgendaTurnSchema).optional(),
|
|
371
419
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
372
420
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
373
421
|
intents: z.array(IntentSchema).optional(),
|
|
@@ -468,7 +516,6 @@ var UserSettingsSchema = z.object({
|
|
|
468
516
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
469
517
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
470
518
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
471
|
-
callBudget: z.number().int().min(1).max(500).optional(),
|
|
472
519
|
/** Per-user voice-call tuning (#318): raw knobs forwarded to the call bot's
|
|
473
520
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
474
521
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
@@ -550,6 +597,49 @@ var HandoffSchema = z.object({
|
|
|
550
597
|
* same primitive; a recap is one whose audience includes you. */
|
|
551
598
|
recap: z.boolean().optional()
|
|
552
599
|
});
|
|
600
|
+
var NoteSourceSchema = z.enum(["app", "call"]);
|
|
601
|
+
var NoteStatusSchema = z.enum(["open", "assigned", "done"]);
|
|
602
|
+
var DecisionSchema = z.object({
|
|
603
|
+
id: z.string(),
|
|
604
|
+
/** The note this decision refines; null = recorded on a bare thread (the
|
|
605
|
+
* extensibility seam — any conversation can accrue decisions). */
|
|
606
|
+
noteId: z.string().nullable(),
|
|
607
|
+
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
608
|
+
question: z.string(),
|
|
609
|
+
/** The user's ruling; null while the question is open. */
|
|
610
|
+
answer: z.string().nullable(),
|
|
611
|
+
decidedAt: z.string().nullable(),
|
|
612
|
+
createdAt: z.string()
|
|
613
|
+
});
|
|
614
|
+
var NoteSchema = z.object({
|
|
615
|
+
id: z.string(),
|
|
616
|
+
/** One-line headline (broker-titled; deterministic floor). */
|
|
617
|
+
title: z.string(),
|
|
618
|
+
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
619
|
+
intent: z.string(),
|
|
620
|
+
source: NoteSourceSchema,
|
|
621
|
+
status: NoteStatusSchema,
|
|
622
|
+
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
623
|
+
assignee: z.string().nullable(),
|
|
624
|
+
/** The request thread minted at assignment; null until assigned. */
|
|
625
|
+
threadId: z.string().nullable(),
|
|
626
|
+
createdAt: z.string()
|
|
627
|
+
});
|
|
628
|
+
var CreateNoteSchema = z.object({
|
|
629
|
+
/** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
|
|
630
|
+
text: z.string().min(1).max(4e3)
|
|
631
|
+
});
|
|
632
|
+
var RecordDecisionSchema = z.object({
|
|
633
|
+
/** An open decision (from /clarify) to answer. */
|
|
634
|
+
decisionId: z.string().optional(),
|
|
635
|
+
/** For an ad-hoc decision: what was ambiguous. Required without `decisionId`. */
|
|
636
|
+
question: z.string().min(1).max(500).optional(),
|
|
637
|
+
/** The ruling. */
|
|
638
|
+
answer: z.string().min(1).max(2e3)
|
|
639
|
+
}).refine((d) => d.decisionId || d.question, { message: "decisionId or question required" });
|
|
640
|
+
var AssignNoteSchema = z.object({
|
|
641
|
+
target: z.string().min(1)
|
|
642
|
+
});
|
|
553
643
|
var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
|
|
554
644
|
var WAKE_EVENT = "wake";
|
|
555
645
|
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
@@ -701,6 +791,8 @@ var NotificationFeedbackKindSchema = z.enum([
|
|
|
701
791
|
// "These recommended answers aren't good — give me new ones."
|
|
702
792
|
"needs_visual",
|
|
703
793
|
// "There should be a picture or design here."
|
|
794
|
+
"should_have_called",
|
|
795
|
+
// "Don't put this in a banner — ring me for something like this."
|
|
704
796
|
"other"
|
|
705
797
|
// anything else — the note carries it.
|
|
706
798
|
]);
|
|
@@ -2277,7 +2277,7 @@ async function reach(url, init) {
|
|
|
2277
2277
|
var TITLE_MAX = 90;
|
|
2278
2278
|
var CHUNKS_MAX = 8;
|
|
2279
2279
|
var CHUNK_MAX = 300;
|
|
2280
|
-
var ASK_MAX =
|
|
2280
|
+
var ASK_MAX = 1e4;
|
|
2281
2281
|
var OPTION_MAX = 80;
|
|
2282
2282
|
var NEEDS_MAX = 6;
|
|
2283
2283
|
var UNSPEAKABLE = /```|\n/;
|
|
@@ -2438,8 +2438,13 @@ var NotifyRequestSchema = z.object({
|
|
|
2438
2438
|
* matters now — the broker derives the optimal shape and channel. Mutually
|
|
2439
2439
|
* exclusive with `context` (and never sent alongside `envelope`: E2EE pairings
|
|
2440
2440
|
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
2441
|
-
|
|
2442
|
-
|
|
2441
|
+
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
2442
|
+
// the ask into topics and gives each one at most three sentences and one question
|
|
2443
|
+
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
2444
|
+
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
2445
|
+
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
2446
|
+
ask: z.string().min(1).max(1e4).optional().describe(
|
|
2447
|
+
'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Write as much as the situation needs (up to 10k characters) \u2014 Paigy breaks it into topics and reads it back a few sentences at a time; do NOT pre-summarize it into one line. Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
|
|
2443
2448
|
),
|
|
2444
2449
|
needs: z.array(z.string().min(1)).optional().describe(
|
|
2445
2450
|
"With `ask` only: the distinct things you need answered when the ask is multi-part \u2014 becomes the coverage contract (`points`), so a half-answer is never silently final."
|
|
@@ -2512,6 +2517,23 @@ function normalizeWaiting(req) {
|
|
|
2512
2517
|
blocking: req.blocking || waiting === "hard"
|
|
2513
2518
|
};
|
|
2514
2519
|
}
|
|
2520
|
+
var DERIVE_CHUNKS_MAX = 8;
|
|
2521
|
+
var DERIVE_CHUNK_MAX = 300;
|
|
2522
|
+
function chunkAsk(text, max = DERIVE_CHUNK_MAX, cap = DERIVE_CHUNKS_MAX) {
|
|
2523
|
+
const body = text.trim();
|
|
2524
|
+
if (!body) return [];
|
|
2525
|
+
const sentences = body.match(/[^.!?]+[.!?]*\s*/g) ?? [body];
|
|
2526
|
+
const chunks = [];
|
|
2527
|
+
for (const raw of sentences) {
|
|
2528
|
+
const s = raw.trim();
|
|
2529
|
+
if (!s) continue;
|
|
2530
|
+
const last = chunks[chunks.length - 1];
|
|
2531
|
+
if (last !== void 0 && `${last} ${s}`.length <= max) chunks[chunks.length - 1] = `${last} ${s}`;
|
|
2532
|
+
else chunks.push(s);
|
|
2533
|
+
}
|
|
2534
|
+
if (chunks.length <= cap) return chunks;
|
|
2535
|
+
return [...chunks.slice(0, cap - 1), chunks.slice(cap - 1).join(" ")];
|
|
2536
|
+
}
|
|
2515
2537
|
function deriveAsk(req) {
|
|
2516
2538
|
req = normalizeWaiting(req);
|
|
2517
2539
|
if (!req.ask) return req;
|
|
@@ -2523,7 +2545,7 @@ function deriveAsk(req) {
|
|
|
2523
2545
|
const { ask: _ask, needs, urgencyHint: _hint, channel: _channel, ...rest } = req;
|
|
2524
2546
|
return {
|
|
2525
2547
|
...rest,
|
|
2526
|
-
context: { title, description:
|
|
2548
|
+
context: { title, description: chunkAsk(text) },
|
|
2527
2549
|
// Options riding alongside the ask (#575: pixels can't be prose) floor to a
|
|
2528
2550
|
// single pick — the model broker may upgrade to many/rank from the wording.
|
|
2529
2551
|
select: req.options?.length ? "one" : "text",
|
|
@@ -2551,7 +2573,11 @@ var UserAnswerSchema = z.discriminatedUnion("kind", [
|
|
|
2551
2573
|
z.object({ kind: z.literal("turns"), turns: z.array(TurnSchema).min(1) })
|
|
2552
2574
|
]);
|
|
2553
2575
|
var IntentSchema = z.object({
|
|
2554
|
-
|
|
2576
|
+
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
2577
|
+
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
2578
|
+
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
2579
|
+
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
2580
|
+
kind: z.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command"]),
|
|
2555
2581
|
detail: z.string(),
|
|
2556
2582
|
/** Landed defer (#397): the MCP parses common spoken forms ("in 20 minutes",
|
|
2557
2583
|
* "after lunch") against the agent machine's clock — the user's — and attaches
|
|
@@ -2598,6 +2624,22 @@ var AwaitItemSchema = z.discriminatedUnion("type", [
|
|
|
2598
2624
|
threadId: z.string(),
|
|
2599
2625
|
notificationId: z.string()
|
|
2600
2626
|
}),
|
|
2627
|
+
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
2628
|
+
* revise any of these until the final reply arrives — partial = intelligence,
|
|
2629
|
+
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
2630
|
+
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
2631
|
+
* contact on the same thread right away — the caller hears it on the same call. */
|
|
2632
|
+
z.object({
|
|
2633
|
+
type: z.literal("partial"),
|
|
2634
|
+
notificationId: z.string(),
|
|
2635
|
+
inFlight: z.literal(true),
|
|
2636
|
+
turn: z.object({
|
|
2637
|
+
idx: z.number(),
|
|
2638
|
+
prompt: z.string(),
|
|
2639
|
+
reply: z.string(),
|
|
2640
|
+
acts: z.array(IntentSchema).nullable().optional()
|
|
2641
|
+
})
|
|
2642
|
+
}),
|
|
2601
2643
|
z.object({ type: z.literal("idle") })
|
|
2602
2644
|
]);
|
|
2603
2645
|
var CallbackTriggerSchema = z.enum(["on_done", "on_blocked", "scheduled"]);
|
|
@@ -2698,6 +2740,27 @@ var UserResponseSchema = z.object({
|
|
|
2698
2740
|
covered: z.array(z.string()).optional()
|
|
2699
2741
|
});
|
|
2700
2742
|
var VoiceKeySchema = z.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
2743
|
+
var AgendaTurnSchema = z.object({
|
|
2744
|
+
/** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
|
|
2745
|
+
* line went out on 2026-07-28 and the caller could not answer it at all. */
|
|
2746
|
+
info: z.array(z.string().min(1)).max(3).default([]),
|
|
2747
|
+
question: z.string().min(1).nullable(),
|
|
2748
|
+
/** True on the one turn carrying the agent's own declared question. */
|
|
2749
|
+
asks: z.boolean().optional(),
|
|
2750
|
+
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
2751
|
+
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
2752
|
+
* which route nothing. */
|
|
2753
|
+
claimId: z.string().optional(),
|
|
2754
|
+
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
2755
|
+
voice: z.string().optional(),
|
|
2756
|
+
select: SelectShapeSchema.optional(),
|
|
2757
|
+
options: z.array(OptionSchema.omit({ id: true })).optional(),
|
|
2758
|
+
/** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
|
|
2759
|
+
* caller already answered this claim in an earlier utterance, quoted here VERBATIM —
|
|
2760
|
+
* the bot speaks the turn's short confirmation, posts these words as the claim's
|
|
2761
|
+
* answer, and never re-asks. Grounded at parse time: an invented settle is #796. */
|
|
2762
|
+
settle: z.string().optional()
|
|
2763
|
+
});
|
|
2701
2764
|
var InboxItemSchema = z.object({
|
|
2702
2765
|
id: z.string(),
|
|
2703
2766
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
@@ -2709,9 +2772,11 @@ var InboxItemSchema = z.object({
|
|
|
2709
2772
|
options: z.array(OptionSchema).optional(),
|
|
2710
2773
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
2711
2774
|
points: z.array(z.string()).optional(),
|
|
2712
|
-
/**
|
|
2713
|
-
*
|
|
2714
|
-
|
|
2775
|
+
/** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
|
|
2776
|
+
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
2777
|
+
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
2778
|
+
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
2779
|
+
agenda: z.array(AgendaTurnSchema).optional(),
|
|
2715
2780
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
2716
2781
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
2717
2782
|
intents: z.array(IntentSchema).optional(),
|
|
@@ -2812,7 +2877,6 @@ var UserSettingsSchema = z.object({
|
|
|
2812
2877
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
2813
2878
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
2814
2879
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
2815
|
-
callBudget: z.number().int().min(1).max(500).optional(),
|
|
2816
2880
|
/** Per-user voice-call tuning (#318): raw knobs forwarded to the call bot's
|
|
2817
2881
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
2818
2882
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
@@ -2894,6 +2958,49 @@ var HandoffSchema = z.object({
|
|
|
2894
2958
|
* same primitive; a recap is one whose audience includes you. */
|
|
2895
2959
|
recap: z.boolean().optional()
|
|
2896
2960
|
});
|
|
2961
|
+
var NoteSourceSchema = z.enum(["app", "call"]);
|
|
2962
|
+
var NoteStatusSchema = z.enum(["open", "assigned", "done"]);
|
|
2963
|
+
var DecisionSchema = z.object({
|
|
2964
|
+
id: z.string(),
|
|
2965
|
+
/** The note this decision refines; null = recorded on a bare thread (the
|
|
2966
|
+
* extensibility seam — any conversation can accrue decisions). */
|
|
2967
|
+
noteId: z.string().nullable(),
|
|
2968
|
+
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
2969
|
+
question: z.string(),
|
|
2970
|
+
/** The user's ruling; null while the question is open. */
|
|
2971
|
+
answer: z.string().nullable(),
|
|
2972
|
+
decidedAt: z.string().nullable(),
|
|
2973
|
+
createdAt: z.string()
|
|
2974
|
+
});
|
|
2975
|
+
var NoteSchema = z.object({
|
|
2976
|
+
id: z.string(),
|
|
2977
|
+
/** One-line headline (broker-titled; deterministic floor). */
|
|
2978
|
+
title: z.string(),
|
|
2979
|
+
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
2980
|
+
intent: z.string(),
|
|
2981
|
+
source: NoteSourceSchema,
|
|
2982
|
+
status: NoteStatusSchema,
|
|
2983
|
+
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
2984
|
+
assignee: z.string().nullable(),
|
|
2985
|
+
/** The request thread minted at assignment; null until assigned. */
|
|
2986
|
+
threadId: z.string().nullable(),
|
|
2987
|
+
createdAt: z.string()
|
|
2988
|
+
});
|
|
2989
|
+
var CreateNoteSchema = z.object({
|
|
2990
|
+
/** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
|
|
2991
|
+
text: z.string().min(1).max(4e3)
|
|
2992
|
+
});
|
|
2993
|
+
var RecordDecisionSchema = z.object({
|
|
2994
|
+
/** An open decision (from /clarify) to answer. */
|
|
2995
|
+
decisionId: z.string().optional(),
|
|
2996
|
+
/** For an ad-hoc decision: what was ambiguous. Required without `decisionId`. */
|
|
2997
|
+
question: z.string().min(1).max(500).optional(),
|
|
2998
|
+
/** The ruling. */
|
|
2999
|
+
answer: z.string().min(1).max(2e3)
|
|
3000
|
+
}).refine((d) => d.decisionId || d.question, { message: "decisionId or question required" });
|
|
3001
|
+
var AssignNoteSchema = z.object({
|
|
3002
|
+
target: z.string().min(1)
|
|
3003
|
+
});
|
|
2897
3004
|
var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
|
|
2898
3005
|
var RegisterDeliverySchema = z.object({ mode: DeliveryModeSchema });
|
|
2899
3006
|
var OAuthStartSchema = z.object({
|
|
@@ -3043,6 +3150,8 @@ var NotificationFeedbackKindSchema = z.enum([
|
|
|
3043
3150
|
// "These recommended answers aren't good — give me new ones."
|
|
3044
3151
|
"needs_visual",
|
|
3045
3152
|
// "There should be a picture or design here."
|
|
3153
|
+
"should_have_called",
|
|
3154
|
+
// "Don't put this in a banner — ring me for something like this."
|
|
3046
3155
|
"other"
|
|
3047
3156
|
// anything else — the note carries it.
|
|
3048
3157
|
]);
|
|
@@ -3645,15 +3754,38 @@ async function pollAnswer(notificationId) {
|
|
|
3645
3754
|
const item = await res.json();
|
|
3646
3755
|
return item.type === "idle" ? item : decryptItem(item);
|
|
3647
3756
|
}
|
|
3757
|
+
var AWAIT_WINDOW_MS = 45e3;
|
|
3758
|
+
var _partialSeen = /* @__PURE__ */ new Map();
|
|
3759
|
+
async function pollPartials(notificationId) {
|
|
3760
|
+
const token = readToken();
|
|
3761
|
+
const after = _partialSeen.get(notificationId) ?? 0;
|
|
3762
|
+
try {
|
|
3763
|
+
const res = await reach(
|
|
3764
|
+
`${BACKEND_URL}/api/await/partials?notificationId=${encodeURIComponent(notificationId)}&afterIdx=${after}`,
|
|
3765
|
+
{ headers: { authorization: `Bearer ${token}` } }
|
|
3766
|
+
);
|
|
3767
|
+
if (!res.ok) return null;
|
|
3768
|
+
const body = await res.json();
|
|
3769
|
+
const next = body.turns?.[0];
|
|
3770
|
+
if (!next) return null;
|
|
3771
|
+
_partialSeen.set(notificationId, next.idx);
|
|
3772
|
+
return { type: "partial", notificationId, inFlight: true, turn: next };
|
|
3773
|
+
} catch {
|
|
3774
|
+
return null;
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3648
3777
|
async function awaitReply(notificationId, opts = {}) {
|
|
3649
3778
|
const intervalMs = opts.intervalMs ?? 5e3;
|
|
3650
|
-
const windowMs = opts.windowMs ??
|
|
3779
|
+
const windowMs = opts.windowMs ?? AWAIT_WINDOW_MS;
|
|
3651
3780
|
const doSleep = opts.sleep ?? sleep2;
|
|
3652
3781
|
const now = opts.now ?? Date.now;
|
|
3653
3782
|
const start = now();
|
|
3654
3783
|
while (true) {
|
|
3784
|
+
if (opts.signal?.aborted) return { type: "idle" };
|
|
3655
3785
|
const item = await pollAnswer(notificationId);
|
|
3656
3786
|
if (item.type !== "idle") return item;
|
|
3787
|
+
const partial = await pollPartials(notificationId);
|
|
3788
|
+
if (partial) return partial;
|
|
3657
3789
|
if (now() - start >= windowMs) return { type: "idle" };
|
|
3658
3790
|
await doSleep(intervalMs);
|
|
3659
3791
|
}
|
|
@@ -3705,6 +3837,15 @@ async function checkReplies() {
|
|
|
3705
3837
|
})
|
|
3706
3838
|
};
|
|
3707
3839
|
}
|
|
3840
|
+
async function answerCallerQuestion(notificationId, answer) {
|
|
3841
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/voice/session-answer`, {
|
|
3842
|
+
method: "POST",
|
|
3843
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${readToken()}` },
|
|
3844
|
+
body: JSON.stringify({ notificationId, answer })
|
|
3845
|
+
}));
|
|
3846
|
+
if (!res.ok) throw new Error(`answer_caller_question failed: ${res.status} ${await res.text()}`);
|
|
3847
|
+
return await res.json();
|
|
3848
|
+
}
|
|
3708
3849
|
async function setTaskState(notificationId, state) {
|
|
3709
3850
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify/${notificationId}/state`, {
|
|
3710
3851
|
method: "PATCH",
|
|
@@ -3773,6 +3914,7 @@ export {
|
|
|
3773
3914
|
getThread,
|
|
3774
3915
|
searchThreads,
|
|
3775
3916
|
checkReplies,
|
|
3917
|
+
answerCallerQuestion,
|
|
3776
3918
|
setTaskState,
|
|
3777
3919
|
registerDelivery,
|
|
3778
3920
|
scheduleCallback,
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
HandoffSchema
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-47E77CDG.js";
|
|
5
5
|
import {
|
|
6
6
|
PAIGY_TOOL_IDS,
|
|
7
7
|
autoConfigureClients,
|
|
8
8
|
claudeInstallHint,
|
|
9
9
|
enablePaigyTools
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-QXPLQKIM.js";
|
|
11
11
|
import {
|
|
12
12
|
clearSurface,
|
|
13
13
|
writeSurface
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
ScheduleCallbackSchema,
|
|
18
18
|
SetTaskStateSchema,
|
|
19
19
|
UnpairedError,
|
|
20
|
+
answerCallerQuestion,
|
|
20
21
|
awaitReply,
|
|
21
22
|
checkReplies,
|
|
22
23
|
deleteKeyFile,
|
|
@@ -39,7 +40,7 @@ import {
|
|
|
39
40
|
sleep,
|
|
40
41
|
startE2ee,
|
|
41
42
|
submitNotification
|
|
42
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-VCEFF2VA.js";
|
|
43
44
|
|
|
44
45
|
// src/index.ts
|
|
45
46
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -83,7 +84,7 @@ var CONTACT_SCHEMA = {
|
|
|
83
84
|
properties: {
|
|
84
85
|
ask: {
|
|
85
86
|
type: "string",
|
|
86
|
-
description: "What to tell the user, or what you need to find out from them. Plain prose
|
|
87
|
+
description: "What to tell the user, or what you need to find out from them. Plain prose \u2014 as long as it needs to be (up to 10k characters); Paigy splits it into topics and reads back a few sentences at a time, so do NOT compress a briefing into one line. May be spoken aloud on a call, so write natural speech and name things (not IDs)."
|
|
87
88
|
},
|
|
88
89
|
waiting: {
|
|
89
90
|
type: "string",
|
|
@@ -220,8 +221,12 @@ function clearPairing(deviceCode) {
|
|
|
220
221
|
|
|
221
222
|
// src/index.ts
|
|
222
223
|
var AwaitReplySchema = z.object({
|
|
223
|
-
notificationId: z.string().describe("The notificationId returned by contact \u2014 waits for the user's reply to THIS notification only.")
|
|
224
|
+
notificationId: z.string().describe("The notificationId returned by contact \u2014 waits for the user's reply to THIS notification only."),
|
|
225
|
+
maxWaitSeconds: z.number().int().min(5).max(300).optional().describe(
|
|
226
|
+
"How long to hold this ONE call before returning { type:'idle' } so you can loop. Default 45 \u2014 safely under the 60s cap most MCP hosts put on a single tool call. Raise it only if you know your host allows longer; a value past the cap means the call is killed and you get nothing."
|
|
227
|
+
)
|
|
224
228
|
});
|
|
229
|
+
var JOIN_CAP_MS = 45e3;
|
|
225
230
|
var PairSchema = z.object({
|
|
226
231
|
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.")
|
|
227
232
|
});
|
|
@@ -238,6 +243,10 @@ var SetTaskStateToolSchema = z.object({
|
|
|
238
243
|
notificationId: z.string(),
|
|
239
244
|
state: SetTaskStateSchema.shape.state
|
|
240
245
|
});
|
|
246
|
+
var AnswerCallerQuestionSchema = z.object({
|
|
247
|
+
notificationId: z.string().describe("The notification whose call carried the caller's question \u2014 from the partial turn or the settled reply."),
|
|
248
|
+
answer: z.string().min(1).max(1500).describe("The answer, as one or two short SPOKEN sentences \u2014 it may be read aloud on the live call.")
|
|
249
|
+
});
|
|
241
250
|
function detectGit() {
|
|
242
251
|
const run = (cmd) => {
|
|
243
252
|
try {
|
|
@@ -319,7 +328,7 @@ function renderPairOutcome(outcome, device_code) {
|
|
|
319
328
|
text: JSON.stringify({
|
|
320
329
|
status: "pending",
|
|
321
330
|
device_code,
|
|
322
|
-
message: "Still awaiting approval. Call pair again with this device_code to keep waiting."
|
|
331
|
+
message: "Still awaiting approval \u2014 the user hasn't entered the code yet. Call pair again with this device_code RIGHT NOW to keep waiting; each call returns after ~45s so it can't be killed by a host tool-call timeout. Nothing is lost between calls: the code stays valid and approval keeps being polled in the background, so a re-call returns the instant they approve."
|
|
323
332
|
})
|
|
324
333
|
}]
|
|
325
334
|
};
|
|
@@ -381,7 +390,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
381
390
|
},
|
|
382
391
|
{
|
|
383
392
|
name: "await_reply",
|
|
384
|
-
description: "Wait for the user's reply to a specific notification you sent (pass the notificationId from contact). This is how you wait for your answer in-context. Polls ~
|
|
393
|
+
description: "Wait for the user's reply to a specific notification you sent (pass the notificationId from contact). This is how you wait for your answer in-context. Polls ~45s per call \u2014 deliberately under the 60s cap most hosts put on a single tool call, so it ALWAYS returns you something (raise it with maxWaitSeconds only if you know your host allows longer). Returns { type:'reply', answer } when they respond, { type:'remind', remindInSeconds } on snooze (ScheduleWakeup then await_reply again), or { type:'idle' } (this window ended, no answer yet). While your contact is being handled on a LIVE call, you may receive { type:'partial', inFlight:true, turn } results: what the user said to each turn, as they say it. Use partials to PREPARE \u2014 fetch the data, draft the thing, warm the build \u2014 never to act irreversibly: the user can still revise any of them until the final reply arrives. Partial = intelligence, settled = authorization. If a partial's acts carry a question aimed at you and you know the answer, call contact on the SAME threadId right away \u2014 the caller hears your answer on the same call instead of waiting for a callback. Keep calling await_reply until you get the final reply \u2014 THAT one is the decision. 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 contact 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 (contact with the reply's threadId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry `intents` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' (\"call me after lunch\") \u2192 register it NOW with schedule_callback \u2014 when the intent carries `dueInSeconds` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' (\"you pick\") \u2192 make the call yourself and tell them what you chose; 'channel' (\"text me next time\") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped \"should I call you back?\" because the follow-up is implied. `transcript` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions (\"yes, IF tests pass\") before acting. If your ask declared `points`, the reply carries `covered` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same threadId) or proceed knowingly partial; never treat a partial answer as complete.",
|
|
385
394
|
inputSchema: json(AwaitReplySchema)
|
|
386
395
|
},
|
|
387
396
|
{
|
|
@@ -404,6 +413,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
404
413
|
description: "Report progress on the follow-up work behind ANY notification you own \u2014 a user-initiated request (from check_replies), or your OWN contact 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 contact 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.",
|
|
405
414
|
inputSchema: json(SetTaskStateToolSchema)
|
|
406
415
|
},
|
|
416
|
+
{
|
|
417
|
+
name: "answer_caller_question",
|
|
418
|
+
description: "Answer a question the user asked DURING a live call, while they're still on it. When a partial turn or a settled reply carries a `question` intent aimed at you, answer it here immediately: if their call is still live, your answer is spoken to them on that same call (returns live: true). If the call already ended (live: false), send the answer as a threaded contact instead \u2014 never drop it. Short spoken sentences only; this may be read aloud.",
|
|
419
|
+
inputSchema: json(AnswerCallerQuestionSchema)
|
|
420
|
+
},
|
|
407
421
|
{
|
|
408
422
|
name: "schedule_callback",
|
|
409
423
|
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 contact on that threadId; check_replies re-lists due callbacks until you do.",
|
|
@@ -417,9 +431,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
417
431
|
];
|
|
418
432
|
return { tools };
|
|
419
433
|
});
|
|
420
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
434
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
421
435
|
try {
|
|
422
|
-
return await handleTool(request);
|
|
436
|
+
return await handleTool(request, extra?.signal);
|
|
423
437
|
} catch (e) {
|
|
424
438
|
if (e instanceof UnpairedError) return pairStartResult(await startPairing(suggestedAgentName()));
|
|
425
439
|
throw e;
|
|
@@ -444,14 +458,14 @@ function suggestedAgentName() {
|
|
|
444
458
|
if (!raw) return void 0;
|
|
445
459
|
return CLIENT_LABELS[raw.toLowerCase()] ?? raw.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
446
460
|
}
|
|
447
|
-
async function handleTool(request) {
|
|
461
|
+
async function handleTool(request, signal) {
|
|
448
462
|
switch (request.params.name) {
|
|
449
463
|
case "pair": {
|
|
450
464
|
const { device_code } = PairSchema.parse(request.params.arguments ?? {});
|
|
451
465
|
if (!device_code) {
|
|
452
466
|
return pairStartResult(await startPairing(suggestedAgentName()));
|
|
453
467
|
}
|
|
454
|
-
const capMs =
|
|
468
|
+
const capMs = JOIN_CAP_MS;
|
|
455
469
|
const outcome = await joinBackgroundPair(device_code, capMs) ?? await resolvePairing(device_code, capMs);
|
|
456
470
|
return renderPairOutcome(outcome, device_code);
|
|
457
471
|
}
|
|
@@ -510,8 +524,11 @@ async function handleTool(request) {
|
|
|
510
524
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
511
525
|
}
|
|
512
526
|
case "await_reply": {
|
|
513
|
-
const { notificationId } = AwaitReplySchema.parse(request.params.arguments);
|
|
514
|
-
const item = await awaitReply(notificationId
|
|
527
|
+
const { notificationId, maxWaitSeconds } = AwaitReplySchema.parse(request.params.arguments);
|
|
528
|
+
const item = await awaitReply(notificationId, {
|
|
529
|
+
...maxWaitSeconds !== void 0 ? { windowMs: maxWaitSeconds * 1e3 } : {},
|
|
530
|
+
...signal ? { signal } : {}
|
|
531
|
+
});
|
|
515
532
|
return { content: [{ type: "text", text: JSON.stringify(item) }] };
|
|
516
533
|
}
|
|
517
534
|
case "check_replies": {
|
|
@@ -526,6 +543,14 @@ async function handleTool(request) {
|
|
|
526
543
|
const { q } = SearchThreadsSchema.parse(request.params.arguments);
|
|
527
544
|
return { content: [{ type: "text", text: JSON.stringify(await searchThreads(q)) }] };
|
|
528
545
|
}
|
|
546
|
+
case "answer_caller_question": {
|
|
547
|
+
const { notificationId, answer } = AnswerCallerQuestionSchema.parse(request.params.arguments);
|
|
548
|
+
const result = await answerCallerQuestion(notificationId, answer);
|
|
549
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
550
|
+
...result,
|
|
551
|
+
...result.live ? {} : { note: "The call already ended \u2014 send this as a threaded contact instead so the answer still reaches them." }
|
|
552
|
+
}) }] };
|
|
553
|
+
}
|
|
529
554
|
case "set_task_state": {
|
|
530
555
|
const { notificationId, state } = SetTaskStateToolSchema.parse(request.params.arguments);
|
|
531
556
|
const result = await setTaskState(notificationId, state);
|
package/dist/listen.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
WAKE_EVENT,
|
|
4
4
|
wakeChannel
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-47E77CDG.js";
|
|
6
6
|
import {
|
|
7
7
|
checkReplies,
|
|
8
8
|
registerDelivery
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-VCEFF2VA.js";
|
|
10
10
|
|
|
11
11
|
// src/listen.ts
|
|
12
12
|
import { createClient } from "@supabase/supabase-js";
|
|
@@ -113,6 +113,55 @@ function uninstallService() {
|
|
|
113
113
|
|
|
114
114
|
// src/listen.ts
|
|
115
115
|
var emit = (obj) => void process.stdout.write(JSON.stringify(obj) + "\n");
|
|
116
|
+
function answerText(answer) {
|
|
117
|
+
switch (answer.kind) {
|
|
118
|
+
case "text":
|
|
119
|
+
return answer.text;
|
|
120
|
+
case "option":
|
|
121
|
+
return answer.label ?? answer.optionId;
|
|
122
|
+
case "multi":
|
|
123
|
+
case "ranked":
|
|
124
|
+
return (answer.labels ?? answer.optionIds).join(", ");
|
|
125
|
+
case "confirm":
|
|
126
|
+
return answer.approved ? "approved" : "declined";
|
|
127
|
+
case "clarify":
|
|
128
|
+
return answer.chunks.join(" ");
|
|
129
|
+
// A call log — PAIGY_TEXT is what the USER said, so the bot's prompts stay out
|
|
130
|
+
// (the full exchange is in PAIGY_WORK for a launcher that wants it).
|
|
131
|
+
case "turns":
|
|
132
|
+
return answer.turns.map((t) => t.reply).join(" ");
|
|
133
|
+
case "ignored":
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function launchEnv(work, reason) {
|
|
138
|
+
const env = { PAIGY_WORK: JSON.stringify(work), PAIGY_EVENT: reason };
|
|
139
|
+
const put = (key, value) => {
|
|
140
|
+
if (value) env[key] = value;
|
|
141
|
+
};
|
|
142
|
+
const lead = work.threads?.find((t) => !t.busy)?.items[0]?.notificationId;
|
|
143
|
+
const reply = lead ? work.replies.find((r) => r.notificationId === lead) : work.replies[0];
|
|
144
|
+
const request = lead ? work.requests.find((r) => r.notificationId === lead) : work.requests[0];
|
|
145
|
+
if (reply) {
|
|
146
|
+
put("PAIGY_THREAD_ID", reply.threadId);
|
|
147
|
+
put("PAIGY_NOTIFICATION_ID", reply.notificationId);
|
|
148
|
+
put("PAIGY_TEXT", answerText(reply.answer));
|
|
149
|
+
return env;
|
|
150
|
+
}
|
|
151
|
+
if (request) {
|
|
152
|
+
put("PAIGY_THREAD_ID", request.threadId);
|
|
153
|
+
put("PAIGY_NOTIFICATION_ID", request.notificationId);
|
|
154
|
+
put("PAIGY_CONTEXT_THREAD_ID", request.contextThreadId);
|
|
155
|
+
put("PAIGY_TEXT", request.text);
|
|
156
|
+
return env;
|
|
157
|
+
}
|
|
158
|
+
const callback = work.owedCallbacks[0];
|
|
159
|
+
if (callback) {
|
|
160
|
+
put("PAIGY_THREAD_ID", callback.threadId);
|
|
161
|
+
put("PAIGY_TEXT", callback.note);
|
|
162
|
+
}
|
|
163
|
+
return env;
|
|
164
|
+
}
|
|
116
165
|
async function sweep(reason) {
|
|
117
166
|
try {
|
|
118
167
|
const work = await checkReplies();
|
|
@@ -121,7 +170,7 @@ async function sweep(reason) {
|
|
|
121
170
|
const inbound = work.replies.length > 0 || work.requests.length > 0;
|
|
122
171
|
const reengage = work.owedCallbacks.length > 0 && (reason === "boot" || reason.includes("callback"));
|
|
123
172
|
if (cmd && (inbound || reengage)) {
|
|
124
|
-
spawn(cmd, { shell: true, stdio: "inherit", env: { ...process.env,
|
|
173
|
+
spawn(cmd, { shell: true, stdio: "inherit", env: { ...process.env, ...launchEnv(work, reason) } });
|
|
125
174
|
}
|
|
126
175
|
} catch (e) {
|
|
127
176
|
emit({ type: "error", reason, message: e.message });
|
|
@@ -178,5 +227,6 @@ if (isEntry()) {
|
|
|
178
227
|
}
|
|
179
228
|
}
|
|
180
229
|
export {
|
|
230
|
+
launchEnv,
|
|
181
231
|
sweep
|
|
182
232
|
};
|
package/dist/onboard.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
autoConfigureClients,
|
|
4
4
|
claudeInstallHint,
|
|
5
5
|
openBrowser
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QXPLQKIM.js";
|
|
7
7
|
import {
|
|
8
8
|
AGENT_NAME,
|
|
9
9
|
TOKEN_PATH,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
requestCode,
|
|
12
12
|
saveToken,
|
|
13
13
|
sleep
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-VCEFF2VA.js";
|
|
15
15
|
|
|
16
16
|
// src/onboard.ts
|
|
17
17
|
async function main() {
|
package/dist/statusline.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paigy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"typescript": "^5.7.2",
|
|
38
38
|
"vitest": "^2.1.8",
|
|
39
39
|
"@paigy/crypto": "0.0.0",
|
|
40
|
-
"@paigy/
|
|
41
|
-
"@paigy/
|
|
40
|
+
"@paigy/sdk": "0.1.0",
|
|
41
|
+
"@paigy/schema": "0.0.0"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsup",
|