@paigy/mcp 0.40.23 → 0.40.24

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
@@ -184,26 +184,39 @@ Goal" from "an empty id".
184
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."' \
185
185
  npx -y -p @paigy/mcp paigy-listen --install
186
186
 
187
- # Codex (or any CLI harness) — the scalars are enough for a one-liner:
188
- PAIGY_ON_WAKE='codex exec "Claim Paigy goal $PAIGY_GOAL_ID and continue thread $PAIGY_THREAD_ID. The user said: $PAIGY_TEXT. Reply with contact."' \
189
- npx -y -p @paigy/mcp paigy-listen --install
187
+ # Codex terminal session: no custom script and no new conversation.
188
+ # CODEX_THREAD_ID is inherited from the running session.
189
+ npx -y -p @paigy/mcp@latest paigy-listen --brief
190
190
  ```
191
191
 
192
192
  ### A session that starts listening (`listen`, #2265)
193
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.
194
+ Run `listen` to obtain the exact background command for the current session. For Codex,
195
+ `paigy-listen` reads `CODEX_THREAD_ID`, checks that the installed `codex queue` supports
196
+ `--thread` and `--message`, then queues a wake into that existing conversation. It uses
197
+ argument arrays, never interpolates user text into a shell, and never starts another
198
+ conversation. Two threads in the same folder have separate listener marks and checkpoints.
199
+ Onboarding forwards the thread identifier into the MCP environment; `listen` forwards it
200
+ into the listener command explicitly. Re-running onboarding repairs an existing Paigy allowlist while preserving custom variables; restart the MCP server to inherit the repaired environment. An older slot-wide listener is reported as already receiving until deliberately stopped, so upgrades do not start a second subscriber.
201
+
202
+ `PAIGY_ON_WAKE` remains an explicit override with its original per-sweep behavior: an
203
+ existing script may ignore boot or timer events. Without a supported adapter, the listener
204
+ still prints events, but reports only `receiving`; a process being alive does not prove it
205
+ can resume an agent. `listening` with a ready adapter means the command is available, and
206
+ `lastQueuedAt` records its last successful exit. Neither claims that the agent read or
207
+ handled a reply. Queue errors are visible in listener output and in `listen` status.
208
+
209
+ Native Codex wakes coalesce unchanged backlog, including across listener restarts. The listener compares source Entry IDs and accepted Answer timestamps on the bounded list of pending review Goals; its own progress and claims are excluded. A later reply is new evidence even if its words are identical. Failed handoffs remain retryable
210
+ on the existing timer, next nudge or reconnect; discovery never claims or acknowledges
211
+ work. Queue commands are serialized, so a burst becomes one fresh discovery read after
212
+ an in-flight handoff. A command accepted just before a process crash may be queued again;
213
+ the resumed agent must still read and acknowledge actual evidence through Paigy.
214
+
215
+ A managed Paigy session carries `PAIGY_HARNESS` and keeps its existing delivery pump;
216
+ it does not start another listener. `PAIGY_SESSION_ID` binds API requests and alone does
217
+ not establish harness delivery. Other hosts need a supported wake command or a host that
218
+ actually surfaces watched-task output. Use a session listener for Codex, not `--install`:
219
+ the service is machine-wide and has no current conversation.
207
220
 
208
221
  `$PAIGY_TEXT` is whatever the user said, expanded inside a shell command — keep it
209
222
  quoted, as above. For anything longer than a one-liner, point `PAIGY_ON_WAKE` at a
@@ -4,11 +4,12 @@ import {
4
4
  serverInstructions,
5
5
  sessionStartHook,
6
6
  withSessionStartHook
7
- } from "./chunk-JLRBZZNT.js";
7
+ } from "./chunk-VUKR2E24.js";
8
8
  import {
9
9
  CODEX_ENV,
10
- agentName
11
- } from "./chunk-PR7ROBN4.js";
10
+ agentName,
11
+ withCodexEnv
12
+ } from "./chunk-WR2M3RME.js";
12
13
 
13
14
  // src/toolset.ts
14
15
  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.";
@@ -49,12 +50,8 @@ var IDENTITY_TOOLS = [
49
50
  inputSchema: mcpInputSchema(z.object({}))
50
51
  },
51
52
  {
52
- // A session asked to "start listening" can, in one call (#2265). Nothing can push an
53
- // answer into a session started in a terminal — an answer waits for its next check_replies —
54
- // and the only thing that reaches a running session mid-turn is a watched background task.
55
- // So this tool decides and hands over: it never subscribes, spawns or writes settings.
56
53
  name: "listen",
57
- 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.",
54
+ description: "Am I listening for answers, and can Paigy resume this session? Takes no arguments. Returns status `listening` for harness delivery or a running listener with a ready wake adapter; `receiving` means a process is running but automatic resume is not confirmed (read its wake error or watch its output); `unpaired` means call onboard; `start` gives the exact command to run NOW as a watched background task for this session. Start it once. Codex queues into CODEX_THREAD_ID automatically when its CLI supports queue; PAIGY_ON_WAKE overrides it. A successful queue is not evidence the agent handled the answer: read check_replies and the relevant Goal, then respond with contact. This tool never subscribes or starts anything itself.",
58
55
  inputSchema: mcpInputSchema(z.object({}))
59
56
  }
60
57
  ];
@@ -89,7 +86,10 @@ function withPaigyMcpJson(existing, agent) {
89
86
  return JSON.stringify(config, null, 2);
90
87
  }
91
88
  function withPaigyMcpToml(existing, agent) {
92
- if (existing.includes("[mcp_servers.paigy]")) return null;
89
+ if (existing.includes("[mcp_servers.paigy]")) {
90
+ const updated = withCodexEnv(existing);
91
+ return updated === existing ? null : updated;
92
+ }
93
93
  const entry = [
94
94
  "",
95
95
  "[mcp_servers.paigy]",
@@ -0,0 +1,110 @@
1
+ // src/wake.ts
2
+ import { execFile, spawn } from "child_process";
3
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
4
+ import { dirname } from "path";
5
+ function wakeTarget(env) {
6
+ if (env.PAIGY_HARNESS) return { kind: "harness" };
7
+ if (env.PAIGY_ON_WAKE) return { kind: "command", command: env.PAIGY_ON_WAKE };
8
+ if (env.CODEX_THREAD_ID) return { kind: "codex", thread: env.CODEX_THREAD_ID };
9
+ return { kind: "output" };
10
+ }
11
+ function readWakeStatus(path) {
12
+ try {
13
+ return JSON.parse(readFileSync(path, "utf8"));
14
+ } catch {
15
+ return void 0;
16
+ }
17
+ }
18
+ var WakeBridge = class {
19
+ constructor(env, path) {
20
+ this.env = env;
21
+ this.path = path;
22
+ const target = wakeTarget(env);
23
+ const saved = readWakeStatus(path);
24
+ this.state = { ...JSON.stringify(saved?.target) === JSON.stringify(target) ? saved : {}, target, ready: false };
25
+ }
26
+ env;
27
+ path;
28
+ state;
29
+ checked = false;
30
+ status() {
31
+ return { ...this.state };
32
+ }
33
+ save() {
34
+ mkdirSync(dirname(this.path), { recursive: true });
35
+ const temp = `${this.path}.${process.pid}.tmp`;
36
+ writeFileSync(temp, JSON.stringify(this.state), { mode: 384 });
37
+ renameSync(temp, this.path);
38
+ }
39
+ run(command, args, extra = {}) {
40
+ return new Promise((resolve, reject) => {
41
+ execFile(command, args, { env: { ...this.env, ...extra }, timeout: 15e3, maxBuffer: 64 * 1024 }, (error, stdout, stderr) => {
42
+ if (error) reject(new Error(`${error.message}${stderr ? `: ${stderr.trim()}` : ""}`));
43
+ else resolve(stdout);
44
+ });
45
+ });
46
+ }
47
+ launch(command, extra) {
48
+ return new Promise((resolve, reject) => {
49
+ const child = spawn("/bin/sh", ["-c", command], { stdio: "inherit", env: { ...this.env, ...extra } });
50
+ child.once("spawn", resolve);
51
+ child.once("error", reject);
52
+ child.once("exit", (code, signal) => {
53
+ if (code === 0) return;
54
+ this.state.ready = false;
55
+ this.state.lastError = `PAIGY_ON_WAKE exited ${signal ?? code}`;
56
+ this.save();
57
+ process.stderr.write(`Paigy wake error: ${this.state.lastError}
58
+ `);
59
+ });
60
+ });
61
+ }
62
+ async prepare() {
63
+ if (this.checked) return;
64
+ try {
65
+ if (this.state.target.kind === "codex") {
66
+ const help = await this.run("codex", ["queue", "--help"]);
67
+ if (!help.includes("--thread") || !help.includes("--message")) throw new Error("Installed Codex does not support queue --thread --message; update Codex or configure PAIGY_ON_WAKE.");
68
+ }
69
+ this.checked = true;
70
+ this.state.ready = this.state.target.kind === "codex" || this.state.target.kind === "command" || this.state.target.kind === "harness";
71
+ delete this.state.lastError;
72
+ this.save();
73
+ } catch (error) {
74
+ this.state.ready = false;
75
+ this.state.lastError = error.message;
76
+ this.save();
77
+ throw error;
78
+ }
79
+ }
80
+ async deliver(fingerprint, message, extra) {
81
+ await this.prepare();
82
+ const target = this.state.target;
83
+ if (target.kind === "output" || target.kind === "harness") return;
84
+ const fingerprints = fingerprint === void 0 ? void 0 : typeof fingerprint === "string" ? [fingerprint] : fingerprint;
85
+ if (target.kind === "codex" && fingerprints?.every((item) => this.state.fingerprints?.includes(item))) {
86
+ this.state.fingerprints = fingerprints;
87
+ this.save();
88
+ return;
89
+ }
90
+ try {
91
+ if (target.kind === "codex") await this.run("codex", ["queue", "--thread", target.thread, "--message", message]);
92
+ else await this.launch(target.command, extra);
93
+ this.state.fingerprints = fingerprints;
94
+ if (target.kind === "codex") this.state.lastQueuedAt = (/* @__PURE__ */ new Date()).toISOString();
95
+ this.state.ready = true;
96
+ delete this.state.lastError;
97
+ this.save();
98
+ } catch (error) {
99
+ this.state.ready = false;
100
+ this.state.lastError = error.message;
101
+ this.save();
102
+ throw error;
103
+ }
104
+ }
105
+ };
106
+
107
+ export {
108
+ readWakeStatus,
109
+ WakeBridge
110
+ };
@@ -6,7 +6,7 @@ import {
6
6
  saveToken,
7
7
  setIdentity,
8
8
  sleep
9
- } from "./chunk-PR7ROBN4.js";
9
+ } from "./chunk-WR2M3RME.js";
10
10
 
11
11
  // src/identity.ts
12
12
  var CLIENT_LABELS = {