@paigy/mcp 0.16.0 → 0.17.1
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-U3WODCOE.js → chunk-IMIXCPTN.js} +1 -1
- package/dist/chunk-MH6AHNUI.js +633 -0
- package/dist/{chunk-R3YGSGPX.js → chunk-RMTTO6BI.js} +108 -3
- package/dist/index.js +42 -4
- package/dist/listen.js +112 -544
- package/dist/onboard.js +2 -2
- package/dist/statusline.js +1 -1
- package/package.json +12 -11
package/dist/listen.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
WAKE_EVENT,
|
|
4
|
+
wakeChannel
|
|
5
|
+
} from "./chunk-MH6AHNUI.js";
|
|
2
6
|
import {
|
|
3
7
|
checkReplies,
|
|
4
8
|
registerDelivery
|
|
5
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-RMTTO6BI.js";
|
|
6
10
|
|
|
7
11
|
// src/listen.ts
|
|
8
12
|
import { createClient } from "@supabase/supabase-js";
|
|
@@ -10,547 +14,102 @@ import { spawn } from "child_process";
|
|
|
10
14
|
import { realpathSync } from "fs";
|
|
11
15
|
import { fileURLToPath } from "url";
|
|
12
16
|
|
|
13
|
-
//
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* (context/select/options/urgency), state what you need to learn and why it
|
|
83
|
-
* matters now — the broker derives the optimal shape and channel. Mutually
|
|
84
|
-
* exclusive with `context` (and never sent alongside `envelope`: E2EE pairings
|
|
85
|
-
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
86
|
-
ask: z.string().min(1).optional().describe(
|
|
87
|
-
'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"). 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.'
|
|
88
|
-
),
|
|
89
|
-
needs: z.array(z.string().min(1)).optional().describe(
|
|
90
|
-
"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."
|
|
91
|
-
),
|
|
92
|
-
urgencyHint: z.enum(["whenever", "soon", "now"]).optional().describe(
|
|
93
|
-
"With `ask` only: how urgently you need the answer \u2014 'whenever' (inbox), 'soon' (worth a heads-up), 'now' (you're blocked this minute). A hint, not a command: the user's settings still have the final word."
|
|
94
|
-
),
|
|
95
|
-
confirmStyle: z.enum(["yesno", "approve"]).default("yesno").describe(
|
|
96
|
-
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
97
|
-
),
|
|
98
|
-
blocking: z.boolean().default(false).describe(
|
|
99
|
-
"Set true when real downstream work is stuck behind this specific decision \u2014 you can't make meaningful progress until it's answered. This is the real signal for how urgently the user should be reached; it's what the premier use case (an agent that stays unblocked instead of going idle) depends on. Independent of `urgency`: a `banner`-level question can still be `blocking` (something IS stuck, just not time-critical enough to ring for immediately) \u2014 if it goes unanswered a while, Paigy escalates it to a real call using this flag rather than guessing from how many other things happen to be pending. Leave false for anything you could work around, defer, or where other useful work exists meanwhile."
|
|
100
|
-
)
|
|
101
|
-
}).superRefine((r, ctx) => {
|
|
102
|
-
const sealed = !!r.envelope;
|
|
103
|
-
if (sealed) {
|
|
104
|
-
if (!r.envelope?.context)
|
|
105
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["envelope", "context"], message: "sealed request must include envelope.context" });
|
|
106
|
-
for (const f of ["context", "options", "visuals"]) {
|
|
107
|
-
if (r[f] !== void 0)
|
|
108
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [f], message: `E2EE request must not carry plaintext ${f} \u2014 it's sealed in envelope.${f}` });
|
|
17
|
+
// src/service.ts
|
|
18
|
+
import { execFileSync } from "child_process";
|
|
19
|
+
import { mkdirSync, unlinkSync, writeFileSync } from "fs";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
import { join } from "path";
|
|
22
|
+
var LABEL = "ai.paigy.listen";
|
|
23
|
+
function frozenEnv() {
|
|
24
|
+
const keys = ["PAIGY_BACKEND_URL", "PAIGY_ON_WAKE", "PAIGY_SUPABASE_URL", "PAIGY_SUPABASE_ANON_KEY"];
|
|
25
|
+
return Object.fromEntries(keys.flatMap((k) => process.env[k] ? [[k, process.env[k]]] : []));
|
|
26
|
+
}
|
|
27
|
+
function renderLaunchd(nodePath, scriptPath, env) {
|
|
28
|
+
const envXml = Object.entries(env).map(([k, v]) => ` <key>${k}</key><string>${v}</string>`).join("\n");
|
|
29
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
30
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
31
|
+
<plist version="1.0">
|
|
32
|
+
<dict>
|
|
33
|
+
<key>Label</key><string>${LABEL}</string>
|
|
34
|
+
<key>ProgramArguments</key>
|
|
35
|
+
<array>
|
|
36
|
+
<string>${nodePath}</string>
|
|
37
|
+
<string>${scriptPath}</string>
|
|
38
|
+
</array>
|
|
39
|
+
<key>RunAtLoad</key><true/>
|
|
40
|
+
<key>KeepAlive</key><true/>
|
|
41
|
+
<key>ThrottleInterval</key><integer>30</integer>
|
|
42
|
+
<key>StandardOutPath</key><string>${join(homedir(), ".paigy", "listen.log")}</string>
|
|
43
|
+
<key>StandardErrorPath</key><string>${join(homedir(), ".paigy", "listen.log")}</string>
|
|
44
|
+
${envXml ? ` <key>EnvironmentVariables</key>
|
|
45
|
+
<dict>
|
|
46
|
+
${envXml}
|
|
47
|
+
</dict>
|
|
48
|
+
` : ""}</dict>
|
|
49
|
+
</plist>
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
function renderSystemd(nodePath, scriptPath, env) {
|
|
53
|
+
const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${v}`).join("\n");
|
|
54
|
+
return `[Unit]
|
|
55
|
+
Description=Paigy listen \u2014 self-hosted push daemon
|
|
56
|
+
|
|
57
|
+
[Service]
|
|
58
|
+
ExecStart=${nodePath} ${scriptPath}
|
|
59
|
+
Restart=always
|
|
60
|
+
RestartSec=30
|
|
61
|
+
${envLines}
|
|
62
|
+
|
|
63
|
+
[Install]
|
|
64
|
+
WantedBy=default.target
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
function paths() {
|
|
68
|
+
const darwin = process.platform === "darwin";
|
|
69
|
+
return {
|
|
70
|
+
darwin,
|
|
71
|
+
unit: darwin ? join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`) : join(homedir(), ".config", "systemd", "user", "paigy-listen.service")
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function installService(nodePath, scriptPath) {
|
|
75
|
+
const { unit, darwin } = paths();
|
|
76
|
+
mkdirSync(join(unit, ".."), { recursive: true });
|
|
77
|
+
mkdirSync(join(homedir(), ".paigy"), { recursive: true });
|
|
78
|
+
writeFileSync(
|
|
79
|
+
unit,
|
|
80
|
+
darwin ? renderLaunchd(nodePath, scriptPath, frozenEnv()) : renderSystemd(nodePath, scriptPath, frozenEnv())
|
|
81
|
+
);
|
|
82
|
+
if (darwin) {
|
|
83
|
+
try {
|
|
84
|
+
execFileSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, unit], { stdio: "ignore" });
|
|
85
|
+
} catch {
|
|
109
86
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (r.needs !== void 0)
|
|
115
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["needs"], message: "E2EE request must not carry plaintext needs" });
|
|
116
|
-
return;
|
|
87
|
+
execFileSync("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, unit], { stdio: "ignore" });
|
|
88
|
+
} else {
|
|
89
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
90
|
+
execFileSync("systemctl", ["--user", "enable", "--now", "paigy-listen"], { stdio: "ignore" });
|
|
117
91
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
92
|
+
return unit;
|
|
93
|
+
}
|
|
94
|
+
function uninstallService() {
|
|
95
|
+
const { unit, darwin } = paths();
|
|
96
|
+
if (darwin) {
|
|
97
|
+
try {
|
|
98
|
+
execFileSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, unit], { stdio: "ignore" });
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
try {
|
|
103
|
+
execFileSync("systemctl", ["--user", "disable", "--now", "paigy-listen"], { stdio: "ignore" });
|
|
104
|
+
} catch {
|
|
122
105
|
}
|
|
123
|
-
return;
|
|
124
106
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
132
|
-
if (needsOptions && !r.options?.length)
|
|
133
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
134
|
-
if (!needsOptions && r.options?.length)
|
|
135
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
136
|
-
});
|
|
137
|
-
var NotifyStatusSchema = z.enum(["pending", "answered", "ignored"]);
|
|
138
|
-
var AgentStateSchema = z.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
139
|
-
var SetTaskStateSchema = z.object({
|
|
140
|
-
state: z.enum(["in_progress", "completed", "needs_input"])
|
|
141
|
-
});
|
|
142
|
-
var TurnSchema = z.object({
|
|
143
|
-
prompt: z.string(),
|
|
144
|
-
reply: z.string()
|
|
145
|
-
});
|
|
146
|
-
var UserAnswerSchema = z.discriminatedUnion("kind", [
|
|
147
|
-
z.object({ kind: z.literal("option"), optionId: z.string() }),
|
|
148
|
-
z.object({ kind: z.literal("text"), text: z.string() }),
|
|
149
|
-
z.object({ kind: z.literal("ignored") }),
|
|
150
|
-
z.object({ kind: z.literal("multi"), optionIds: z.array(z.string()) }),
|
|
151
|
-
z.object({ kind: z.literal("ranked"), optionIds: z.array(z.string()) }),
|
|
152
|
-
z.object({ kind: z.literal("clarify"), chunks: z.array(z.string()).min(1) }),
|
|
153
|
-
z.object({ kind: z.literal("confirm"), approved: z.boolean() }),
|
|
154
|
-
z.object({ kind: z.literal("turns"), turns: z.array(TurnSchema).min(1) })
|
|
155
|
-
]);
|
|
156
|
-
var IntentSchema = z.object({
|
|
157
|
-
kind: z.enum(["defer", "delegate", "channel"]),
|
|
158
|
-
detail: z.string(),
|
|
159
|
-
/** Landed defer (#397): the MCP parses common spoken forms ("in 20 minutes",
|
|
160
|
-
* "after lunch") against the agent machine's clock — the user's — and attaches
|
|
161
|
-
* the seconds, ready to pass straight to schedule_callback. Absent when the
|
|
162
|
-
* detail didn't parse (the agent interprets it) or the kind isn't defer. */
|
|
163
|
-
dueInSeconds: z.number().int().positive().optional()
|
|
164
|
-
});
|
|
165
|
-
var AwaitItemSchema = z.discriminatedUnion("type", [
|
|
166
|
-
z.object({
|
|
167
|
-
type: z.literal("reply"),
|
|
168
|
-
threadId: z.string(),
|
|
169
|
-
notificationId: z.string(),
|
|
170
|
-
answer: UserAnswerSchema,
|
|
171
|
-
/** E2EE: present when the answer is sealed. The server relays the opaque answer
|
|
172
|
-
* envelope + the plaintext `ignored` status hint; the receiving agent OPENS it
|
|
173
|
-
* with its device key and re-derives the real UserAnswer, treating a mismatch
|
|
174
|
-
* against `ignored` as tampering. Absent = today's plaintext answer (in `answer`).
|
|
175
|
-
* On a sealed reply the plaintext `answer` is a placeholder (kind reflects only
|
|
176
|
-
* the `ignored` bit) — never the real content, which stays sealed. */
|
|
177
|
-
sealed: z.lazy(() => SealedAnswerSchema).optional(),
|
|
178
|
-
/** Broker extras (#381), present when the answer was mapped from a call: next
|
|
179
|
-
* steps the user attached ("call me after lunch" → defer — act on it via
|
|
180
|
-
* schedule_callback) and the raw words the shaped answer was mapped from. */
|
|
181
|
-
intents: z.array(IntentSchema).optional(),
|
|
182
|
-
transcript: z.string().optional(),
|
|
183
|
-
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
184
|
-
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
185
|
-
covered: z.array(z.string()).optional()
|
|
186
|
-
}),
|
|
187
|
-
z.object({
|
|
188
|
-
type: z.literal("remind"),
|
|
189
|
-
threadId: z.string(),
|
|
190
|
-
notificationId: z.string(),
|
|
191
|
-
remindAt: z.string().datetime({ offset: true }),
|
|
192
|
-
/** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
|
|
193
|
-
remindInSeconds: z.number()
|
|
194
|
-
}),
|
|
195
|
-
z.object({ type: z.literal("idle") })
|
|
196
|
-
]);
|
|
197
|
-
var CallbackTriggerSchema = z.enum(["on_done", "on_blocked", "scheduled"]);
|
|
198
|
-
var ScheduleCallbackSchema = z.object({
|
|
199
|
-
threadId: z.string().describe("The thread to call back on (from a prior notify_user / reply / request)."),
|
|
200
|
-
trigger: CallbackTriggerSchema,
|
|
201
|
-
dueInSeconds: z.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
|
|
202
|
-
note: z.string().optional().describe("What to tell the user when you follow up.")
|
|
203
|
-
});
|
|
204
|
-
var PendingRepliesSchema = z.object({
|
|
205
|
-
replies: z.array(
|
|
206
|
-
z.object({
|
|
207
|
-
threadId: z.string(),
|
|
208
|
-
notificationId: z.string(),
|
|
209
|
-
answer: UserAnswerSchema,
|
|
210
|
-
/** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
|
|
211
|
-
* pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
|
|
212
|
-
* plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
|
|
213
|
-
sealed: z.lazy(() => SealedAnswerSchema).optional(),
|
|
214
|
-
/** Broker extras (#381): next steps the user attached to a call-mapped answer
|
|
215
|
-
* ("call me after lunch" → defer — act on it via schedule_callback) and the
|
|
216
|
-
* raw words the shaped answer was mapped from. */
|
|
217
|
-
intents: z.array(IntentSchema).optional(),
|
|
218
|
-
transcript: z.string().optional(),
|
|
219
|
-
/** Coverage report (#396): which declared `points` this answer addressed. */
|
|
220
|
-
covered: z.array(z.string()).optional()
|
|
221
|
-
})
|
|
222
|
-
),
|
|
223
|
-
pending: z.array(
|
|
224
|
-
z.object({ threadId: z.string(), notificationId: z.string(), createdAt: z.string() })
|
|
225
|
-
),
|
|
226
|
-
/** User-initiated requests addressed to this agent; act on them and reply via
|
|
227
|
-
* notify_user on the same threadId. Keeps reappearing until you call
|
|
228
|
-
* set_task_state on its notificationId. */
|
|
229
|
-
requests: z.array(
|
|
230
|
-
z.object({ threadId: z.string(), notificationId: z.string(), text: z.string(), createdAt: z.string() })
|
|
231
|
-
),
|
|
232
|
-
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
233
|
-
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
234
|
-
* fulfill one by calling notify_user on its threadId. */
|
|
235
|
-
owedCallbacks: z.array(
|
|
236
|
-
z.object({ threadId: z.string(), trigger: CallbackTriggerSchema, note: z.string() })
|
|
237
|
-
),
|
|
238
|
-
/** Work (either direction) you reported in_progress a while ago and never reported
|
|
239
|
-
* completed — likely left half-done by this session or a prior one that crashed or
|
|
240
|
-
* went idle. Report a real state (set_task_state) or continue the work. */
|
|
241
|
-
stalled: z.array(
|
|
242
|
-
z.object({ threadId: z.string(), notificationId: z.string(), title: z.string().nullable(), startedAt: z.string() })
|
|
243
|
-
)
|
|
244
|
-
});
|
|
245
|
-
var NotifyResponseSchema = z.object({
|
|
246
|
-
notificationId: z.string(),
|
|
247
|
-
status: NotifyStatusSchema,
|
|
248
|
-
createdAt: z.string().datetime(),
|
|
249
|
-
answer: UserAnswerSchema.optional(),
|
|
250
|
-
answeredAt: z.string().datetime().optional()
|
|
251
|
-
});
|
|
252
|
-
var UserResponseSchema = z.object({
|
|
253
|
-
requestId: z.string(),
|
|
254
|
-
answer: UserAnswerSchema,
|
|
255
|
-
answeredAt: z.string().datetime(),
|
|
256
|
-
intents: z.array(IntentSchema).optional(),
|
|
257
|
-
transcript: z.string().optional(),
|
|
258
|
-
/** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the item was
|
|
259
|
-
* E2EE-sealed. Present → the server persists it opaquely and branches status on `ignored`;
|
|
260
|
-
* the plaintext `answer` is a placeholder (`{ kind: "ignored" }`) the server ignores for a
|
|
261
|
-
* sealed row. Absent = today's plaintext answer, unchanged. */
|
|
262
|
-
sealed: z.lazy(() => SealedAnswerSchema).optional(),
|
|
263
|
-
/** Coverage report (#396): which of the ask's declared `points` were addressed. */
|
|
264
|
-
covered: z.array(z.string()).optional()
|
|
265
|
-
});
|
|
266
|
-
var InboxItemSchema = z.object({
|
|
267
|
-
id: z.string(),
|
|
268
|
-
status: NotifyStatusSchema,
|
|
269
|
-
context: ContextSchema,
|
|
270
|
-
options: z.array(OptionSchema).optional(),
|
|
271
|
-
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
272
|
-
points: z.array(z.string()).optional(),
|
|
273
|
-
/** Natural spoken questions for the points (#417 follow-up), phrased at ring time
|
|
274
|
-
* and index-aligned — the call bot prefers these over the raw shorthand. */
|
|
275
|
-
pointsSpeak: z.array(z.string()).optional(),
|
|
276
|
-
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
277
|
-
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
278
|
-
intents: z.array(IntentSchema).optional(),
|
|
279
|
-
visuals: z.array(VisualSchema).optional(),
|
|
280
|
-
agent: z.string(),
|
|
281
|
-
nickname: z.string(),
|
|
282
|
-
repo: z.string().optional(),
|
|
283
|
-
branch: z.string().optional(),
|
|
284
|
-
createdAt: z.string().datetime(),
|
|
285
|
-
snoozedUntil: z.string().datetime().optional(),
|
|
286
|
-
agentState: AgentStateSchema.default("idle"),
|
|
287
|
-
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
288
|
-
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
289
|
-
* agent (held in the inbox until the agent replies on the thread). */
|
|
290
|
-
turn: z.enum(["you", "agent"]).default("you"),
|
|
291
|
-
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
292
|
-
* the agent (provider-agnostic; set server-side). Absent = no hard error, though the
|
|
293
|
-
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
294
|
-
error: z.string().optional(),
|
|
295
|
-
parentId: z.string().optional(),
|
|
296
|
-
select: z.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
297
|
-
confirmStyle: z.enum(["yesno", "approve"]).default("yesno").describe(
|
|
298
|
-
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
299
|
-
),
|
|
300
|
-
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
301
|
-
* urgency (see the main README's "premier use case" + notify/states.md). Drives the
|
|
302
|
-
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
303
|
-
blocking: z.boolean().default(false),
|
|
304
|
-
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
305
|
-
answer: UserAnswerSchema.optional(),
|
|
306
|
-
/** E2EE (text lane): the sealed content this item carries when the pairing is E2EE, in
|
|
307
|
-
* place of the plaintext `context`/`options`/`visuals` (which the operator would
|
|
308
|
-
* otherwise read). A per-field map of opaque Envelopes — the SAME shape as
|
|
309
|
-
* NotifyRequest.envelope. The native app decrypts it locally via openNotification;
|
|
310
|
-
* web / an un-enrolled device can't and renders a locked placeholder. Absent = today's
|
|
311
|
-
* plaintext item (context carries the cleartext), so plaintext items are unchanged. */
|
|
312
|
-
envelope: z.object({
|
|
313
|
-
context: z.lazy(() => EnvelopeSchema).optional(),
|
|
314
|
-
options: z.lazy(() => EnvelopeSchema).optional(),
|
|
315
|
-
visuals: z.lazy(() => EnvelopeSchema).optional()
|
|
316
|
-
}).optional(),
|
|
317
|
-
/** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
|
|
318
|
-
* sender the phone replies to). Sourced server-side from this item's pairing credential.
|
|
319
|
-
* Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
|
|
320
|
-
agentX25519: z.string().optional()
|
|
321
|
-
});
|
|
322
|
-
var SnoozeRequestSchema = z.object({
|
|
323
|
-
requestId: z.string(),
|
|
324
|
-
until: z.string().datetime()
|
|
325
|
-
});
|
|
326
|
-
var PushTokenSchema = z.object({
|
|
327
|
-
voipToken: z.string().min(1).optional(),
|
|
328
|
-
alertToken: z.string().min(1).optional(),
|
|
329
|
-
fcmToken: z.string().min(1).optional(),
|
|
330
|
-
platform: z.enum(["ios", "android"])
|
|
331
|
-
});
|
|
332
|
-
var MissedCallSchema = z.enum([
|
|
333
|
-
"retry_10m",
|
|
334
|
-
"retry_30m",
|
|
335
|
-
"retry_60m",
|
|
336
|
-
"backoff_gentle",
|
|
337
|
-
"backoff_standard",
|
|
338
|
-
"backoff_aggressive",
|
|
339
|
-
"inbox",
|
|
340
|
-
"dismiss"
|
|
341
|
-
]);
|
|
342
|
-
var BrokerTuningSchema = z.object({
|
|
343
|
-
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
344
|
-
ackVerbosity: z.enum(["normal", "none"]).optional(),
|
|
345
|
-
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
346
|
-
* uninterpretable, 'high' = whenever not fully certain. */
|
|
347
|
-
clarifyEagerness: z.enum(["low", "normal", "high"]).optional(),
|
|
348
|
-
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
349
|
-
phrasebook: z.array(z.object({ say: z.string().min(1).max(60), mean: z.string().min(1).max(120) })).max(24).optional()
|
|
350
|
-
});
|
|
351
|
-
var UserSettingsSchema = z.object({
|
|
352
|
-
permissions: z.object({
|
|
353
|
-
call: z.boolean(),
|
|
354
|
-
banner: z.boolean(),
|
|
355
|
-
push: z.boolean()
|
|
356
|
-
}),
|
|
357
|
-
sessionMode: z.enum(["default", "all_calls", "silent"]),
|
|
358
|
-
silentPush: z.boolean(),
|
|
359
|
-
autoCallback: z.boolean(),
|
|
360
|
-
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
361
|
-
improveConsent: z.boolean(),
|
|
362
|
-
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
363
|
-
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
364
|
-
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
365
|
-
* synthesizes and transcribes locally — no audio or spoken text leaves it.
|
|
366
|
-
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
367
|
-
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
368
|
-
* write, 'hosted' on read (see store.ts). */
|
|
369
|
-
voiceMode: z.enum(["hosted", "on_device"]).optional(),
|
|
370
|
-
/** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
|
|
371
|
-
* content is sealed end-to-end between the local agent and the phone. Like
|
|
372
|
-
* voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
|
|
373
|
-
* settings object without it can't silently flip the account's E2EE state.
|
|
374
|
-
* Absent = leave unchanged on write, 'off' on read (see store.ts). The demo
|
|
375
|
-
* account is plaintext by construction and refuses any non-'off' value. */
|
|
376
|
-
e2eeMode: z.enum(["off", "on"]).optional(),
|
|
377
|
-
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
378
|
-
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
379
|
-
broker: BrokerTuningSchema.optional()
|
|
380
|
-
});
|
|
381
|
-
var HistoryItemSchema = z.object({
|
|
382
|
-
id: z.string(),
|
|
383
|
-
threadId: z.string(),
|
|
384
|
-
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
385
|
-
initiator: z.enum(["user", "agent"]),
|
|
386
|
-
title: z.string(),
|
|
387
|
-
/** The agent on the other end (nickname). */
|
|
388
|
-
agent: z.string(),
|
|
389
|
-
createdAt: z.string(),
|
|
390
|
-
/** When the agent fetched your request (user→agent only). */
|
|
391
|
-
agentAckedAt: z.string().nullable(),
|
|
392
|
-
/** When you answered the agent's notification (agent→user only). */
|
|
393
|
-
humanAckedAt: z.string().nullable()
|
|
394
|
-
});
|
|
395
|
-
var ConnectionSummarySchema = z.object({
|
|
396
|
-
/** The connection = the agent's token id (used to address a request). */
|
|
397
|
-
id: z.string(),
|
|
398
|
-
agent: z.string(),
|
|
399
|
-
device: z.string().nullable(),
|
|
400
|
-
nickname: z.string(),
|
|
401
|
-
createdAt: z.string().datetime(),
|
|
402
|
-
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
403
|
-
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
404
|
-
lastContactAt: z.string().datetime().nullable(),
|
|
405
|
-
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
406
|
-
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
407
|
-
managed: z.boolean()
|
|
408
|
-
});
|
|
409
|
-
var CreateRequestSchema = z.object({
|
|
410
|
-
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
411
|
-
tokenId: z.string(),
|
|
412
|
-
/** The user's message to the agent. */
|
|
413
|
-
text: z.string().min(1)
|
|
414
|
-
});
|
|
415
|
-
var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
|
|
416
|
-
var WAKE_EVENT = "wake";
|
|
417
|
-
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
418
|
-
var RegisterDeliverySchema = z.object({ mode: DeliveryModeSchema });
|
|
419
|
-
var OAuthStartSchema = z.object({
|
|
420
|
-
provider: z.enum(["cma"]),
|
|
421
|
-
returnTo: z.string().min(1)
|
|
422
|
-
});
|
|
423
|
-
var DeliveryConfigSchema = z.object({
|
|
424
|
-
tokenId: z.string(),
|
|
425
|
-
mode: DeliveryModeSchema,
|
|
426
|
-
/** null when the server has no SUPABASE_ANON_KEY set — the listener then falls
|
|
427
|
-
* back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
|
|
428
|
-
realtime: z.object({ url: z.string(), anonKey: z.string() }).nullable()
|
|
429
|
-
});
|
|
430
|
-
var StatusSchema = z.object({
|
|
431
|
-
agent: z.string(),
|
|
432
|
-
nickname: z.string(),
|
|
433
|
-
sessionMode: z.enum(["default", "all_calls", "silent"]),
|
|
434
|
-
/** A phone is registered for push/ring (any push token on the account). */
|
|
435
|
-
phone: z.boolean()
|
|
436
|
-
});
|
|
437
|
-
var EnvelopeRecipientSchema = z.object({
|
|
438
|
-
keyId: z.string(),
|
|
439
|
-
epk: z.string(),
|
|
440
|
-
wnonce: z.string(),
|
|
441
|
-
wrap: z.string()
|
|
442
|
-
});
|
|
443
|
-
var EnvelopeHeaderSchema = z.object({
|
|
444
|
-
field: z.enum(["context", "options", "visuals", "answer"]),
|
|
445
|
-
kind: z.string(),
|
|
446
|
-
senderRole: z.enum(["agent", "user"]),
|
|
447
|
-
recipientKeyIds: z.array(z.string()),
|
|
448
|
-
seq: z.number().int().nonnegative()
|
|
449
|
-
});
|
|
450
|
-
var EnvelopeSchema = z.object({
|
|
451
|
-
v: z.literal(1),
|
|
452
|
-
alg: z.literal("x25519-xsalsa20poly1305"),
|
|
453
|
-
msgId: z.string(),
|
|
454
|
-
hdr: EnvelopeHeaderSchema,
|
|
455
|
-
recipients: z.array(EnvelopeRecipientSchema).min(1),
|
|
456
|
-
nonce: z.string(),
|
|
457
|
-
ct: z.string()
|
|
458
|
-
});
|
|
459
|
-
var SealedAnswerSchema = z.object({
|
|
460
|
-
ignored: z.boolean(),
|
|
461
|
-
envelope: EnvelopeSchema
|
|
462
|
-
});
|
|
463
|
-
var DeviceCredentialSchema = z.object({
|
|
464
|
-
deviceId: z.string(),
|
|
465
|
-
kind: z.enum(["phone", "web", "agent"]),
|
|
466
|
-
x25519Pub: z.string(),
|
|
467
|
-
ed25519Pub: z.string(),
|
|
468
|
-
sig: z.string()
|
|
469
|
-
});
|
|
470
|
-
var DeviceRosterSchema = z.object({
|
|
471
|
-
uikPub: z.string(),
|
|
472
|
-
devices: z.array(DeviceCredentialSchema)
|
|
473
|
-
});
|
|
474
|
-
var WakeNudgeSchema = z.object({
|
|
475
|
-
kind: z.enum(["reply", "request", "callback"]),
|
|
476
|
-
notificationId: z.string().optional(),
|
|
477
|
-
threadId: z.string()
|
|
478
|
-
});
|
|
479
|
-
var DeviceAgentTokenSchema = z.object({
|
|
480
|
-
token: z.string(),
|
|
481
|
-
deviceId: z.string(),
|
|
482
|
-
agentName: z.string(),
|
|
483
|
-
/** User-chosen session label; defaults to `<agent> <device>`. */
|
|
484
|
-
nickname: z.string(),
|
|
485
|
-
createdAt: z.string().datetime()
|
|
486
|
-
});
|
|
487
|
-
var PairingStatusSchema = z.enum(["pending", "approved", "denied", "expired"]);
|
|
488
|
-
var PairingRevealSchema = z.object({
|
|
489
|
-
x25519: z.string(),
|
|
490
|
-
ed25519: z.string(),
|
|
491
|
-
nonce: z.string()
|
|
492
|
-
});
|
|
493
|
-
var DeviceCodeRequestSchema = z.object({
|
|
494
|
-
agent: z.union([z.string(), z.object({ name: z.string().optional() })]).optional(),
|
|
495
|
-
device: z.string().optional(),
|
|
496
|
-
proto: z.string().optional(),
|
|
497
|
-
// compiled-in tag, e.g. "paigy-pair-v2|sas=24"
|
|
498
|
-
commitment: z.string().optional()
|
|
499
|
-
// agent's Ca
|
|
500
|
-
});
|
|
501
|
-
var DeviceCodeSchema = z.object({
|
|
502
|
-
device_code: z.string(),
|
|
503
|
-
user_code: z.string(),
|
|
504
|
-
verification_uri: z.string().url(),
|
|
505
|
-
verification_uri_complete: z.string().url(),
|
|
506
|
-
interval: z.number(),
|
|
507
|
-
expires_in: z.number()
|
|
508
|
-
});
|
|
509
|
-
var DeviceInfoSchema = z.object({
|
|
510
|
-
code: z.string(),
|
|
511
|
-
agent: z.string(),
|
|
512
|
-
device: z.string().nullable(),
|
|
513
|
-
status: PairingStatusSchema,
|
|
514
|
-
proto: z.string().nullable().optional(),
|
|
515
|
-
agent_commitment: z.string().nullable().optional(),
|
|
516
|
-
// Ca
|
|
517
|
-
agent_reveal: PairingRevealSchema.nullable().optional(),
|
|
518
|
-
// present once the agent reveals
|
|
519
|
-
agent_credential: DeviceCredentialSchema.nullable().optional(),
|
|
520
|
-
// the phone's UIK-signed cred over the agent's keys — the agent fetches it here to finalizeE2ee
|
|
521
|
-
// The phone's reveal + identity key, exposed here so a GATED agent (e2ee_mode='on', token
|
|
522
|
-
// withheld until e2ee=true) can compute the SAS WITHOUT a token. Public keys — same safety
|
|
523
|
-
// class as agent_reveal above. Present only once the phone reveals; null otherwise.
|
|
524
|
-
phone_reveal: PairingRevealSchema.nullable().optional(),
|
|
525
|
-
uik_pub: z.string().nullable().optional()
|
|
526
|
-
});
|
|
527
|
-
var DeviceTokenRequestSchema = z.object({
|
|
528
|
-
device_code: z.string(),
|
|
529
|
-
reveal: PairingRevealSchema.optional()
|
|
530
|
-
// agent's reveal {x25519, ed25519, nonce}
|
|
531
|
-
});
|
|
532
|
-
var DeviceTokenSchema = z.object({
|
|
533
|
-
access_token: z.string(),
|
|
534
|
-
nickname: z.string(),
|
|
535
|
-
agent: z.string(),
|
|
536
|
-
device: z.string().nullable(),
|
|
537
|
-
phone_reveal: PairingRevealSchema.nullable().optional(),
|
|
538
|
-
// present once the phone reveals
|
|
539
|
-
uik_pub: z.string().nullable().optional()
|
|
540
|
-
});
|
|
541
|
-
var DeviceCommitRequestSchema = z.object({
|
|
542
|
-
code: z.string(),
|
|
543
|
-
commitment: z.string()
|
|
544
|
-
// Cb
|
|
545
|
-
});
|
|
546
|
-
var DevicePeerCommitSchema = z.object({
|
|
547
|
-
commitment: z.string().nullable()
|
|
548
|
-
});
|
|
549
|
-
var SupportRequestSchema = z.object({
|
|
550
|
-
email: z.string().email().max(320),
|
|
551
|
-
message: z.string().trim().min(1).max(5e3),
|
|
552
|
-
name: z.string().trim().max(120).optional()
|
|
553
|
-
});
|
|
107
|
+
try {
|
|
108
|
+
unlinkSync(unit);
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
return unit;
|
|
112
|
+
}
|
|
554
113
|
|
|
555
114
|
// src/listen.ts
|
|
556
115
|
var emit = (obj) => void process.stdout.write(JSON.stringify(obj) + "\n");
|
|
@@ -559,8 +118,10 @@ async function sweep(reason) {
|
|
|
559
118
|
const work = await checkReplies();
|
|
560
119
|
emit({ type: "sweep", reason, ...work });
|
|
561
120
|
const cmd = process.env.PAIGY_ON_WAKE;
|
|
562
|
-
|
|
563
|
-
|
|
121
|
+
const inbound = work.replies.length > 0 || work.requests.length > 0;
|
|
122
|
+
const reengage = work.owedCallbacks.length > 0 && (reason === "boot" || reason.includes("callback"));
|
|
123
|
+
if (cmd && (inbound || reengage)) {
|
|
124
|
+
spawn(cmd, { shell: true, stdio: "inherit", env: { ...process.env, PAIGY_WORK: JSON.stringify(work) } });
|
|
564
125
|
}
|
|
565
126
|
} catch (e) {
|
|
566
127
|
emit({ type: "error", reason, message: e.message });
|
|
@@ -604,10 +165,17 @@ function isEntry() {
|
|
|
604
165
|
}
|
|
605
166
|
}
|
|
606
167
|
if (isEntry()) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
})
|
|
168
|
+
if (process.argv.includes("--install")) {
|
|
169
|
+
const unit = installService(process.execPath, realpathSync(process.argv[1]));
|
|
170
|
+
emit({ type: "installed", unit });
|
|
171
|
+
} else if (process.argv.includes("--uninstall")) {
|
|
172
|
+
emit({ type: "uninstalled", unit: uninstallService() });
|
|
173
|
+
} else {
|
|
174
|
+
main().catch((e) => {
|
|
175
|
+
emit({ type: "fatal", message: e.message });
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
611
179
|
}
|
|
612
180
|
export {
|
|
613
181
|
sweep
|