@paigy/mcp 0.15.0 → 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.")
@@ -2674,8 +2690,6 @@ var CreateRequestSchema = z.object({
2674
2690
  text: z.string().min(1)
2675
2691
  });
2676
2692
  var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
2677
- var WAKE_EVENT = "wake";
2678
- var wakeChannel = (tokenId) => `wake:${tokenId}`;
2679
2693
  var RegisterDeliverySchema = z.object({ mode: DeliveryModeSchema });
2680
2694
  var OAuthStartSchema = z.object({
2681
2695
  provider: z.enum(["cma"]),
@@ -2812,25 +2826,60 @@ var SupportRequestSchema = z.object({
2812
2826
  message: z.string().trim().min(1).max(5e3),
2813
2827
  name: z.string().trim().max(120).optional()
2814
2828
  });
2815
-
2816
- // src/http.ts
2817
- var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
2818
- 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.`;
2819
- async function reach(url, init) {
2820
- try {
2821
- return await fetch(url, init);
2822
- } catch (e) {
2823
- 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);
2875
+ }
2876
+ if (/tonight|this evening/.test(t)) {
2877
+ return now.getHours() < 20 ? untilHour(20, now) : 3600;
2824
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;
2825
2882
  }
2826
-
2827
- // src/device.ts
2828
- import { execFile } from "child_process";
2829
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
2830
- import { homedir, platform } from "os";
2831
- import { join, dirname } from "path";
2832
-
2833
- // ../../packages/crypto/dist/index.js
2834
2883
  var import_tweetnacl = __toESM(require_nacl_fast(), 1);
2835
2884
  var import_tweetnacl2 = __toESM(require_nacl_fast(), 1);
2836
2885
  var import_tweetnacl3 = __toESM(require_nacl_fast(), 1);
@@ -3099,45 +3148,11 @@ function verifyPairingCommit(commitment, proto, role, x25519Pub, ed25519Pub, non
3099
3148
  return false;
3100
3149
  }
3101
3150
  }
3102
-
3103
- // src/device.ts
3104
3151
  var AGENT_NAME = process.env.PAIGY_AGENT ?? "mcp-agent";
3105
3152
  var PROTO = "paigy-pair-v2|sas=24";
3106
3153
  var SAS_BITS = 24;
3107
3154
  var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
3108
3155
  var KEY_PATH = join(homedir(), ".paigy", "key.json");
3109
- var SURFACE_PATH = join(homedir(), ".paigy", "surface.json");
3110
- function writeSurface(label, code, ttlSeconds) {
3111
- try {
3112
- mkdirSync(join(homedir(), ".paigy"), { recursive: true });
3113
- writeFileSync(
3114
- SURFACE_PATH,
3115
- JSON.stringify({ label, code, expiresAt: Date.now() + ttlSeconds * 1e3 }),
3116
- { mode: 384 }
3117
- );
3118
- } catch {
3119
- }
3120
- }
3121
- function clearSurface() {
3122
- try {
3123
- rmSync(SURFACE_PATH, { force: true });
3124
- } catch {
3125
- }
3126
- }
3127
- function readSurface(now = Date.now) {
3128
- try {
3129
- const { label, code, expiresAt } = JSON.parse(readFileSync(SURFACE_PATH, "utf8"));
3130
- return label && code && typeof expiresAt === "number" && expiresAt > now() ? { label, code } : null;
3131
- } catch {
3132
- return null;
3133
- }
3134
- }
3135
- function openBrowser(url) {
3136
- const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
3137
- const args = platform() === "win32" ? ["/c", "start", url] : [url];
3138
- execFile(cmd, args, () => {
3139
- });
3140
- }
3141
3156
  function saveToken(token) {
3142
3157
  mkdirSync(join(homedir(), ".paigy"), { recursive: true });
3143
3158
  writeFileSync(TOKEN_PATH, JSON.stringify(token, null, 2) + "\n", { mode: 384 });
@@ -3165,11 +3180,11 @@ async function revokeToken(token) {
3165
3180
  });
3166
3181
  return res.ok;
3167
3182
  }
3168
- async function requestCode(agent = AGENT_NAME, e2ee) {
3183
+ async function requestCode(agent2 = AGENT_NAME, e2ee) {
3169
3184
  const res = await reach(`${BACKEND_URL}/api/device/code`, {
3170
3185
  method: "POST",
3171
3186
  headers: { "content-type": "application/json" },
3172
- 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 })
3173
3188
  });
3174
3189
  if (!res.ok) throw new Error(`/api/device/code failed: ${res.status} ${await res.text()}`);
3175
3190
  return await res.json();
@@ -3319,90 +3334,194 @@ function openSealedAnswer(sealed, keypair) {
3319
3334
  }
3320
3335
  return answer;
3321
3336
  }
3322
- function withPaigyMcpJson(existing, agent) {
3323
- let config = {};
3324
- if (existing) {
3325
- try {
3326
- config = JSON.parse(existing);
3327
- } catch {
3328
- return null;
3329
- }
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.");
3330
3353
  }
3331
- config.mcpServers = {
3332
- ...config.mcpServers ?? {},
3333
- paigy: { command: "npx", args: ["-y", "@paigy/mcp@latest"], env: { PAIGY_AGENT: agent } }
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 } : {}
3334
3366
  };
3335
- return JSON.stringify(config, null, 2);
3336
- }
3337
- function withPaigyMcpToml(existing, agent) {
3338
- if (existing.includes("[mcp_servers.paigy]")) return null;
3339
- const entry = [
3340
- "",
3341
- "[mcp_servers.paigy]",
3342
- 'command = "npx"',
3343
- 'args = ["-y", "@paigy/mcp@latest"]',
3344
- `env = { PAIGY_AGENT = "${agent}" }`,
3345
- ""
3346
- ].join("\n");
3347
- return existing + entry;
3348
- }
3349
- function autoConfigureClients(skip = AGENT_NAME) {
3350
- const home = homedir();
3351
- const registered = [];
3352
- if (skip !== "antigravity") {
3353
- const antigravityConfigs = [
3354
- join(home, ".gemini", "config", "mcp_config.json"),
3355
- join(home, ".gemini", "antigravity-cli", "mcp_config.json")
3356
- ];
3357
- for (const path of antigravityConfigs) {
3358
- try {
3359
- if (!existsSync(dirname(path))) continue;
3360
- const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
3361
- const updated = withPaigyMcpJson(existing, "antigravity");
3362
- if (updated === null) continue;
3363
- writeFileSync(path, updated);
3364
- registered.push(`Google Antigravity (${path})`);
3365
- } catch {
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)`);
3367
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;
3368
3494
  }
3369
- if (skip !== "codex") {
3370
- const codexConfig = join(home, ".codex", "config.toml");
3371
- try {
3372
- if (existsSync(dirname(codexConfig))) {
3373
- const existing = existsSync(codexConfig) ? readFileSync(codexConfig, "utf8") : "";
3374
- const updated = withPaigyMcpToml(existing, "codex");
3375
- if (updated !== null) {
3376
- writeFileSync(codexConfig, updated);
3377
- registered.push(`OpenAI Codex (${codexConfig})`);
3378
- }
3379
- }
3380
- } catch {
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}`);
3381
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)");
3382
3509
  }
3383
- return registered;
3384
- }
3385
- function claudeInstallHint(skip = AGENT_NAME) {
3386
- if (skip.startsWith("claude")) return null;
3387
- if (!existsSync(join(homedir(), ".claude"))) return null;
3388
- 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).";
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;
3389
3515
  }
3390
3516
 
3391
3517
  export {
3518
+ BACKEND_URL,
3519
+ reach,
3392
3520
  NotifyRequestSchema,
3393
- deriveAsk,
3394
3521
  SetTaskStateSchema,
3395
3522
  ScheduleCallbackSchema,
3396
- WAKE_EVENT,
3397
- wakeChannel,
3398
- BACKEND_URL,
3399
- reach,
3400
3523
  AGENT_NAME,
3401
3524
  TOKEN_PATH,
3402
- writeSurface,
3403
- clearSurface,
3404
- readSurface,
3405
- openBrowser,
3406
3525
  saveToken,
3407
3526
  sleep,
3408
3527
  readToken,
@@ -3417,10 +3536,12 @@ export {
3417
3536
  finalizeE2ee,
3418
3537
  fetchCredential,
3419
3538
  pairStep,
3420
- fetchRoster,
3421
- verifiedRecipients,
3422
- sealFields,
3423
- openSealedAnswer,
3424
- autoConfigureClients,
3425
- claudeInstallHint
3539
+ UnpairedError,
3540
+ submitNotification,
3541
+ awaitReply,
3542
+ checkReplies,
3543
+ setTaskState,
3544
+ registerDelivery,
3545
+ scheduleCallback,
3546
+ lintNotify
3426
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
+ };