@paigy/mcp 0.40.11 → 0.40.15

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
@@ -117,8 +117,8 @@ Then pair: `PAIGY_AGENT=gemini npx -p @paigy/mcp@latest paigy-mcp-onboard`.
117
117
  ## Tools
118
118
 
119
119
  One catalog, on every transport (2026-09-11): the agent tools are `AGENT_TOOLS` in
120
- `packages/schema/src/tools.ts` — contact, check_replies, get_thread, search_threads,
121
- create_goal, claim_goal, get_goal, update_goal — and both this server and the hosted MCP
120
+ `packages/schema/src/tools.ts` — contact, check_replies, create_goal, claim_goal,
121
+ get_goal, update_goal — and both this server and the hosted MCP
122
122
  (`apps/api/src/mcp`) publish that list and dispatch it through the SDK's one `runTool`.
123
123
  This server adds only onboard, pair and unpair, the tools that mint and delete the token
124
124
  file this machine holds. See the [SDK contract](../../packages/sdk/README.md) for the
@@ -171,18 +171,17 @@ To launch an agent — any agent, not just Claude — when work arrives, set
171
171
  | `PAIGY_PARENT_ID` | the thread to continue on (`PAIGY_THREAD_ID` is the same value, for existing scripts) |
172
172
  | `PAIGY_DELIVERY_ID` | the Delivery being acted on — `contact({deliveryId})` rereads it |
173
173
  | `PAIGY_GOAL_ID` | the Goal it belongs to — `claim_goal` it **first** |
174
- | `PAIGY_CONTEXT_THREAD_ID` | a past conversation this Goal names — `get_thread` it **first** |
175
174
  | `PAIGY_TEXT` | what the person said, in prose |
176
175
 
177
176
  Hand `$PAIGY_WORK` to a harness that can read JSON and decide for itself; use the
178
177
  scalars for a plain shell launcher that shouldn't need `jq`. They describe **one**
179
178
  item — the oldest open Delivery — because the contract is one thread at a time. An
180
- absent fact is unset rather than empty, so `${PAIGY_CONTEXT_THREAD_ID:-}`
181
- distinguishes "no seed" from "seeded with nothing".
179
+ absent fact is unset rather than empty, so `${PAIGY_GOAL_ID:-}` distinguishes "no
180
+ Goal" from "an empty id".
182
181
 
183
182
  ```sh
184
183
  # Claude Code — hand it everything and let it plan:
185
- PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — claim_goal first, and rehydrate threads you do not recognize via get_thread."' \
184
+ PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — claim_goal first, and read a Goal you do not recognize with get_goal."' \
186
185
  npx -y -p @paigy/mcp paigy-listen --install
187
186
 
188
187
  # Codex (or any CLI harness) — the scalars are enough for a one-liner:
@@ -190,6 +189,22 @@ PAIGY_ON_WAKE='codex exec "Claim Paigy goal $PAIGY_GOAL_ID and continue thread $
190
189
  npx -y -p @paigy/mcp paigy-listen --install
191
190
  ```
192
191
 
192
+ ### A session that starts listening (`listen`, #2265)
193
+
194
+ Nothing pushes an answer into a session started in a terminal — it waits for that
195
+ session's next `check_replies`. What can reach a running session mid-turn is a watched
196
+ background task, so a terminal session runs `paigy-listen --brief` as its own watched task
197
+ (one human line per event: `Paigy · wake:reply · 2 waiting · lead <deliveryId> on Goal
198
+ <goalId>: "…"`). The `listen` tool is how a session asked to "start listening" gets there
199
+ in one call: it decides and hands over — `listening` (nothing to do), `unpaired` (call
200
+ `onboard`), or `start` with the exact command (this server's own node and `listen.js`,
201
+ `PAIGY_AGENT` and `PAIGY_SESSION_ID` set to this session's slot and id — a session-bound
202
+ identity is refused from any other) for the agent to run as its background task in the
203
+ same turn. It never subscribes, spawns or writes settings. "Already running" is a fact: the
204
+ daemon marks its pid under `~/.paigy/listen/<slot>.pid` while it runs. A session started by
205
+ the Paigy harness (`paigy-harness host` / `run`) needs none of this — its pump types each
206
+ answer in as it lands, and `listen` says so.
207
+
193
208
  `$PAIGY_TEXT` is whatever the user said, expanded inside a shell command — keep it
194
209
  quoted, as above. For anything longer than a one-liner, point `PAIGY_ON_WAKE` at a
195
210
  script and branch on `$PAIGY_EVENT` there:
@@ -197,12 +212,23 @@ script and branch on `$PAIGY_EVENT` there:
197
212
  ```sh
198
213
  #!/bin/sh
199
214
  # ~/.paigy/on-wake.sh — chmod +x, then PAIGY_ON_WAKE=~/.paigy/on-wake.sh
200
- seed=""
201
- [ -n "${PAIGY_CONTEXT_THREAD_ID:-}" ] && seed="It continues thread $PAIGY_CONTEXT_THREAD_ID — get_thread that first."
215
+ codex exec "Paigy goal $PAIGY_GOAL_ID. claim_goal it first. The user said: $PAIGY_TEXT. Reply with contact when done."
216
+ ```
217
+
218
+ ## Stalled work (Claude Code stop hook)
202
219
 
203
- codex exec "Paigy goal $PAIGY_GOAL_ID on thread $PAIGY_THREAD_ID. claim_goal it first. The user said: $PAIGY_TEXT. $seed Reply with contact when done."
220
+ An agent that goes quiet leaves its Goals behind. Three things hand it that work back, all
221
+ reading one rule (no progress for 3 days, `COLD_AFTER_MS`): `check_replies` lists it under
222
+ `stalled`, `paigy-listen` carries it in `PAIGY_WORK` (and launches for it on boot), and this
223
+ hook holds a Claude Code session's stop once a day to list it:
224
+
225
+ ```json
226
+ { "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "npx -y -p @paigy/mcp paigy-stalled" }] }] } }
204
227
  ```
205
228
 
229
+ It is silent when nothing is stalled, when the stop is already being continued by a hook, and
230
+ after it has reminded that session once today.
231
+
206
232
  ## Statusline (Claude Code)
207
233
 
208
234
  `paigy-statusline` prints which Paigy identity this Claude Code session is, for the
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  AGENT_TOOLS,
3
3
  mcpInputSchema,
4
- serverInstructions
5
- } from "./chunk-I5QINUN6.js";
4
+ serverInstructions,
5
+ sessionStartHook,
6
+ withSessionStartHook
7
+ } from "./chunk-SE2JCKDI.js";
6
8
  import {
7
9
  agentName
8
- } from "./chunk-H5GGWCPS.js";
10
+ } from "./chunk-PHTQITTC.js";
9
11
 
10
12
  // src/toolset.ts
11
13
  var ONBOARD_DESCRIPTION = "Get this agent talking to Paigy \u2014 call it FIRST, before contact, 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_command`, PRINT that command for the user to run \u2014 you cannot apply it yourself (it writes your own permission allowlist, which hosts block as privilege escalation), so print it, don't wait for it, and carry on.";
@@ -44,6 +46,15 @@ var IDENTITY_TOOLS = [
44
46
  name: "unpair",
45
47
  description: "Log out / unpair this agent from the user's Paigy account: revokes the token server-side (it stops working everywhere) and deletes the local ~/.paigy/token.json. Takes no arguments. After this, contact won't work until the user pairs again with the pair tool.",
46
48
  inputSchema: mcpInputSchema(z.object({}))
49
+ },
50
+ {
51
+ // A session asked to "start listening" can, in one call (#2265). Nothing can push an
52
+ // answer into a session started in a terminal — an answer waits for its next check_replies —
53
+ // and the only thing that reaches a running session mid-turn is a watched background task.
54
+ // So this tool decides and hands over: it never subscribes, spawns or writes settings.
55
+ name: "listen",
56
+ description: "Am I listening for answers, and if not, how do I start? Call it when the user asks you to start listening (or to stay reachable), and whenever you are unsure whether you are. Takes no arguments. Returns `status`: `listening` \u2014 nothing to do (a session started by the Paigy harness is ALWAYS listening: its pump types each answer in as it lands; or your own paigy-listen is already running, with its pid); `unpaired` \u2014 call onboard first; `start` \u2014 a `command` you MUST run NOW, in this same turn, as a watched BACKGROUND task (your harness's background-task/monitor tool), and leave running for the session. Each line that task prints is a Delivery waiting for you: claim_goal the Goal it names, read it, answer with contact. Start it once. This tool never subscribes or starts anything itself \u2014 the listener has to be YOUR watched task, or its lines reach no one.",
57
+ inputSchema: mcpInputSchema(z.object({}))
47
58
  }
48
59
  ];
49
60
  var TOOLS = [...AGENT_TOOLS, ...IDENTITY_TOOLS];
@@ -52,7 +63,7 @@ var HIDDEN_ALIASES = [];
52
63
 
53
64
  // src/clients.ts
54
65
  import { execFile } from "child_process";
55
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
66
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
56
67
  import { homedir, platform } from "os";
57
68
  import { join, dirname } from "path";
58
69
  function openBrowser(url) {
@@ -208,6 +219,45 @@ function enablePaigyTools(scope = "user", cwd = process.cwd()) {
208
219
  already: PAIGY_TOOL_IDS.filter((t) => prevAllow.includes(t))
209
220
  };
210
221
  }
222
+ var HOOK_CONFIGS = [
223
+ { harness: "Claude Code", path: () => join(homedir(), ".claude", "settings.json") },
224
+ { harness: "Codex", path: () => join(homedir(), ".codex", "hooks.json") }
225
+ ];
226
+ function installSessionListening() {
227
+ const script = join(homedir(), ".paigy", "hooks", "session-start.sh");
228
+ try {
229
+ mkdirSync(dirname(script), { recursive: true });
230
+ writeFileSync(script, sessionStartHook(), { mode: 493 });
231
+ chmodSync(script, 493);
232
+ } catch (e) {
233
+ return [{ harness: "all", path: script, ok: false, why: e.message }];
234
+ }
235
+ const out = [];
236
+ for (const { harness, path: at } of HOOK_CONFIGS) {
237
+ const path = at();
238
+ if (!existsSync(dirname(path))) continue;
239
+ let existing = null;
240
+ try {
241
+ existing = existsSync(path) ? readFileSync(path, "utf8") : null;
242
+ } catch (e) {
243
+ out.push({ harness, path, ok: false, why: e.message });
244
+ continue;
245
+ }
246
+ const already = existing?.includes(script) ?? false;
247
+ const updated = withSessionStartHook(existing, script);
248
+ if (updated === null) {
249
+ out.push({ harness, path, ok: false, why: `${path} didn't parse as JSON \u2014 not overwriting it` });
250
+ continue;
251
+ }
252
+ try {
253
+ writeFileSync(path, updated + "\n");
254
+ out.push({ harness, path, ok: true, already });
255
+ } catch (e) {
256
+ out.push({ harness, path, ok: false, why: e.message });
257
+ }
258
+ }
259
+ return out;
260
+ }
211
261
 
212
262
  export {
213
263
  SERVER_INSTRUCTIONS,
@@ -220,5 +270,6 @@ export {
220
270
  ENABLE_COMMAND,
221
271
  PAIGY_TOOL_IDS,
222
272
  paigyToolsAllowlisted,
223
- enablePaigyTools
273
+ enablePaigyTools,
274
+ installSessionListening
224
275
  };