@paigy/mcp 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/dist/chunk-3SVNPJPN.js +36 -0
- package/dist/{chunk-W2ZS7IBA.js → chunk-4LQAS5ZB.js} +295 -142
- package/dist/chunk-ZIOTFQPK.js +89 -0
- package/dist/index.js +40 -60
- package/dist/listen.js +678 -11
- package/dist/onboard.js +6 -5
- package/dist/statusline.js +6 -4
- package/package.json +13 -10
- 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,38 @@ 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.
|
|
114
|
+
|
|
115
|
+
## Wake any harness (`paigy-listen`)
|
|
116
|
+
|
|
117
|
+
`paigy-listen` is the self-hosted push daemon: it subscribes to this connection's
|
|
118
|
+
wake channel and sweeps on every nudge. Keep it alive past the terminal with
|
|
119
|
+
`paigy-listen --install` (launchd on macOS, systemd user unit on Linux; `--uninstall`
|
|
120
|
+
removes it).
|
|
121
|
+
|
|
122
|
+
To launch an agent — any agent, not just Claude — when work arrives, set
|
|
123
|
+
`PAIGY_ON_WAKE` to a command before `--install`. The daemon runs it with the swept
|
|
124
|
+
work in `$PAIGY_WORK` (JSON: `replies`, `requests`, `owedCallbacks`); the sweep has
|
|
125
|
+
already claimed the work, so read `$PAIGY_WORK` rather than calling `check_replies`
|
|
126
|
+
again. A request carrying a `contextThreadId` (or an unfamiliar `threadId`) means
|
|
127
|
+
the user is resuming/seeding a past conversation — rehydrate with `get_thread` first.
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
# Claude Code:
|
|
131
|
+
PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — rehydrate threads you do not recognize via get_thread first."' \
|
|
132
|
+
npx -y -p @paigy/mcp paigy-listen --install
|
|
133
|
+
|
|
134
|
+
# Codex (or any CLI harness) works the same way:
|
|
135
|
+
PAIGY_ON_WAKE='codex exec "Handle the Paigy work in $PAIGY_WORK ..."' \
|
|
136
|
+
npx -y -p @paigy/mcp paigy-listen --install
|
|
137
|
+
```
|
|
106
138
|
|
|
107
139
|
## Statusline (Claude Code)
|
|
108
140
|
|
|
@@ -116,7 +148,7 @@ paigy: all calls · phone ✓
|
|
|
116
148
|
Enable it in `~/.claude/settings.json`:
|
|
117
149
|
|
|
118
150
|
```json
|
|
119
|
-
"statusLine": { "type": "command", "command": "npx -y @paigy/mcp@latest paigy-statusline" }
|
|
151
|
+
"statusLine": { "type": "command", "command": "npx -y -p @paigy/mcp@latest paigy-statusline" }
|
|
120
152
|
```
|
|
121
153
|
|
|
122
154
|
It reads `GET /api/status` with the pairing token and caches the result in
|
|
@@ -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.")
|
|
@@ -2488,7 +2504,15 @@ var PendingRepliesSchema = z.object({
|
|
|
2488
2504
|
* notify_user on the same threadId. Keeps reappearing until you call
|
|
2489
2505
|
* set_task_state on its notificationId. */
|
|
2490
2506
|
requests: z.array(
|
|
2491
|
-
z.object({
|
|
2507
|
+
z.object({
|
|
2508
|
+
threadId: z.string(),
|
|
2509
|
+
notificationId: z.string(),
|
|
2510
|
+
text: z.string(),
|
|
2511
|
+
createdAt: z.string(),
|
|
2512
|
+
/** The user seeded this request with a past conversation — call get_thread on it
|
|
2513
|
+
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
2514
|
+
contextThreadId: z.string().optional()
|
|
2515
|
+
})
|
|
2492
2516
|
),
|
|
2493
2517
|
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
2494
2518
|
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
@@ -2524,8 +2548,13 @@ var UserResponseSchema = z.object({
|
|
|
2524
2548
|
/** Coverage report (#396): which of the ask's declared `points` were addressed. */
|
|
2525
2549
|
covered: z.array(z.string()).optional()
|
|
2526
2550
|
});
|
|
2551
|
+
var VoiceKeySchema = z.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
2527
2552
|
var InboxItemSchema = z.object({
|
|
2528
2553
|
id: z.string(),
|
|
2554
|
+
/** The conversation thread + connection this item lives on. Present on the replied
|
|
2555
|
+
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
2556
|
+
threadId: z.string().optional(),
|
|
2557
|
+
tokenId: z.string().optional(),
|
|
2529
2558
|
status: NotifyStatusSchema,
|
|
2530
2559
|
context: ContextSchema,
|
|
2531
2560
|
options: z.array(OptionSchema).optional(),
|
|
@@ -2540,6 +2569,8 @@ var InboxItemSchema = z.object({
|
|
|
2540
2569
|
visuals: z.array(VisualSchema).optional(),
|
|
2541
2570
|
agent: z.string(),
|
|
2542
2571
|
nickname: z.string(),
|
|
2572
|
+
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
2573
|
+
voice: VoiceKeySchema.optional(),
|
|
2543
2574
|
repo: z.string().optional(),
|
|
2544
2575
|
branch: z.string().optional(),
|
|
2545
2576
|
createdAt: z.string().datetime(),
|
|
@@ -2659,6 +2690,8 @@ var ConnectionSummarySchema = z.object({
|
|
|
2659
2690
|
agent: z.string(),
|
|
2660
2691
|
device: z.string().nullable(),
|
|
2661
2692
|
nickname: z.string(),
|
|
2693
|
+
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
2694
|
+
voice: VoiceKeySchema.nullable(),
|
|
2662
2695
|
createdAt: z.string().datetime(),
|
|
2663
2696
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
2664
2697
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
@@ -2671,11 +2704,16 @@ var CreateRequestSchema = z.object({
|
|
|
2671
2704
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
2672
2705
|
tokenId: z.string(),
|
|
2673
2706
|
/** The user's message to the agent. */
|
|
2674
|
-
text: z.string().min(1)
|
|
2707
|
+
text: z.string().min(1),
|
|
2708
|
+
/** Land the request on an existing conversation thread (History → "Continue")
|
|
2709
|
+
* instead of minting a fresh one. Must belong to the requesting user. */
|
|
2710
|
+
threadId: z.string().optional(),
|
|
2711
|
+
/** Point the agent at a past conversation (possibly with a different agent) as
|
|
2712
|
+
* starting context (History → "New session from this"). A reference, not a copy —
|
|
2713
|
+
* the agent reads it via get_thread. Must belong to the requesting user. */
|
|
2714
|
+
contextThreadId: z.string().optional()
|
|
2675
2715
|
});
|
|
2676
2716
|
var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
|
|
2677
|
-
var WAKE_EVENT = "wake";
|
|
2678
|
-
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
2679
2717
|
var RegisterDeliverySchema = z.object({ mode: DeliveryModeSchema });
|
|
2680
2718
|
var OAuthStartSchema = z.object({
|
|
2681
2719
|
provider: z.enum(["cma"]),
|
|
@@ -2812,25 +2850,60 @@ var SupportRequestSchema = z.object({
|
|
|
2812
2850
|
message: z.string().trim().min(1).max(5e3),
|
|
2813
2851
|
name: z.string().trim().max(120).optional()
|
|
2814
2852
|
});
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2853
|
+
var WORD_NUMS = {
|
|
2854
|
+
a: 1,
|
|
2855
|
+
an: 1,
|
|
2856
|
+
one: 1,
|
|
2857
|
+
two: 2,
|
|
2858
|
+
three: 3,
|
|
2859
|
+
four: 4,
|
|
2860
|
+
five: 5,
|
|
2861
|
+
six: 6,
|
|
2862
|
+
seven: 7,
|
|
2863
|
+
eight: 8,
|
|
2864
|
+
nine: 9,
|
|
2865
|
+
ten: 10,
|
|
2866
|
+
fifteen: 15,
|
|
2867
|
+
twenty: 20,
|
|
2868
|
+
thirty: 30,
|
|
2869
|
+
forty: 40,
|
|
2870
|
+
"forty-five": 45,
|
|
2871
|
+
fifty: 50,
|
|
2872
|
+
sixty: 60,
|
|
2873
|
+
"half an": 0.5,
|
|
2874
|
+
"a couple of": 2,
|
|
2875
|
+
"a few": 3
|
|
2876
|
+
};
|
|
2877
|
+
function untilHour(hour, now) {
|
|
2878
|
+
const due = new Date(now);
|
|
2879
|
+
due.setHours(hour, 0, 0, 0);
|
|
2880
|
+
if (due.getTime() <= now.getTime()) due.setDate(due.getDate() + 1);
|
|
2881
|
+
return Math.round((due.getTime() - now.getTime()) / 1e3);
|
|
2882
|
+
}
|
|
2883
|
+
function parseDue(detail, now = /* @__PURE__ */ new Date()) {
|
|
2884
|
+
const t = detail.toLowerCase().trim();
|
|
2885
|
+
const rel = /(?:^|\b)in\s+(half an|a couple of|a few|an?|[\w-]+|\d+)\s*(minutes?|mins?|hours?|hrs?)\b/.exec(t);
|
|
2886
|
+
if (rel) {
|
|
2887
|
+
const n = /^\d+$/.test(rel[1]) ? Number(rel[1]) : WORD_NUMS[rel[1]];
|
|
2888
|
+
if (n !== void 0) {
|
|
2889
|
+
const unit = rel[2].startsWith("h") ? 3600 : 60;
|
|
2890
|
+
return Math.round(n * unit);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
if (/after lunch|lunchtime/.test(t)) return untilHour(13, now);
|
|
2894
|
+
if (/tomorrow/.test(t)) {
|
|
2895
|
+
const due = new Date(now);
|
|
2896
|
+
due.setDate(due.getDate() + 1);
|
|
2897
|
+
due.setHours(9, 0, 0, 0);
|
|
2898
|
+
return Math.round((due.getTime() - now.getTime()) / 1e3);
|
|
2899
|
+
}
|
|
2900
|
+
if (/tonight|this evening/.test(t)) {
|
|
2901
|
+
return now.getHours() < 20 ? untilHour(20, now) : 3600;
|
|
2824
2902
|
}
|
|
2903
|
+
if (/end of (the )?day|eod/.test(t)) return untilHour(17, now);
|
|
2904
|
+
if (/later|in a (bit|while)/.test(t)) return 1800;
|
|
2905
|
+
return null;
|
|
2825
2906
|
}
|
|
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
2907
|
var import_tweetnacl = __toESM(require_nacl_fast(), 1);
|
|
2835
2908
|
var import_tweetnacl2 = __toESM(require_nacl_fast(), 1);
|
|
2836
2909
|
var import_tweetnacl3 = __toESM(require_nacl_fast(), 1);
|
|
@@ -3099,45 +3172,11 @@ function verifyPairingCommit(commitment, proto, role, x25519Pub, ed25519Pub, non
|
|
|
3099
3172
|
return false;
|
|
3100
3173
|
}
|
|
3101
3174
|
}
|
|
3102
|
-
|
|
3103
|
-
// src/device.ts
|
|
3104
3175
|
var AGENT_NAME = process.env.PAIGY_AGENT ?? "mcp-agent";
|
|
3105
3176
|
var PROTO = "paigy-pair-v2|sas=24";
|
|
3106
3177
|
var SAS_BITS = 24;
|
|
3107
3178
|
var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
|
|
3108
3179
|
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
3180
|
function saveToken(token) {
|
|
3142
3181
|
mkdirSync(join(homedir(), ".paigy"), { recursive: true });
|
|
3143
3182
|
writeFileSync(TOKEN_PATH, JSON.stringify(token, null, 2) + "\n", { mode: 384 });
|
|
@@ -3165,11 +3204,11 @@ async function revokeToken(token) {
|
|
|
3165
3204
|
});
|
|
3166
3205
|
return res.ok;
|
|
3167
3206
|
}
|
|
3168
|
-
async function requestCode(
|
|
3207
|
+
async function requestCode(agent2 = AGENT_NAME, e2ee) {
|
|
3169
3208
|
const res = await reach(`${BACKEND_URL}/api/device/code`, {
|
|
3170
3209
|
method: "POST",
|
|
3171
3210
|
headers: { "content-type": "application/json" },
|
|
3172
|
-
body: JSON.stringify(e2ee ? { agent, proto: e2ee.proto, commitment: e2ee.commitment } : { agent })
|
|
3211
|
+
body: JSON.stringify(e2ee ? { agent: agent2, proto: e2ee.proto, commitment: e2ee.commitment } : { agent: agent2 })
|
|
3173
3212
|
});
|
|
3174
3213
|
if (!res.ok) throw new Error(`/api/device/code failed: ${res.status} ${await res.text()}`);
|
|
3175
3214
|
return await res.json();
|
|
@@ -3319,90 +3358,201 @@ function openSealedAnswer(sealed, keypair) {
|
|
|
3319
3358
|
}
|
|
3320
3359
|
return answer;
|
|
3321
3360
|
}
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3361
|
+
var UnpairedError = class extends Error {
|
|
3362
|
+
constructor() {
|
|
3363
|
+
super("Not paired with Paigy \u2014 no valid token (the API answered 401). Run the pairing flow, then retry.");
|
|
3364
|
+
this.name = "UnpairedError";
|
|
3365
|
+
}
|
|
3366
|
+
};
|
|
3367
|
+
function ensureAuthed(res) {
|
|
3368
|
+
if (res.status === 401) throw new UnpairedError();
|
|
3369
|
+
return res;
|
|
3370
|
+
}
|
|
3371
|
+
async function sealForE2ee(req, token, deps = {}) {
|
|
3372
|
+
const keyFile = (deps.readKeyFile ?? readKeyFile)();
|
|
3373
|
+
if (!keyFile?.e2ee) return req;
|
|
3374
|
+
req = deriveAsk(req);
|
|
3375
|
+
if (!keyFile.uikPub) {
|
|
3376
|
+
throw new Error("E2EE pairing is missing its pinned identity key (key.json.uikPub) \u2014 re-pair; refusing to send plaintext.");
|
|
3377
|
+
}
|
|
3378
|
+
const roster = await (deps.fetchRoster ?? fetchRoster)(token);
|
|
3379
|
+
if (!roster) {
|
|
3380
|
+
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.");
|
|
3381
|
+
}
|
|
3382
|
+
const recipients = verifiedRecipients(roster, keyFile.uikPub);
|
|
3383
|
+
if (recipients.length === 0) {
|
|
3384
|
+
throw new Error("No roster device verified against the pinned identity key \u2014 refusing to send plaintext (a server-injected device is dropped).");
|
|
3330
3385
|
}
|
|
3331
|
-
|
|
3332
|
-
...
|
|
3333
|
-
|
|
3386
|
+
const sealedContext = {
|
|
3387
|
+
...req.context,
|
|
3388
|
+
...req.repo !== void 0 ? { repo: req.repo } : {},
|
|
3389
|
+
...req.branch !== void 0 ? { branch: req.branch } : {}
|
|
3334
3390
|
};
|
|
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
|
-
|
|
3391
|
+
const envelope = sealFields({ context: sealedContext, options: req.options, visuals: req.visuals }, recipients);
|
|
3392
|
+
const { context: _c, options: _o, visuals: _v, repo: _r, branch: _b, ...meta } = req;
|
|
3393
|
+
return { ...meta, envelope };
|
|
3394
|
+
}
|
|
3395
|
+
async function submitNotification(req) {
|
|
3396
|
+
const token = readToken();
|
|
3397
|
+
const body = await sealForE2ee(req, token);
|
|
3398
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify`, {
|
|
3399
|
+
method: "POST",
|
|
3400
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
3401
|
+
body: JSON.stringify(body)
|
|
3402
|
+
}));
|
|
3403
|
+
if (!res.ok) throw new Error(`notify failed: ${res.status} ${await res.text()}`);
|
|
3404
|
+
return await res.json();
|
|
3405
|
+
}
|
|
3406
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3407
|
+
function decryptAnswer(reply, deps = {}) {
|
|
3408
|
+
if (!reply.sealed) return reply.answer;
|
|
3409
|
+
const keyFile = (deps.readKeyFile ?? readKeyFile)();
|
|
3410
|
+
if (!keyFile?.keypair) {
|
|
3411
|
+
throw new Error("received a sealed answer but this agent has no device key to open it \u2014 re-pair.");
|
|
3412
|
+
}
|
|
3413
|
+
return openSealedAnswer(reply.sealed, keyFile.keypair);
|
|
3414
|
+
}
|
|
3415
|
+
async function pollAnswer(notificationId) {
|
|
3416
|
+
const token = readToken();
|
|
3417
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`, {
|
|
3418
|
+
headers: { authorization: `Bearer ${token}` }
|
|
3419
|
+
}));
|
|
3420
|
+
if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
|
|
3421
|
+
const item = await res.json();
|
|
3422
|
+
return item.type === "idle" ? item : decryptItem(item);
|
|
3423
|
+
}
|
|
3424
|
+
async function awaitReply(notificationId, opts = {}) {
|
|
3425
|
+
const intervalMs = opts.intervalMs ?? 5e3;
|
|
3426
|
+
const windowMs = opts.windowMs ?? 3e5;
|
|
3427
|
+
const doSleep = opts.sleep ?? sleep2;
|
|
3428
|
+
const now = opts.now ?? Date.now;
|
|
3429
|
+
const start = now();
|
|
3430
|
+
while (true) {
|
|
3431
|
+
const item = await pollAnswer(notificationId);
|
|
3432
|
+
if (item.type !== "idle") return item;
|
|
3433
|
+
if (now() - start >= windowMs) return { type: "idle" };
|
|
3434
|
+
await doSleep(intervalMs);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
function decryptItem(item) {
|
|
3438
|
+
if (item.type !== "reply") return item;
|
|
3439
|
+
const enriched = { ...item, intents: landIntents(item.intents) };
|
|
3440
|
+
if (!enriched.sealed) return enriched;
|
|
3441
|
+
const answer = decryptAnswer(enriched);
|
|
3442
|
+
const { sealed: _s, ...rest } = enriched;
|
|
3443
|
+
return { ...rest, answer };
|
|
3444
|
+
}
|
|
3445
|
+
function landIntents(intents) {
|
|
3446
|
+
if (!intents?.length) return intents;
|
|
3447
|
+
return intents.map((i) => {
|
|
3448
|
+
if (i.kind !== "defer") return i;
|
|
3449
|
+
const due = parseDue(i.detail);
|
|
3450
|
+
return due !== null ? { ...i, dueInSeconds: due } : i;
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
async function getThread(threadId) {
|
|
3454
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/thread/${encodeURIComponent(threadId)}`, {
|
|
3455
|
+
headers: { authorization: `Bearer ${readToken()}` }
|
|
3456
|
+
}));
|
|
3457
|
+
if (!res.ok) throw new Error(`get_thread failed: ${res.status} ${await res.text()}`);
|
|
3458
|
+
return await res.json();
|
|
3459
|
+
}
|
|
3460
|
+
async function checkReplies() {
|
|
3461
|
+
const token = readToken();
|
|
3462
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
|
|
3463
|
+
headers: { authorization: `Bearer ${token}` }
|
|
3464
|
+
}));
|
|
3465
|
+
if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
|
|
3466
|
+
const pending = await res.json();
|
|
3467
|
+
return {
|
|
3468
|
+
...pending,
|
|
3469
|
+
replies: pending.replies.map((raw) => {
|
|
3470
|
+
const r = { ...raw, intents: landIntents(raw.intents) };
|
|
3471
|
+
if (!r.sealed) return r;
|
|
3472
|
+
const { sealed: _s, ...rest } = r;
|
|
3473
|
+
return { ...rest, answer: decryptAnswer(r) };
|
|
3474
|
+
})
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
async function setTaskState(notificationId, state) {
|
|
3478
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify/${notificationId}/state`, {
|
|
3479
|
+
method: "PATCH",
|
|
3480
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${readToken()}` },
|
|
3481
|
+
body: JSON.stringify({ state })
|
|
3482
|
+
}));
|
|
3483
|
+
if (!res.ok) throw new Error(`set_task_state failed: ${res.status} ${await res.text()}`);
|
|
3484
|
+
return await res.json();
|
|
3485
|
+
}
|
|
3486
|
+
async function registerDelivery(mode) {
|
|
3487
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
|
|
3488
|
+
method: "POST",
|
|
3489
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${readToken()}` },
|
|
3490
|
+
body: JSON.stringify({ mode })
|
|
3491
|
+
}));
|
|
3492
|
+
if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
|
|
3493
|
+
return await res.json();
|
|
3494
|
+
}
|
|
3495
|
+
async function scheduleCallback(req) {
|
|
3496
|
+
const token = readToken();
|
|
3497
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/callback`, {
|
|
3498
|
+
method: "POST",
|
|
3499
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
3500
|
+
body: JSON.stringify(req)
|
|
3501
|
+
}));
|
|
3502
|
+
if (!res.ok) throw new Error(`schedule_callback failed: ${res.status} ${await res.text()}`);
|
|
3503
|
+
return await res.json();
|
|
3504
|
+
}
|
|
3505
|
+
var TITLE_MAX = 90;
|
|
3506
|
+
var CHUNKS_MAX = 8;
|
|
3507
|
+
var CHUNK_MAX = 300;
|
|
3508
|
+
var ASK_MAX = 600;
|
|
3509
|
+
var OPTION_MAX = 80;
|
|
3510
|
+
var NEEDS_MAX = 6;
|
|
3511
|
+
var UNSPEAKABLE = /```|\n/;
|
|
3512
|
+
function lintNotify(req) {
|
|
3513
|
+
const problems = [];
|
|
3514
|
+
if (req.ask !== void 0) {
|
|
3515
|
+
if (req.ask.length > ASK_MAX)
|
|
3516
|
+
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`);
|
|
3517
|
+
if (UNSPEAKABLE.test(req.ask))
|
|
3518
|
+
problems.push("ask contains code fences or newlines \u2014 write it as plain prose (it may be read aloud on a call)");
|
|
3519
|
+
for (const n of req.needs ?? []) {
|
|
3520
|
+
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
3521
|
}
|
|
3522
|
+
if ((req.needs?.length ?? 0) > NEEDS_MAX)
|
|
3523
|
+
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`);
|
|
3524
|
+
return problems;
|
|
3368
3525
|
}
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
} catch {
|
|
3526
|
+
const spoken = req.urgency === "call" || req.urgency === "banner";
|
|
3527
|
+
if (req.context) {
|
|
3528
|
+
if (req.context.title.length > TITLE_MAX)
|
|
3529
|
+
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)`);
|
|
3530
|
+
if (UNSPEAKABLE.test(req.context.title))
|
|
3531
|
+
problems.push("title contains code fences or newlines \u2014 one plain-prose line");
|
|
3532
|
+
if (req.context.description.length > CHUNKS_MAX)
|
|
3533
|
+
problems.push(`${req.context.description.length} description chunks \u2014 cap at ${CHUNKS_MAX}; merge or drop the rest`);
|
|
3534
|
+
for (const [i, chunk] of req.context.description.entries()) {
|
|
3535
|
+
if (chunk.length > CHUNK_MAX)
|
|
3536
|
+
problems.push(`description[${i}] is ${chunk.length} chars \u2014 split it into standalone points of \u2264${CHUNK_MAX}`);
|
|
3381
3537
|
}
|
|
3538
|
+
if (spoken && UNSPEAKABLE.test(req.context.description.join(" ")))
|
|
3539
|
+
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
3540
|
}
|
|
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).";
|
|
3541
|
+
for (const o of req.options ?? []) {
|
|
3542
|
+
if (o.label.length > OPTION_MAX)
|
|
3543
|
+
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`);
|
|
3544
|
+
}
|
|
3545
|
+
return problems;
|
|
3389
3546
|
}
|
|
3390
3547
|
|
|
3391
3548
|
export {
|
|
3549
|
+
BACKEND_URL,
|
|
3550
|
+
reach,
|
|
3392
3551
|
NotifyRequestSchema,
|
|
3393
|
-
deriveAsk,
|
|
3394
3552
|
SetTaskStateSchema,
|
|
3395
3553
|
ScheduleCallbackSchema,
|
|
3396
|
-
WAKE_EVENT,
|
|
3397
|
-
wakeChannel,
|
|
3398
|
-
BACKEND_URL,
|
|
3399
|
-
reach,
|
|
3400
3554
|
AGENT_NAME,
|
|
3401
3555
|
TOKEN_PATH,
|
|
3402
|
-
writeSurface,
|
|
3403
|
-
clearSurface,
|
|
3404
|
-
readSurface,
|
|
3405
|
-
openBrowser,
|
|
3406
3556
|
saveToken,
|
|
3407
3557
|
sleep,
|
|
3408
3558
|
readToken,
|
|
@@ -3417,10 +3567,13 @@ export {
|
|
|
3417
3567
|
finalizeE2ee,
|
|
3418
3568
|
fetchCredential,
|
|
3419
3569
|
pairStep,
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3570
|
+
UnpairedError,
|
|
3571
|
+
submitNotification,
|
|
3572
|
+
awaitReply,
|
|
3573
|
+
getThread,
|
|
3574
|
+
checkReplies,
|
|
3575
|
+
setTaskState,
|
|
3576
|
+
registerDelivery,
|
|
3577
|
+
scheduleCallback,
|
|
3578
|
+
lintNotify
|
|
3426
3579
|
};
|