@paigy/mcp 0.29.0 → 0.30.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/dist/{chunk-BJW6Z2OY.js → chunk-3QSGCROD.js} +135 -22
- package/dist/{chunk-JG2XL7Y3.js → chunk-57N3NFYG.js} +17 -2
- package/dist/{chunk-ZFBYZWK7.js → chunk-KMPAFUT6.js} +75 -1
- package/dist/index.js +111 -28
- package/dist/listen.js +2 -2
- package/dist/onboard.js +55 -3
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// ../../packages/sdk/dist/index.js
|
|
2
2
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
5
|
-
import { randomUUID } from "crypto";
|
|
6
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
6
7
|
import { homedir } from "os";
|
|
7
8
|
import { join } from "path";
|
|
8
9
|
var require2 = __sdkCreateRequire(import.meta.url);
|
|
@@ -2268,9 +2269,16 @@ async function proxy() {
|
|
|
2268
2269
|
if (!PROXY_ENV.some((k) => process.env[k])) return void 0;
|
|
2269
2270
|
return agent ??= new (await import("undici")).EnvHttpProxyAgent();
|
|
2270
2271
|
}
|
|
2272
|
+
var INSTANCE_ID = randomUUID();
|
|
2273
|
+
var SESSION_ID = process.env.PAIGY_SESSION_ID ?? process.env.CLAUDE_CODE_SESSION_ID ?? INSTANCE_ID;
|
|
2271
2274
|
async function reach(url, init) {
|
|
2272
2275
|
try {
|
|
2273
|
-
|
|
2276
|
+
const headers = {
|
|
2277
|
+
...init?.headers,
|
|
2278
|
+
"x-paigy-instance": INSTANCE_ID,
|
|
2279
|
+
"x-paigy-session": SESSION_ID
|
|
2280
|
+
};
|
|
2281
|
+
return await fetch(url, { ...init, headers, dispatcher: await proxy() });
|
|
2274
2282
|
} catch (e) {
|
|
2275
2283
|
throw new Error(`${NETWORK_MSG} (${e?.message ?? String(e)})`);
|
|
2276
2284
|
}
|
|
@@ -2333,7 +2341,9 @@ function lintNotify(req) {
|
|
|
2333
2341
|
}
|
|
2334
2342
|
var ContextSchema = z.object({
|
|
2335
2343
|
title: z.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
2336
|
-
description: z.array(z.string().min(1)).
|
|
2344
|
+
description: z.array(z.string().min(1)).describe(
|
|
2345
|
+
"Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
|
|
2346
|
+
)
|
|
2337
2347
|
});
|
|
2338
2348
|
var ParticipantSchema = z.object({
|
|
2339
2349
|
kind: z.enum(["human", "agent"]),
|
|
@@ -2538,35 +2548,45 @@ function normalizeWaiting(req) {
|
|
|
2538
2548
|
blocking: req.blocking || waiting === "hard"
|
|
2539
2549
|
};
|
|
2540
2550
|
}
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2551
|
+
function unitsOf(text) {
|
|
2552
|
+
const spans = [];
|
|
2553
|
+
const re = /\n\s*\n+/g;
|
|
2554
|
+
let cursor = 0;
|
|
2555
|
+
const push = (from, to) => {
|
|
2556
|
+
const slice = text.slice(from, to);
|
|
2557
|
+
const lead = slice.length - slice.trimStart().length;
|
|
2558
|
+
const tail = slice.length - slice.trimEnd().length;
|
|
2559
|
+
if (from + lead < to - tail) spans.push({ start: from + lead, end: to - tail });
|
|
2560
|
+
};
|
|
2561
|
+
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
2562
|
+
push(cursor, m.index);
|
|
2563
|
+
cursor = m.index + m[0].length;
|
|
2554
2564
|
}
|
|
2555
|
-
|
|
2556
|
-
return
|
|
2565
|
+
push(cursor, text.length);
|
|
2566
|
+
return spans;
|
|
2567
|
+
}
|
|
2568
|
+
function headline(text) {
|
|
2569
|
+
const line = text.split("\n").map((l) => l.trim()).filter((l) => l && !/^`{3,}/.test(l)).map((l) => l.replace(/^(?:#{1,6}|[-*>]|\d{1,3}\.)\s+/, "")).find(Boolean) ?? "";
|
|
2570
|
+
return (/^[^.!?\n]+[.!?]?/.exec(line)?.[0] ?? line).trim();
|
|
2571
|
+
}
|
|
2572
|
+
function bodyAfterHeadline(text) {
|
|
2573
|
+
const body = text.trim();
|
|
2574
|
+
const title = headline(body);
|
|
2575
|
+
const rest = body.slice(title.length).replace(/^[\s.!?—–-]+/, "").trim();
|
|
2576
|
+
if (!rest) return { title, description: [] };
|
|
2577
|
+
const chunks = unitsOf(rest).map((u) => rest.slice(u.start, u.end)).filter(Boolean);
|
|
2578
|
+
return { title, description: chunks.length ? chunks : [rest] };
|
|
2557
2579
|
}
|
|
2558
2580
|
function deriveAsk(req) {
|
|
2559
2581
|
req = normalizeWaiting(req);
|
|
2560
2582
|
if (!req.ask) return req;
|
|
2561
2583
|
const text = req.ask.trim();
|
|
2562
|
-
const firstSentence = (/^[^.!?\n]+[.!?]?/.exec(text)?.[0] ?? text).trim();
|
|
2563
|
-
const title = firstSentence.length > 90 ? `${firstSentence.slice(0, 87).trimEnd()}\u2026` : firstSentence;
|
|
2564
2584
|
const hinted = req.urgencyHint === "now" ? "call" : req.urgencyHint === "soon" ? "banner" : req.urgencyHint === "whenever" ? "inbox" : req.urgency;
|
|
2565
2585
|
const urgency = req.channel === "call" ? "call" : req.channel === "message" && hinted === "call" ? "banner" : hinted;
|
|
2566
2586
|
const { ask: _ask, needs, urgencyHint: _hint, channel: _channel, ...rest } = req;
|
|
2567
2587
|
return {
|
|
2568
2588
|
...rest,
|
|
2569
|
-
context:
|
|
2589
|
+
context: bodyAfterHeadline(text),
|
|
2570
2590
|
// Options riding alongside the ask (#575: pixels can't be prose) floor to a
|
|
2571
2591
|
// single pick — the model broker may upgrade to many/rank from the wording.
|
|
2572
2592
|
select: req.options?.length ? "one" : "text",
|
|
@@ -2820,6 +2840,11 @@ var InboxItemSchema = z.object({
|
|
|
2820
2840
|
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
2821
2841
|
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
2822
2842
|
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
2843
|
+
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
2844
|
+
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
2845
|
+
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
2846
|
+
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
2847
|
+
asks: z.boolean().optional(),
|
|
2823
2848
|
agenda: z.array(AgendaTurnSchema).optional(),
|
|
2824
2849
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
2825
2850
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -2844,6 +2869,23 @@ var InboxItemSchema = z.object({
|
|
|
2844
2869
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
2845
2870
|
error: z.string().optional(),
|
|
2846
2871
|
clarifies: z.string().optional(),
|
|
2872
|
+
/** The ring ladder ran out while this was still pending — we tried to reach you and
|
|
2873
|
+
* STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
|
|
2874
|
+
* agent with nothing to say, which the roster drew identically until now: "nothing to
|
|
2875
|
+
* say" and "gave up saying it" are opposite situations wearing the same face
|
|
2876
|
+
* (navigation-design.md, gap 1). False for anything that never rang. */
|
|
2877
|
+
gaveUp: z.boolean().default(false),
|
|
2878
|
+
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
2879
|
+
* Absent for anything never delivered through a push, and for older rows written before
|
|
2880
|
+
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
2881
|
+
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
2882
|
+
* feedback ABOUT. */
|
|
2883
|
+
why: z.object({
|
|
2884
|
+
asked: NotifyLevelSchema,
|
|
2885
|
+
got: NotifyLevelSchema,
|
|
2886
|
+
because: z.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned"]).optional(),
|
|
2887
|
+
line: z.string()
|
|
2888
|
+
}).optional(),
|
|
2847
2889
|
select: z.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
2848
2890
|
confirmStyle: z.enum(["yesno", "approve"]).default("yesno").describe(
|
|
2849
2891
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
@@ -2965,6 +3007,14 @@ var ConnectionSummarySchema = z.object({
|
|
|
2965
3007
|
provider: z.string().nullable(),
|
|
2966
3008
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
2967
3009
|
voice: VoiceKeySchema.nullable(),
|
|
3010
|
+
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
3011
|
+
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
3012
|
+
* connection). Android binds importance to a relationship rather than to each message,
|
|
3013
|
+
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
3014
|
+
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
3015
|
+
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
3016
|
+
* set once must not overrule a rule they set about one agent. */
|
|
3017
|
+
reach: NotifyLevelSchema.nullable().optional(),
|
|
2968
3018
|
createdAt: z.string().datetime(),
|
|
2969
3019
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
2970
3020
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
@@ -2983,6 +3033,40 @@ var ConnectionSummarySchema = z.object({
|
|
|
2983
3033
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
2984
3034
|
managed: z.boolean()
|
|
2985
3035
|
});
|
|
3036
|
+
var MoveRingSchema = z.enum(["home", "travels", "retired", "quarantined"]);
|
|
3037
|
+
var MoveSchema = z.object({
|
|
3038
|
+
id: z.string(),
|
|
3039
|
+
/** The reusable question, as distill normalized it. */
|
|
3040
|
+
question: z.string(),
|
|
3041
|
+
/** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
|
|
3042
|
+
answer: z.string(),
|
|
3043
|
+
/** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
|
|
3044
|
+
* told so, and the ruling only derives essentially the same question in the same scope. */
|
|
3045
|
+
rationale: z.string().nullable(),
|
|
3046
|
+
/** Where the ruling lives: a repo/workspace, or 'global'. */
|
|
3047
|
+
scope: z.string(),
|
|
3048
|
+
ring: MoveRingSchema,
|
|
3049
|
+
/** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
|
|
3050
|
+
pinned: z.boolean(),
|
|
3051
|
+
/** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
|
|
3052
|
+
* break is visible instead of a pin silently disappearing. */
|
|
3053
|
+
pinBroken: z.boolean(),
|
|
3054
|
+
/** When the ruling was distilled. */
|
|
3055
|
+
learnedAt: z.string(),
|
|
3056
|
+
/** Last time it answered an ask. Null = never fired. */
|
|
3057
|
+
lastUsedAt: z.string().nullable(),
|
|
3058
|
+
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
3059
|
+
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
3060
|
+
usedCount: z.number(),
|
|
3061
|
+
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
3062
|
+
confirms: z.number(),
|
|
3063
|
+
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
3064
|
+
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
3065
|
+
counters: z.number(),
|
|
3066
|
+
/** The agent that asked the question this move came from, when known. Null for a move
|
|
3067
|
+
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
3068
|
+
learnedFrom: z.object({ id: z.string(), name: z.string() }).nullable()
|
|
3069
|
+
});
|
|
2986
3070
|
var CreateRequestSchema = z.object({
|
|
2987
3071
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
2988
3072
|
tokenId: z.string(),
|
|
@@ -3189,6 +3273,12 @@ var DeviceTokenSchema = z.object({
|
|
|
3189
3273
|
* a default silly name). */
|
|
3190
3274
|
name: z.string(),
|
|
3191
3275
|
device: z.string().nullable(),
|
|
3276
|
+
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
3277
|
+
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
3278
|
+
voice: z.string().nullable().optional(),
|
|
3279
|
+
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
3280
|
+
* that survives a rename. Cached by the host's identity beat. */
|
|
3281
|
+
token_id: z.string().nullable().optional(),
|
|
3192
3282
|
phone_reveal: PairingRevealSchema.nullable().optional(),
|
|
3193
3283
|
// present once the phone reveals
|
|
3194
3284
|
uik_pub: z.string().nullable().optional()
|
|
@@ -3215,6 +3305,8 @@ var NotificationFeedbackKindSchema = z.enum([
|
|
|
3215
3305
|
// "There should be a picture or design here."
|
|
3216
3306
|
"should_have_called",
|
|
3217
3307
|
// "Don't put this in a banner — ring me for something like this."
|
|
3308
|
+
"should_have_messaged",
|
|
3309
|
+
// the inverse: "that didn't deserve a ring — a message would do."
|
|
3218
3310
|
"other"
|
|
3219
3311
|
// anything else — the note carries it.
|
|
3220
3312
|
]);
|
|
@@ -3557,7 +3649,7 @@ function verifyPairingCommit(commitment, proto, role, x25519Pub, ed25519Pub, non
|
|
|
3557
3649
|
}
|
|
3558
3650
|
}
|
|
3559
3651
|
function sessionSlot(sessionId) {
|
|
3560
|
-
const id = sessionId ?? process.env.PAIGY_SESSION_ID ?? process.env.CLAUDE_CODE_SESSION_ID ??
|
|
3652
|
+
const id = sessionId ?? process.env.PAIGY_SESSION_ID ?? process.env.CLAUDE_CODE_SESSION_ID ?? randomUUID2();
|
|
3561
3653
|
return `session:${id.slice(0, 8)}`;
|
|
3562
3654
|
}
|
|
3563
3655
|
var AGENT_NAME = process.env.PAIGY_AGENT || sessionSlot();
|
|
@@ -3956,6 +4048,25 @@ async function hatch(name, voice = null) {
|
|
|
3956
4048
|
if (!res.ok) throw new Error(`hatch failed: ${res.status} ${await res.text()}`);
|
|
3957
4049
|
return await res.json();
|
|
3958
4050
|
}
|
|
4051
|
+
async function whoAmI(opts = {}) {
|
|
4052
|
+
try {
|
|
4053
|
+
const res = await reach(`${BACKEND_URL}/api/identity`, {
|
|
4054
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
4055
|
+
});
|
|
4056
|
+
return res.ok ? await res.json() : null;
|
|
4057
|
+
} catch {
|
|
4058
|
+
return null;
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
async function setIdentity(patch, opts = {}) {
|
|
4062
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/identity`, {
|
|
4063
|
+
method: "PATCH",
|
|
4064
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
4065
|
+
body: JSON.stringify(patch)
|
|
4066
|
+
}));
|
|
4067
|
+
if (!res.ok) throw new Error(`set_identity failed: ${res.status} ${await res.text()}`);
|
|
4068
|
+
return await res.json();
|
|
4069
|
+
}
|
|
3959
4070
|
async function setTaskState(notificationId, state, opts = {}) {
|
|
3960
4071
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify/${notificationId}/state`, {
|
|
3961
4072
|
method: "PATCH",
|
|
@@ -4030,6 +4141,8 @@ export {
|
|
|
4030
4141
|
answerCallerQuestion,
|
|
4031
4142
|
overrideToken,
|
|
4032
4143
|
hatch,
|
|
4144
|
+
whoAmI,
|
|
4145
|
+
setIdentity,
|
|
4033
4146
|
setTaskState,
|
|
4034
4147
|
registerDelivery,
|
|
4035
4148
|
scheduleCallback,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AGENT_NAME
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-3QSGCROD.js";
|
|
4
4
|
|
|
5
5
|
// src/clients.ts
|
|
6
6
|
import { execFile } from "child_process";
|
|
@@ -112,6 +112,20 @@ function withPaigyAllowlist(existing, tools) {
|
|
|
112
112
|
config.permissions = { ...permissions, allow };
|
|
113
113
|
return JSON.stringify(config, null, 2);
|
|
114
114
|
}
|
|
115
|
+
function settingsPathFor(scope, cwd) {
|
|
116
|
+
return scope === "project" ? join(cwd, ".claude", "settings.json") : join(homedir(), ".claude", "settings.json");
|
|
117
|
+
}
|
|
118
|
+
function paigyToolsAllowlisted(scope = "user", cwd = process.cwd()) {
|
|
119
|
+
try {
|
|
120
|
+
const path = settingsPathFor(scope, cwd);
|
|
121
|
+
if (!path || !existsSync(path)) return false;
|
|
122
|
+
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
123
|
+
const allow = Array.isArray(config.permissions?.allow) ? config.permissions.allow : [];
|
|
124
|
+
return PAIGY_TOOL_IDS.every((t) => allow.includes(t));
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
115
129
|
function enablePaigyTools(scope = "user", cwd = process.cwd()) {
|
|
116
130
|
if (scope === "user" && !existsSync(join(homedir(), ".claude"))) {
|
|
117
131
|
return {
|
|
@@ -119,7 +133,7 @@ function enablePaigyTools(scope = "user", cwd = process.cwd()) {
|
|
|
119
133
|
reason: `No ~/.claude on this machine \u2014 the hosting tool uses its own permission allowlist. Add these ids there by hand: ${PAIGY_TOOL_IDS.join(", ")}.`
|
|
120
134
|
};
|
|
121
135
|
}
|
|
122
|
-
const path = scope
|
|
136
|
+
const path = settingsPathFor(scope, cwd);
|
|
123
137
|
let existing = null;
|
|
124
138
|
try {
|
|
125
139
|
existing = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
@@ -156,5 +170,6 @@ export {
|
|
|
156
170
|
autoConfigureClients,
|
|
157
171
|
claudeInstallHint,
|
|
158
172
|
PAIGY_TOOL_IDS,
|
|
173
|
+
paigyToolsAllowlisted,
|
|
159
174
|
enablePaigyTools
|
|
160
175
|
};
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
var ContextSchema = z.object({
|
|
4
4
|
title: z.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
5
|
-
description: z.array(z.string().min(1)).
|
|
5
|
+
description: z.array(z.string().min(1)).describe(
|
|
6
|
+
"Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
|
|
7
|
+
)
|
|
6
8
|
});
|
|
7
9
|
var ParticipantSchema = z.object({
|
|
8
10
|
kind: z.enum(["human", "agent"]),
|
|
@@ -444,6 +446,11 @@ var InboxItemSchema = z.object({
|
|
|
444
446
|
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
445
447
|
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
446
448
|
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
449
|
+
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
450
|
+
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
451
|
+
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
452
|
+
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
453
|
+
asks: z.boolean().optional(),
|
|
447
454
|
agenda: z.array(AgendaTurnSchema).optional(),
|
|
448
455
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
449
456
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -468,6 +475,23 @@ var InboxItemSchema = z.object({
|
|
|
468
475
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
469
476
|
error: z.string().optional(),
|
|
470
477
|
clarifies: z.string().optional(),
|
|
478
|
+
/** The ring ladder ran out while this was still pending — we tried to reach you and
|
|
479
|
+
* STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
|
|
480
|
+
* agent with nothing to say, which the roster drew identically until now: "nothing to
|
|
481
|
+
* say" and "gave up saying it" are opposite situations wearing the same face
|
|
482
|
+
* (navigation-design.md, gap 1). False for anything that never rang. */
|
|
483
|
+
gaveUp: z.boolean().default(false),
|
|
484
|
+
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
485
|
+
* Absent for anything never delivered through a push, and for older rows written before
|
|
486
|
+
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
487
|
+
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
488
|
+
* feedback ABOUT. */
|
|
489
|
+
why: z.object({
|
|
490
|
+
asked: NotifyLevelSchema,
|
|
491
|
+
got: NotifyLevelSchema,
|
|
492
|
+
because: z.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned"]).optional(),
|
|
493
|
+
line: z.string()
|
|
494
|
+
}).optional(),
|
|
471
495
|
select: z.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
472
496
|
confirmStyle: z.enum(["yesno", "approve"]).default("yesno").describe(
|
|
473
497
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
@@ -589,6 +613,14 @@ var ConnectionSummarySchema = z.object({
|
|
|
589
613
|
provider: z.string().nullable(),
|
|
590
614
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
591
615
|
voice: VoiceKeySchema.nullable(),
|
|
616
|
+
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
617
|
+
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
618
|
+
* connection). Android binds importance to a relationship rather than to each message,
|
|
619
|
+
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
620
|
+
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
621
|
+
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
622
|
+
* set once must not overrule a rule they set about one agent. */
|
|
623
|
+
reach: NotifyLevelSchema.nullable().optional(),
|
|
592
624
|
createdAt: z.string().datetime(),
|
|
593
625
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
594
626
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
@@ -607,6 +639,40 @@ var ConnectionSummarySchema = z.object({
|
|
|
607
639
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
608
640
|
managed: z.boolean()
|
|
609
641
|
});
|
|
642
|
+
var MoveRingSchema = z.enum(["home", "travels", "retired", "quarantined"]);
|
|
643
|
+
var MoveSchema = z.object({
|
|
644
|
+
id: z.string(),
|
|
645
|
+
/** The reusable question, as distill normalized it. */
|
|
646
|
+
question: z.string(),
|
|
647
|
+
/** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
|
|
648
|
+
answer: z.string(),
|
|
649
|
+
/** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
|
|
650
|
+
* told so, and the ruling only derives essentially the same question in the same scope. */
|
|
651
|
+
rationale: z.string().nullable(),
|
|
652
|
+
/** Where the ruling lives: a repo/workspace, or 'global'. */
|
|
653
|
+
scope: z.string(),
|
|
654
|
+
ring: MoveRingSchema,
|
|
655
|
+
/** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
|
|
656
|
+
pinned: z.boolean(),
|
|
657
|
+
/** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
|
|
658
|
+
* break is visible instead of a pin silently disappearing. */
|
|
659
|
+
pinBroken: z.boolean(),
|
|
660
|
+
/** When the ruling was distilled. */
|
|
661
|
+
learnedAt: z.string(),
|
|
662
|
+
/** Last time it answered an ask. Null = never fired. */
|
|
663
|
+
lastUsedAt: z.string().nullable(),
|
|
664
|
+
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
665
|
+
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
666
|
+
usedCount: z.number(),
|
|
667
|
+
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
668
|
+
confirms: z.number(),
|
|
669
|
+
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
670
|
+
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
671
|
+
counters: z.number(),
|
|
672
|
+
/** The agent that asked the question this move came from, when known. Null for a move
|
|
673
|
+
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
674
|
+
learnedFrom: z.object({ id: z.string(), name: z.string() }).nullable()
|
|
675
|
+
});
|
|
610
676
|
var CreateRequestSchema = z.object({
|
|
611
677
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
612
678
|
tokenId: z.string(),
|
|
@@ -815,6 +881,12 @@ var DeviceTokenSchema = z.object({
|
|
|
815
881
|
* a default silly name). */
|
|
816
882
|
name: z.string(),
|
|
817
883
|
device: z.string().nullable(),
|
|
884
|
+
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
885
|
+
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
886
|
+
voice: z.string().nullable().optional(),
|
|
887
|
+
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
888
|
+
* that survives a rename. Cached by the host's identity beat. */
|
|
889
|
+
token_id: z.string().nullable().optional(),
|
|
818
890
|
phone_reveal: PairingRevealSchema.nullable().optional(),
|
|
819
891
|
// present once the phone reveals
|
|
820
892
|
uik_pub: z.string().nullable().optional()
|
|
@@ -841,6 +913,8 @@ var NotificationFeedbackKindSchema = z.enum([
|
|
|
841
913
|
// "There should be a picture or design here."
|
|
842
914
|
"should_have_called",
|
|
843
915
|
// "Don't put this in a banner — ring me for something like this."
|
|
916
|
+
"should_have_messaged",
|
|
917
|
+
// the inverse: "that didn't deserve a ring — a message would do."
|
|
844
918
|
"other"
|
|
845
919
|
// anything else — the note carries it.
|
|
846
920
|
]);
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
HandoffSchema
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KMPAFUT6.js";
|
|
5
5
|
import {
|
|
6
6
|
PAIGY_TOOL_IDS,
|
|
7
7
|
autoConfigureClients,
|
|
8
8
|
claudeInstallHint,
|
|
9
|
-
enablePaigyTools
|
|
10
|
-
|
|
9
|
+
enablePaigyTools,
|
|
10
|
+
paigyToolsAllowlisted
|
|
11
|
+
} from "./chunk-57N3NFYG.js";
|
|
11
12
|
import {
|
|
12
13
|
clearSurface,
|
|
13
14
|
writeSurface
|
|
@@ -41,12 +42,14 @@ import {
|
|
|
41
42
|
saveToken,
|
|
42
43
|
scheduleCallback,
|
|
43
44
|
searchThreads,
|
|
45
|
+
setIdentity,
|
|
44
46
|
setTaskState,
|
|
45
47
|
sleep,
|
|
46
48
|
slotName,
|
|
47
49
|
startE2ee,
|
|
48
|
-
submitNotification
|
|
49
|
-
|
|
50
|
+
submitNotification,
|
|
51
|
+
whoAmI
|
|
52
|
+
} from "./chunk-3QSGCROD.js";
|
|
50
53
|
|
|
51
54
|
// src/index.ts
|
|
52
55
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -90,7 +93,7 @@ var CONTACT_SCHEMA = {
|
|
|
90
93
|
properties: {
|
|
91
94
|
ask: {
|
|
92
95
|
type: "string",
|
|
93
|
-
description: "What to tell the user, or what you need to find out from them. Plain prose \u2014 as long as it needs to be (up to 10k characters); Paigy splits it into topics and reads back a few sentences at a time, so do NOT compress a briefing into one line. May be spoken aloud on a call, so write natural speech and name things (not IDs)."
|
|
96
|
+
description: "What to tell the user, or what you need to find out from them. Plain prose \u2014 as long as it needs to be (up to 10k characters); Paigy splits it into topics and reads back a few sentences at a time, so do NOT compress a briefing into one line. May be spoken aloud on a call, so write natural speech and name things (not IDs). Contact at exactly two moments: BLOCKED on a decision only they can make, or DONE (one short report \u2014 what shipped, how you verified it, what you flagged). Progress is never a contact: set_task_state carries it, and working narration stays in your own terminal \u2014 the user sees you're working without being interrupted by it."
|
|
94
97
|
},
|
|
95
98
|
waiting: {
|
|
96
99
|
type: "string",
|
|
@@ -126,6 +129,7 @@ var CONTACT_SCHEMA = {
|
|
|
126
129
|
required: ["ask"]
|
|
127
130
|
};
|
|
128
131
|
var CONTACT_DESCRIPTION = "Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in `ask`, say what happens to your work while you wait in `waiting`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId } \u2014 pass notificationId to await_reply for the answer, parentId to a later contact to continue the conversation. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use `needs` for a genuinely multi-part NEW ask.";
|
|
132
|
+
var ONBOARD_DESCRIPTION = "Get this agent talking to Paigy \u2014 call it FIRST, before contact/await_reply, and any time you're unsure who you are. One call, and it does whatever the situation needs: NOT SET UP \u2192 hatches an identity instantly if this machine holds a device credential (the user ran the Paigy desktop app or harness), otherwise starts the code ceremony; ALREADY SET UP \u2192 returns your current identity and offers the two things left to decide, renaming it or unpairing; TOKEN NO LONGER VALID \u2192 says so, then re-pairs. Pass { name, voice } to choose who you are when hatching, or to RENAME yourself when already set up (voices: rachel, george, jessica, brian, lily). Safe to call any time: idempotent, and it never writes settings \u2014 the tool-allowlist state it reports is read-only. If it returns a `user_code`, print it to the user immediately and call onboard again with the `device_code`. If it returns `enable_prompt`, ask the user that question and call `enable_tools` on yes.";
|
|
129
133
|
|
|
130
134
|
// src/pairing.ts
|
|
131
135
|
async function resolvePairing(deviceCode, capMs, pollMs = 2e3) {
|
|
@@ -233,6 +237,14 @@ var AwaitReplySchema = z.object({
|
|
|
233
237
|
)
|
|
234
238
|
});
|
|
235
239
|
var JOIN_CAP_MS = 45e3;
|
|
240
|
+
var OnboardSchema = z.object({
|
|
241
|
+
name: z.string().max(60).optional(),
|
|
242
|
+
voice: z.string().max(40).optional(),
|
|
243
|
+
/** Continue a code ceremony already in flight — same meaning as `pair`'s. */
|
|
244
|
+
device_code: z.string().optional(),
|
|
245
|
+
/** Which allowlist to REPORT on (never written by `start`). */
|
|
246
|
+
scope: z.enum(["user", "project"]).optional()
|
|
247
|
+
});
|
|
236
248
|
var PairSchema = z.object({
|
|
237
249
|
device_code: z.string().optional().describe("Omit to start pairing (returns an approval link to show the user). Pass the device_code from that first call to finish, once the user has approved."),
|
|
238
250
|
name: z.string().min(1).max(60).optional().describe("Hatch path only: the name you choose for this identity. Pick your own \u2014 ONE or TWO words, the way you'd introduce yourself on a call (it is spoken aloud and shown in lists). 'Piper', 'Blue Heron' \u2014 never a sentence or a task description."),
|
|
@@ -378,6 +390,35 @@ var server = new Server(
|
|
|
378
390
|
instructions: "On startup, call check_replies once to pick up any replies or pending work you missed while away. A check_replies request whose parentId you don't recognize, or one carrying a contextParentId, means the user is resuming or seeding a past conversation \u2014 call get_thread on it FIRST and treat the transcript as prior conversation, not new input. To wait for the answer to something you just asked, call await_reply with that notificationId \u2014 it's scoped to that one notification, so it never returns replies meant for other notifications. Use check_replies again only when re-booting or after waiting a long time on something else. Never end a turn that still needs the user without contact + await_reply. When you need a decision or input, MATCH the answer shape to the question \u2014 don't default everything to free text, and don't reflexively make everything yes/no. Pick the best tool for the job: yes/no \u2192 select:'confirm'; approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve'; pick one of several \u2192 options + select:'one'; pick several / a subset \u2192 options + select:'many'; rank or prioritize \u2192 options + select:'rank'. Reserve select:'text' (free-form reply only) for plain updates and answers that genuinely can't be structured (the user can always add free text on top of any shape). On a { kind: 'clarify' } reply, see contact's own description for how to respond. When you send waiting:'hard' (or the user asked you to call), remember the ask may be spoken aloud \u2014 write it short and conversational, and name things instead of using IDs (e.g. 'the pull request about the agents page', not 'PR #235'). When the user asks you to follow up later \u2014 when you're done, if you're blocked, or at a set time \u2014 record it with schedule_callback so you don't drop it if you go idle. If you're about to start a genuinely long-running or blocking piece of work \u2014 one where the user would otherwise sit and wait \u2014 mention ONCE, in passing, that you can reach them when it's done or if you hit a blocker, instead of them needing to babysit the terminal. Don't offer this for quick tasks, and don't repeat the offer if they've already said yes or no earlier in the conversation. NEVER go quietly idle while something might still be pending for you: whenever you end a turn with any Paigy notification unanswered (or any chance the user replied through the app while you worked), schedule your own ~2-minute wake-up (harness ScheduleWakeup or equivalent) and call check_replies when it fires; if still nothing, re-schedule and keep looping until resolved or the user says stop. For legibility, always use this exact wording \u2014 reason: 'Paigy idle check \u2014 waiting on <thing>', wake-up prompt: 'Paigy idle check: call check_replies and engage with anything unacknowledged; if idle, re-schedule (~2min).' \u2014 so the user can recognize every idle check at a glance. This self-polling in your own live session (full context intact) is the PRIMARY mechanism; the plugin's Stop hooks are only the dead-session safety net."
|
|
379
391
|
}
|
|
380
392
|
);
|
|
393
|
+
function prependNote(result, note) {
|
|
394
|
+
return { content: [{ type: "text", text: note }, ...result.content] };
|
|
395
|
+
}
|
|
396
|
+
async function runPair(device_code, name, voice, note) {
|
|
397
|
+
if (!device_code && listSlots().includes("Desktop")) {
|
|
398
|
+
const device = readToken("Desktop");
|
|
399
|
+
overrideToken(device);
|
|
400
|
+
try {
|
|
401
|
+
const minted = await hatch(name ?? suggestedAgentName() ?? "Agent", voice ?? null);
|
|
402
|
+
const dt = { ok: true, access_token: minted.token, name: minted.name, device: null };
|
|
403
|
+
saveToken(dt);
|
|
404
|
+
return pairedResult(
|
|
405
|
+
dt,
|
|
406
|
+
void 0,
|
|
407
|
+
"Hatched instantly under this device's credential \u2014 no code needed. " + (name ? "" : "You were given a default name \u2014 choose your own name and voice and update them via the identity tools or by re-calling pair with { name, voice }.")
|
|
408
|
+
);
|
|
409
|
+
} catch {
|
|
410
|
+
} finally {
|
|
411
|
+
overrideToken(null);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (!device_code) {
|
|
415
|
+
const started2 = pairStartResult(await startPairing(suggestedAgentName()));
|
|
416
|
+
return note ? prependNote(started2, note) : started2;
|
|
417
|
+
}
|
|
418
|
+
const capMs = JOIN_CAP_MS;
|
|
419
|
+
const outcome = await joinBackgroundPair(device_code, capMs) ?? await resolvePairing(device_code, capMs);
|
|
420
|
+
return renderPairOutcome(outcome, device_code);
|
|
421
|
+
}
|
|
381
422
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
382
423
|
const tools = [
|
|
383
424
|
{
|
|
@@ -388,6 +429,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
388
429
|
description: CONTACT_DESCRIPTION,
|
|
389
430
|
inputSchema: CONTACT_SCHEMA
|
|
390
431
|
},
|
|
432
|
+
{
|
|
433
|
+
// ONE rail for starting a session (#875). `pair`, the `paigy-mcp-onboard` CLI and
|
|
434
|
+
// `enable_tools` were three doors into one flow, and "onboard" was the word people
|
|
435
|
+
// reached for attached to the tool that did the least. It's the door now.
|
|
436
|
+
name: "onboard",
|
|
437
|
+
description: ONBOARD_DESCRIPTION,
|
|
438
|
+
inputSchema: json(OnboardSchema)
|
|
439
|
+
},
|
|
391
440
|
{
|
|
392
441
|
name: "pair",
|
|
393
442
|
description: "Pair this agent with the user's Paigy account (one-time) \u2014 required before contact/await_reply work. FAST PATH: if this machine already holds a device credential (the user ran the Paigy desktop harness or app), calling pair hatches a fresh identity INSTANTLY \u2014 no code, no approval. Pass { name, voice } to choose who you are (pick your own; voices: rachel, george, jessica, brian, lily). Only when no device credential exists does the code ceremony below run. It does NOT open a browser; the user enters the code in the Paigy app (or scans `qr`). Step 1: call with NO args \u2014 returns { user_code, device_code, qr, user_message } AND starts polling for approval in the background. REQUIRED: You MUST immediately print the `user_message` (the bare code) as a text message to the user, AND in that same turn call step 2 (pair with the device_code). This ensures the user sees the code in chat while the tool blocks/polls in the background for approval. Step 2: call with that device_code to collect the result. Because approval is already being polled in the background, this returns the moment the user approves; on { status:'pending' } just call again to keep waiting; on { status:'awaiting_confirmation' } (E2EE) show the bare `user_message` verify code and call again to finish. The leading text block of every result states the code plainly, so it shows even if you emit no prose. On { status:'paired' } ALWAYS follow the `enable_prompt` \u2014 ask the user to allowlist Paigy's tools so notify/await don't prompt each time.",
|
|
@@ -479,30 +528,64 @@ function suggestedAgentName() {
|
|
|
479
528
|
async function handleTool(request, signal) {
|
|
480
529
|
switch (request.params.name) {
|
|
481
530
|
case "pair": {
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
531
|
+
const a = PairSchema.parse(request.params.arguments ?? {});
|
|
532
|
+
return runPair(a.device_code, a.name, a.voice);
|
|
533
|
+
}
|
|
534
|
+
// ONE rail for "get this agent talking to Paigy" (#875). `pair`, the `paigy-mcp-onboard`
|
|
535
|
+
// CLI and `enable_tools` were three doors into one flow, each knowing a different slice
|
|
536
|
+
// of the state — so "onboard" was the word people reached for and the tool that did the
|
|
537
|
+
// least, and a half-configured agent (paired but not allowlisted, or holding a token for
|
|
538
|
+
// a slot it can no longer resolve) had nothing to ask.
|
|
539
|
+
//
|
|
540
|
+
// It reports state and does the next thing, and it is IDEMPOTENT: calling it when
|
|
541
|
+
// everything is fine costs one round trip and returns who you are. Reporting never
|
|
542
|
+
// mutates — the allowlist check is read-only on purpose, because a status call that
|
|
543
|
+
// quietly edits the user's settings is the opposite of what this is for.
|
|
544
|
+
// `start` stays accepted, unlisted — it shipped under that name for one afternoon.
|
|
545
|
+
case "start":
|
|
546
|
+
case "onboard": {
|
|
547
|
+
const { name, voice, device_code, scope } = OnboardSchema.parse(request.params.arguments ?? {});
|
|
548
|
+
if (device_code) return runPair(device_code, name, voice);
|
|
549
|
+
const slot = AGENT_NAME;
|
|
550
|
+
const secret = readToken();
|
|
551
|
+
if (secret) {
|
|
552
|
+
let who = await whoAmI().catch(() => null);
|
|
553
|
+
if (who) {
|
|
554
|
+
let renamed = false;
|
|
555
|
+
if (name || voice) {
|
|
556
|
+
try {
|
|
557
|
+
who = await setIdentity({ ...name ? { name } : {}, ...voice ? { voice } : {} });
|
|
558
|
+
renamed = true;
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const allowlisted = paigyToolsAllowlisted(scope ?? "user");
|
|
563
|
+
const display = who?.name ?? slotName(slot) ?? slot;
|
|
564
|
+
return {
|
|
565
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
566
|
+
status: "ready",
|
|
567
|
+
slot,
|
|
568
|
+
name: display,
|
|
569
|
+
voice: who?.voice ?? null,
|
|
570
|
+
tools_allowlisted: allowlisted,
|
|
571
|
+
...allowlisted ? {} : {
|
|
572
|
+
enable_tool_ids: PAIGY_TOOL_IDS,
|
|
573
|
+
enable_prompt: "Ask the user: \"Want me to let Paigy's tools run without an approval prompt each time? I'll only allowlist the notify/await tools \u2014 pair and unpair still ask.\" On yes, call `enable_tools`."
|
|
574
|
+
},
|
|
575
|
+
// Setup is done, so the only things left are the user's to decide. Named here
|
|
576
|
+
// rather than left implicit: an agent that reports "ready" and stops gives the
|
|
577
|
+
// user no way to act on an identity they may not want.
|
|
578
|
+
actions: {
|
|
579
|
+
rename: "Call onboard again with { name } (and optionally { voice }) to change it.",
|
|
580
|
+
unpair: "Call the `unpair` tool to disconnect this agent from the account."
|
|
581
|
+
},
|
|
582
|
+
message: renamed ? `Renamed \u2014 this agent is "${display}" now.` : `Already set up as "${display}". Ask the user if they want to rename it or unpair; otherwise carry on.`
|
|
583
|
+
}) }]
|
|
584
|
+
};
|
|
498
585
|
}
|
|
586
|
+
return runPair(void 0, name, voice, "The token in this session's slot is no longer valid \u2014 re-pairing.");
|
|
499
587
|
}
|
|
500
|
-
|
|
501
|
-
return pairStartResult(await startPairing(suggestedAgentName()));
|
|
502
|
-
}
|
|
503
|
-
const capMs = JOIN_CAP_MS;
|
|
504
|
-
const outcome = await joinBackgroundPair(device_code, capMs) ?? await resolvePairing(device_code, capMs);
|
|
505
|
-
return renderPairOutcome(outcome, device_code);
|
|
588
|
+
return runPair(void 0, name, voice);
|
|
506
589
|
}
|
|
507
590
|
case "unpair": {
|
|
508
591
|
const token = readToken();
|
package/dist/listen.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
WAKE_EVENT,
|
|
4
4
|
wakeChannel
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-KMPAFUT6.js";
|
|
6
6
|
import {
|
|
7
7
|
checkReplies,
|
|
8
8
|
registerDelivery
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-3QSGCROD.js";
|
|
10
10
|
|
|
11
11
|
// src/listen.ts
|
|
12
12
|
import { createClient } from "@supabase/supabase-js";
|
package/dist/onboard.js
CHANGED
|
@@ -3,21 +3,73 @@ import {
|
|
|
3
3
|
autoConfigureClients,
|
|
4
4
|
claudeInstallHint,
|
|
5
5
|
openBrowser
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-57N3NFYG.js";
|
|
7
7
|
import {
|
|
8
8
|
AGENT_NAME,
|
|
9
9
|
TOKEN_PATH,
|
|
10
|
+
hatch,
|
|
11
|
+
listSlots,
|
|
12
|
+
overrideToken,
|
|
10
13
|
pollToken,
|
|
14
|
+
readToken,
|
|
11
15
|
requestCode,
|
|
12
16
|
saveToken,
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
setIdentity,
|
|
18
|
+
sleep,
|
|
19
|
+
whoAmI
|
|
20
|
+
} from "./chunk-3QSGCROD.js";
|
|
15
21
|
|
|
16
22
|
// src/onboard.ts
|
|
17
23
|
async function main() {
|
|
18
24
|
console.log(`
|
|
19
25
|
Paigy MCP onboarding (agent: "${AGENT_NAME}")
|
|
20
26
|
`);
|
|
27
|
+
if (readToken()) {
|
|
28
|
+
let who = await whoAmI().catch(() => null);
|
|
29
|
+
if (who) {
|
|
30
|
+
const wanted = process.env.PAIGY_NAME;
|
|
31
|
+
const wantedVoice = process.env.PAIGY_VOICE;
|
|
32
|
+
if (wanted || wantedVoice) {
|
|
33
|
+
try {
|
|
34
|
+
who = await setIdentity({ ...wanted ? { name: wanted } : {}, ...wantedVoice ? { voice: wantedVoice } : {} });
|
|
35
|
+
console.log(`Renamed \u2014 this agent is "${who.name}"${who.voice ? ` (voice: ${who.voice})` : ""} now.
|
|
36
|
+
`);
|
|
37
|
+
return;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.log(`Couldn't rename it: ${err.message}
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
console.log(`Already set up as "${who.name}"${who.voice ? ` (voice: ${who.voice})` : ""}.
|
|
44
|
+
`);
|
|
45
|
+
console.log("Nothing to do. What's left is yours to decide:");
|
|
46
|
+
console.log(' rename \u2014 PAIGY_NAME="New name" paigy-mcp-onboard (or the `onboard` MCP tool)');
|
|
47
|
+
console.log(" unpair \u2014 the MCP `unpair` tool, or delete this slot from ~/.paigy/token.json\n");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log("The saved token is no longer valid \u2014 re-pairing.\n");
|
|
51
|
+
}
|
|
52
|
+
if (listSlots().includes("Desktop")) {
|
|
53
|
+
overrideToken(readToken("Desktop"));
|
|
54
|
+
try {
|
|
55
|
+
const minted = await hatch(process.env.PAIGY_NAME ?? "Agent", null);
|
|
56
|
+
saveToken({ ok: true, access_token: minted.token, name: minted.name, device: null });
|
|
57
|
+
console.log(`Hatched instantly under this machine's credential \u2014 no code needed.`);
|
|
58
|
+
console.log(` Name : ${minted.name}
|
|
59
|
+
`);
|
|
60
|
+
const auto = autoConfigureClients();
|
|
61
|
+
if (auto.length) {
|
|
62
|
+
console.log("Also registered the Paigy MCP server in:");
|
|
63
|
+
for (const r of auto) console.log(` - ${r}`);
|
|
64
|
+
console.log("(Restart those tools to pick it up.)\n");
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
} catch {
|
|
68
|
+
console.log("(This machine's saved credential didn't work \u2014 falling back to a code.)\n");
|
|
69
|
+
} finally {
|
|
70
|
+
overrideToken(null);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
21
73
|
const code = await requestCode();
|
|
22
74
|
console.log("Open this URL in your browser to approve the connection:\n");
|
|
23
75
|
console.log(` ${code.verification_uri_complete}
|
package/dist/statusline.js
CHANGED