@paigy/mcp 0.40.22 → 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 +29 -16
- package/dist/{chunk-2ED4GL4N.js → chunk-J7T3EB5Y.js} +11 -9
- package/dist/chunk-RK5LT7NH.js +110 -0
- package/dist/{chunk-BFN2GJ42.js → chunk-TP7DM2MP.js} +1 -1
- package/dist/{chunk-WDZ67U4G.js → chunk-VUKR2E24.js} +688 -505
- package/dist/{chunk-KSXJCZ2L.js → chunk-WR2M3RME.js} +762 -535
- package/dist/{dist-YAKVSYVQ.js → dist-EKLTLANV.js} +13 -3
- package/dist/enable.js +3 -3
- package/dist/index.js +18 -7
- package/dist/listen.js +83 -15
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/stalled.js +1 -1
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
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
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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,10 +4,12 @@ import {
|
|
|
4
4
|
serverInstructions,
|
|
5
5
|
sessionStartHook,
|
|
6
6
|
withSessionStartHook
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-VUKR2E24.js";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
CODEX_ENV,
|
|
10
|
+
agentName,
|
|
11
|
+
withCodexEnv
|
|
12
|
+
} from "./chunk-WR2M3RME.js";
|
|
11
13
|
|
|
12
14
|
// src/toolset.ts
|
|
13
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.";
|
|
@@ -48,12 +50,8 @@ var IDENTITY_TOOLS = [
|
|
|
48
50
|
inputSchema: mcpInputSchema(z.object({}))
|
|
49
51
|
},
|
|
50
52
|
{
|
|
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
53
|
name: "listen",
|
|
56
|
-
description: "Am I listening for answers, and
|
|
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.",
|
|
57
55
|
inputSchema: mcpInputSchema(z.object({}))
|
|
58
56
|
}
|
|
59
57
|
];
|
|
@@ -88,12 +86,16 @@ function withPaigyMcpJson(existing, agent) {
|
|
|
88
86
|
return JSON.stringify(config, null, 2);
|
|
89
87
|
}
|
|
90
88
|
function withPaigyMcpToml(existing, agent) {
|
|
91
|
-
if (existing.includes("[mcp_servers.paigy]"))
|
|
89
|
+
if (existing.includes("[mcp_servers.paigy]")) {
|
|
90
|
+
const updated = withCodexEnv(existing);
|
|
91
|
+
return updated === existing ? null : updated;
|
|
92
|
+
}
|
|
92
93
|
const entry = [
|
|
93
94
|
"",
|
|
94
95
|
"[mcp_servers.paigy]",
|
|
95
96
|
'command = "npx"',
|
|
96
97
|
'args = ["-y", "@paigy/mcp@latest"]',
|
|
98
|
+
`env_vars = [${CODEX_ENV.map((name) => JSON.stringify(name)).join(", ")}]`,
|
|
97
99
|
`env = { PAIGY_AGENT = "${agent}" }`,
|
|
98
100
|
""
|
|
99
101
|
].join("\n");
|
|
@@ -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
|
+
};
|