@paigy/mcp 0.21.0 → 0.24.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 +17 -1
- package/dist/{chunk-VWQQ3VJF.js → chunk-F4SA2TWM.js} +53 -14
- package/dist/{chunk-HO3OJHRA.js → chunk-JIQKMDUU.js} +9 -3
- package/dist/{chunk-SLWESECN.js → chunk-ZLN2C6IY.js} +29 -0
- package/dist/index.js +5 -5
- package/dist/listen.js +2 -2
- package/dist/onboard.js +2 -2
- package/dist/statusline.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -15,6 +15,9 @@ The Paigy MCP connects automatically. The first time an agent uses it while
|
|
|
15
15
|
unpaired, it prompts you to pair — run `/paigy-onboard` (opens your browser to
|
|
16
16
|
approve).
|
|
17
17
|
|
|
18
|
+
> [!NOTE]
|
|
19
|
+
> If you are installing the plugin inside an active Claude Code session, you must type `/reload-plugins` (or restart the session) afterward so the terminal client starts the MCP server and exposes the new tools to the agent.
|
|
20
|
+
|
|
18
21
|
### Or add the MCP directly
|
|
19
22
|
|
|
20
23
|
No clone needed. Add it to Claude Code (`-s user` = available in every project; drop it for just the current one):
|
|
@@ -47,7 +50,20 @@ It talks to the hosted backend by default — no config needed. Set
|
|
|
47
50
|
|
|
48
51
|
### Codex CLI (OpenAI)
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
Recommended: install the Paigy Codex plugin (MCP configuration plus the Paigy workflow skill):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
codex plugin marketplace add paigy-ai/mcp --ref main
|
|
57
|
+
codex plugin add paigy@paigy-ai
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Then pair your phone:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx -y -p @paigy/mcp@latest paigy-mcp-onboard
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or add the MCP server directly (writes `~/.codex/config.toml`):
|
|
51
67
|
|
|
52
68
|
```bash
|
|
53
69
|
codex mcp add paigy --env PAIGY_AGENT=codex -- npx -y @paigy/mcp@latest
|
|
@@ -2711,6 +2711,9 @@ var UserSettingsSchema = z.object({
|
|
|
2711
2711
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
2712
2712
|
* write, 'hosted' on read (see store.ts). */
|
|
2713
2713
|
voiceMode: z.enum(["hosted", "on_device"]).optional(),
|
|
2714
|
+
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
2715
|
+
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
2716
|
+
pstnCalls: z.boolean().optional(),
|
|
2714
2717
|
/** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
|
|
2715
2718
|
* content is sealed end-to-end between the local agent and the phone. Like
|
|
2716
2719
|
* voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
|
|
@@ -2916,6 +2919,32 @@ var SupportRequestSchema = z.object({
|
|
|
2916
2919
|
message: z.string().trim().min(1).max(5e3),
|
|
2917
2920
|
name: z.string().trim().max(120).optional()
|
|
2918
2921
|
});
|
|
2922
|
+
var NotificationFeedbackKindSchema = z.enum([
|
|
2923
|
+
"break_down",
|
|
2924
|
+
// "This should be more than one ask — break it down."
|
|
2925
|
+
"regenerate_options",
|
|
2926
|
+
// "These recommended answers aren't good — give me new ones."
|
|
2927
|
+
"needs_visual",
|
|
2928
|
+
// "There should be a picture or design here."
|
|
2929
|
+
"other"
|
|
2930
|
+
// anything else — the note carries it.
|
|
2931
|
+
]);
|
|
2932
|
+
var NotificationFeedbackSchema = z.object({
|
|
2933
|
+
notificationId: z.string(),
|
|
2934
|
+
kind: NotificationFeedbackKindSchema,
|
|
2935
|
+
/** Optional free-text elaboration for a preset; required (non-empty) for 'other'. */
|
|
2936
|
+
note: z.string().trim().max(2e3).optional()
|
|
2937
|
+
}).superRefine((r, ctx) => {
|
|
2938
|
+
if (r.kind === "other" && !r.note)
|
|
2939
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["note"], message: "note is required for 'other' feedback" });
|
|
2940
|
+
});
|
|
2941
|
+
var FeedbackResolutionSchema = z.enum(["broker_fixed", "sent_to_agent"]);
|
|
2942
|
+
var FeedbackOutcomeSchema = z.object({
|
|
2943
|
+
resolution: FeedbackResolutionSchema,
|
|
2944
|
+
transform: TransformSchema,
|
|
2945
|
+
message: z.string(),
|
|
2946
|
+
childIds: z.array(z.string()).optional()
|
|
2947
|
+
});
|
|
2919
2948
|
var WORD_NUMS = {
|
|
2920
2949
|
a: 1,
|
|
2921
2950
|
an: 1,
|
|
@@ -3243,24 +3272,34 @@ var PROTO = "paigy-pair-v2|sas=24";
|
|
|
3243
3272
|
var SAS_BITS = 24;
|
|
3244
3273
|
var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
|
|
3245
3274
|
var KEY_PATH = join(homedir(), ".paigy", "key.json");
|
|
3246
|
-
function
|
|
3275
|
+
function readTokenFile() {
|
|
3276
|
+
if (!existsSync(TOKEN_PATH)) return {};
|
|
3277
|
+
try {
|
|
3278
|
+
const parsed = JSON.parse(readFileSync(TOKEN_PATH, "utf8"));
|
|
3279
|
+
if (typeof parsed.access_token === "string") return {};
|
|
3280
|
+
const slots = parsed;
|
|
3281
|
+
delete slots["*"];
|
|
3282
|
+
return slots;
|
|
3283
|
+
} catch {
|
|
3284
|
+
return {};
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
function saveToken(token, agent2 = AGENT_NAME) {
|
|
3288
|
+
const slots = { ...readTokenFile(), [agent2]: token };
|
|
3247
3289
|
mkdirSync(join(homedir(), ".paigy"), { recursive: true });
|
|
3248
|
-
writeFileSync(TOKEN_PATH, JSON.stringify(
|
|
3290
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(slots, null, 2) + "\n", { mode: 384 });
|
|
3249
3291
|
}
|
|
3250
3292
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3251
|
-
function readToken() {
|
|
3293
|
+
function readToken(agent2 = AGENT_NAME) {
|
|
3252
3294
|
if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
}
|
|
3261
|
-
function deleteToken() {
|
|
3262
|
-
if (!existsSync(TOKEN_PATH)) return false;
|
|
3263
|
-
rmSync(TOKEN_PATH);
|
|
3295
|
+
return readTokenFile()[agent2]?.access_token ?? "";
|
|
3296
|
+
}
|
|
3297
|
+
function deleteToken(agent2 = AGENT_NAME) {
|
|
3298
|
+
const slots = readTokenFile();
|
|
3299
|
+
if (!(agent2 in slots)) return false;
|
|
3300
|
+
delete slots[agent2];
|
|
3301
|
+
if (Object.keys(slots).length === 0) rmSync(TOKEN_PATH);
|
|
3302
|
+
else writeFileSync(TOKEN_PATH, JSON.stringify(slots, null, 2) + "\n", { mode: 384 });
|
|
3264
3303
|
return true;
|
|
3265
3304
|
}
|
|
3266
3305
|
async function revokeToken(token) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AGENT_NAME
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-F4SA2TWM.js";
|
|
4
4
|
|
|
5
5
|
// src/clients.ts
|
|
6
6
|
import { execFile } from "child_process";
|
|
@@ -78,8 +78,14 @@ function autoConfigureClients(skip = AGENT_NAME) {
|
|
|
78
78
|
}
|
|
79
79
|
function claudeInstallHint(skip = AGENT_NAME) {
|
|
80
80
|
if (skip.startsWith("claude")) return null;
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
const claudeDir = join(homedir(), ".claude");
|
|
82
|
+
if (!existsSync(claudeDir)) return null;
|
|
83
|
+
try {
|
|
84
|
+
const registry = join(claudeDir, "plugins", "installed_plugins.json");
|
|
85
|
+
if (existsSync(registry) && readFileSync(registry, "utf8").includes('"paigy@')) return null;
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
return "Claude Code detected \u2014 to add Paigy there, run /plugin marketplace add paigy-ai/claude then /plugin install paigy (pairing carries over; no need to pair again). Note: If you run this inside a live session, type /reload-plugins afterward so the agent connects to the new tools.";
|
|
83
89
|
}
|
|
84
90
|
var PAIGY_TOOL_IDS = [
|
|
85
91
|
"mcp__paigy__notify_user",
|
|
@@ -422,6 +422,9 @@ var UserSettingsSchema = z.object({
|
|
|
422
422
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
423
423
|
* write, 'hosted' on read (see store.ts). */
|
|
424
424
|
voiceMode: z.enum(["hosted", "on_device"]).optional(),
|
|
425
|
+
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
426
|
+
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
427
|
+
pstnCalls: z.boolean().optional(),
|
|
425
428
|
/** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
|
|
426
429
|
* content is sealed end-to-end between the local agent and the phone. Like
|
|
427
430
|
* voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
|
|
@@ -629,6 +632,32 @@ var SupportRequestSchema = z.object({
|
|
|
629
632
|
message: z.string().trim().min(1).max(5e3),
|
|
630
633
|
name: z.string().trim().max(120).optional()
|
|
631
634
|
});
|
|
635
|
+
var NotificationFeedbackKindSchema = z.enum([
|
|
636
|
+
"break_down",
|
|
637
|
+
// "This should be more than one ask — break it down."
|
|
638
|
+
"regenerate_options",
|
|
639
|
+
// "These recommended answers aren't good — give me new ones."
|
|
640
|
+
"needs_visual",
|
|
641
|
+
// "There should be a picture or design here."
|
|
642
|
+
"other"
|
|
643
|
+
// anything else — the note carries it.
|
|
644
|
+
]);
|
|
645
|
+
var NotificationFeedbackSchema = z.object({
|
|
646
|
+
notificationId: z.string(),
|
|
647
|
+
kind: NotificationFeedbackKindSchema,
|
|
648
|
+
/** Optional free-text elaboration for a preset; required (non-empty) for 'other'. */
|
|
649
|
+
note: z.string().trim().max(2e3).optional()
|
|
650
|
+
}).superRefine((r, ctx) => {
|
|
651
|
+
if (r.kind === "other" && !r.note)
|
|
652
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["note"], message: "note is required for 'other' feedback" });
|
|
653
|
+
});
|
|
654
|
+
var FeedbackResolutionSchema = z.enum(["broker_fixed", "sent_to_agent"]);
|
|
655
|
+
var FeedbackOutcomeSchema = z.object({
|
|
656
|
+
resolution: FeedbackResolutionSchema,
|
|
657
|
+
transform: TransformSchema,
|
|
658
|
+
message: z.string(),
|
|
659
|
+
childIds: z.array(z.string()).optional()
|
|
660
|
+
});
|
|
632
661
|
|
|
633
662
|
export {
|
|
634
663
|
HandoffSchema,
|
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-ZLN2C6IY.js";
|
|
5
5
|
import {
|
|
6
6
|
PAIGY_TOOL_IDS,
|
|
7
7
|
autoConfigureClients,
|
|
8
8
|
claudeInstallHint,
|
|
9
9
|
enablePaigyTools
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-JIQKMDUU.js";
|
|
11
11
|
import {
|
|
12
12
|
clearSurface,
|
|
13
13
|
writeSurface
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
sleep,
|
|
39
39
|
startE2ee,
|
|
40
40
|
submitNotification
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-F4SA2TWM.js";
|
|
42
42
|
|
|
43
43
|
// src/index.ts
|
|
44
44
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -260,7 +260,7 @@ var server = new Server(
|
|
|
260
260
|
{ name: "paigy", version: "0.0.0" },
|
|
261
261
|
{
|
|
262
262
|
capabilities: { tools: {} },
|
|
263
|
-
instructions: "On startup, call check_replies once to pick up any replies or pending work you missed while away. A check_replies request whose threadId you don't recognize, or one carrying a contextThreadId, means the user is resuming or seeding a past conversation \u2014 call get_thread on it FIRST and treat the transcript as prior conversation, not new input. 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
|
|
263
|
+
instructions: "On startup, call check_replies once to pick up any replies or pending work you missed while away. A check_replies request whose threadId you don't recognize, or one carrying a contextThreadId, means the user is resuming or seeding a past conversation \u2014 call get_thread on it FIRST and treat the transcript as prior conversation, not new input. 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, re-send notify_user on the SAME threadId at urgency:'call' with the SAME content \u2014 identical content on an existing thread escalates the notification already sitting there instead of creating a second one, so don't reword the title/description just to escalate. Reword only when you genuinely have new information, which correctly becomes a new notification on the thread. Skip this for anything that isn't truly blocking; a normal question can just sit in the inbox. NEVER go quietly idle while something might still be pending for you: whenever you end a turn with any Paigy notification unanswered (or any chance the user replied through the app while you worked), schedule your own ~2-minute wake-up (harness ScheduleWakeup or equivalent) and call check_replies when it fires; if still nothing, re-schedule and keep looping until resolved or the user says stop. For legibility, always use this exact wording \u2014 reason: 'Paigy idle check \u2014 waiting on <thing>', wake-up prompt: 'Paigy idle check: call check_replies and engage with anything unacknowledged; if idle, re-schedule (~2min).' \u2014 so the user can recognize every idle check at a glance. This self-polling in your own live session (full context intact) is the PRIMARY mechanism; the plugin's Stop hooks are only the dead-session safety net."
|
|
264
264
|
}
|
|
265
265
|
);
|
|
266
266
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -282,7 +282,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
282
282
|
},
|
|
283
283
|
{
|
|
284
284
|
name: "notify_user",
|
|
285
|
-
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. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread (they leave the user's inbox; the response reports supersededCount) \u2014 right for updates to one ask, WRONG for a checklist: send parallel to-dos as separate un-threaded notifications. SIMPLEST FORM \u2014 just state what you need: { ask: \"I need to know whether to deploy the auth fix \u2014 tests are green, staging verified\", urgencyHint: 'now'|'soon'|'whenever', needs?: [\"deploy?\", \"keep the flag?\"] }. Paigy's broker derives the title, answer shape, options, and delivery channel for you \u2014 prefer this unless you specifically need to control the exact options/shape. The fully-shaped form below remains available and unchanged (the two are mutually exclusive: send `ask` OR `context`+`select`). FULLY-SHAPED FORM: 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.",
|
|
285
|
+
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. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread (they leave the user's inbox; the response reports supersededCount) \u2014 right for updates to one ask, WRONG for a checklist: send parallel to-dos as separate un-threaded notifications. A threaded re-send with IDENTICAL content instead escalates the pending ask in place (the response reports escalated: true and returns the SAME notificationId, so any await_reply you already have on it keeps working). SIMPLEST FORM \u2014 just state what you need: { ask: \"I need to know whether to deploy the auth fix \u2014 tests are green, staging verified\", urgencyHint: 'now'|'soon'|'whenever', needs?: [\"deploy?\", \"keep the flag?\"] }. Paigy's broker derives the title, answer shape, options, and delivery channel for you \u2014 prefer this unless you specifically need to control the exact options/shape. The fully-shaped form below remains available and unchanged (the two are mutually exclusive: send `ask` OR `context`+`select`). FULLY-SHAPED FORM: 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.",
|
|
286
286
|
inputSchema: json(NotifyRequestSchema)
|
|
287
287
|
},
|
|
288
288
|
{
|
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-ZLN2C6IY.js";
|
|
6
6
|
import {
|
|
7
7
|
checkReplies,
|
|
8
8
|
registerDelivery
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-F4SA2TWM.js";
|
|
10
10
|
|
|
11
11
|
// src/listen.ts
|
|
12
12
|
import { createClient } from "@supabase/supabase-js";
|
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-JIQKMDUU.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-F4SA2TWM.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.24.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",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"tsup": "^8.3.5",
|
|
37
37
|
"typescript": "^5.7.2",
|
|
38
38
|
"vitest": "^2.1.8",
|
|
39
|
-
"@paigy/crypto": "0.0.0",
|
|
40
39
|
"@paigy/schema": "0.0.0",
|
|
41
|
-
"@paigy/sdk": "0.1.0"
|
|
40
|
+
"@paigy/sdk": "0.1.0",
|
|
41
|
+
"@paigy/crypto": "0.0.0"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsup",
|