@paigy/mcp 0.40.39 → 0.40.40

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.
@@ -11,6 +11,7 @@ import {
11
11
  NotifyRequestSchema,
12
12
  PROTO,
13
13
  RevokedError,
14
+ ServerFault,
14
15
  TOKEN_PATH,
15
16
  UnpairedError,
16
17
  acknowledge,
@@ -62,7 +63,7 @@ import {
62
63
  updateSlot,
63
64
  whoAmI,
64
65
  withCodexEnv
65
- } from "./chunk-X4UIGUEX.js";
66
+ } from "./chunk-V4OVLFMP.js";
66
67
  export {
67
68
  AGENT_TOOLS,
68
69
  AGENT_TOOL_NAMES,
@@ -76,6 +77,7 @@ export {
76
77
  NotifyRequestSchema,
77
78
  PROTO,
78
79
  RevokedError,
80
+ ServerFault,
79
81
  TOKEN_PATH,
80
82
  UnpairedError,
81
83
  acknowledge,
package/dist/enable.js CHANGED
@@ -3,9 +3,9 @@ import {
3
3
  PAIGY_TOOL_IDS,
4
4
  enablePaigyTools,
5
5
  installSessionListening
6
- } from "./chunk-4IAK4ATB.js";
7
- import "./chunk-QE33A5XK.js";
8
- import "./chunk-X4UIGUEX.js";
6
+ } from "./chunk-PKJJSHLW.js";
7
+ import "./chunk-H2EBILBE.js";
8
+ import "./chunk-V4OVLFMP.js";
9
9
 
10
10
  // src/enable.ts
11
11
  function main() {
package/dist/index.js CHANGED
@@ -6,10 +6,11 @@ import {
6
6
  clearPairing,
7
7
  identify,
8
8
  joinBackgroundPair,
9
+ pendingText,
9
10
  resolvePairing,
10
11
  startPairing,
11
12
  suggestedAgentName
12
- } from "./chunk-PMZBYLLJ.js";
13
+ } from "./chunk-6FG3PZR2.js";
13
14
  import {
14
15
  ConfigureSchema,
15
16
  ENABLE_COMMAND,
@@ -17,14 +18,18 @@ import {
17
18
  SERVER_INSTRUCTIONS,
18
19
  TOOLS,
19
20
  paigyToolsAllowlisted
20
- } from "./chunk-4IAK4ATB.js";
21
+ } from "./chunk-PKJJSHLW.js";
21
22
  import {
23
+ ReachTeller,
22
24
  decideListen,
23
25
  existingListener,
24
26
  harnessDriven,
27
+ hookStale,
28
+ listenBlock,
25
29
  listenerKey,
26
- wakeStatusPath
27
- } from "./chunk-QE33A5XK.js";
30
+ wakeStatusPath,
31
+ withReach
32
+ } from "./chunk-H2EBILBE.js";
28
33
  import {
29
34
  clearSurface,
30
35
  writeSurface
@@ -45,7 +50,7 @@ import {
45
50
  setClient,
46
51
  slotName,
47
52
  whoAmI
48
- } from "./chunk-X4UIGUEX.js";
53
+ } from "./chunk-V4OVLFMP.js";
49
54
 
50
55
  // src/index.ts
51
56
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -97,7 +102,25 @@ function afterUnpaired(hatched, deviceCredential) {
97
102
  }
98
103
 
99
104
  // src/index.ts
105
+ var REACH = new ReachTeller();
100
106
  var JOIN_CAP_MS = 45e3;
107
+ function listenFacts() {
108
+ const session = sessionId();
109
+ const slot = agentName();
110
+ const pid = existingListener(slot, process.env);
111
+ const client = server.getClientVersion?.()?.name;
112
+ return {
113
+ env: process.env,
114
+ pidAlive: pid !== null,
115
+ wake: readWakeStatus(wakeStatusPath(listenerKey(slot, process.env))),
116
+ ...pid !== null ? { pid } : {},
117
+ execPath: process.execPath,
118
+ listenJs: fileURLToPath(new URL("./listen.js", import.meta.url)),
119
+ slot,
120
+ session,
121
+ ...client ? { client } : {}
122
+ };
123
+ }
101
124
  function pairedResult(token, sas, note) {
102
125
  clearSurface();
103
126
  const base = {
@@ -113,6 +136,11 @@ function pairedResult(token, sas, note) {
113
136
  base.enable_tool_ids = PAIGY_TOOL_IDS;
114
137
  base.enable_command = ENABLE_COMMAND;
115
138
  base.enable_prompt = "REQUIRED \u2014 before you do anything else, print `enable_command` to the user and say: \"Run this to let Paigy's tools work without an approval prompt each time \u2014 it only allowlists Paigy's own tools.\" Print it every time you pair; don't skip it.";
139
+ try {
140
+ const facts = listenFacts();
141
+ Object.assign(base, listenBlock(decideListen(facts), facts, hookStale()));
142
+ } catch {
143
+ }
116
144
  base.enable_apply = "You cannot apply this yourself, and must not try: it writes YOUR OWN permission allowlist, which hosts block as privilege escalation no matter what the user said here. PRINT the command and move on \u2014 don't wait for them to run it, and don't re-ask. Paigy's tools still work meanwhile; they just prompt for approval each time until the user runs it. If the host isn't Claude Code, tell them to add `enable_tool_ids` to that tool's own allowlist instead, merging \u2014 never clobbering.";
117
145
  return { content: [{ type: "text", text: JSON.stringify(base) }] };
118
146
  }
@@ -151,7 +179,7 @@ function renderPairOutcome(outcome, device_code) {
151
179
  text: JSON.stringify({
152
180
  status: "pending",
153
181
  device_code,
154
- message: "Still awaiting approval \u2014 the user hasn't entered the code yet. Call configure again with this device_code RIGHT NOW to keep waiting; each call returns after ~45s so it can't be killed by a host tool-call timeout. Nothing is lost between calls: the code stays valid and approval keeps being polled in the background, so a re-call returns the instant they approve."
182
+ message: pendingText(outcome.kind === "pending" ? outcome.fault : void 0)
155
183
  })
156
184
  }]
157
185
  };
@@ -353,27 +381,23 @@ async function handleTool(request, signal) {
353
381
  // daemon is the `listen.js` beside our own entry, so the command never resolves a package.
354
382
  case "listen": {
355
383
  if (!harnessDriven(process.env) && !readToken()) throw new UnpairedError();
356
- const slot = agentName();
357
- const key = listenerKey(slot, process.env);
358
- const pid = existingListener(slot, process.env);
359
- const decision = decideListen({
360
- env: process.env,
361
- pidAlive: pid !== null,
362
- wake: readWakeStatus(wakeStatusPath(key)),
363
- ...pid !== null ? { pid } : {},
364
- execPath: process.execPath,
365
- listenJs: fileURLToPath(new URL("./listen.js", import.meta.url)),
366
- slot,
367
- session: sessionId()
368
- });
369
- return { content: [{ type: "text", text: JSON.stringify(decision) }] };
384
+ return { content: [{ type: "text", text: JSON.stringify(decideListen(listenFacts())) }] };
370
385
  }
371
386
  default: {
372
387
  const name = request.params.name;
373
388
  const result = await runTool(name, request.params.arguments ?? {}, { waits: true, signal });
374
389
  const sentAs = name === "contact" ? slotName(agentName()) : null;
375
390
  const shaped = sentAs && result && typeof result === "object" ? { ...result, sentAs } : result;
376
- return { content: [{ type: "text", text: JSON.stringify(shaped) }] };
391
+ let reach = null;
392
+ try {
393
+ reach = await REACH.note(agentName(), name, {
394
+ facts: listenFacts,
395
+ poll: () => runTool("contact", { wait: false }, { waits: false, signal }),
396
+ now: Date.now
397
+ });
398
+ } catch {
399
+ }
400
+ return { content: [{ type: "text", text: JSON.stringify(withReach(shaped, reach)) }] };
377
401
  }
378
402
  }
379
403
  }
package/dist/listen.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  removeListenMark,
12
12
  wakeStatusPath,
13
13
  writeListenMark
14
- } from "./chunk-QE33A5XK.js";
14
+ } from "./chunk-H2EBILBE.js";
15
15
  import {
16
16
  agentName,
17
17
  getGoals,
@@ -20,7 +20,7 @@ import {
20
20
  receive,
21
21
  slotName,
22
22
  subscribeWake
23
- } from "./chunk-X4UIGUEX.js";
23
+ } from "./chunk-V4OVLFMP.js";
24
24
 
25
25
  // src/listen.ts
26
26
  import { createHash } from "crypto";
@@ -131,28 +131,51 @@ function listenOptions(argv) {
131
131
  }
132
132
  var { brief: BRIEF, release: RELEASE } = listenOptions(process.argv);
133
133
  var line = (text) => void process.stdout.write(text + "\n");
134
+ var briefSeen = /* @__PURE__ */ new Set();
134
135
  var emit = (obj) => line(JSON.stringify(obj));
135
136
  var listeningAs = () => slotName(agentName()) ?? agentName();
136
137
  var SWEEP_EVERY_MS = Number(process.env.PAIGY_SWEEP_MS) || 9e4;
137
138
  function saidIn(delivery) {
138
139
  return ("entries" in delivery ? delivery.entries : []).filter((entry) => entry.kind === "contribution" && !entryUnit(entry)).map((entry) => entryWords(entry)).filter(Boolean).join("\n");
139
140
  }
141
+ function newestSaid(delivery) {
142
+ const said = ("entries" in delivery ? delivery.entries : []).filter((entry) => entry.kind === "contribution" && !entryUnit(entry) && entryWords(entry));
143
+ const newest = said.reduce(
144
+ (best, e) => !best || Date.parse(e.createdAt) >= Date.parse(best.createdAt) ? e : best,
145
+ void 0
146
+ );
147
+ return newest ? entryWords(newest) : "";
148
+ }
140
149
  function leadOf(deliveries, goalId) {
141
150
  return (goalId ? deliveries.find((d) => d.goalIds.includes(goalId)) : void 0) ?? deliveries[0];
142
151
  }
152
+ function briefItems(waiting) {
153
+ return [
154
+ // A list row carries its newest words as `about`; a Delivery read in full carries its entries.
155
+ ...waiting.deliveries.map((d) => `d:${d.deliveryId}:${"entries" in d ? newestSaid(d) : d.about}`),
156
+ ...(waiting.review ?? []).map((g) => `r:${g.goalId}@${g.saidAt ?? ""}`),
157
+ ...(waiting.assigned ?? []).map((g) => `a:${g.goalId}`)
158
+ ];
159
+ }
160
+ function briefIsNew(reason, items, seen) {
161
+ const gained = items.some((i) => !seen.has(i));
162
+ for (const i of items) seen.add(i);
163
+ return reason === "boot" || gained;
164
+ }
143
165
  function brief(waiting, reason, as, goalId) {
144
166
  const lead = leadOf(waiting.deliveries, goalId);
145
- const stalled = waiting.stalled?.length ? ` \xB7 ${waiting.stalled.length} of your Goals stalled 3+ days: update, finish or cancel each` : "";
167
+ const stalled = waiting.stalled?.length ? ` \xB7 ${waiting.stalled.length} of your Goals stalled 3+ days: update, finish, cancel or defer each` : "";
146
168
  const review = waiting.review ?? [];
147
169
  const named = (goalId ? review.find((g) => g.goalId === goalId) : void 0) ?? review[0];
148
170
  const fresh = review.length ? ` \xB7 new on ${review.length === 1 ? "your Goal" : `${review.length} of your Goals`}, first ${named.goalId} ("${named.title}"): get_goals to read it` : "";
149
171
  const assigned = waiting.assigned ?? [];
150
172
  const mine = assigned.length ? ` \xB7 assigned to you: ${assigned.length === 1 ? "1 Goal" : `${assigned.length} Goals`}, first ${assigned[0].goalId} ("${assigned[0].title}"): get_goals({goalIds:[goalId]}) to read it; your first write starts it` : "";
151
173
  if (!lead) return `Paigy \xB7 ${reason === "boot" ? `listening as ${as}` : reason}${mine}${fresh || (mine ? "" : " \xB7 nothing waiting")}${stalled}`;
152
- const said = saidIn(lead) || ("about" in lead ? lead.about : "");
174
+ const said = newestSaid(lead) || ("about" in lead ? lead.about : "");
153
175
  const shown = said.length > 160 ? `${said.slice(0, 160)}\u2026` : said;
154
176
  const goal = waiting.goal?.goalId ?? lead.goalIds[0];
155
- return `Paigy \xB7 ${reason} \xB7 ${waiting.deliveries.length} waiting \xB7 lead ${lead.deliveryId} on Goal ${goal}: "${shown}"${mine}${fresh}${stalled}`;
177
+ const act = ` \xB7 get_goals({goalIds:["${goal}"]}) to read it in full, then contact to answer; nothing is acknowledged until contact({ackEventIds})`;
178
+ return `Paigy \xB7 ${reason} \xB7 ${waiting.deliveries.length} waiting \xB7 lead ${lead.deliveryId} on Goal ${goal}: "${shown}"${mine}${fresh}${stalled}${act}`;
156
179
  }
157
180
  function launchEnv(waiting, reason, goalId) {
158
181
  const env = { PAIGY_WORK: JSON.stringify(waiting), PAIGY_EVENT: reason };
@@ -177,7 +200,7 @@ async function sweep(reason, goalId, bridge = new WakeBridge(process.env, wakeSt
177
200
  const leadGoalId = lead?.goalIds[0];
178
201
  const goal = leadGoalId ? await getGoals([leadGoalId]).then(([g]) => ({ goalId: leadGoalId, progress: g.progress })).catch(() => null) : null;
179
202
  const waiting = { deliveries, goal, ...stalled?.length ? { stalled } : {}, ...review?.length ? { review } : {}, ...assigned?.length ? { assigned } : {} };
180
- if (BRIEF) line(brief(waiting, reason, listeningAs(), goalId));
203
+ if (BRIEF && briefIsNew(reason, briefItems(waiting), briefSeen)) line(brief(waiting, reason, listeningAs(), goalId));
181
204
  else emit({ type: "sweep", reason, ...goalId ? { about: goalId } : {}, ...waiting });
182
205
  if (deliveries.length || waiting.review?.length || waiting.assigned?.length || reason === "boot" && waiting.stalled?.length) {
183
206
  let unreadable = false;
@@ -298,11 +321,14 @@ if (isEntry()) {
298
321
  export {
299
322
  SWEEP_EVERY_MS,
300
323
  brief,
324
+ briefIsNew,
325
+ briefItems,
301
326
  catchUpReason,
302
327
  coalesceSweeps,
303
328
  launchEnv,
304
329
  leadOf,
305
330
  listenOptions,
331
+ newestSaid,
306
332
  saidIn,
307
333
  sweep
308
334
  };
package/dist/onboard.js CHANGED
@@ -3,13 +3,14 @@ import {
3
3
  resolvePairing,
4
4
  startPairing,
5
5
  suggestedAgentName
6
- } from "./chunk-PMZBYLLJ.js";
6
+ } from "./chunk-6FG3PZR2.js";
7
7
  import {
8
8
  autoConfigureClients,
9
9
  claudeInstallHint,
10
- openBrowser
11
- } from "./chunk-4IAK4ATB.js";
12
- import "./chunk-QE33A5XK.js";
10
+ openBrowser,
11
+ removeOldGeminiHooks
12
+ } from "./chunk-PKJJSHLW.js";
13
+ import "./chunk-H2EBILBE.js";
13
14
  import {
14
15
  TOKEN_PATH,
15
16
  agentName,
@@ -20,7 +21,7 @@ import {
20
21
  saveToken,
21
22
  setIdentity,
22
23
  whoAmI
23
- } from "./chunk-X4UIGUEX.js";
24
+ } from "./chunk-V4OVLFMP.js";
24
25
 
25
26
  // src/onboard.ts
26
27
  function reportRegistered() {
@@ -38,6 +39,12 @@ async function main() {
38
39
  console.log(`
39
40
  Paigy MCP onboarding (agent: "${agentName()}")
40
41
  `);
42
+ const removed = removeOldGeminiHooks();
43
+ if (removed.length) {
44
+ console.log("Removed Paigy's old turn-end reminder, which named the wrong agent or never ran, from:");
45
+ for (const path of removed) console.log(` - ${path}`);
46
+ console.log("");
47
+ }
41
48
  const chosen = process.env.PAIGY_NAME ?? suggestedAgentName();
42
49
  if (readToken()) {
43
50
  let who = await whoAmI().catch(() => null);
@@ -90,6 +97,7 @@ Paigy MCP onboarding (agent: "${agentName()}")
90
97
  process.stdout.write("Waiting for approval");
91
98
  const deadline = Date.now() + start.expiresIn * 1e3;
92
99
  let shownSas = null;
100
+ let fault;
93
101
  while (Date.now() < deadline) {
94
102
  const outcome = await resolvePairing(start.deviceCode, Math.max(1e3, deadline - Date.now()));
95
103
  if (outcome.kind === "awaiting_confirm") {
@@ -107,6 +115,7 @@ This pairing is end-to-end encrypted. Verify this code matches the one on the ph
107
115
  continue;
108
116
  }
109
117
  if (outcome.kind === "pending") {
118
+ fault = outcome.fault;
110
119
  process.stdout.write(".");
111
120
  continue;
112
121
  }
@@ -127,7 +136,10 @@ Paired! Token saved to ${TOKEN_PATH} (mode 0600).`);
127
136
  reportRegistered();
128
137
  return;
129
138
  }
130
- console.error("\n\nDevice code expired without approval. Please re-run onboarding.\n");
139
+ console.error(fault ? `
140
+
141
+ Device code expired before approval could be checked: ${fault} Please re-run onboarding.
142
+ ` : "\n\nDevice code expired without approval. Please re-run onboarding.\n");
131
143
  process.exit(1);
132
144
  }
133
145
  main().catch((err) => {
package/dist/slot.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  agentName
4
- } from "./chunk-X4UIGUEX.js";
4
+ } from "./chunk-V4OVLFMP.js";
5
5
 
6
6
  // src/slot.ts
7
7
  process.stdout.write(agentName());
package/dist/stalled.js CHANGED
@@ -4,6 +4,9 @@
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
5
  import { homedir } from "os";
6
6
  import { join } from "path";
7
+ function speaksOn(event) {
8
+ return event !== "AfterAgent";
9
+ }
7
10
  function agentFlag(argv) {
8
11
  const inline = argv.find((arg) => arg.startsWith("--agent="));
9
12
  if (inline) return inline.slice("--agent=".length) || null;
@@ -11,15 +14,16 @@ function agentFlag(argv) {
11
14
  if (at === -1) return null;
12
15
  return argv[at + 1] ?? null;
13
16
  }
14
- function stopReply(stalled, event = "Stop") {
17
+ function stopReply(stalled) {
15
18
  if (!stalled.length) return null;
16
19
  const list = stalled.slice(0, 8).map((g) => `"${g.title}" (${g.goalId})`).join(", ");
17
20
  const more = stalled.length > 8 ? ` and ${stalled.length - 8} more` : "";
18
- const reason = `Before you stop: ${stalled.length} of your Paigy Goals have had no progress for 3 days: ${list}${more}. For each, report where it stands with a contact update, or finish or cancel it with manage_goals.`;
19
- return event === "AfterAgent" ? { decision: "block", reason, systemMessage: reason } : { decision: "block", reason };
21
+ const reason = `Before you stop: ${stalled.length} of your Paigy Goals have had no progress for 3 days: ${list}${more}. For each, report where it stands with a contact update, or finish, cancel or defer it with manage_goals.`;
22
+ return { decision: "block", reason };
20
23
  }
21
24
  async function main() {
22
25
  const input = JSON.parse(readFileSync(0, "utf8") || "{}");
26
+ if (!speaksOn(input.hook_event_name ?? "Stop")) return;
23
27
  if (input.stop_hook_active) return;
24
28
  const session = input.session_id ?? "";
25
29
  if (session) process.env.PAIGY_SESSION_ID = session;
@@ -29,8 +33,8 @@ async function main() {
29
33
  const dir = join(homedir(), ".paigy", "stalled-reminded");
30
34
  const mark = join(dir, `${session || "session"}-${day}`);
31
35
  if (existsSync(mark)) return;
32
- const { receive } = await import("./dist-DOFR2565.js");
33
- const reply = stopReply((await receive()).stalled ?? [], input.hook_event_name ?? "Stop");
36
+ const { receive } = await import("./dist-EJWDUF5J.js");
37
+ const reply = stopReply((await receive()).stalled ?? []);
34
38
  if (!reply) return;
35
39
  mkdirSync(dir, { recursive: true });
36
40
  writeFileSync(mark, "");
@@ -39,5 +43,6 @@ async function main() {
39
43
  if (process.argv[1] && /stalled(\.[jt]s)?$/.test(process.argv[1])) main().catch(() => void 0);
40
44
  export {
41
45
  agentFlag,
46
+ speaksOn,
42
47
  stopReply
43
48
  };
@@ -7,7 +7,7 @@ import {
7
7
  readToken,
8
8
  sessionSlot,
9
9
  slotName
10
- } from "./chunk-X4UIGUEX.js";
10
+ } from "./chunk-V4OVLFMP.js";
11
11
 
12
12
  // src/statusline.ts
13
13
  import { realpathSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/mcp",
3
- "version": "0.40.39",
3
+ "version": "0.40.40",
4
4
  "description": "Paigy MCP server — the AI agent harness that calls you. Lets an agent notify a user and await their reply.",
5
5
  "license": "MIT",
6
6
  "type": "module",