@paigy/mcp 0.14.2 → 0.16.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @paigy/mcp
2
2
 
3
- A voice inbox for your AI agents. This MCP server lets an agent **notify a user** and **await their reply** — so a long-running agent can ask a question, hand off, and resume on the answer.
3
+ A voice inbox for your AI agents. This MCP server lets an agent **notify a user** and **await their reply** — so a long-running agent can ask a question, hand off, and resume on the answer. It's a thin MCP-tool wrapper over `@paigy/sdk` (`packages/sdk`) — use the SDK directly from any Node process that isn't an MCP client.
4
4
 
5
5
  ## Install
6
6
 
@@ -103,6 +103,14 @@ Then pair: `npx -p @paigy/mcp@latest paigy-mcp-onboard`.
103
103
  ## Configuration
104
104
 
105
105
  - `PAIGY_BACKEND_URL` — the Paigy API base (defaults to the hosted backend).
106
+ - `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` — honored for every REST call, with
107
+ curl's semantics. Node's `fetch` ignores these (unlike curl), so the server
108
+ wires them up itself; in environments where egress only flows through a proxy
109
+ (Claude Code on the web, corporate CI), calls route through it automatically —
110
+ no extra setup. Two edges: with `HTTP_PROXY` set, an `http://localhost` backend
111
+ proxies too unless `NO_PROXY=localhost` (hostname entries work; CIDR ranges are
112
+ ignored), and `paigy-listen`'s realtime wake channel is a websocket that doesn't
113
+ proxy — its REST sweeps do.
106
114
 
107
115
  ## Statusline (Claude Code)
108
116
 
@@ -0,0 +1,36 @@
1
+ // src/surface.ts
2
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ var SURFACE_PATH = join(homedir(), ".paigy", "surface.json");
6
+ function writeSurface(label, code, ttlSeconds) {
7
+ try {
8
+ mkdirSync(join(homedir(), ".paigy"), { recursive: true });
9
+ writeFileSync(
10
+ SURFACE_PATH,
11
+ JSON.stringify({ label, code, expiresAt: Date.now() + ttlSeconds * 1e3 }),
12
+ { mode: 384 }
13
+ );
14
+ } catch {
15
+ }
16
+ }
17
+ function clearSurface() {
18
+ try {
19
+ rmSync(SURFACE_PATH, { force: true });
20
+ } catch {
21
+ }
22
+ }
23
+ function readSurface(now = Date.now) {
24
+ try {
25
+ const { label, code, expiresAt } = JSON.parse(readFileSync(SURFACE_PATH, "utf8"));
26
+ return label && code && typeof expiresAt === "number" && expiresAt > now() ? { label, code } : null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ export {
33
+ writeSurface,
34
+ clearSurface,
35
+ readSurface
36
+ };
@@ -1,14 +1,20 @@
1
- import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
1
+ // ../../packages/sdk/dist/index.js
2
+ import { createRequire as __sdkCreateRequire } from "module";
3
+ import { z } from "zod";
4
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+ var require2 = __sdkCreateRequire(import.meta.url);
2
8
  var __create = Object.create;
3
9
  var __defProp = Object.defineProperty;
4
10
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
11
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
12
  var __getProtoOf = Object.getPrototypeOf;
7
13
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
14
+ var __require = /* @__PURE__ */ ((x) => typeof require2 !== "undefined" ? require2 : typeof Proxy !== "undefined" ? new Proxy(x, {
15
+ get: (a, b) => (typeof require2 !== "undefined" ? require2 : a)[b]
10
16
  }) : x)(function(x) {
11
- if (typeof require !== "undefined") return require.apply(this, arguments);
17
+ if (typeof require2 !== "undefined") return require2.apply(this, arguments);
12
18
  throw Error('Dynamic require of "' + x + '" is not supported');
13
19
  });
14
20
  var __commonJS = (cb, mod) => function __require2() {
@@ -30,8 +36,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
36
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
37
  mod
32
38
  ));
33
-
34
- // ../../node_modules/.pnpm/tweetnacl@1.0.3/node_modules/tweetnacl/nacl-fast.js
35
39
  var require_nacl_fast = __commonJS({
36
40
  "../../node_modules/.pnpm/tweetnacl@1.0.3/node_modules/tweetnacl/nacl-fast.js"(exports, module) {
37
41
  "use strict";
@@ -2255,9 +2259,21 @@ var require_nacl_fast = __commonJS({
2255
2259
  })(typeof module !== "undefined" && module.exports ? module.exports : self.nacl = self.nacl || {});
2256
2260
  }
2257
2261
  });
2258
-
2259
- // ../../packages/schema/dist/index.js
2260
- import { z } from "zod";
2262
+ var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
2263
+ var NETWORK_MSG = `Can't reach ${BACKEND_URL} \u2014 the connection was blocked or dropped before an HTTP response. If this agent runs in a sandboxed environment with a network allowlist (e.g. Claude Code on the web, CI), ask the user to add the Paigy domain (paigy.ai) to the environment's allowed domains, then retry.`;
2264
+ var PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
2265
+ var agent;
2266
+ async function proxy() {
2267
+ if (!PROXY_ENV.some((k) => process.env[k])) return void 0;
2268
+ return agent ??= new (await import("undici")).EnvHttpProxyAgent();
2269
+ }
2270
+ async function reach(url, init) {
2271
+ try {
2272
+ return await fetch(url, { ...init, dispatcher: await proxy() });
2273
+ } catch (e) {
2274
+ throw new Error(`${NETWORK_MSG} (${e?.message ?? String(e)})`);
2275
+ }
2276
+ }
2261
2277
  var ContextSchema = z.object({
2262
2278
  title: z.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
2263
2279
  description: z.array(z.string().min(1)).min(1).describe("Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand.")
@@ -2531,6 +2547,9 @@ var InboxItemSchema = z.object({
2531
2547
  options: z.array(OptionSchema).optional(),
2532
2548
  /** The ask's declared coverage points (#396), when the agent sent them. */
2533
2549
  points: z.array(z.string()).optional(),
2550
+ /** Natural spoken questions for the points (#417 follow-up), phrased at ring time
2551
+ * and index-aligned — the call bot prefers these over the raw shorthand. */
2552
+ pointsSpeak: z.array(z.string()).optional(),
2534
2553
  /** On a replied detail (#397): the next steps the user attached to the answer
2535
2554
  * ("call back after lunch") — shown so they can see the commitment was captured. */
2536
2555
  intents: z.array(IntentSchema).optional(),
@@ -2671,8 +2690,6 @@ var CreateRequestSchema = z.object({
2671
2690
  text: z.string().min(1)
2672
2691
  });
2673
2692
  var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
2674
- var WAKE_EVENT = "wake";
2675
- var wakeChannel = (tokenId) => `wake:${tokenId}`;
2676
2693
  var RegisterDeliverySchema = z.object({ mode: DeliveryModeSchema });
2677
2694
  var OAuthStartSchema = z.object({
2678
2695
  provider: z.enum(["cma"]),
@@ -2809,25 +2826,60 @@ var SupportRequestSchema = z.object({
2809
2826
  message: z.string().trim().min(1).max(5e3),
2810
2827
  name: z.string().trim().max(120).optional()
2811
2828
  });
2812
-
2813
- // src/http.ts
2814
- var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
2815
- var NETWORK_MSG = `Can't reach ${BACKEND_URL} \u2014 the connection was blocked or dropped before an HTTP response. If this agent runs in a sandboxed environment with a network allowlist (e.g. Claude Code on the web, CI), ask the user to add the Paigy domain (paigy.ai) to the environment's allowed domains, then retry.`;
2816
- async function reach(url, init) {
2817
- try {
2818
- return await fetch(url, init);
2819
- } catch (e) {
2820
- throw new Error(`${NETWORK_MSG} (${e?.message ?? String(e)})`);
2829
+ var WORD_NUMS = {
2830
+ a: 1,
2831
+ an: 1,
2832
+ one: 1,
2833
+ two: 2,
2834
+ three: 3,
2835
+ four: 4,
2836
+ five: 5,
2837
+ six: 6,
2838
+ seven: 7,
2839
+ eight: 8,
2840
+ nine: 9,
2841
+ ten: 10,
2842
+ fifteen: 15,
2843
+ twenty: 20,
2844
+ thirty: 30,
2845
+ forty: 40,
2846
+ "forty-five": 45,
2847
+ fifty: 50,
2848
+ sixty: 60,
2849
+ "half an": 0.5,
2850
+ "a couple of": 2,
2851
+ "a few": 3
2852
+ };
2853
+ function untilHour(hour, now) {
2854
+ const due = new Date(now);
2855
+ due.setHours(hour, 0, 0, 0);
2856
+ if (due.getTime() <= now.getTime()) due.setDate(due.getDate() + 1);
2857
+ return Math.round((due.getTime() - now.getTime()) / 1e3);
2858
+ }
2859
+ function parseDue(detail, now = /* @__PURE__ */ new Date()) {
2860
+ const t = detail.toLowerCase().trim();
2861
+ const rel = /(?:^|\b)in\s+(half an|a couple of|a few|an?|[\w-]+|\d+)\s*(minutes?|mins?|hours?|hrs?)\b/.exec(t);
2862
+ if (rel) {
2863
+ const n = /^\d+$/.test(rel[1]) ? Number(rel[1]) : WORD_NUMS[rel[1]];
2864
+ if (n !== void 0) {
2865
+ const unit = rel[2].startsWith("h") ? 3600 : 60;
2866
+ return Math.round(n * unit);
2867
+ }
2868
+ }
2869
+ if (/after lunch|lunchtime/.test(t)) return untilHour(13, now);
2870
+ if (/tomorrow/.test(t)) {
2871
+ const due = new Date(now);
2872
+ due.setDate(due.getDate() + 1);
2873
+ due.setHours(9, 0, 0, 0);
2874
+ return Math.round((due.getTime() - now.getTime()) / 1e3);
2821
2875
  }
2876
+ if (/tonight|this evening/.test(t)) {
2877
+ return now.getHours() < 20 ? untilHour(20, now) : 3600;
2878
+ }
2879
+ if (/end of (the )?day|eod/.test(t)) return untilHour(17, now);
2880
+ if (/later|in a (bit|while)/.test(t)) return 1800;
2881
+ return null;
2822
2882
  }
2823
-
2824
- // src/device.ts
2825
- import { execFile } from "child_process";
2826
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
2827
- import { homedir, platform } from "os";
2828
- import { join } from "path";
2829
-
2830
- // ../../packages/crypto/dist/index.js
2831
2883
  var import_tweetnacl = __toESM(require_nacl_fast(), 1);
2832
2884
  var import_tweetnacl2 = __toESM(require_nacl_fast(), 1);
2833
2885
  var import_tweetnacl3 = __toESM(require_nacl_fast(), 1);
@@ -3096,45 +3148,11 @@ function verifyPairingCommit(commitment, proto, role, x25519Pub, ed25519Pub, non
3096
3148
  return false;
3097
3149
  }
3098
3150
  }
3099
-
3100
- // src/device.ts
3101
3151
  var AGENT_NAME = process.env.PAIGY_AGENT ?? "mcp-agent";
3102
3152
  var PROTO = "paigy-pair-v2|sas=24";
3103
3153
  var SAS_BITS = 24;
3104
3154
  var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
3105
3155
  var KEY_PATH = join(homedir(), ".paigy", "key.json");
3106
- var SURFACE_PATH = join(homedir(), ".paigy", "surface.json");
3107
- function writeSurface(label, code, ttlSeconds) {
3108
- try {
3109
- mkdirSync(join(homedir(), ".paigy"), { recursive: true });
3110
- writeFileSync(
3111
- SURFACE_PATH,
3112
- JSON.stringify({ label, code, expiresAt: Date.now() + ttlSeconds * 1e3 }),
3113
- { mode: 384 }
3114
- );
3115
- } catch {
3116
- }
3117
- }
3118
- function clearSurface() {
3119
- try {
3120
- rmSync(SURFACE_PATH, { force: true });
3121
- } catch {
3122
- }
3123
- }
3124
- function readSurface(now = Date.now) {
3125
- try {
3126
- const { label, code, expiresAt } = JSON.parse(readFileSync(SURFACE_PATH, "utf8"));
3127
- return label && code && typeof expiresAt === "number" && expiresAt > now() ? { label, code } : null;
3128
- } catch {
3129
- return null;
3130
- }
3131
- }
3132
- function openBrowser(url) {
3133
- const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
3134
- const args = platform() === "win32" ? ["/c", "start", url] : [url];
3135
- execFile(cmd, args, () => {
3136
- });
3137
- }
3138
3156
  function saveToken(token) {
3139
3157
  mkdirSync(join(homedir(), ".paigy"), { recursive: true });
3140
3158
  writeFileSync(TOKEN_PATH, JSON.stringify(token, null, 2) + "\n", { mode: 384 });
@@ -3162,11 +3180,11 @@ async function revokeToken(token) {
3162
3180
  });
3163
3181
  return res.ok;
3164
3182
  }
3165
- async function requestCode(agent = AGENT_NAME, e2ee) {
3183
+ async function requestCode(agent2 = AGENT_NAME, e2ee) {
3166
3184
  const res = await reach(`${BACKEND_URL}/api/device/code`, {
3167
3185
  method: "POST",
3168
3186
  headers: { "content-type": "application/json" },
3169
- body: JSON.stringify(e2ee ? { agent, proto: e2ee.proto, commitment: e2ee.commitment } : { agent })
3187
+ body: JSON.stringify(e2ee ? { agent: agent2, proto: e2ee.proto, commitment: e2ee.commitment } : { agent: agent2 })
3170
3188
  });
3171
3189
  if (!res.ok) throw new Error(`/api/device/code failed: ${res.status} ${await res.text()}`);
3172
3190
  return await res.json();
@@ -3316,22 +3334,194 @@ function openSealedAnswer(sealed, keypair) {
3316
3334
  }
3317
3335
  return answer;
3318
3336
  }
3337
+ var UnpairedError = class extends Error {
3338
+ constructor() {
3339
+ super("Not paired with Paigy \u2014 no valid token (the API answered 401). Run the pairing flow, then retry.");
3340
+ this.name = "UnpairedError";
3341
+ }
3342
+ };
3343
+ function ensureAuthed(res) {
3344
+ if (res.status === 401) throw new UnpairedError();
3345
+ return res;
3346
+ }
3347
+ async function sealForE2ee(req, token, deps = {}) {
3348
+ const keyFile = (deps.readKeyFile ?? readKeyFile)();
3349
+ if (!keyFile?.e2ee) return req;
3350
+ req = deriveAsk(req);
3351
+ if (!keyFile.uikPub) {
3352
+ throw new Error("E2EE pairing is missing its pinned identity key (key.json.uikPub) \u2014 re-pair; refusing to send plaintext.");
3353
+ }
3354
+ const roster = await (deps.fetchRoster ?? fetchRoster)(token);
3355
+ if (!roster) {
3356
+ throw new Error("E2EE pairing has no device roster to seal to \u2014 refusing to send plaintext. Approve a device on your phone, then retry.");
3357
+ }
3358
+ const recipients = verifiedRecipients(roster, keyFile.uikPub);
3359
+ if (recipients.length === 0) {
3360
+ throw new Error("No roster device verified against the pinned identity key \u2014 refusing to send plaintext (a server-injected device is dropped).");
3361
+ }
3362
+ const sealedContext = {
3363
+ ...req.context,
3364
+ ...req.repo !== void 0 ? { repo: req.repo } : {},
3365
+ ...req.branch !== void 0 ? { branch: req.branch } : {}
3366
+ };
3367
+ const envelope = sealFields({ context: sealedContext, options: req.options, visuals: req.visuals }, recipients);
3368
+ const { context: _c, options: _o, visuals: _v, repo: _r, branch: _b, ...meta } = req;
3369
+ return { ...meta, envelope };
3370
+ }
3371
+ async function submitNotification(req) {
3372
+ const token = readToken();
3373
+ const body = await sealForE2ee(req, token);
3374
+ const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify`, {
3375
+ method: "POST",
3376
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
3377
+ body: JSON.stringify(body)
3378
+ }));
3379
+ if (!res.ok) throw new Error(`notify failed: ${res.status} ${await res.text()}`);
3380
+ return await res.json();
3381
+ }
3382
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
3383
+ function decryptAnswer(reply, deps = {}) {
3384
+ if (!reply.sealed) return reply.answer;
3385
+ const keyFile = (deps.readKeyFile ?? readKeyFile)();
3386
+ if (!keyFile?.keypair) {
3387
+ throw new Error("received a sealed answer but this agent has no device key to open it \u2014 re-pair.");
3388
+ }
3389
+ return openSealedAnswer(reply.sealed, keyFile.keypair);
3390
+ }
3391
+ async function pollAnswer(notificationId) {
3392
+ const token = readToken();
3393
+ const res = ensureAuthed(await reach(`${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`, {
3394
+ headers: { authorization: `Bearer ${token}` }
3395
+ }));
3396
+ if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
3397
+ const item = await res.json();
3398
+ return item.type === "idle" ? item : decryptItem(item);
3399
+ }
3400
+ async function awaitReply(notificationId, opts = {}) {
3401
+ const intervalMs = opts.intervalMs ?? 5e3;
3402
+ const windowMs = opts.windowMs ?? 3e5;
3403
+ const doSleep = opts.sleep ?? sleep2;
3404
+ const now = opts.now ?? Date.now;
3405
+ const start = now();
3406
+ while (true) {
3407
+ const item = await pollAnswer(notificationId);
3408
+ if (item.type !== "idle") return item;
3409
+ if (now() - start >= windowMs) return { type: "idle" };
3410
+ await doSleep(intervalMs);
3411
+ }
3412
+ }
3413
+ function decryptItem(item) {
3414
+ if (item.type !== "reply") return item;
3415
+ const enriched = { ...item, intents: landIntents(item.intents) };
3416
+ if (!enriched.sealed) return enriched;
3417
+ const answer = decryptAnswer(enriched);
3418
+ const { sealed: _s, ...rest } = enriched;
3419
+ return { ...rest, answer };
3420
+ }
3421
+ function landIntents(intents) {
3422
+ if (!intents?.length) return intents;
3423
+ return intents.map((i) => {
3424
+ if (i.kind !== "defer") return i;
3425
+ const due = parseDue(i.detail);
3426
+ return due !== null ? { ...i, dueInSeconds: due } : i;
3427
+ });
3428
+ }
3429
+ async function checkReplies() {
3430
+ const token = readToken();
3431
+ const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
3432
+ headers: { authorization: `Bearer ${token}` }
3433
+ }));
3434
+ if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
3435
+ const pending = await res.json();
3436
+ return {
3437
+ ...pending,
3438
+ replies: pending.replies.map((raw) => {
3439
+ const r = { ...raw, intents: landIntents(raw.intents) };
3440
+ if (!r.sealed) return r;
3441
+ const { sealed: _s, ...rest } = r;
3442
+ return { ...rest, answer: decryptAnswer(r) };
3443
+ })
3444
+ };
3445
+ }
3446
+ async function setTaskState(notificationId, state) {
3447
+ const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify/${notificationId}/state`, {
3448
+ method: "PATCH",
3449
+ headers: { "content-type": "application/json", authorization: `Bearer ${readToken()}` },
3450
+ body: JSON.stringify({ state })
3451
+ }));
3452
+ if (!res.ok) throw new Error(`set_task_state failed: ${res.status} ${await res.text()}`);
3453
+ return await res.json();
3454
+ }
3455
+ async function registerDelivery(mode) {
3456
+ const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
3457
+ method: "POST",
3458
+ headers: { "content-type": "application/json", authorization: `Bearer ${readToken()}` },
3459
+ body: JSON.stringify({ mode })
3460
+ }));
3461
+ if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
3462
+ return await res.json();
3463
+ }
3464
+ async function scheduleCallback(req) {
3465
+ const token = readToken();
3466
+ const res = ensureAuthed(await reach(`${BACKEND_URL}/api/callback`, {
3467
+ method: "POST",
3468
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
3469
+ body: JSON.stringify(req)
3470
+ }));
3471
+ if (!res.ok) throw new Error(`schedule_callback failed: ${res.status} ${await res.text()}`);
3472
+ return await res.json();
3473
+ }
3474
+ var TITLE_MAX = 90;
3475
+ var CHUNKS_MAX = 8;
3476
+ var CHUNK_MAX = 300;
3477
+ var ASK_MAX = 600;
3478
+ var OPTION_MAX = 80;
3479
+ var NEEDS_MAX = 6;
3480
+ var UNSPEAKABLE = /```|\n/;
3481
+ function lintNotify(req) {
3482
+ const problems = [];
3483
+ if (req.ask !== void 0) {
3484
+ if (req.ask.length > ASK_MAX)
3485
+ problems.push(`ask is ${req.ask.length} chars \u2014 state the need and why it matters now in \u2264${ASK_MAX}; move detail into a smaller follow-up`);
3486
+ if (UNSPEAKABLE.test(req.ask))
3487
+ problems.push("ask contains code fences or newlines \u2014 write it as plain prose (it may be read aloud on a call)");
3488
+ for (const n of req.needs ?? []) {
3489
+ if (n.length > OPTION_MAX) problems.push(`need "${n.slice(0, 40)}\u2026" is too long \u2014 each need is a short phrase (\u2264${OPTION_MAX} chars)`);
3490
+ }
3491
+ if ((req.needs?.length ?? 0) > NEEDS_MAX)
3492
+ problems.push(`${req.needs?.length} needs \u2014 cap at ${NEEDS_MAX}; a call can't cover more in one conversation, split the rest into a second ask`);
3493
+ return problems;
3494
+ }
3495
+ const spoken = req.urgency === "call" || req.urgency === "banner";
3496
+ if (req.context) {
3497
+ if (req.context.title.length > TITLE_MAX)
3498
+ problems.push(`title is ${req.context.title.length} chars \u2014 shorten to \u2264${TITLE_MAX} (it's what shows on the banner / gets spoken on a ring)`);
3499
+ if (UNSPEAKABLE.test(req.context.title))
3500
+ problems.push("title contains code fences or newlines \u2014 one plain-prose line");
3501
+ if (req.context.description.length > CHUNKS_MAX)
3502
+ problems.push(`${req.context.description.length} description chunks \u2014 cap at ${CHUNKS_MAX}; merge or drop the rest`);
3503
+ for (const [i, chunk] of req.context.description.entries()) {
3504
+ if (chunk.length > CHUNK_MAX)
3505
+ problems.push(`description[${i}] is ${chunk.length} chars \u2014 split it into standalone points of \u2264${CHUNK_MAX}`);
3506
+ }
3507
+ if (spoken && UNSPEAKABLE.test(req.context.description.join(" ")))
3508
+ problems.push("urgency is 'call'/'banner' but the description has code fences/newlines-in-chunk \u2014 rewrite in spoken register (it will be read aloud)");
3509
+ }
3510
+ for (const o of req.options ?? []) {
3511
+ if (o.label.length > OPTION_MAX)
3512
+ problems.push(`option label "${o.label.slice(0, 40)}\u2026" is ${o.label.length} chars \u2014 labels must read at a glance (\u2264${OPTION_MAX}); move detail into the description`);
3513
+ }
3514
+ return problems;
3515
+ }
3319
3516
 
3320
3517
  export {
3518
+ BACKEND_URL,
3519
+ reach,
3321
3520
  NotifyRequestSchema,
3322
- deriveAsk,
3323
3521
  SetTaskStateSchema,
3324
3522
  ScheduleCallbackSchema,
3325
- WAKE_EVENT,
3326
- wakeChannel,
3327
- BACKEND_URL,
3328
- reach,
3329
3523
  AGENT_NAME,
3330
3524
  TOKEN_PATH,
3331
- writeSurface,
3332
- clearSurface,
3333
- readSurface,
3334
- openBrowser,
3335
3525
  saveToken,
3336
3526
  sleep,
3337
3527
  readToken,
@@ -3346,8 +3536,12 @@ export {
3346
3536
  finalizeE2ee,
3347
3537
  fetchCredential,
3348
3538
  pairStep,
3349
- fetchRoster,
3350
- verifiedRecipients,
3351
- sealFields,
3352
- openSealedAnswer
3539
+ UnpairedError,
3540
+ submitNotification,
3541
+ awaitReply,
3542
+ checkReplies,
3543
+ setTaskState,
3544
+ registerDelivery,
3545
+ scheduleCallback,
3546
+ lintNotify
3353
3547
  };
@@ -0,0 +1,89 @@
1
+ import {
2
+ AGENT_NAME
3
+ } from "./chunk-R3YGSGPX.js";
4
+
5
+ // src/clients.ts
6
+ import { execFile } from "child_process";
7
+ import { existsSync, readFileSync, writeFileSync } from "fs";
8
+ import { homedir, platform } from "os";
9
+ import { join, dirname } from "path";
10
+ function openBrowser(url) {
11
+ const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
12
+ const args = platform() === "win32" ? ["/c", "start", url] : [url];
13
+ execFile(cmd, args, () => {
14
+ });
15
+ }
16
+ function withPaigyMcpJson(existing, agent) {
17
+ let config = {};
18
+ if (existing) {
19
+ try {
20
+ config = JSON.parse(existing);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ config.mcpServers = {
26
+ ...config.mcpServers ?? {},
27
+ paigy: { command: "npx", args: ["-y", "@paigy/mcp@latest"], env: { PAIGY_AGENT: agent } }
28
+ };
29
+ return JSON.stringify(config, null, 2);
30
+ }
31
+ function withPaigyMcpToml(existing, agent) {
32
+ if (existing.includes("[mcp_servers.paigy]")) return null;
33
+ const entry = [
34
+ "",
35
+ "[mcp_servers.paigy]",
36
+ 'command = "npx"',
37
+ 'args = ["-y", "@paigy/mcp@latest"]',
38
+ `env = { PAIGY_AGENT = "${agent}" }`,
39
+ ""
40
+ ].join("\n");
41
+ return existing + entry;
42
+ }
43
+ function autoConfigureClients(skip = AGENT_NAME) {
44
+ const home = homedir();
45
+ const registered = [];
46
+ if (skip !== "antigravity") {
47
+ const antigravityConfigs = [
48
+ join(home, ".gemini", "config", "mcp_config.json"),
49
+ join(home, ".gemini", "antigravity-cli", "mcp_config.json")
50
+ ];
51
+ for (const path of antigravityConfigs) {
52
+ try {
53
+ if (!existsSync(dirname(path))) continue;
54
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
55
+ const updated = withPaigyMcpJson(existing, "antigravity");
56
+ if (updated === null) continue;
57
+ writeFileSync(path, updated);
58
+ registered.push(`Google Antigravity (${path})`);
59
+ } catch {
60
+ }
61
+ }
62
+ }
63
+ if (skip !== "codex") {
64
+ const codexConfig = join(home, ".codex", "config.toml");
65
+ try {
66
+ if (existsSync(dirname(codexConfig))) {
67
+ const existing = existsSync(codexConfig) ? readFileSync(codexConfig, "utf8") : "";
68
+ const updated = withPaigyMcpToml(existing, "codex");
69
+ if (updated !== null) {
70
+ writeFileSync(codexConfig, updated);
71
+ registered.push(`OpenAI Codex (${codexConfig})`);
72
+ }
73
+ }
74
+ } catch {
75
+ }
76
+ }
77
+ return registered;
78
+ }
79
+ function claudeInstallHint(skip = AGENT_NAME) {
80
+ if (skip.startsWith("claude")) return null;
81
+ if (!existsSync(join(homedir(), ".claude"))) return null;
82
+ 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).";
83
+ }
84
+
85
+ export {
86
+ openBrowser,
87
+ autoConfigureClients,
88
+ claudeInstallHint
89
+ };