@paigy/mcp 0.40.39 → 0.40.41

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 CHANGED
@@ -109,26 +109,27 @@ Or add the entry to `~/.gemini/settings.json`:
109
109
 
110
110
  Then pair: `PAIGY_AGENT=gemini npx -p @paigy/mcp@latest paigy-mcp-onboard`.
111
111
 
112
- > `PAIGY_AGENT` names the agent AND keys its token slot (defaults to
113
- > `mcp-agent`). Each client reads only its own slot, so pair with the same
114
- > `PAIGY_AGENT` the client's config uses — and set it per client so you can
115
- > tell your connected agents apart.
112
+ > `PAIGY_AGENT` names the agent AND keys its token slot. Without it, each session gets a private slot
113
+ > (`session:<id>`); the shared `mcp-agent` default went on 2026-08-07. Each client reads only its own slot, so pair with the same
114
+ > `PAIGY_AGENT` the client's config uses. A fixed `PAIGY_AGENT` makes every session of that client one agent: two running at once
115
+ > collide, and the copy set aside is refused `holder_conflict` when it acts (#2933). `paigy-harness setup` wires clients without it,
116
+ > so each session hatches its own identity.
116
117
 
117
118
  ## Tools
118
119
 
119
120
  One catalog, on every transport (2026-09-11): the agent tools are `AGENT_TOOLS` in
120
- `packages/schema/src/tools.ts` — contact, manage_goals, get_goals, search, check_activity,
121
- send_feedback — and both this server and the hosted MCP
121
+ `packages/schema/src/tools.ts` (configure, contact, manage_goals, get_delivery, get_goals, search, check_activity,
122
+ send_feedback), and both this server and the hosted MCP
122
123
  (`apps/api/src/mcp`) publish that list and dispatch it through the SDK's one `runTool`.
123
- This server adds only `configure`, the one identity tool, which pairs, reports who you are,
124
- renames and logs this agent out (`logOut: true`) over the token file this machine holds, and listen.
124
+ This server extends `configure`, the one identity tool, which on the hosted MCP only reports who you are: here it also pairs,
125
+ renames and logs this agent out (`logOut: true`) over the token file this machine holds. It adds `listen`.
125
126
  `onboard` is now `configure`; `pair` and `unpair` are folded into it (2026-10-07). See the [SDK contract](../../packages/sdk/README.md) for the
126
127
  contact shape. Sending returns immediately unless it asks to `wait`: `wait: true` holds one
127
128
  cancellable ~45-second window here for a response to what was sent (the hosted transport
128
129
  returns after one read), and `contact({})` holds one for anything addressed to you; after an
129
130
  `expired` window, wait again without sending again. Every Goal a contact names must already
130
131
  exist (create it with manage_goals); a question with `workItBlocks` asks for a call; a
131
- question, an update or an answer is at most 250 characters with its context in `units`; and
132
+ question, an update or an answer is at most 80 characters with its context in `units`; and
132
133
  `withdrawQuestionIds` takes back a question of yours. `update_goal` is gone (2026-10-07): progress
133
134
  is a contact update (one the person does not need yet is kept as the Goal's progress), a review is
134
135
  acknowledged with `ackEventIds`, and a Goal is postponed with manage_goals `defer` (you are woken
@@ -145,9 +146,8 @@ time. Bookkeeping (`operationId`, the manage request id, the revision an edit wa
145
146
  minted here, never asked of the model. Retired
146
147
  reply-lease/ACK/work/callback tools and hidden notification aliases are rejected.
147
148
 
148
- The standalone listener below is still a **legacy host and target-release blocker**.
149
- Its old reply intake must migrate before enabling it against the target runtime; it is
150
- not a compatibility fallback for target MCP tools.
149
+ Until 2026-09-13 the standalone listener below was a legacy host with its own reply intake. Since #1957 it reads
150
+ the same `receive` as `contact({})`, as the next section says.
151
151
 
152
152
  ## Configuration
153
153
 
@@ -242,12 +242,12 @@ script and branch on `$PAIGY_EVENT` there:
242
242
  codex exec "Paigy goal $PAIGY_GOAL_ID. Read it with get_goals first. The user said: $PAIGY_TEXT. Reply with contact when done."
243
243
  ```
244
244
 
245
- ## Stalled work (turn-end hook — Claude Code, Codex, Gemini CLI, Antigravity)
245
+ ## Stalled work (turn-end hook — Claude Code, Codex)
246
246
 
247
247
  An agent that goes quiet leaves its Goals behind. Three things hand it that work back, all
248
248
  reading one rule (no progress for 3 days, `COLD_AFTER_MS`): `contact({})` lists it under
249
249
  `stalled`, `paigy-listen` carries it in `PAIGY_WORK` (and launches for it on boot), and this
250
- hook holds a session's turn end once a day to list it. One program, four tools:
250
+ hook holds a session's turn end once a day to list it. One program, two tools:
251
251
 
252
252
  **Claude Code** (`~/.claude/settings.json`) — installed by the paigy plugin:
253
253
 
@@ -267,19 +267,18 @@ command = "npx -y -p @paigy/mcp@latest paigy-stalled --agent codex"
267
267
  timeout = 60
268
268
  ```
269
269
 
270
- **Gemini CLI / Antigravity** (`~/.gemini/settings.json`, `~/.gemini/antigravity-cli/settings.json`)
271
- — the turn-end event there is `AfterAgent`, and the reminder also arrives as a `systemMessage`,
272
- which is the field that event promises will reach somebody:
273
-
274
- ```json
275
- { "hooks": { "AfterAgent": [{ "matcher": "*", "hooks": [{ "name": "paigy-stalled", "type": "command",
276
- "command": "npx -y -p @paigy/mcp@latest paigy-stalled --agent antigravity", "timeout": 60000 }] }] } }
277
- ```
270
+ **Antigravity and Gemini CLI have no turn-end reminder (#3648).** Onboarding used to write one on
271
+ `AfterAgent` into their `settings.json`; agy 1.4.1 reads hooks only from `hooks.json` and ends a turn
272
+ on `Stop`, so it never fired, and in Gemini CLI's own file it named the wrong slot. `paigy-stalled`
273
+ answers nothing on `AfterAgent` from this release on, so a copy goes quiet once its `npx` fetches
274
+ it (the hook still starts `npx` each turn until removed), and onboarding (on every run) and
275
+ `scripts/paigy-wipe.sh` take it out. A `Stop` hook in agy's `hooks.json` waits on a check of its
276
+ payload on a real turn. The listener's wake and `contact({})`'s `stalled` still reach these agents.
278
277
 
279
278
  `--agent <slot>` is not optional outside Claude Code: a hook is not the MCP server, so it never
280
279
  inherits the `PAIGY_AGENT` that server was registered with, and without the flag it reads this
281
280
  session's own slot — a different identity, with no stalled work of its own. Pass the same value as
282
- the tool's MCP registration (`codex`, `antigravity`, or whatever you set `PAIGY_AGENT` to).
281
+ the tool's MCP registration (`codex`, or whatever you set `PAIGY_AGENT` to).
283
282
 
284
283
  It is silent when nothing is stalled, when the stop is already being continued by a hook, and
285
284
  after it has reminded that session once today. Codex's older `notify` program is not a place for
@@ -306,12 +305,12 @@ machine is paired and this session's identity is minted by its first tool call;
306
305
 
307
306
  ## Publishing (maintainers)
308
307
 
309
- Tool input schemas are generated from zod in `src/schema.ts` as draft-2020-12 JSON
308
+ Tool input schemas are generated from zod by `mcpInputSchema` (`packages/schema/src/mcp-schema.ts`) as draft-2020-12 JSON
310
309
  Schema, and `src/schema.test.ts` guards that every tool stays valid (strict clients
311
310
  like the Anthropic API reject anything else). **A schema fix only reaches agents once
312
311
  a new version is published to npm** — 0.8.0 once shipped *without* a committed fix and
313
- 400'd strict clients for weeks. So after any change under `src/schema*` or the tool
314
- definitions: **bump the version and `pnpm publish`** (don't rely on the commit alone).
312
+ 400'd strict clients for weeks. So after any change to the tool schemas (`packages/schema`) or the tool
313
+ definitions: **bump the version in `apps/mcp/package.json`**; publishing is `pnpm release` from the repo root, which the owner runs (`docs/infra/releasing.md`). Don't rely on the commit alone.
315
314
  Install recipes pin `@paigy/mcp@latest` so a fresh `npx` picks up the new version; if a
316
315
  stale one sticks, `rm -rf ~/.npm/_npx` and restart the client.
317
316
 
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  NameTakenError,
3
+ ServerFault,
3
4
  isNetworkError,
4
5
  pollToken,
5
6
  requestCode,
6
7
  saveToken,
7
8
  setIdentity,
8
9
  sleep
9
- } from "./chunk-X4UIGUEX.js";
10
+ } from "./chunk-CPDYOHSY.js";
10
11
 
11
12
  // src/identity.ts
12
13
  var CLIENT_LABELS = {
@@ -44,8 +45,9 @@ async function identify(who, ask) {
44
45
  }
45
46
 
46
47
  // src/pairing.ts
47
- async function resolvePairing(deviceCode, capMs, pollMs = 2e3) {
48
+ async function resolvePairing(deviceCode, capMs, pollMs = 2e3, onFault) {
48
49
  const start = Date.now();
50
+ let fault;
49
51
  while (Date.now() - start < capMs) {
50
52
  try {
51
53
  const token = await pollToken(deviceCode);
@@ -53,8 +55,12 @@ async function resolvePairing(deviceCode, capMs, pollMs = 2e3) {
53
55
  saveToken(token);
54
56
  return { kind: "paired", token };
55
57
  }
58
+ fault = void 0;
59
+ onFault?.(fault);
56
60
  } catch (e) {
57
- if (isNetworkError(e)) {
61
+ if (isNetworkError(e) || e instanceof ServerFault) {
62
+ fault = e.message;
63
+ onFault?.(fault);
58
64
  await sleep(pollMs);
59
65
  continue;
60
66
  }
@@ -62,12 +68,25 @@ async function resolvePairing(deviceCode, capMs, pollMs = 2e3) {
62
68
  }
63
69
  await sleep(pollMs);
64
70
  }
65
- return { kind: "pending" };
71
+ return fault ? { kind: "pending", fault } : { kind: "pending" };
72
+ }
73
+ function pendingText(fault) {
74
+ if (fault) {
75
+ const said = /[.!?]$/.test(fault) ? fault : `${fault}.`;
76
+ return `Approval can't be checked right now: ${said} This is not the user being slow. Tell them once that Paigy is having trouble reaching its server, then call configure again with this device_code to keep trying; the code stays valid until it expires.`;
77
+ }
78
+ return "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.";
66
79
  }
67
80
  var bg = null;
68
81
  var started = null;
82
+ var startedUntil = 0;
83
+ function activePairing() {
84
+ if (started && Date.now() >= startedUntil) cancelBackgroundPair();
85
+ return started;
86
+ }
69
87
  async function startPairing(agent) {
70
- if (started) return started;
88
+ const live = activePairing();
89
+ if (live) return { ...live, expiresIn: Math.max(1, Math.round((startedUntil - Date.now()) / 1e3)) };
71
90
  const code = await requestCode(agent);
72
91
  started = {
73
92
  verificationUri: code.verification_uri_complete,
@@ -75,11 +94,14 @@ async function startPairing(agent) {
75
94
  deviceCode: code.device_code,
76
95
  expiresIn: code.expires_in
77
96
  };
97
+ startedUntil = Date.now() + code.expires_in * 1e3;
78
98
  startBackgroundPair(code.device_code, code.expires_in * 1e3);
79
99
  return started;
80
100
  }
81
101
  function startBackgroundPair(deviceCode, budgetMs) {
82
- const promise = resolvePairing(deviceCode, budgetMs).catch((e) => ({ kind: "error", message: e.message })).then((o) => {
102
+ const promise = resolvePairing(deviceCode, budgetMs, void 0, (fault) => {
103
+ if (bg?.deviceCode === deviceCode) bg.fault = fault;
104
+ }).catch((e) => ({ kind: "error", message: e.message })).then((o) => {
83
105
  if (bg?.deviceCode === deviceCode) bg.settled = o;
84
106
  return o;
85
107
  });
@@ -106,7 +128,7 @@ async function joinBackgroundPair(deviceCode, capMs) {
106
128
  if (s.kind === "paired" || s.kind === "error") started = null;
107
129
  return s;
108
130
  }
109
- return { kind: "pending" };
131
+ return bg?.fault ? { kind: "pending", fault: bg.fault } : { kind: "pending" };
110
132
  }
111
133
  function cancelBackgroundPair() {
112
134
  bg = null;
@@ -121,6 +143,7 @@ export {
121
143
  suggestedAgentName,
122
144
  identify,
123
145
  resolvePairing,
146
+ pendingText,
124
147
  startPairing,
125
148
  joinBackgroundPair,
126
149
  clearPairing