@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 +9 -1
- package/dist/chunk-3SVNPJPN.js +36 -0
- package/dist/{chunk-W2ZS7IBA.js → chunk-R3YGSGPX.js} +261 -140
- package/dist/chunk-U3WODCOE.js +89 -0
- package/dist/index.js +25 -59
- package/dist/listen.js +545 -6
- package/dist/onboard.js +6 -5
- package/dist/statusline.js +6 -4
- package/package.json +4 -2
- package/dist/chunk-7S6W7NEZ.js +0 -224
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
|
-
|
|
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
|
|
9
|
-
get: (a, b) => (typeof
|
|
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
|
|
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
|
-
|
|
2260
|
-
|
|
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
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
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(
|
|
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
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
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
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
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
|
-
|
|
3336
|
-
}
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
if (
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
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
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
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
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
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
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
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
|
+
};
|