@scp-dev/cli 0.2.0 → 0.2.1
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 +1 -0
- package/dist/{chunk-GH7AXFUL.js → chunk-25ZVL5LD.js} +8 -10
- package/dist/{chunk-RXKBND3T.js → chunk-HF4KP6AR.js} +229 -13
- package/dist/{chunk-5DYFYLAQ.js → chunk-KC5AV5NQ.js} +3 -5
- package/dist/{chunk-M2DCR2J4.js → chunk-MNTIEWKF.js} +13 -3
- package/dist/{chunk-U2RSSTNW.js → chunk-MXQ7B3CQ.js} +3 -5
- package/dist/{chunk-WV5FINTW.js → chunk-NJLFHBSS.js} +20 -10
- package/dist/chunk-ZLL5EGI2.js +805 -0
- package/dist/{claude-YJLGFQ2P.js → claude-WTCPUYU3.js} +21 -11
- package/dist/cli.js +1 -1
- package/dist/codex-YZZNH6IQ.js +565 -0
- package/dist/{daemon-SUTLTY5U.js → daemon-OMJY7T2F.js} +1 -1
- package/dist/{hooks-EI7M3YKY.js → hooks-4B5NOH7X.js} +19 -3
- package/dist/install-2ZM3IYXG.js +14 -0
- package/dist/join-BH5CS6SL.js +45 -0
- package/dist/{login-V2WT7QX5.js → login-UB72MU7W.js} +4 -7
- package/dist/{main-HPBTO3JK.js → main-I4F53MMA.js} +79 -31
- package/dist/{mcp-DGAQNFKO.js → mcp-RXAVQBMP.js} +11 -11
- package/dist/{peek-WLG3ZROU.js → peek-M2HQVO4O.js} +2 -3
- package/dist/{pi-HMESQA64.js → pi-IAVFVOOO.js} +2 -3
- package/dist/{relay-5Z7IEXKD.js → relay-QDX2BNZH.js} +6 -4
- package/dist/{remote-FYN5ZXE4.js → remote-2OWBWLJH.js} +3 -5
- package/dist/{setup-LXG346FJ.js → setup-HPH7HE6O.js} +5 -6
- package/dist/{spawn-DLPMAS5M.js → spawn-V66GYUNC.js} +6 -7
- package/dist/{up-GPM4LWY3.js → up-2YGEDA5J.js} +6 -9
- package/package.json +7 -5
- package/dist/chunk-3RRPUYTE.js +0 -147
- package/dist/chunk-3V3DRXB2.js +0 -340
- package/dist/chunk-E53YHYEP.js +0 -305
- package/dist/install-MYHVFKT4.js +0 -15
|
@@ -3,16 +3,17 @@ import {
|
|
|
3
3
|
mcpFile,
|
|
4
4
|
mountedEntry,
|
|
5
5
|
whichOnPath
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-NJLFHBSS.js";
|
|
7
|
+
import "./chunk-KC5AV5NQ.js";
|
|
8
|
+
import "./chunk-HF4KP6AR.js";
|
|
9
|
+
import "./chunk-YYSLYVNZ.js";
|
|
8
10
|
import {
|
|
9
11
|
CHANNELS_ENV,
|
|
10
|
-
RelayUsage
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-M2DCR2J4.js";
|
|
12
|
+
RelayUsage,
|
|
13
|
+
deriveHandle,
|
|
14
|
+
joinMode
|
|
15
|
+
} from "./chunk-ZLL5EGI2.js";
|
|
16
|
+
import "./chunk-MNTIEWKF.js";
|
|
16
17
|
|
|
17
18
|
// src/claude.ts
|
|
18
19
|
import { spawn } from "child_process";
|
|
@@ -33,6 +34,14 @@ function claudeArgv(argv) {
|
|
|
33
34
|
if (name !== void 0 && !validHandle(name)) throw new RelayUsage(`--name ${JSON.stringify(name)} is not a valid agent handle`);
|
|
34
35
|
return { ...name !== void 0 ? { name } : {}, args: argv.slice(i) };
|
|
35
36
|
}
|
|
37
|
+
function namePrompt(name, args, env, cwd) {
|
|
38
|
+
if (name !== void 0 || joinMode(env) === "manual" || args.some((arg) => /^--(append-)?system-prompt/.test(arg))) return [];
|
|
39
|
+
const handle = env.SCP_NAME || deriveHandle("claude-code", cwd);
|
|
40
|
+
return [
|
|
41
|
+
"--append-system-prompt",
|
|
42
|
+
`You are on the SCP network as ${handle}. Pick a short descriptive name for yourself with \`${CLI_NAME} name <name>\` when you know what you're working on.`
|
|
43
|
+
];
|
|
44
|
+
}
|
|
36
45
|
function mountHint(env) {
|
|
37
46
|
const file = mcpFile("claude", env);
|
|
38
47
|
let entry;
|
|
@@ -47,7 +56,7 @@ function mountHint(env) {
|
|
|
47
56
|
return void 0;
|
|
48
57
|
}
|
|
49
58
|
async function claudeCommand(argv, env = process.env, err = (line) => void process.stderr.write(`${line}
|
|
50
|
-
`)) {
|
|
59
|
+
`), cwd = process.cwd()) {
|
|
51
60
|
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
52
61
|
process.stdout.write(`${CLI_NAME} claude [--name N] [--] [claude args...] start Claude Code with the scp channel loaded, so peer messages wake it
|
|
53
62
|
`);
|
|
@@ -58,7 +67,7 @@ async function claudeCommand(argv, env = process.env, err = (line) => void proce
|
|
|
58
67
|
if (!bin) throw new RelayUsage("claude is not on PATH");
|
|
59
68
|
const hint = mountHint(env);
|
|
60
69
|
if (hint) err(hint);
|
|
61
|
-
const child = spawn(bin, [...CHANNEL_FLAGS, ...args], {
|
|
70
|
+
const child = spawn(bin, [...CHANNEL_FLAGS, ...namePrompt(name, args, env, cwd), ...args], {
|
|
62
71
|
stdio: "inherit",
|
|
63
72
|
env: { ...env, ...name !== void 0 ? { SCP_NAME: name } : {}, [CHANNELS_ENV]: String(process.pid) }
|
|
64
73
|
});
|
|
@@ -84,5 +93,6 @@ export {
|
|
|
84
93
|
CHANNEL_FLAGS,
|
|
85
94
|
claudeArgv,
|
|
86
95
|
claudeCommand,
|
|
87
|
-
mountHint
|
|
96
|
+
mountHint,
|
|
97
|
+
namePrompt
|
|
88
98
|
};
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,6 @@ function quietSqliteWarning() {
|
|
|
17
17
|
// src/cli.ts
|
|
18
18
|
if (!await subagentFastPath(process.argv.slice(2))) {
|
|
19
19
|
quietSqliteWarning();
|
|
20
|
-
const { run } = await import("./main-
|
|
20
|
+
const { run } = await import("./main-I4F53MMA.js");
|
|
21
21
|
await run(process.argv.slice(2));
|
|
22
22
|
}
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import {
|
|
2
|
+
codexMount,
|
|
3
|
+
codexOverrides
|
|
4
|
+
} from "./chunk-25ZVL5LD.js";
|
|
5
|
+
import "./chunk-KN7WEIE4.js";
|
|
6
|
+
import "./chunk-NJLFHBSS.js";
|
|
7
|
+
import "./chunk-KC5AV5NQ.js";
|
|
8
|
+
import "./chunk-HF4KP6AR.js";
|
|
9
|
+
import "./chunk-YYSLYVNZ.js";
|
|
10
|
+
import {
|
|
11
|
+
RelayUsage,
|
|
12
|
+
readPsTable,
|
|
13
|
+
relayIdentity,
|
|
14
|
+
startDaemon
|
|
15
|
+
} from "./chunk-ZLL5EGI2.js";
|
|
16
|
+
import {
|
|
17
|
+
SELF
|
|
18
|
+
} from "./chunk-MNTIEWKF.js";
|
|
19
|
+
|
|
20
|
+
// src/codex.ts
|
|
21
|
+
import { spawn } from "child_process";
|
|
22
|
+
import { createHash, randomBytes } from "crypto";
|
|
23
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
24
|
+
import { connect } from "net";
|
|
25
|
+
import { tmpdir } from "os";
|
|
26
|
+
import { join } from "path";
|
|
27
|
+
import { PassThrough } from "stream";
|
|
28
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
29
|
+
import { connectBroker, relayDir } from "@scp-dev/broker";
|
|
30
|
+
import { CLI_NAME, validHandle } from "@scp-dev/core";
|
|
31
|
+
import { MCP_PROTOCOLS, runRelayMcp } from "@scp-dev/mcp";
|
|
32
|
+
var CODEX_HELP = `${CLI_NAME} codex [--name N | --as N] [--headless] [--resume THREAD] [--] [codex args...]
|
|
33
|
+
runs codex with relay messages waking an idle thread or steering a running turn; approvals stay with the TUI.
|
|
34
|
+
--headless runs an ambient worker without a TUI; --resume THREAD reopens its thread.
|
|
35
|
+
$SCP_CODEX_BIN picks the codex binary (default codex on PATH).`;
|
|
36
|
+
function codexLaunch(args, env, _self) {
|
|
37
|
+
let name;
|
|
38
|
+
let headless = false;
|
|
39
|
+
const codexArgs = [];
|
|
40
|
+
for (let i = 0; i < args.length; i++) {
|
|
41
|
+
const arg = args[i];
|
|
42
|
+
if (arg === "--") {
|
|
43
|
+
codexArgs.push(...args.slice(i + 1));
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (arg === "--name" || arg === "--as") {
|
|
47
|
+
name = args[++i];
|
|
48
|
+
if (name === void 0) throw new RelayUsage(`${CLI_NAME} codex ${arg} takes a name`);
|
|
49
|
+
} else if (arg.startsWith("--name=") || arg.startsWith("--as=")) name = arg.slice(arg.indexOf("=") + 1);
|
|
50
|
+
else if (arg === "--headless") headless = true;
|
|
51
|
+
else codexArgs.push(arg);
|
|
52
|
+
}
|
|
53
|
+
name = name?.replace(/^me\//, "");
|
|
54
|
+
if (name !== void 0 && !validHandle(name)) throw new RelayUsage(`${JSON.stringify(name)} is not a valid agent handle`);
|
|
55
|
+
const { SCP_SESSION: _session, SCP_GRANT: _grant, SCP_PARENT: _parent, ...clean } = env;
|
|
56
|
+
return {
|
|
57
|
+
...name !== void 0 ? { name } : {},
|
|
58
|
+
headless,
|
|
59
|
+
codexArgs,
|
|
60
|
+
env: { ...clean, SCP_HARNESS: "codex", ...name !== void 0 ? { SCP_NAME: name } : {} }
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
var CodexSocket = class {
|
|
64
|
+
constructor(path, onMessage = () => {
|
|
65
|
+
}) {
|
|
66
|
+
this.onMessage = onMessage;
|
|
67
|
+
this.socket = connect(path);
|
|
68
|
+
this.socket.on("connect", () => {
|
|
69
|
+
this.socket.write(
|
|
70
|
+
`GET / HTTP/1.1\r
|
|
71
|
+
Host: localhost\r
|
|
72
|
+
Upgrade: websocket\r
|
|
73
|
+
Connection: Upgrade\r
|
|
74
|
+
Sec-WebSocket-Key: ${this.key}\r
|
|
75
|
+
Sec-WebSocket-Version: 13\r
|
|
76
|
+
\r
|
|
77
|
+
`
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
this.socket.on("data", (data) => {
|
|
81
|
+
try {
|
|
82
|
+
this.feed(typeof data === "string" ? Buffer.from(data) : data);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
this.socket.on("error", (error) => this.fail(error));
|
|
88
|
+
this.socket.on("close", () => this.fail(new Error("codex socket closed")));
|
|
89
|
+
}
|
|
90
|
+
onMessage;
|
|
91
|
+
socket;
|
|
92
|
+
buffer = Buffer.alloc(0);
|
|
93
|
+
opened = false;
|
|
94
|
+
resolveReady;
|
|
95
|
+
rejectReady;
|
|
96
|
+
ready = new Promise((resolve, reject) => {
|
|
97
|
+
this.resolveReady = resolve;
|
|
98
|
+
this.rejectReady = reject;
|
|
99
|
+
});
|
|
100
|
+
next = 1;
|
|
101
|
+
pending = /* @__PURE__ */ new Map();
|
|
102
|
+
key = randomBytes(16).toString("base64");
|
|
103
|
+
fragments = [];
|
|
104
|
+
fail(error) {
|
|
105
|
+
this.rejectReady(error);
|
|
106
|
+
for (const pending of this.pending.values()) {
|
|
107
|
+
clearTimeout(pending.timer);
|
|
108
|
+
pending.reject(error);
|
|
109
|
+
}
|
|
110
|
+
this.pending.clear();
|
|
111
|
+
this.socket.destroy();
|
|
112
|
+
}
|
|
113
|
+
feed(data) {
|
|
114
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
115
|
+
if (!this.opened) {
|
|
116
|
+
const end = this.buffer.indexOf("\r\n\r\n");
|
|
117
|
+
if (end < 0) return;
|
|
118
|
+
const header = this.buffer.subarray(0, end).toString();
|
|
119
|
+
const accept = createHash("sha1").update(`${this.key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
|
|
120
|
+
if (!/^HTTP\/1\.1 101\b/.test(header) || !header.toLowerCase().includes(`sec-websocket-accept: ${accept}`.toLowerCase()))
|
|
121
|
+
throw new Error("codex WebSocket upgrade failed");
|
|
122
|
+
this.buffer = this.buffer.subarray(end + 4);
|
|
123
|
+
this.opened = true;
|
|
124
|
+
this.resolveReady();
|
|
125
|
+
}
|
|
126
|
+
while (this.buffer.length >= 2) {
|
|
127
|
+
const opcode = this.buffer[0] & 15;
|
|
128
|
+
const final = (this.buffer[0] & 128) !== 0;
|
|
129
|
+
const masked = (this.buffer[1] & 128) !== 0;
|
|
130
|
+
let length = this.buffer[1] & 127;
|
|
131
|
+
let offset = 2;
|
|
132
|
+
if (length === 126) {
|
|
133
|
+
if (this.buffer.length < 4) return;
|
|
134
|
+
length = this.buffer.readUInt16BE(2);
|
|
135
|
+
offset = 4;
|
|
136
|
+
} else if (length === 127) {
|
|
137
|
+
if (this.buffer.length < 10) return;
|
|
138
|
+
const size = this.buffer.readBigUInt64BE(2);
|
|
139
|
+
if (size > 64n * 1024n * 1024n) throw new Error("codex WebSocket frame too large");
|
|
140
|
+
length = Number(size);
|
|
141
|
+
offset = 10;
|
|
142
|
+
}
|
|
143
|
+
const start = offset + (masked ? 4 : 0);
|
|
144
|
+
if (this.buffer.length < start + length) return;
|
|
145
|
+
const payload = Buffer.from(this.buffer.subarray(start, start + length));
|
|
146
|
+
if (masked) for (let i = 0; i < length; i++) payload[i] = payload[i] ^ this.buffer[offset + i % 4];
|
|
147
|
+
this.buffer = this.buffer.subarray(start + length);
|
|
148
|
+
if (opcode === 8) {
|
|
149
|
+
this.fail(new Error("codex WebSocket closed"));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (opcode === 9) this.frame(10, payload);
|
|
153
|
+
if (opcode !== 1 && opcode !== 0) continue;
|
|
154
|
+
this.fragments.push(payload);
|
|
155
|
+
if (!final) continue;
|
|
156
|
+
const message = JSON.parse(Buffer.concat(this.fragments).toString());
|
|
157
|
+
this.fragments = [];
|
|
158
|
+
const pending = typeof message.id === "number" && !message.method ? this.pending.get(message.id) : void 0;
|
|
159
|
+
if (pending) {
|
|
160
|
+
this.pending.delete(message.id);
|
|
161
|
+
clearTimeout(pending.timer);
|
|
162
|
+
if (message.error) pending.reject(new Error(JSON.stringify(message.error)));
|
|
163
|
+
else pending.resolve(message.result ?? {});
|
|
164
|
+
} else this.onMessage(message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
frame(opcode, payload) {
|
|
168
|
+
const length = payload.length;
|
|
169
|
+
const header = Buffer.alloc(length < 126 ? 2 : length < 65536 ? 4 : 10);
|
|
170
|
+
header[0] = 128 | opcode;
|
|
171
|
+
header[1] = 128 | (length < 126 ? length : length < 65536 ? 126 : 127);
|
|
172
|
+
if (header.length === 4) header.writeUInt16BE(length, 2);
|
|
173
|
+
if (header.length === 10) header.writeBigUInt64BE(BigInt(length), 2);
|
|
174
|
+
const mask = randomBytes(4);
|
|
175
|
+
const body = Buffer.from(payload);
|
|
176
|
+
for (let i = 0; i < length; i++) body[i] = body[i] ^ mask[i % 4];
|
|
177
|
+
this.socket.write(Buffer.concat([header, mask, body]));
|
|
178
|
+
}
|
|
179
|
+
send(message) {
|
|
180
|
+
if (this.socket.destroyed) throw new Error("codex socket closed");
|
|
181
|
+
this.frame(1, Buffer.from(JSON.stringify(message)));
|
|
182
|
+
}
|
|
183
|
+
async request(method, params) {
|
|
184
|
+
await this.ready;
|
|
185
|
+
const id = this.next++;
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
const timer = setTimeout(() => {
|
|
188
|
+
this.pending.delete(id);
|
|
189
|
+
reject(new Error(`codex ${method} timed out`));
|
|
190
|
+
}, 15e3);
|
|
191
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
192
|
+
try {
|
|
193
|
+
this.send({ jsonrpc: "2.0", id, method, params });
|
|
194
|
+
} catch (error) {
|
|
195
|
+
this.pending.delete(id);
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
reject(error);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
close() {
|
|
202
|
+
this.fail(new Error("codex socket closed"));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
var CodexWaker = class {
|
|
206
|
+
constructor(transport, threadId) {
|
|
207
|
+
this.transport = transport;
|
|
208
|
+
this.threadId = threadId;
|
|
209
|
+
}
|
|
210
|
+
transport;
|
|
211
|
+
threadId;
|
|
212
|
+
status = /* @__PURE__ */ new Map();
|
|
213
|
+
changed = /* @__PURE__ */ new Map();
|
|
214
|
+
parents = /* @__PURE__ */ new Map();
|
|
215
|
+
waiting = /* @__PURE__ */ new Set();
|
|
216
|
+
queue = [];
|
|
217
|
+
serial;
|
|
218
|
+
again = false;
|
|
219
|
+
timer;
|
|
220
|
+
stopped = false;
|
|
221
|
+
clock = 0;
|
|
222
|
+
push(text) {
|
|
223
|
+
this.queue.push(text);
|
|
224
|
+
void this.flush();
|
|
225
|
+
}
|
|
226
|
+
notification(message) {
|
|
227
|
+
if (message.method !== "thread/status/changed") return;
|
|
228
|
+
const params = message.params;
|
|
229
|
+
this.status.set(params.threadId, params.status.type);
|
|
230
|
+
this.changed.set(params.threadId, ++this.clock);
|
|
231
|
+
if (params.status.type === "idle") this.waiting.delete(params.threadId);
|
|
232
|
+
void this.flush();
|
|
233
|
+
}
|
|
234
|
+
async target() {
|
|
235
|
+
if (this.threadId) return this.threadId;
|
|
236
|
+
const result = await this.transport.request("thread/loaded/list", {});
|
|
237
|
+
const threads = [];
|
|
238
|
+
for (const id of result.data) {
|
|
239
|
+
if (!this.parents.has(id)) {
|
|
240
|
+
const read = await this.transport.request("thread/read", { threadId: id });
|
|
241
|
+
this.parents.set(id, Boolean(read.thread.parentThreadId));
|
|
242
|
+
}
|
|
243
|
+
if (!this.parents.get(id)) threads.push(id);
|
|
244
|
+
}
|
|
245
|
+
return threads.reduce((best, id) => !best || (this.changed.get(id) ?? 0) >= (this.changed.get(best) ?? 0) ? id : best, void 0);
|
|
246
|
+
}
|
|
247
|
+
flush() {
|
|
248
|
+
if (this.serial) {
|
|
249
|
+
this.again = true;
|
|
250
|
+
return this.serial;
|
|
251
|
+
}
|
|
252
|
+
if (this.stopped || !this.queue.length) return Promise.resolve();
|
|
253
|
+
this.serial = (async () => {
|
|
254
|
+
do {
|
|
255
|
+
this.again = false;
|
|
256
|
+
if (this.stopped || !this.queue.length) break;
|
|
257
|
+
try {
|
|
258
|
+
const threadId = await this.target();
|
|
259
|
+
if (!threadId || this.stopped) break;
|
|
260
|
+
const status = this.status.get(threadId);
|
|
261
|
+
if (this.waiting.has(threadId) || status && status !== "idle" && status !== "active") break;
|
|
262
|
+
const count = this.queue.length;
|
|
263
|
+
const input = [{ type: "text", text: this.queue.slice(0, count).join("\n\n") }];
|
|
264
|
+
if (status === "active") {
|
|
265
|
+
try {
|
|
266
|
+
const turns = await this.transport.request("thread/turns/list", { threadId });
|
|
267
|
+
const turn = turns.data.find((turn2) => turn2.status === "inProgress");
|
|
268
|
+
if (!turn) throw new Error("no running turn");
|
|
269
|
+
await this.transport.request("turn/steer", { threadId, expectedTurnId: turn.id, input });
|
|
270
|
+
} catch {
|
|
271
|
+
if (this.status.get(threadId) === "idle") {
|
|
272
|
+
this.again = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
this.waiting.add(threadId);
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
await this.transport.request("turn/start", { threadId, input });
|
|
280
|
+
}
|
|
281
|
+
this.queue.splice(0, count);
|
|
282
|
+
} catch {
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
} while (this.again && this.queue.length);
|
|
286
|
+
})().finally(() => {
|
|
287
|
+
this.serial = void 0;
|
|
288
|
+
if (!this.queue.length) {
|
|
289
|
+
clearTimeout(this.timer);
|
|
290
|
+
this.timer = void 0;
|
|
291
|
+
}
|
|
292
|
+
if (!this.stopped && this.queue.length && !this.timer) {
|
|
293
|
+
this.timer = setTimeout(() => {
|
|
294
|
+
this.timer = void 0;
|
|
295
|
+
void this.flush();
|
|
296
|
+
}, 2e3);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
return this.serial;
|
|
300
|
+
}
|
|
301
|
+
close() {
|
|
302
|
+
this.stopped = true;
|
|
303
|
+
clearTimeout(this.timer);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
function option(args, ...names) {
|
|
307
|
+
let value;
|
|
308
|
+
for (let i = 0; i < args.length; i++) {
|
|
309
|
+
const arg = args[i];
|
|
310
|
+
if (names.includes(arg)) {
|
|
311
|
+
value = args[++i];
|
|
312
|
+
if (value === void 0) throw new RelayUsage(`${CLI_NAME} codex ${arg} takes a value`);
|
|
313
|
+
} else if (names.some((name) => arg.startsWith(`${name}=`))) value = arg.slice(arg.indexOf("=") + 1);
|
|
314
|
+
}
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
function serverOptions(args) {
|
|
318
|
+
const result = [];
|
|
319
|
+
const names = ["-c", "--config", "--enable", "--disable"];
|
|
320
|
+
for (let i = 0; i < args.length; i++) {
|
|
321
|
+
const arg = args[i];
|
|
322
|
+
if (names.includes(arg)) {
|
|
323
|
+
const value = args[++i];
|
|
324
|
+
if (value === void 0) throw new RelayUsage(`${CLI_NAME} codex ${arg} takes a value`);
|
|
325
|
+
result.push(arg, value);
|
|
326
|
+
} else if (names.some((name) => arg.startsWith(`${name}=`))) result.push(arg);
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
async function terminate(child) {
|
|
331
|
+
if (child.exitCode !== null || child.signalCode !== null || !child.pid) return;
|
|
332
|
+
await new Promise((resolve) => {
|
|
333
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 2e3);
|
|
334
|
+
child.once("exit", () => {
|
|
335
|
+
clearTimeout(timer);
|
|
336
|
+
resolve();
|
|
337
|
+
});
|
|
338
|
+
child.kill("SIGTERM");
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function appServerPid(pid) {
|
|
342
|
+
const table = readPsTable();
|
|
343
|
+
const seen = /* @__PURE__ */ new Set();
|
|
344
|
+
while (!seen.has(pid)) {
|
|
345
|
+
seen.add(pid);
|
|
346
|
+
const child = [...table ?? []].find(([_, entry]) => entry.ppid === pid && /\sapp-server(?:\s|$)/.test(entry.args));
|
|
347
|
+
if (!child) break;
|
|
348
|
+
pid = child[0];
|
|
349
|
+
}
|
|
350
|
+
return pid;
|
|
351
|
+
}
|
|
352
|
+
async function codexCommand(args) {
|
|
353
|
+
const own = args.includes("--") ? args.slice(0, args.indexOf("--")) : args;
|
|
354
|
+
if (own.includes("--help") || own.includes("-h")) {
|
|
355
|
+
process.stdout.write(`${CODEX_HELP}
|
|
356
|
+
`);
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
const launch = codexLaunch(args, process.env, SELF);
|
|
360
|
+
const cwd = process.cwd();
|
|
361
|
+
const bin = launch.env.SCP_CODEX_BIN || "codex";
|
|
362
|
+
const overrides = [...codexOverrides(launch.env, codexMount(launch.env), SELF), ...serverOptions(launch.codexArgs)];
|
|
363
|
+
const dir = mkdtempSync(join(tmpdir(), "scp-codex-"));
|
|
364
|
+
const path = join(dir, "app.sock");
|
|
365
|
+
const server = spawn(bin, ["app-server", "--listen", `unix://${path}`, ...overrides], { env: launch.env, stdio: ["ignore", "ignore", "pipe"] });
|
|
366
|
+
let stderr = Buffer.alloc(0);
|
|
367
|
+
server.stderr.on("data", (data) => {
|
|
368
|
+
stderr = Buffer.concat([stderr, data]).subarray(-8192);
|
|
369
|
+
});
|
|
370
|
+
let serverError;
|
|
371
|
+
let died = false;
|
|
372
|
+
let finish;
|
|
373
|
+
const done = new Promise((resolve) => {
|
|
374
|
+
finish = resolve;
|
|
375
|
+
});
|
|
376
|
+
server.on("error", (error) => {
|
|
377
|
+
serverError = error;
|
|
378
|
+
died = true;
|
|
379
|
+
finish(1);
|
|
380
|
+
});
|
|
381
|
+
server.on("exit", () => {
|
|
382
|
+
died = true;
|
|
383
|
+
finish(1);
|
|
384
|
+
});
|
|
385
|
+
const failure = () => new RelayUsage(
|
|
386
|
+
`${CLI_NAME} codex: app-server failed${serverError ? `: ${serverError.message}` : ""}${stderr.length ? `
|
|
387
|
+
${stderr.toString().trim()}` : ""}`
|
|
388
|
+
);
|
|
389
|
+
let socket;
|
|
390
|
+
let waker;
|
|
391
|
+
let tui;
|
|
392
|
+
let channel;
|
|
393
|
+
const input = new PassThrough();
|
|
394
|
+
const ignore = () => {
|
|
395
|
+
};
|
|
396
|
+
let stopping = false;
|
|
397
|
+
const stop = () => {
|
|
398
|
+
stopping = true;
|
|
399
|
+
finish(0);
|
|
400
|
+
};
|
|
401
|
+
const forward = (signal) => {
|
|
402
|
+
if (tui) tui.kill(signal);
|
|
403
|
+
else stop();
|
|
404
|
+
};
|
|
405
|
+
process.on("SIGINT", launch.headless ? stop : ignore);
|
|
406
|
+
process.on("SIGTERM", launch.headless ? stop : forward);
|
|
407
|
+
process.on("SIGHUP", launch.headless ? stop : forward);
|
|
408
|
+
try {
|
|
409
|
+
const end = Date.now() + 2e4;
|
|
410
|
+
while (!socket && Date.now() < end && !died && !stopping) {
|
|
411
|
+
const candidate = new CodexSocket(path, (message) => {
|
|
412
|
+
waker?.notification(message);
|
|
413
|
+
if (!launch.headless) return;
|
|
414
|
+
if (message.id !== void 0 && typeof message.method === "string") {
|
|
415
|
+
if (message.method.endsWith("/requestApproval")) {
|
|
416
|
+
process.stderr.write(`${CLI_NAME} codex: declined ${message.method}
|
|
417
|
+
`);
|
|
418
|
+
candidate.send({ id: message.id, result: { decision: "decline" } });
|
|
419
|
+
} else candidate.send({ id: message.id, error: { code: -32601, message: "unsupported server request" } });
|
|
420
|
+
}
|
|
421
|
+
if (message.method === "item/completed") {
|
|
422
|
+
const item = message.params.item;
|
|
423
|
+
if (item.type === "agentMessage" && typeof item.text === "string") process.stdout.write(`${item.text}
|
|
424
|
+
`);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
let timeout;
|
|
428
|
+
try {
|
|
429
|
+
await Promise.race([
|
|
430
|
+
(async () => {
|
|
431
|
+
await candidate.ready;
|
|
432
|
+
await candidate.request("initialize", { clientInfo: { name: "scp-waker", version: "0.1.0" } });
|
|
433
|
+
candidate.send({ method: "initialized" });
|
|
434
|
+
})(),
|
|
435
|
+
new Promise((_, reject) => {
|
|
436
|
+
timeout = setTimeout(() => reject(new Error("initialize timed out")), Math.min(1e3, end - Date.now()));
|
|
437
|
+
})
|
|
438
|
+
]);
|
|
439
|
+
socket = candidate;
|
|
440
|
+
} catch {
|
|
441
|
+
candidate.close();
|
|
442
|
+
if (!died) await sleep(100);
|
|
443
|
+
} finally {
|
|
444
|
+
clearTimeout(timeout);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (stopping) return 0;
|
|
448
|
+
if (!socket || died) throw failure();
|
|
449
|
+
let threadId;
|
|
450
|
+
if (launch.headless) {
|
|
451
|
+
const resume = option(own, "--resume");
|
|
452
|
+
const model = option(launch.codexArgs, "-m", "--model");
|
|
453
|
+
const sandbox = option(launch.codexArgs, "-s", "--sandbox");
|
|
454
|
+
const result = await socket.request(resume ? "thread/resume" : "thread/start", {
|
|
455
|
+
...resume ? { threadId: resume } : { cwd },
|
|
456
|
+
approvalPolicy: "never",
|
|
457
|
+
...model !== void 0 ? { model } : {},
|
|
458
|
+
...sandbox !== void 0 ? { sandbox } : {}
|
|
459
|
+
});
|
|
460
|
+
threadId = result.thread.id;
|
|
461
|
+
}
|
|
462
|
+
if (stopping) return 0;
|
|
463
|
+
waker = new CodexWaker(socket, threadId);
|
|
464
|
+
const pid = appServerPid(server.pid);
|
|
465
|
+
const claim = relayIdentity({ harness: "codex", fallbackHarness: "codex", cwd, env: launch.env, harnessPid: () => pid });
|
|
466
|
+
const broker = relayDir(launch.env);
|
|
467
|
+
let lines = "";
|
|
468
|
+
channel = runRelayMcp({
|
|
469
|
+
connect: async () => {
|
|
470
|
+
while (!input.writableEnded) {
|
|
471
|
+
const client = await connectBroker(broker, () => startDaemon(broker));
|
|
472
|
+
try {
|
|
473
|
+
const hello = await client.hello({ observe: true, self: [claim.process], cwd });
|
|
474
|
+
if (hello.endpoint) {
|
|
475
|
+
client.hello = async () => hello;
|
|
476
|
+
return client;
|
|
477
|
+
}
|
|
478
|
+
} catch (error) {
|
|
479
|
+
client.close();
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
client.close();
|
|
483
|
+
await sleep(100);
|
|
484
|
+
}
|
|
485
|
+
throw new Error("codex channel stopped");
|
|
486
|
+
},
|
|
487
|
+
claim,
|
|
488
|
+
channels: true,
|
|
489
|
+
input,
|
|
490
|
+
env: launch.env,
|
|
491
|
+
output: (chunk) => {
|
|
492
|
+
lines += chunk;
|
|
493
|
+
for (; ; ) {
|
|
494
|
+
const end2 = lines.indexOf("\n");
|
|
495
|
+
if (end2 < 0) break;
|
|
496
|
+
const message = JSON.parse(lines.slice(0, end2));
|
|
497
|
+
lines = lines.slice(end2 + 1);
|
|
498
|
+
if (message.method === "notifications/claude/channel") {
|
|
499
|
+
const content = message.params.content;
|
|
500
|
+
if (typeof content === "string") waker.push(content);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
log: (line) => {
|
|
505
|
+
process.stderr.write(`${line}
|
|
506
|
+
`);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
void channel.catch((error) => {
|
|
510
|
+
process.stderr.write(`${CLI_NAME} codex: ${String(error)}
|
|
511
|
+
`);
|
|
512
|
+
finish(1);
|
|
513
|
+
});
|
|
514
|
+
input.write(
|
|
515
|
+
`${JSON.stringify({ id: 1, method: "initialize", params: { protocolVersion: MCP_PROTOCOLS[0], capabilities: { experimental: { "claude/channel": {} } }, clientInfo: { name: "scp-waker", version: "0.1.0" } } })}
|
|
516
|
+
`
|
|
517
|
+
);
|
|
518
|
+
input.write(`${JSON.stringify({ method: "notifications/initialized" })}
|
|
519
|
+
`);
|
|
520
|
+
if (launch.headless) {
|
|
521
|
+
process.stderr.write(`${CLI_NAME} codex: ${launch.name ?? claim.handle} waiting for messages (thread ${threadId})
|
|
522
|
+
`);
|
|
523
|
+
} else {
|
|
524
|
+
tui = spawn(bin, ["--remote", `unix://${path}`, ...launch.codexArgs], { env: launch.env, stdio: "inherit" });
|
|
525
|
+
tui.once("error", (error) => {
|
|
526
|
+
process.stderr.write(`${CLI_NAME} codex: ${error.message}
|
|
527
|
+
`);
|
|
528
|
+
finish(1);
|
|
529
|
+
});
|
|
530
|
+
tui.once("exit", (code2, signal) => finish(code2 ?? (signal ? 1 : 0)));
|
|
531
|
+
}
|
|
532
|
+
const code = await done;
|
|
533
|
+
if (died) {
|
|
534
|
+
process.stderr.write(`${failure().message}
|
|
535
|
+
`);
|
|
536
|
+
return 1;
|
|
537
|
+
}
|
|
538
|
+
return code;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (died) throw failure();
|
|
541
|
+
throw error;
|
|
542
|
+
} finally {
|
|
543
|
+
process.off("SIGINT", stop);
|
|
544
|
+
process.off("SIGINT", ignore);
|
|
545
|
+
process.off("SIGTERM", stop);
|
|
546
|
+
process.off("SIGHUP", stop);
|
|
547
|
+
process.off("SIGTERM", forward);
|
|
548
|
+
process.off("SIGHUP", forward);
|
|
549
|
+
waker?.close();
|
|
550
|
+
input.end();
|
|
551
|
+
await channel?.catch(() => {
|
|
552
|
+
});
|
|
553
|
+
socket?.close();
|
|
554
|
+
if (tui) await terminate(tui);
|
|
555
|
+
await terminate(server);
|
|
556
|
+
rmSync(dir, { recursive: true, force: true });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
export {
|
|
560
|
+
CODEX_HELP,
|
|
561
|
+
CodexSocket,
|
|
562
|
+
CodexWaker,
|
|
563
|
+
codexCommand,
|
|
564
|
+
codexLaunch
|
|
565
|
+
};
|