@paigy/mcp 0.16.0 → 0.17.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 +25 -1
- package/dist/{chunk-R3YGSGPX.js → chunk-4LQAS5ZB.js} +34 -2
- package/dist/{chunk-U3WODCOE.js → chunk-ZIOTFQPK.js} +1 -1
- package/dist/index.js +18 -4
- package/dist/listen.js +136 -8
- package/dist/onboard.js +2 -2
- package/dist/statusline.js +1 -1
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -112,6 +112,30 @@ Then pair: `npx -p @paigy/mcp@latest paigy-mcp-onboard`.
|
|
|
112
112
|
ignored), and `paigy-listen`'s realtime wake channel is a websocket that doesn't
|
|
113
113
|
proxy — its REST sweeps do.
|
|
114
114
|
|
|
115
|
+
## Wake any harness (`paigy-listen`)
|
|
116
|
+
|
|
117
|
+
`paigy-listen` is the self-hosted push daemon: it subscribes to this connection's
|
|
118
|
+
wake channel and sweeps on every nudge. Keep it alive past the terminal with
|
|
119
|
+
`paigy-listen --install` (launchd on macOS, systemd user unit on Linux; `--uninstall`
|
|
120
|
+
removes it).
|
|
121
|
+
|
|
122
|
+
To launch an agent — any agent, not just Claude — when work arrives, set
|
|
123
|
+
`PAIGY_ON_WAKE` to a command before `--install`. The daemon runs it with the swept
|
|
124
|
+
work in `$PAIGY_WORK` (JSON: `replies`, `requests`, `owedCallbacks`); the sweep has
|
|
125
|
+
already claimed the work, so read `$PAIGY_WORK` rather than calling `check_replies`
|
|
126
|
+
again. A request carrying a `contextThreadId` (or an unfamiliar `threadId`) means
|
|
127
|
+
the user is resuming/seeding a past conversation — rehydrate with `get_thread` first.
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
# Claude Code:
|
|
131
|
+
PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — rehydrate threads you do not recognize via get_thread first."' \
|
|
132
|
+
npx -y -p @paigy/mcp paigy-listen --install
|
|
133
|
+
|
|
134
|
+
# Codex (or any CLI harness) works the same way:
|
|
135
|
+
PAIGY_ON_WAKE='codex exec "Handle the Paigy work in $PAIGY_WORK ..."' \
|
|
136
|
+
npx -y -p @paigy/mcp paigy-listen --install
|
|
137
|
+
```
|
|
138
|
+
|
|
115
139
|
## Statusline (Claude Code)
|
|
116
140
|
|
|
117
141
|
`paigy-statusline` prints a one-line connection status — the account's session
|
|
@@ -124,7 +148,7 @@ paigy: all calls · phone ✓
|
|
|
124
148
|
Enable it in `~/.claude/settings.json`:
|
|
125
149
|
|
|
126
150
|
```json
|
|
127
|
-
"statusLine": { "type": "command", "command": "npx -y @paigy/mcp@latest paigy-statusline" }
|
|
151
|
+
"statusLine": { "type": "command", "command": "npx -y -p @paigy/mcp@latest paigy-statusline" }
|
|
128
152
|
```
|
|
129
153
|
|
|
130
154
|
It reads `GET /api/status` with the pairing token and caches the result in
|
|
@@ -2504,7 +2504,15 @@ var PendingRepliesSchema = z.object({
|
|
|
2504
2504
|
* notify_user on the same threadId. Keeps reappearing until you call
|
|
2505
2505
|
* set_task_state on its notificationId. */
|
|
2506
2506
|
requests: z.array(
|
|
2507
|
-
z.object({
|
|
2507
|
+
z.object({
|
|
2508
|
+
threadId: z.string(),
|
|
2509
|
+
notificationId: z.string(),
|
|
2510
|
+
text: z.string(),
|
|
2511
|
+
createdAt: z.string(),
|
|
2512
|
+
/** The user seeded this request with a past conversation — call get_thread on it
|
|
2513
|
+
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
2514
|
+
contextThreadId: z.string().optional()
|
|
2515
|
+
})
|
|
2508
2516
|
),
|
|
2509
2517
|
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
2510
2518
|
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
@@ -2540,8 +2548,13 @@ var UserResponseSchema = z.object({
|
|
|
2540
2548
|
/** Coverage report (#396): which of the ask's declared `points` were addressed. */
|
|
2541
2549
|
covered: z.array(z.string()).optional()
|
|
2542
2550
|
});
|
|
2551
|
+
var VoiceKeySchema = z.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
2543
2552
|
var InboxItemSchema = z.object({
|
|
2544
2553
|
id: z.string(),
|
|
2554
|
+
/** The conversation thread + connection this item lives on. Present on the replied
|
|
2555
|
+
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
2556
|
+
threadId: z.string().optional(),
|
|
2557
|
+
tokenId: z.string().optional(),
|
|
2545
2558
|
status: NotifyStatusSchema,
|
|
2546
2559
|
context: ContextSchema,
|
|
2547
2560
|
options: z.array(OptionSchema).optional(),
|
|
@@ -2556,6 +2569,8 @@ var InboxItemSchema = z.object({
|
|
|
2556
2569
|
visuals: z.array(VisualSchema).optional(),
|
|
2557
2570
|
agent: z.string(),
|
|
2558
2571
|
nickname: z.string(),
|
|
2572
|
+
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
2573
|
+
voice: VoiceKeySchema.optional(),
|
|
2559
2574
|
repo: z.string().optional(),
|
|
2560
2575
|
branch: z.string().optional(),
|
|
2561
2576
|
createdAt: z.string().datetime(),
|
|
@@ -2675,6 +2690,8 @@ var ConnectionSummarySchema = z.object({
|
|
|
2675
2690
|
agent: z.string(),
|
|
2676
2691
|
device: z.string().nullable(),
|
|
2677
2692
|
nickname: z.string(),
|
|
2693
|
+
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
2694
|
+
voice: VoiceKeySchema.nullable(),
|
|
2678
2695
|
createdAt: z.string().datetime(),
|
|
2679
2696
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
2680
2697
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
@@ -2687,7 +2704,14 @@ var CreateRequestSchema = z.object({
|
|
|
2687
2704
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
2688
2705
|
tokenId: z.string(),
|
|
2689
2706
|
/** The user's message to the agent. */
|
|
2690
|
-
text: z.string().min(1)
|
|
2707
|
+
text: z.string().min(1),
|
|
2708
|
+
/** Land the request on an existing conversation thread (History → "Continue")
|
|
2709
|
+
* instead of minting a fresh one. Must belong to the requesting user. */
|
|
2710
|
+
threadId: z.string().optional(),
|
|
2711
|
+
/** Point the agent at a past conversation (possibly with a different agent) as
|
|
2712
|
+
* starting context (History → "New session from this"). A reference, not a copy —
|
|
2713
|
+
* the agent reads it via get_thread. Must belong to the requesting user. */
|
|
2714
|
+
contextThreadId: z.string().optional()
|
|
2691
2715
|
});
|
|
2692
2716
|
var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
|
|
2693
2717
|
var RegisterDeliverySchema = z.object({ mode: DeliveryModeSchema });
|
|
@@ -3426,6 +3450,13 @@ function landIntents(intents) {
|
|
|
3426
3450
|
return due !== null ? { ...i, dueInSeconds: due } : i;
|
|
3427
3451
|
});
|
|
3428
3452
|
}
|
|
3453
|
+
async function getThread(threadId) {
|
|
3454
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/thread/${encodeURIComponent(threadId)}`, {
|
|
3455
|
+
headers: { authorization: `Bearer ${readToken()}` }
|
|
3456
|
+
}));
|
|
3457
|
+
if (!res.ok) throw new Error(`get_thread failed: ${res.status} ${await res.text()}`);
|
|
3458
|
+
return await res.json();
|
|
3459
|
+
}
|
|
3429
3460
|
async function checkReplies() {
|
|
3430
3461
|
const token = readToken();
|
|
3431
3462
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
|
|
@@ -3539,6 +3570,7 @@ export {
|
|
|
3539
3570
|
UnpairedError,
|
|
3540
3571
|
submitNotification,
|
|
3541
3572
|
awaitReply,
|
|
3573
|
+
getThread,
|
|
3542
3574
|
checkReplies,
|
|
3543
3575
|
setTaskState,
|
|
3544
3576
|
registerDelivery,
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
autoConfigureClients,
|
|
4
4
|
claudeInstallHint
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ZIOTFQPK.js";
|
|
6
6
|
import {
|
|
7
7
|
clearSurface,
|
|
8
8
|
writeSurface
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
deleteToken,
|
|
19
19
|
fetchCredential,
|
|
20
20
|
finalizeE2ee,
|
|
21
|
+
getThread,
|
|
21
22
|
lintNotify,
|
|
22
23
|
pairStep,
|
|
23
24
|
readKeyFile,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
sleep,
|
|
32
33
|
startE2ee,
|
|
33
34
|
submitNotification
|
|
34
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-4LQAS5ZB.js";
|
|
35
36
|
|
|
36
37
|
// src/index.ts
|
|
37
38
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -70,13 +71,16 @@ function json(s) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
// src/index.ts
|
|
73
|
-
var ONBOARD_MSG = "Not paired with Paigy yet \u2014 call the `pair` tool to connect this agent (it returns an approval link to show the user), then retry. Manual fallback: `npx -y @paigy/mcp paigy-mcp-onboard`.";
|
|
74
|
+
var ONBOARD_MSG = "Not paired with Paigy yet \u2014 call the `pair` tool to connect this agent (it returns an approval link to show the user), then retry. Manual fallback: `npx -y -p @paigy/mcp paigy-mcp-onboard`.";
|
|
74
75
|
var AwaitReplySchema = z.object({
|
|
75
76
|
notificationId: z.string().describe("The notificationId returned by notify_user \u2014 waits for the user's reply to THIS notification only.")
|
|
76
77
|
});
|
|
77
78
|
var PairSchema = z.object({
|
|
78
79
|
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.")
|
|
79
80
|
});
|
|
81
|
+
var GetThreadSchema = z.object({
|
|
82
|
+
threadId: z.string().describe("The thread to read \u2014 from a reply, request, or past notification.")
|
|
83
|
+
});
|
|
80
84
|
var SetTaskStateToolSchema = z.object({
|
|
81
85
|
notificationId: z.string(),
|
|
82
86
|
state: SetTaskStateSchema.shape.state
|
|
@@ -121,6 +125,7 @@ function pairedResult(token, sas, note, e2ee) {
|
|
|
121
125
|
if (hint) base.claude_hint = hint;
|
|
122
126
|
} catch {
|
|
123
127
|
}
|
|
128
|
+
base.permissions_hint = "Offer the user ONCE: allowlist Paigy's tools so they run without prompting (in Claude Code, add mcp__paigy__notify_user, mcp__paigy__await_reply, mcp__paigy__check_replies, mcp__paigy__set_task_state, mcp__paigy__schedule_callback, mcp__paigy__get_thread to permissions.allow in ~/.claude/settings.json \u2014 or the project's .claude/settings.json if they prefer; other tools have their own allowlist). Merge into the existing file, never clobber it. Leave pair/unpair human-approved. If they decline, drop it.";
|
|
124
129
|
return { content: [{ type: "text", text: JSON.stringify(base) }] };
|
|
125
130
|
}
|
|
126
131
|
function awaitingConfirmResult(sas, device_code) {
|
|
@@ -148,7 +153,7 @@ var server = new Server(
|
|
|
148
153
|
{ name: "paigy", version: "0.0.0" },
|
|
149
154
|
{
|
|
150
155
|
capabilities: { tools: {} },
|
|
151
|
-
instructions: "On startup, call check_replies once to pick up any replies or pending work you missed while away. To wait for the answer to something you just asked, call await_reply with that notificationId \u2014 it's scoped to that one notification, so it never returns replies meant for other notifications. Use check_replies again only when re-booting or after waiting a long time on something else. Never end a turn that still needs the user without notify_user + await_reply. When you need a decision or input, MATCH the answer shape to the question \u2014 don't default everything to free text, and don't reflexively make everything yes/no. Pick the best tool for the job: yes/no \u2192 select:'confirm'; approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve'; pick one of several \u2192 options + select:'one'; pick several / a subset \u2192 options + select:'many'; rank or prioritize \u2192 options + select:'rank'. Reserve select:'text' (free-form reply only) for plain updates and answers that genuinely can't be structured (the user can always add free text on top of any shape). On a { kind: 'clarify' } reply, see notify_user's own description for how to respond. When urgency is 'call', remember the title + description are spoken aloud \u2014 write them short and conversational, and name things instead of using IDs (e.g. 'the pull request about the agents page', not 'PR #235'). When the user asks you to follow up later \u2014 when you're done, if you're blocked, or at a set time \u2014 record it with schedule_callback so you don't drop it if you go idle. If you're about to start a genuinely long-running or blocking piece of work \u2014 one where the user would otherwise sit and wait \u2014 mention ONCE, in passing, that you can reach them when it's done or if you hit a blocker, instead of them needing to babysit the terminal. Don't offer this for quick tasks, and don't repeat the offer if they've already said yes or no earlier in the conversation. Escalate silence, don't just wait on it: if you notified at a lower urgency (inbox/push/banner) for something that's genuinely blocking real progress, call await_reply up to twice (~5 min each, ~10 min total) \u2014 if it's still idle after that AND the item is genuinely blocking, send a fresh notify_user on the SAME threadId at urgency:'call'. Skip this for anything that isn't truly blocking; a normal question can just sit in the inbox. 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."
|
|
156
|
+
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 a fresh notify_user on the SAME threadId at urgency:'call'. Skip this for anything that isn't truly blocking; a normal question can just sit in the inbox. 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."
|
|
152
157
|
}
|
|
153
158
|
);
|
|
154
159
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -178,6 +183,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
178
183
|
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. Replies may carry `intents`/`transcript`/`covered` (call-mapped answers) \u2014 handle intents exactly as await_reply's description says (defer \u2192 schedule_callback now; delegate \u2192 decide and say so; channel \u2192 honor next contact), and treat a `covered` list missing one of your declared points as that part still unanswered.",
|
|
179
184
|
inputSchema: json(z.object({}))
|
|
180
185
|
},
|
|
186
|
+
{
|
|
187
|
+
name: "get_thread",
|
|
188
|
+
description: "The chronological transcript of one Paigy conversation thread \u2014 every past ask, answer, and user request on it. Call this to REHYDRATE when you're resuming or being seeded: a check_replies request whose threadId you don't recognize means the user is continuing an old conversation with you, and one carrying a contextThreadId means they want a past conversation (possibly with a DIFFERENT agent) as your starting context \u2014 in both cases call get_thread FIRST and read the turns as prior conversation you were part of, not as new input. Turns: { role:'agent', title, description[], answer } and { role:'user', text }, oldest first, capped at the most recent 30.",
|
|
189
|
+
inputSchema: json(GetThreadSchema)
|
|
190
|
+
},
|
|
181
191
|
{
|
|
182
192
|
name: "set_task_state",
|
|
183
193
|
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.",
|
|
@@ -335,6 +345,10 @@ Enter it in the Paigy app (Inbox \u2192 Add a new agent).`,
|
|
|
335
345
|
const result = await checkReplies();
|
|
336
346
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
337
347
|
}
|
|
348
|
+
case "get_thread": {
|
|
349
|
+
const { threadId } = GetThreadSchema.parse(request.params.arguments);
|
|
350
|
+
return { content: [{ type: "text", text: JSON.stringify(await getThread(threadId)) }] };
|
|
351
|
+
}
|
|
338
352
|
case "set_task_state": {
|
|
339
353
|
const { notificationId, state } = SetTaskStateToolSchema.parse(request.params.arguments);
|
|
340
354
|
const result = await setTaskState(notificationId, state);
|
package/dist/listen.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
checkReplies,
|
|
4
4
|
registerDelivery
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-4LQAS5ZB.js";
|
|
6
6
|
|
|
7
7
|
// src/listen.ts
|
|
8
8
|
import { createClient } from "@supabase/supabase-js";
|
|
@@ -227,7 +227,15 @@ var PendingRepliesSchema = z.object({
|
|
|
227
227
|
* notify_user on the same threadId. Keeps reappearing until you call
|
|
228
228
|
* set_task_state on its notificationId. */
|
|
229
229
|
requests: z.array(
|
|
230
|
-
z.object({
|
|
230
|
+
z.object({
|
|
231
|
+
threadId: z.string(),
|
|
232
|
+
notificationId: z.string(),
|
|
233
|
+
text: z.string(),
|
|
234
|
+
createdAt: z.string(),
|
|
235
|
+
/** The user seeded this request with a past conversation — call get_thread on it
|
|
236
|
+
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
237
|
+
contextThreadId: z.string().optional()
|
|
238
|
+
})
|
|
231
239
|
),
|
|
232
240
|
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
233
241
|
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
@@ -263,8 +271,13 @@ var UserResponseSchema = z.object({
|
|
|
263
271
|
/** Coverage report (#396): which of the ask's declared `points` were addressed. */
|
|
264
272
|
covered: z.array(z.string()).optional()
|
|
265
273
|
});
|
|
274
|
+
var VoiceKeySchema = z.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
266
275
|
var InboxItemSchema = z.object({
|
|
267
276
|
id: z.string(),
|
|
277
|
+
/** The conversation thread + connection this item lives on. Present on the replied
|
|
278
|
+
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
279
|
+
threadId: z.string().optional(),
|
|
280
|
+
tokenId: z.string().optional(),
|
|
268
281
|
status: NotifyStatusSchema,
|
|
269
282
|
context: ContextSchema,
|
|
270
283
|
options: z.array(OptionSchema).optional(),
|
|
@@ -279,6 +292,8 @@ var InboxItemSchema = z.object({
|
|
|
279
292
|
visuals: z.array(VisualSchema).optional(),
|
|
280
293
|
agent: z.string(),
|
|
281
294
|
nickname: z.string(),
|
|
295
|
+
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
296
|
+
voice: VoiceKeySchema.optional(),
|
|
282
297
|
repo: z.string().optional(),
|
|
283
298
|
branch: z.string().optional(),
|
|
284
299
|
createdAt: z.string().datetime(),
|
|
@@ -398,6 +413,8 @@ var ConnectionSummarySchema = z.object({
|
|
|
398
413
|
agent: z.string(),
|
|
399
414
|
device: z.string().nullable(),
|
|
400
415
|
nickname: z.string(),
|
|
416
|
+
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
417
|
+
voice: VoiceKeySchema.nullable(),
|
|
401
418
|
createdAt: z.string().datetime(),
|
|
402
419
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
403
420
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
@@ -410,7 +427,14 @@ var CreateRequestSchema = z.object({
|
|
|
410
427
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
411
428
|
tokenId: z.string(),
|
|
412
429
|
/** The user's message to the agent. */
|
|
413
|
-
text: z.string().min(1)
|
|
430
|
+
text: z.string().min(1),
|
|
431
|
+
/** Land the request on an existing conversation thread (History → "Continue")
|
|
432
|
+
* instead of minting a fresh one. Must belong to the requesting user. */
|
|
433
|
+
threadId: z.string().optional(),
|
|
434
|
+
/** Point the agent at a past conversation (possibly with a different agent) as
|
|
435
|
+
* starting context (History → "New session from this"). A reference, not a copy —
|
|
436
|
+
* the agent reads it via get_thread. Must belong to the requesting user. */
|
|
437
|
+
contextThreadId: z.string().optional()
|
|
414
438
|
});
|
|
415
439
|
var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
|
|
416
440
|
var WAKE_EVENT = "wake";
|
|
@@ -552,6 +576,103 @@ var SupportRequestSchema = z.object({
|
|
|
552
576
|
name: z.string().trim().max(120).optional()
|
|
553
577
|
});
|
|
554
578
|
|
|
579
|
+
// src/service.ts
|
|
580
|
+
import { execFileSync } from "child_process";
|
|
581
|
+
import { mkdirSync, unlinkSync, writeFileSync } from "fs";
|
|
582
|
+
import { homedir } from "os";
|
|
583
|
+
import { join } from "path";
|
|
584
|
+
var LABEL = "ai.paigy.listen";
|
|
585
|
+
function frozenEnv() {
|
|
586
|
+
const keys = ["PAIGY_BACKEND_URL", "PAIGY_ON_WAKE", "PAIGY_SUPABASE_URL", "PAIGY_SUPABASE_ANON_KEY"];
|
|
587
|
+
return Object.fromEntries(keys.flatMap((k) => process.env[k] ? [[k, process.env[k]]] : []));
|
|
588
|
+
}
|
|
589
|
+
function renderLaunchd(nodePath, scriptPath, env) {
|
|
590
|
+
const envXml = Object.entries(env).map(([k, v]) => ` <key>${k}</key><string>${v}</string>`).join("\n");
|
|
591
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
592
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
593
|
+
<plist version="1.0">
|
|
594
|
+
<dict>
|
|
595
|
+
<key>Label</key><string>${LABEL}</string>
|
|
596
|
+
<key>ProgramArguments</key>
|
|
597
|
+
<array>
|
|
598
|
+
<string>${nodePath}</string>
|
|
599
|
+
<string>${scriptPath}</string>
|
|
600
|
+
</array>
|
|
601
|
+
<key>RunAtLoad</key><true/>
|
|
602
|
+
<key>KeepAlive</key><true/>
|
|
603
|
+
<key>ThrottleInterval</key><integer>30</integer>
|
|
604
|
+
<key>StandardOutPath</key><string>${join(homedir(), ".paigy", "listen.log")}</string>
|
|
605
|
+
<key>StandardErrorPath</key><string>${join(homedir(), ".paigy", "listen.log")}</string>
|
|
606
|
+
${envXml ? ` <key>EnvironmentVariables</key>
|
|
607
|
+
<dict>
|
|
608
|
+
${envXml}
|
|
609
|
+
</dict>
|
|
610
|
+
` : ""}</dict>
|
|
611
|
+
</plist>
|
|
612
|
+
`;
|
|
613
|
+
}
|
|
614
|
+
function renderSystemd(nodePath, scriptPath, env) {
|
|
615
|
+
const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${v}`).join("\n");
|
|
616
|
+
return `[Unit]
|
|
617
|
+
Description=Paigy listen \u2014 self-hosted push daemon
|
|
618
|
+
|
|
619
|
+
[Service]
|
|
620
|
+
ExecStart=${nodePath} ${scriptPath}
|
|
621
|
+
Restart=always
|
|
622
|
+
RestartSec=30
|
|
623
|
+
${envLines}
|
|
624
|
+
|
|
625
|
+
[Install]
|
|
626
|
+
WantedBy=default.target
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
function paths() {
|
|
630
|
+
const darwin = process.platform === "darwin";
|
|
631
|
+
return {
|
|
632
|
+
darwin,
|
|
633
|
+
unit: darwin ? join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`) : join(homedir(), ".config", "systemd", "user", "paigy-listen.service")
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function installService(nodePath, scriptPath) {
|
|
637
|
+
const { unit, darwin } = paths();
|
|
638
|
+
mkdirSync(join(unit, ".."), { recursive: true });
|
|
639
|
+
mkdirSync(join(homedir(), ".paigy"), { recursive: true });
|
|
640
|
+
writeFileSync(
|
|
641
|
+
unit,
|
|
642
|
+
darwin ? renderLaunchd(nodePath, scriptPath, frozenEnv()) : renderSystemd(nodePath, scriptPath, frozenEnv())
|
|
643
|
+
);
|
|
644
|
+
if (darwin) {
|
|
645
|
+
try {
|
|
646
|
+
execFileSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, unit], { stdio: "ignore" });
|
|
647
|
+
} catch {
|
|
648
|
+
}
|
|
649
|
+
execFileSync("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, unit], { stdio: "ignore" });
|
|
650
|
+
} else {
|
|
651
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
652
|
+
execFileSync("systemctl", ["--user", "enable", "--now", "paigy-listen"], { stdio: "ignore" });
|
|
653
|
+
}
|
|
654
|
+
return unit;
|
|
655
|
+
}
|
|
656
|
+
function uninstallService() {
|
|
657
|
+
const { unit, darwin } = paths();
|
|
658
|
+
if (darwin) {
|
|
659
|
+
try {
|
|
660
|
+
execFileSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, unit], { stdio: "ignore" });
|
|
661
|
+
} catch {
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
try {
|
|
665
|
+
execFileSync("systemctl", ["--user", "disable", "--now", "paigy-listen"], { stdio: "ignore" });
|
|
666
|
+
} catch {
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
unlinkSync(unit);
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
return unit;
|
|
674
|
+
}
|
|
675
|
+
|
|
555
676
|
// src/listen.ts
|
|
556
677
|
var emit = (obj) => void process.stdout.write(JSON.stringify(obj) + "\n");
|
|
557
678
|
async function sweep(reason) {
|
|
@@ -560,7 +681,7 @@ async function sweep(reason) {
|
|
|
560
681
|
emit({ type: "sweep", reason, ...work });
|
|
561
682
|
const cmd = process.env.PAIGY_ON_WAKE;
|
|
562
683
|
if (cmd && (work.replies.length > 0 || work.requests.length > 0 || work.owedCallbacks.length > 0)) {
|
|
563
|
-
spawn(cmd, { shell: true, stdio: "inherit" });
|
|
684
|
+
spawn(cmd, { shell: true, stdio: "inherit", env: { ...process.env, PAIGY_WORK: JSON.stringify(work) } });
|
|
564
685
|
}
|
|
565
686
|
} catch (e) {
|
|
566
687
|
emit({ type: "error", reason, message: e.message });
|
|
@@ -604,10 +725,17 @@ function isEntry() {
|
|
|
604
725
|
}
|
|
605
726
|
}
|
|
606
727
|
if (isEntry()) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
})
|
|
728
|
+
if (process.argv.includes("--install")) {
|
|
729
|
+
const unit = installService(process.execPath, realpathSync(process.argv[1]));
|
|
730
|
+
emit({ type: "installed", unit });
|
|
731
|
+
} else if (process.argv.includes("--uninstall")) {
|
|
732
|
+
emit({ type: "uninstalled", unit: uninstallService() });
|
|
733
|
+
} else {
|
|
734
|
+
main().catch((e) => {
|
|
735
|
+
emit({ type: "fatal", message: e.message });
|
|
736
|
+
process.exit(1);
|
|
737
|
+
});
|
|
738
|
+
}
|
|
611
739
|
}
|
|
612
740
|
export {
|
|
613
741
|
sweep
|
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-ZIOTFQPK.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-4LQAS5ZB.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.17.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",
|
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
"url": "git+https://github.com/mauurda/paigy.git",
|
|
23
23
|
"directory": "apps/mcp"
|
|
24
24
|
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"prepublishOnly": "pnpm --filter @paigy/schema build && pnpm --filter @paigy/crypto build && pnpm --filter @paigy/sdk build && pnpm build"
|
|
31
|
+
},
|
|
25
32
|
"dependencies": {
|
|
26
33
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
27
34
|
"@supabase/supabase-js": "^2.47.10",
|
|
@@ -31,19 +38,13 @@
|
|
|
31
38
|
"zod-to-json-schema": "^3.24.1"
|
|
32
39
|
},
|
|
33
40
|
"devDependencies": {
|
|
41
|
+
"@paigy/crypto": "workspace:*",
|
|
42
|
+
"@paigy/schema": "workspace:*",
|
|
43
|
+
"@paigy/sdk": "workspace:*",
|
|
34
44
|
"@types/node": "^22.0.0",
|
|
35
45
|
"@types/qrcode-generator": "^1.0.6",
|
|
36
46
|
"tsup": "^8.3.5",
|
|
37
47
|
"typescript": "^5.7.2",
|
|
38
|
-
"vitest": "^2.1.8"
|
|
39
|
-
"@paigy/schema": "0.0.0",
|
|
40
|
-
"@paigy/crypto": "0.0.0",
|
|
41
|
-
"@paigy/sdk": "0.1.0"
|
|
42
|
-
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"build": "tsup",
|
|
45
|
-
"dev": "tsup --watch",
|
|
46
|
-
"typecheck": "tsc --noEmit",
|
|
47
|
-
"test": "vitest run"
|
|
48
|
+
"vitest": "^2.1.8"
|
|
48
49
|
}
|
|
49
50
|
}
|