@paigy/mcp 0.13.0 → 0.14.1
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.
|
@@ -2597,6 +2597,15 @@ var MissedCallSchema = z.enum([
|
|
|
2597
2597
|
"inbox",
|
|
2598
2598
|
"dismiss"
|
|
2599
2599
|
]);
|
|
2600
|
+
var BrokerTuningSchema = z.object({
|
|
2601
|
+
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
2602
|
+
ackVerbosity: z.enum(["normal", "none"]).optional(),
|
|
2603
|
+
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
2604
|
+
* uninterpretable, 'high' = whenever not fully certain. */
|
|
2605
|
+
clarifyEagerness: z.enum(["low", "normal", "high"]).optional(),
|
|
2606
|
+
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
2607
|
+
phrasebook: z.array(z.object({ say: z.string().min(1).max(60), mean: z.string().min(1).max(120) })).max(24).optional()
|
|
2608
|
+
});
|
|
2600
2609
|
var UserSettingsSchema = z.object({
|
|
2601
2610
|
permissions: z.object({
|
|
2602
2611
|
call: z.boolean(),
|
|
@@ -2622,7 +2631,10 @@ var UserSettingsSchema = z.object({
|
|
|
2622
2631
|
* settings object without it can't silently flip the account's E2EE state.
|
|
2623
2632
|
* Absent = leave unchanged on write, 'off' on read (see store.ts). The demo
|
|
2624
2633
|
* account is plaintext by construction and refuses any non-'off' value. */
|
|
2625
|
-
e2eeMode: z.enum(["off", "on"]).optional()
|
|
2634
|
+
e2eeMode: z.enum(["off", "on"]).optional(),
|
|
2635
|
+
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
2636
|
+
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
2637
|
+
broker: BrokerTuningSchema.optional()
|
|
2626
2638
|
});
|
|
2627
2639
|
var HistoryItemSchema = z.object({
|
|
2628
2640
|
id: z.string(),
|
|
@@ -3091,6 +3103,32 @@ var PROTO = "paigy-pair-v2|sas=24";
|
|
|
3091
3103
|
var SAS_BITS = 24;
|
|
3092
3104
|
var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
|
|
3093
3105
|
var KEY_PATH = join(homedir(), ".paigy", "key.json");
|
|
3106
|
+
var SURFACE_PATH = join(homedir(), ".paigy", "surface.json");
|
|
3107
|
+
function writeSurface(label, code, ttlSeconds) {
|
|
3108
|
+
try {
|
|
3109
|
+
mkdirSync(join(homedir(), ".paigy"), { recursive: true });
|
|
3110
|
+
writeFileSync(
|
|
3111
|
+
SURFACE_PATH,
|
|
3112
|
+
JSON.stringify({ label, code, expiresAt: Date.now() + ttlSeconds * 1e3 }),
|
|
3113
|
+
{ mode: 384 }
|
|
3114
|
+
);
|
|
3115
|
+
} catch {
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
function clearSurface() {
|
|
3119
|
+
try {
|
|
3120
|
+
rmSync(SURFACE_PATH, { force: true });
|
|
3121
|
+
} catch {
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
function readSurface(now = Date.now) {
|
|
3125
|
+
try {
|
|
3126
|
+
const { label, code, expiresAt } = JSON.parse(readFileSync(SURFACE_PATH, "utf8"));
|
|
3127
|
+
return label && code && typeof expiresAt === "number" && expiresAt > now() ? { label, code } : null;
|
|
3128
|
+
} catch {
|
|
3129
|
+
return null;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3094
3132
|
function openBrowser(url) {
|
|
3095
3133
|
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
3096
3134
|
const args = platform() === "win32" ? ["/c", "start", url] : [url];
|
|
@@ -3290,6 +3328,9 @@ export {
|
|
|
3290
3328
|
reach,
|
|
3291
3329
|
AGENT_NAME,
|
|
3292
3330
|
TOKEN_PATH,
|
|
3331
|
+
writeSurface,
|
|
3332
|
+
clearSurface,
|
|
3333
|
+
readSurface,
|
|
3293
3334
|
openBrowser,
|
|
3294
3335
|
saveToken,
|
|
3295
3336
|
sleep,
|
package/dist/index.js
CHANGED
|
@@ -6,16 +6,16 @@ import {
|
|
|
6
6
|
scheduleCallback,
|
|
7
7
|
setTaskState,
|
|
8
8
|
submitNotification
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-OSQ3UXY6.js";
|
|
10
10
|
import {
|
|
11
11
|
NotifyRequestSchema,
|
|
12
12
|
ScheduleCallbackSchema,
|
|
13
13
|
SetTaskStateSchema,
|
|
14
|
+
clearSurface,
|
|
14
15
|
deleteKeyFile,
|
|
15
16
|
deleteToken,
|
|
16
17
|
fetchCredential,
|
|
17
18
|
finalizeE2ee,
|
|
18
|
-
openBrowser,
|
|
19
19
|
pairStep,
|
|
20
20
|
readKeyFile,
|
|
21
21
|
readToken,
|
|
@@ -24,8 +24,9 @@ import {
|
|
|
24
24
|
saveKeyFile,
|
|
25
25
|
saveToken,
|
|
26
26
|
sleep,
|
|
27
|
-
startE2ee
|
|
28
|
-
|
|
27
|
+
startE2ee,
|
|
28
|
+
writeSurface
|
|
29
|
+
} from "./chunk-WGOGS2JC.js";
|
|
29
30
|
|
|
30
31
|
// src/index.ts
|
|
31
32
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -137,6 +138,7 @@ function detectGit() {
|
|
|
137
138
|
};
|
|
138
139
|
}
|
|
139
140
|
function pairedResult(token, sas, note, e2ee) {
|
|
141
|
+
clearSurface();
|
|
140
142
|
const base = {
|
|
141
143
|
status: "paired",
|
|
142
144
|
nickname: token.nickname,
|
|
@@ -152,6 +154,7 @@ function pairedResult(token, sas, note, e2ee) {
|
|
|
152
154
|
return { content: [{ type: "text", text: JSON.stringify(base) }] };
|
|
153
155
|
}
|
|
154
156
|
function awaitingConfirmResult(sas, device_code) {
|
|
157
|
+
writeSurface("verify code", sas, 600);
|
|
155
158
|
return {
|
|
156
159
|
content: [{
|
|
157
160
|
type: "text",
|
|
@@ -160,8 +163,9 @@ function awaitingConfirmResult(sas, device_code) {
|
|
|
160
163
|
e2ee: true,
|
|
161
164
|
sas,
|
|
162
165
|
device_code,
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
user_message: `# ${sas}`,
|
|
167
|
+
verify_message: `Send \`user_message\` to the user VERBATIM as your entire next message \u2014 it is pre-composed; do not rewrite or wrap it. YOUR NEXT MESSAGE TO THE USER MUST BE EXACTLY THIS CODE AND NOTHING ELSE, displayed large (a markdown heading): ${sas} \u2014 it is the verification code they must compare and type on their phone. Do not wrap it in explanation; every extra sentence buries it. This code REPLACES any earlier one (older pairing attempts are now stale).`,
|
|
168
|
+
message: "No token is issued until the user confirms the code match on their phone (E2EE account). After sending the bare code, call pair again with this device_code to keep waiting \u2014 the token arrives only after they confirm."
|
|
165
169
|
})
|
|
166
170
|
}]
|
|
167
171
|
};
|
|
@@ -187,7 +191,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
187
191
|
},
|
|
188
192
|
{
|
|
189
193
|
name: "notify_user",
|
|
190
|
-
description: "Notify the user via Paigy. Returns { notificationId, threadId } \u2014 pass notificationId to await_reply for the answer, threadId to notify_user to continue the conversation. SIMPLEST FORM \u2014 just state what you need: { ask: \"I need to know whether to deploy the auth fix \u2014 tests are green, staging verified\", urgencyHint: 'now'|'soon'|'whenever', needs?: [\"deploy?\", \"keep the flag?\"] }. Paigy's broker derives the title, answer shape, options, and delivery channel for you \u2014 prefer this unless you specifically need to control the exact options/shape. The fully-shaped form below remains available and unchanged (the two are mutually exclusive: send `ask` OR `context`+`select`). FULLY-SHAPED FORM: provide context.title (a specific, non-empty one-line headline \u2014 this is what the user sees first, and what shows on the ring for a call) and context.description (an array of standalone, non-empty detail chunks the user can selectively ask you to expand). Set `urgency`: 'inbox' (default) drops it silently in their inbox; 'push' is a quiet passive notification (no sound); 'banner' sends a time-sensitive banner/lock-screen push (a 'paige') they tap to open \u2014 for when you need them soon-ish but not enough to ring them; 'call' rings their phone now as a voice call \u2014 only when you genuinely need them in the moment (blocked/waiting, time-sensitive). This is the premier use case for Paigy: getting UNBLOCKED so you can keep working, not just reporting that you're stuck. The user started a big task and went to do something else \u2014 the cardinal failure is going idle on one small decision and silently waiting to be checked on, so they come back to find you never actually progressed. Judge urgency by how much WORK IS BLOCKED behind this decision (a lot of dependent downstream work \u2192 call, even if it's a single question), not by how many things happen to be pending \u2014 one blocking decision outweighs five independent low-stakes ones sitting in the inbox. Set `blocking: true` whenever that's the case, independent of `urgency` \u2014 it's what lets Paigy escalate this to a real call later on its own if it goes unanswered, even if you sent it at a lower urgency. Don't set it for things you could work around, defer, or where other useful work exists meanwhile. ON A CALL, your title + description are READ ALOUD by a voice \u2014 write them to be HEARD, not read: keep it short and conversational, front-load the ask, and refer to things BY NAME, not by ID or code (say 'the pull request about the agents page', not 'PR #235'; 'the login-bug ticket', not 'ABC-1234'). Spell out only what's natural to say out loud. MATCH the answer shape to the question \u2014 `select` is required; pick the best tool for the job, not always yes/no. The user can ALWAYS add free text on top of any shape, so structuring loses nothing. Choose `select`: yes/no \u2192 select:'confirm' \u2192 {kind:'confirm', approved:boolean}. Approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve' \u2192 {kind:'confirm', approved:boolean}. Both are answerable right from the banner \u2014 no need to open the app. Pick one of several \u2192 options + select:'one' \u2192 {kind:'option', optionId}. Pick several / a subset \u2192 options + select:'many' \u2192 {kind:'multi', optionIds:[...]}. Rank or prioritize \u2192 options + select:'rank', user taps in preferred order \u2192 {kind:'ranked', optionIds:[...]}. One/many/rank/text all need the user to open the app to answer \u2014 only confirm is answerable straight from the banner. Options carry no ids \u2014 they're assigned by position ('1', '2', \u2026), and the answer's optionId(s) are those positions. For visual choices give each option a sandboxed `html` or an `image` preview (e.g. layout/UI alternatives); use `visuals` for images that set context for the whole question. select:'text' = free-form reply only (plain updates, or answers that genuinely can't be structured). If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail on those chunks \u2014 respond via notify_user with the SAME threadId and an expanded description. Pass `threadId` from a prior notify_user result or an await_reply reply to continue that conversation thread; omit it to start a new one. To follow up on a call (e.g. the user asked you to 'call me back when it's done'), reuse the threadId from that call's reply so it threads as the same conversation.",
|
|
194
|
+
description: "Notify the user via Paigy. Returns { notificationId, threadId } \u2014 pass notificationId to await_reply for the answer, threadId to notify_user to continue the conversation. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread (they leave the user's inbox; the response reports supersededCount) \u2014 right for updates to one ask, WRONG for a checklist: send parallel to-dos as separate un-threaded notifications. SIMPLEST FORM \u2014 just state what you need: { ask: \"I need to know whether to deploy the auth fix \u2014 tests are green, staging verified\", urgencyHint: 'now'|'soon'|'whenever', needs?: [\"deploy?\", \"keep the flag?\"] }. Paigy's broker derives the title, answer shape, options, and delivery channel for you \u2014 prefer this unless you specifically need to control the exact options/shape. The fully-shaped form below remains available and unchanged (the two are mutually exclusive: send `ask` OR `context`+`select`). FULLY-SHAPED FORM: provide context.title (a specific, non-empty one-line headline \u2014 this is what the user sees first, and what shows on the ring for a call) and context.description (an array of standalone, non-empty detail chunks the user can selectively ask you to expand). Set `urgency`: 'inbox' (default) drops it silently in their inbox; 'push' is a quiet passive notification (no sound); 'banner' sends a time-sensitive banner/lock-screen push (a 'paige') they tap to open \u2014 for when you need them soon-ish but not enough to ring them; 'call' rings their phone now as a voice call \u2014 only when you genuinely need them in the moment (blocked/waiting, time-sensitive). This is the premier use case for Paigy: getting UNBLOCKED so you can keep working, not just reporting that you're stuck. The user started a big task and went to do something else \u2014 the cardinal failure is going idle on one small decision and silently waiting to be checked on, so they come back to find you never actually progressed. Judge urgency by how much WORK IS BLOCKED behind this decision (a lot of dependent downstream work \u2192 call, even if it's a single question), not by how many things happen to be pending \u2014 one blocking decision outweighs five independent low-stakes ones sitting in the inbox. Set `blocking: true` whenever that's the case, independent of `urgency` \u2014 it's what lets Paigy escalate this to a real call later on its own if it goes unanswered, even if you sent it at a lower urgency. Don't set it for things you could work around, defer, or where other useful work exists meanwhile. ON A CALL, your title + description are READ ALOUD by a voice \u2014 write them to be HEARD, not read: keep it short and conversational, front-load the ask, and refer to things BY NAME, not by ID or code (say 'the pull request about the agents page', not 'PR #235'; 'the login-bug ticket', not 'ABC-1234'). Spell out only what's natural to say out loud. MATCH the answer shape to the question \u2014 `select` is required; pick the best tool for the job, not always yes/no. The user can ALWAYS add free text on top of any shape, so structuring loses nothing. Choose `select`: yes/no \u2192 select:'confirm' \u2192 {kind:'confirm', approved:boolean}. Approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve' \u2192 {kind:'confirm', approved:boolean}. Both are answerable right from the banner \u2014 no need to open the app. Pick one of several \u2192 options + select:'one' \u2192 {kind:'option', optionId}. Pick several / a subset \u2192 options + select:'many' \u2192 {kind:'multi', optionIds:[...]}. Rank or prioritize \u2192 options + select:'rank', user taps in preferred order \u2192 {kind:'ranked', optionIds:[...]}. One/many/rank/text all need the user to open the app to answer \u2014 only confirm is answerable straight from the banner. Options carry no ids \u2014 they're assigned by position ('1', '2', \u2026), and the answer's optionId(s) are those positions. For visual choices give each option a sandboxed `html` or an `image` preview (e.g. layout/UI alternatives); use `visuals` for images that set context for the whole question. select:'text' = free-form reply only (plain updates, or answers that genuinely can't be structured). If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail on those chunks \u2014 respond via notify_user with the SAME threadId and an expanded description. Pass `threadId` from a prior notify_user result or an await_reply reply to continue that conversation thread; omit it to start a new one. To follow up on a call (e.g. the user asked you to 'call me back when it's done'), reuse the threadId from that call's reply so it threads as the same conversation.",
|
|
191
195
|
inputSchema: json(NotifyRequestSchema)
|
|
192
196
|
},
|
|
193
197
|
{
|
|
@@ -220,7 +224,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
220
224
|
const { keyFile, offer } = startE2ee();
|
|
221
225
|
const code = await requestCode(void 0, offer);
|
|
222
226
|
saveKeyFile({ ...keyFile, userCode: code.user_code });
|
|
223
|
-
|
|
227
|
+
writeSurface("pairing code", code.user_code, code.expires_in);
|
|
224
228
|
const qr = qrcode(0, "M");
|
|
225
229
|
qr.addData(code.verification_uri_complete);
|
|
226
230
|
qr.make();
|
|
@@ -234,7 +238,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
234
238
|
device_code: code.device_code,
|
|
235
239
|
expires_in: code.expires_in,
|
|
236
240
|
qr: qr.createASCII(1, 2),
|
|
237
|
-
|
|
241
|
+
user_message: `# ${code.user_code}
|
|
242
|
+
|
|
243
|
+
Enter it in the Paigy app (Inbox \u2192 Add a new agent).`,
|
|
244
|
+
message: `Send \`user_message\` to the user VERBATIM as your entire next message \u2014 it is pre-composed; do not rewrite or wrap it. YOUR NEXT MESSAGE TO THE USER MUST BE EXACTLY THIS CODE displayed large (a markdown heading): ${code.user_code} \u2014 plus ONE short line: enter it in the Paigy app (Inbox \u2192 Add a new agent). No other prose. Do NOT open a browser for the user. If their phone is handy they may prefer scanning \`qr\` (print it verbatim in a fenced code block on request). Then immediately call pair again with this device_code \u2014 it polls for the approval.`
|
|
238
245
|
})
|
|
239
246
|
}]
|
|
240
247
|
};
|
|
@@ -243,7 +250,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
243
250
|
const start = Date.now();
|
|
244
251
|
const capMs = 9e4;
|
|
245
252
|
while (Date.now() - start < capMs) {
|
|
246
|
-
|
|
253
|
+
let step;
|
|
254
|
+
try {
|
|
255
|
+
step = await pairStep(device_code, kf);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
clearSurface();
|
|
258
|
+
throw e;
|
|
259
|
+
}
|
|
247
260
|
if (step.kind === "e2ee_aborted") {
|
|
248
261
|
deleteKeyFile();
|
|
249
262
|
kf = null;
|
package/dist/listen.js
CHANGED
|
@@ -3,11 +3,11 @@ import { createRequire as __createRequire } from 'node:module'; const require =
|
|
|
3
3
|
import {
|
|
4
4
|
checkReplies,
|
|
5
5
|
registerDelivery
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-OSQ3UXY6.js";
|
|
7
7
|
import {
|
|
8
8
|
WAKE_EVENT,
|
|
9
9
|
wakeChannel
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-WGOGS2JC.js";
|
|
11
11
|
|
|
12
12
|
// src/listen.ts
|
|
13
13
|
import { createClient } from "@supabase/supabase-js";
|
package/dist/onboard.js
CHANGED
package/dist/statusline.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
BACKEND_URL,
|
|
5
|
+
readSurface,
|
|
5
6
|
readToken
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-WGOGS2JC.js";
|
|
7
8
|
|
|
8
9
|
// src/statusline.ts
|
|
9
10
|
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
|
|
@@ -18,7 +19,8 @@ var MODE_LABEL = {
|
|
|
18
19
|
all_calls: "all calls",
|
|
19
20
|
silent: "silent"
|
|
20
21
|
};
|
|
21
|
-
function render(status) {
|
|
22
|
+
function render(status, surface = null) {
|
|
23
|
+
if (surface) return `paigy \u26A0 ${surface.label}: ${surface.code}`;
|
|
22
24
|
if (!status) return "paigy: unpaired";
|
|
23
25
|
return `paigy: ${MODE_LABEL[status.sessionMode]} \xB7 ${status.phone ? "phone \u2713" : "no phone"}`;
|
|
24
26
|
}
|
|
@@ -44,6 +46,8 @@ async function fetchStatus(token) {
|
|
|
44
46
|
return await res.json();
|
|
45
47
|
}
|
|
46
48
|
async function main() {
|
|
49
|
+
const surface = readSurface();
|
|
50
|
+
if (surface) return render(null, surface);
|
|
47
51
|
const token = readToken();
|
|
48
52
|
if (!token) return render(null);
|
|
49
53
|
const cached = readCache();
|