@scp-dev/cli 0.2.0 → 0.2.2
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-E7JG5PJB.js +806 -0
- package/dist/{chunk-5DYFYLAQ.js → chunk-F5QVHJIT.js} +3 -5
- package/dist/{chunk-GH7AXFUL.js → chunk-G2SQPY6E.js} +8 -10
- package/dist/{chunk-M2DCR2J4.js → chunk-MNTIEWKF.js} +13 -3
- package/dist/{chunk-WV5FINTW.js → chunk-NOYMSD4F.js} +20 -10
- package/dist/{chunk-RXKBND3T.js → chunk-OTO6DBU6.js} +229 -13
- package/dist/{chunk-U2RSSTNW.js → chunk-ZDU53ZRF.js} +3 -5
- package/dist/{claude-YJLGFQ2P.js → claude-CVWECMRQ.js} +21 -11
- package/dist/cli.js +1 -1
- package/dist/codex-Y7SGLZ64.js +565 -0
- package/dist/{daemon-SUTLTY5U.js → daemon-OMJY7T2F.js} +1 -1
- package/dist/{hooks-EI7M3YKY.js → hooks-EKGY5O5N.js} +19 -3
- package/dist/install-4VBSX6RI.js +14 -0
- package/dist/join-RGFHOSLE.js +48 -0
- package/dist/{login-V2WT7QX5.js → login-IPS75GLQ.js} +4 -7
- package/dist/{main-HPBTO3JK.js → main-YN3J6MGW.js} +116 -32
- package/dist/{mcp-DGAQNFKO.js → mcp-BVC4PLZ7.js} +11 -11
- package/dist/mesh-VNZV3POO.js +28 -0
- package/dist/{peek-WLG3ZROU.js → peek-4D4VGKOW.js} +2 -3
- package/dist/{pi-HMESQA64.js → pi-R23XR4AC.js} +2 -3
- package/dist/{relay-5Z7IEXKD.js → relay-U634RHIJ.js} +6 -4
- package/dist/{remote-FYN5ZXE4.js → remote-NTHRHVEP.js} +3 -5
- package/dist/{setup-LXG346FJ.js → setup-H7GGUID2.js} +5 -6
- package/dist/{spawn-DLPMAS5M.js → spawn-AJAYD6CW.js} +6 -7
- package/dist/{up-GPM4LWY3.js → up-HZKGURPI.js} +6 -9
- package/package.json +8 -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
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
RelayUsage
|
|
3
|
-
} from "./chunk-3V3DRXB2.js";
|
|
4
1
|
import {
|
|
5
2
|
AGY_HOOK_SET,
|
|
6
3
|
AGY_UNVERIFIED,
|
|
@@ -13,10 +10,11 @@ import {
|
|
|
13
10
|
mergeHooks,
|
|
14
11
|
musePlugin,
|
|
15
12
|
opencodePlugin
|
|
16
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-OTO6DBU6.js";
|
|
17
14
|
import {
|
|
15
|
+
RelayUsage,
|
|
18
16
|
gitToplevel
|
|
19
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-E7JG5PJB.js";
|
|
20
18
|
|
|
21
19
|
// src/install.ts
|
|
22
20
|
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
@@ -3,28 +3,26 @@ import {
|
|
|
3
3
|
agySettingsFile,
|
|
4
4
|
mcpFile,
|
|
5
5
|
mountedEntry
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-NOYMSD4F.js";
|
|
7
|
+
import {
|
|
8
|
+
steerPrompt
|
|
9
|
+
} from "./chunk-OTO6DBU6.js";
|
|
7
10
|
import {
|
|
8
11
|
OPTIONS,
|
|
9
12
|
RelayUsage,
|
|
10
13
|
SPAWN_OPTIONS,
|
|
14
|
+
detectHarness,
|
|
11
15
|
grantParams,
|
|
12
16
|
localClient,
|
|
13
17
|
open,
|
|
14
18
|
selectorList,
|
|
19
|
+
spawnerKey,
|
|
15
20
|
walkNow
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import {
|
|
18
|
-
steerPrompt
|
|
19
|
-
} from "./chunk-RXKBND3T.js";
|
|
20
|
-
import {
|
|
21
|
-
detectHarness,
|
|
22
|
-
spawnerKey
|
|
23
|
-
} from "./chunk-E53YHYEP.js";
|
|
21
|
+
} from "./chunk-E7JG5PJB.js";
|
|
24
22
|
import {
|
|
25
23
|
SELF,
|
|
26
24
|
note
|
|
27
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-MNTIEWKF.js";
|
|
28
26
|
|
|
29
27
|
// src/spawn.ts
|
|
30
28
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/term.ts
|
|
2
2
|
import { spawnSync } from "child_process";
|
|
3
|
+
import { fstatSync } from "fs";
|
|
3
4
|
import { CLI_NAME } from "@scp-dev/core";
|
|
4
5
|
var FAILED = 125;
|
|
5
6
|
var SELF = [process.execPath, ...process.versions.bun ? ["--no-env-file", "--config=/dev/null"] : [], process.argv[1]];
|
|
@@ -11,16 +12,25 @@ function note(message) {
|
|
|
11
12
|
process.stderr.write(`${message.startsWith(`${CLI_NAME} `) ? "" : `${CLI_NAME}: `}${message}
|
|
12
13
|
`);
|
|
13
14
|
}
|
|
15
|
+
function isSocket(fd) {
|
|
16
|
+
try {
|
|
17
|
+
return fstatSync(fd).isSocket();
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
14
22
|
async function exitFlushed(code, waitMs = 1e4) {
|
|
15
|
-
const ended = (stream) => new Promise((resolve) => {
|
|
23
|
+
const ended = (stream, fd) => new Promise((resolve) => {
|
|
16
24
|
try {
|
|
17
|
-
stream.end(() => resolve());
|
|
25
|
+
if (!isSocket(fd)) stream.end(() => resolve());
|
|
26
|
+
else if (stream.writableNeedDrain) stream.once("drain", () => resolve());
|
|
27
|
+
else stream.write("", () => resolve());
|
|
18
28
|
} catch {
|
|
19
29
|
resolve();
|
|
20
30
|
}
|
|
21
31
|
});
|
|
22
32
|
let timer;
|
|
23
|
-
await Promise.race([Promise.all([ended(process.stdout), ended(process.stderr)]), new Promise((resolve) => timer = setTimeout(resolve, waitMs))]);
|
|
33
|
+
await Promise.race([Promise.all([ended(process.stdout, 1), ended(process.stderr, 2)]), new Promise((resolve) => timer = setTimeout(resolve, waitMs))]);
|
|
24
34
|
clearTimeout(timer);
|
|
25
35
|
process.exit(code);
|
|
26
36
|
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
hooksTarget
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import {
|
|
5
|
-
RelayUsage
|
|
6
|
-
} from "./chunk-3V3DRXB2.js";
|
|
3
|
+
} from "./chunk-F5QVHJIT.js";
|
|
7
4
|
import {
|
|
8
5
|
AGY_HOOK_SET,
|
|
9
6
|
BLOCK_STOP,
|
|
@@ -15,10 +12,12 @@ import {
|
|
|
15
12
|
mergeHooks,
|
|
16
13
|
musePlugin,
|
|
17
14
|
opencodePlugin
|
|
18
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-OTO6DBU6.js";
|
|
19
16
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
RelayUsage,
|
|
18
|
+
gitToplevel,
|
|
19
|
+
setJoinMode
|
|
20
|
+
} from "./chunk-E7JG5PJB.js";
|
|
22
21
|
|
|
23
22
|
// src/setup.ts
|
|
24
23
|
import { spawnSync } from "child_process";
|
|
@@ -50,7 +49,7 @@ var SETUP_HARNESSES = ["claude", "codex", "opencode", "grok", "muse", "agy"];
|
|
|
50
49
|
var SERVER = "scp";
|
|
51
50
|
var HOOKED = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "agy", "muse"]);
|
|
52
51
|
var VERIFY_TIMEOUT_MS = 6e4;
|
|
53
|
-
var SETUP_HELP = `${CLI_NAME} setup [--write] [--no-hooks] [--block-stop-on-unread] [--undo] [--bin PATH] [--harness H]
|
|
52
|
+
var SETUP_HELP = `${CLI_NAME} setup [--write] [--no-hooks] [--block-stop-on-unread] [--join auto|manual] [--undo] [--bin PATH] [--harness H]
|
|
54
53
|
mounts the relay MCP shim in every installed harness (claude, codex, opencode, grok, muse, agy) at user scope, running
|
|
55
54
|
the ${CLI_NAME} PATH finds (by that path) or --bin, and installs the relay hooks at user scope (Muse's as a plugin). Prints
|
|
56
55
|
the plan; --write applies it (each file backed up next to itself as <file>.bak-<time>, its mode kept), then checks
|
|
@@ -60,6 +59,8 @@ var SETUP_HELP = `${CLI_NAME} setup [--write] [--no-hooks] [--block-stop-on-unre
|
|
|
60
59
|
messages are unread; without it a hooks file keeps the stop hook it has. ${CLI_NAME} spawn returns at once from an
|
|
61
60
|
agent's shell; while the workers it started run, a stop waits up to 600 s for their next message or handback (hooks
|
|
62
61
|
install --push-wait), so results arrive by themselves. --no-hooks mounts the MCP shim only.
|
|
62
|
+
--join auto (the default) puts every hooked session on the relay when it starts; --join manual keeps a session off it
|
|
63
|
+
until it runs ${CLI_NAME} join <name> (or calls relay_join); saved in ~/.config/scp/config.json with --write.
|
|
63
64
|
--undo removes only what setup added (it keeps a list in ~/.config/scp/setup.json), its hooks too unless --no-hooks.`;
|
|
64
65
|
var KNOWN = {
|
|
65
66
|
claude: [".local/bin/claude", ".claude/local/claude"],
|
|
@@ -954,6 +955,7 @@ function relaySetup(options, deps) {
|
|
|
954
955
|
throw new RelayUsage(`--harness is one of ${SETUP_HARNESSES.join(", ")}`);
|
|
955
956
|
}
|
|
956
957
|
if (options.blockStop && (options.hooks === false || options.undo)) throw new RelayUsage(`${BLOCK_STOP} goes with the hooks: not with --no-hooks or --undo`);
|
|
958
|
+
if (options.join !== void 0 && options.undo) throw new RelayUsage("--join sets how sessions join: not with --undo");
|
|
957
959
|
let added;
|
|
958
960
|
try {
|
|
959
961
|
added = readManifest(env);
|
|
@@ -976,6 +978,12 @@ function relaySetup(options, deps) {
|
|
|
976
978
|
out(`${CLI_NAME} setup${options.undo ? " --undo" : ""}: ${options.write ? "applying" : "dry run, nothing is written (add --write to apply)"}`);
|
|
977
979
|
let hooks = options.hooks !== false;
|
|
978
980
|
let unresolved = 0;
|
|
981
|
+
if (options.join !== void 0) {
|
|
982
|
+
if (options.write) setJoinMode(env, options.join);
|
|
983
|
+
out(
|
|
984
|
+
`join: ${options.join}${options.join === "manual" ? ` (a session stays off the relay until it runs ${CLI_NAME} join <name>)` : " (every hooked session joins when it starts)"}${options.write ? "" : ", saved with --write"}`
|
|
985
|
+
);
|
|
986
|
+
}
|
|
979
987
|
if (options.hooks === false) out(`hooks: ${options.undo ? "kept (--no-hooks)" : "not installed (--no-hooks); only the MCP shim is mounted"}`);
|
|
980
988
|
if (!options.undo) {
|
|
981
989
|
if (options.bin !== void 0) out(`mounts run: ${bin} (from --bin; they break if that file moves)`);
|
|
@@ -1136,6 +1144,7 @@ function parseSetupArgs(args) {
|
|
|
1136
1144
|
"block-stop-on-unread": { type: "boolean" },
|
|
1137
1145
|
bin: { type: "string" },
|
|
1138
1146
|
harness: { type: "string" },
|
|
1147
|
+
join: { type: "string" },
|
|
1139
1148
|
help: { type: "boolean", short: "h" }
|
|
1140
1149
|
};
|
|
1141
1150
|
let parsed;
|
|
@@ -1149,10 +1158,11 @@ function parseSetupArgs(args) {
|
|
|
1149
1158
|
} catch (error) {
|
|
1150
1159
|
throw new RelayUsage(message(error));
|
|
1151
1160
|
}
|
|
1152
|
-
const { help, hooks, "no-hooks": noHooks, "block-stop-on-unread": blockStop, ...rest } = parsed.values;
|
|
1161
|
+
const { help, hooks, "no-hooks": noHooks, "block-stop-on-unread": blockStop, join: joins, ...rest } = parsed.values;
|
|
1153
1162
|
if (help) return "help";
|
|
1163
|
+
if (joins !== void 0 && joins !== "auto" && joins !== "manual") throw new RelayUsage("--join takes auto or manual");
|
|
1154
1164
|
if (hooks && noHooks) throw new RelayUsage("--hooks and --no-hooks contradict each other; the hooks are installed unless --no-hooks");
|
|
1155
|
-
return { ...rest, ...noHooks ? { hooks: false } : {}, ...blockStop ? { blockStop } : {} };
|
|
1165
|
+
return { ...rest, ...noHooks ? { hooks: false } : {}, ...blockStop ? { blockStop } : {}, ...joins !== void 0 ? { join: joins } : {} };
|
|
1156
1166
|
}
|
|
1157
1167
|
function relaySetupCommand(args, cwd, out) {
|
|
1158
1168
|
const options = parseSetupArgs(args);
|
|
@@ -6,12 +6,17 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
hookHarness,
|
|
8
8
|
hookHarnessPid,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
membership,
|
|
10
|
+
normalizeHarness,
|
|
11
|
+
processKey,
|
|
12
|
+
psStart,
|
|
13
|
+
relayIdentity,
|
|
14
|
+
sessionKey
|
|
15
|
+
} from "./chunk-E7JG5PJB.js";
|
|
11
16
|
|
|
12
17
|
// src/hooks.ts
|
|
13
18
|
import { Readable } from "stream";
|
|
14
|
-
import { readFileSync, writeSync } from "fs";
|
|
19
|
+
import { closeSync, fstatSync, openSync, readFileSync, readSync, writeSync } from "fs";
|
|
15
20
|
import { homedir } from "os";
|
|
16
21
|
import { basename, join } from "path";
|
|
17
22
|
import {
|
|
@@ -24,6 +29,7 @@ import {
|
|
|
24
29
|
redactSecrets,
|
|
25
30
|
shortId,
|
|
26
31
|
stripControl,
|
|
32
|
+
TITLE_CHARS,
|
|
27
33
|
validHandle,
|
|
28
34
|
wrapMessage
|
|
29
35
|
} from "@scp-dev/core";
|
|
@@ -33,6 +39,8 @@ var HOOK_COMMAND = `${CLI_NAME} hook`;
|
|
|
33
39
|
var BLOCK_STOP = "--block-stop-on-unread";
|
|
34
40
|
var NO_INJECT = "--no-inject";
|
|
35
41
|
var PUSH_WAIT = "--push-wait";
|
|
42
|
+
var WAKE = "--wake";
|
|
43
|
+
var OPENCODE_WAKE = "opencode-plugin";
|
|
36
44
|
var PUSH_WAIT_DEFAULT_S = 600;
|
|
37
45
|
var PUSH_TIMEOUT_MARGIN_S = 30;
|
|
38
46
|
var PUSH_PRINT_MS = 1e3;
|
|
@@ -286,6 +294,57 @@ function claudeVersion(payload, env) {
|
|
|
286
294
|
return void 0;
|
|
287
295
|
}
|
|
288
296
|
}
|
|
297
|
+
var TITLE_TAIL_BYTES = 128 * 1024;
|
|
298
|
+
var TITLE_LINE = /^\{"type":"(custom-title|ai-title|summary)"/;
|
|
299
|
+
var OPENCODE_UNNAMED = /^(New|Child) session - \d{4}-/;
|
|
300
|
+
function titleText(text) {
|
|
301
|
+
if (!text) return void 0;
|
|
302
|
+
const line = redactSecrets(stripControl(text.slice(0, 4096)).replace(/\s+/g, " ").trim()).slice(0, TITLE_CHARS).trim();
|
|
303
|
+
return line || void 0;
|
|
304
|
+
}
|
|
305
|
+
function promptTitle(prompt) {
|
|
306
|
+
return titleText(prompt?.split("\n").find((line) => line.trim()));
|
|
307
|
+
}
|
|
308
|
+
function transcriptTitle(path, session) {
|
|
309
|
+
if (!path || !session || !path.startsWith("/") || !path.endsWith(`/${session}.jsonl`)) return void 0;
|
|
310
|
+
let fd;
|
|
311
|
+
try {
|
|
312
|
+
fd = openSync(path, "r");
|
|
313
|
+
const size = fstatSync(fd).size;
|
|
314
|
+
const start = Math.max(0, size - TITLE_TAIL_BYTES);
|
|
315
|
+
const buffer = Buffer.alloc(size - start);
|
|
316
|
+
const read = readSync(fd, buffer, 0, buffer.length, start);
|
|
317
|
+
const lines = buffer.subarray(0, read).toString("utf8").split("\n");
|
|
318
|
+
if (start > 0) lines.shift();
|
|
319
|
+
const found = {};
|
|
320
|
+
for (const line of lines) {
|
|
321
|
+
const kind = TITLE_LINE.exec(line)?.[1];
|
|
322
|
+
if (!kind) continue;
|
|
323
|
+
try {
|
|
324
|
+
const record = JSON.parse(line);
|
|
325
|
+
const text = asString(record.customTitle ?? record.aiTitle ?? record.summary);
|
|
326
|
+
if (text) found[kind] = text;
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return titleText(found["custom-title"] ?? found["ai-title"] ?? found.summary);
|
|
331
|
+
} catch {
|
|
332
|
+
return void 0;
|
|
333
|
+
} finally {
|
|
334
|
+
if (fd !== void 0) closeSync(fd);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function inferredTitle(harness, event, named, payload) {
|
|
338
|
+
if (event.agent) return void 0;
|
|
339
|
+
if (event.kind === "prompt" && named === "userpromptsubmit") {
|
|
340
|
+
const text2 = promptTitle(asString(payload.prompt));
|
|
341
|
+
return text2 ? { text: text2, from: "prompt" } : void 0;
|
|
342
|
+
}
|
|
343
|
+
if (event.kind !== "stop" || named !== "stop") return void 0;
|
|
344
|
+
const own = harness === "opencode" ? asString(payload.session_title) : void 0;
|
|
345
|
+
const text = harness === "claude-code" ? transcriptTitle(asString(field(payload, "transcript_path", "transcriptPath")), asString(field(payload, "session_id", "sessionId"))) : own && !OPENCODE_UNNAMED.test(own) ? titleText(own) : void 0;
|
|
346
|
+
return text ? { text, from: "summary" } : void 0;
|
|
347
|
+
}
|
|
289
348
|
function hookReply(harness, kind, text, unread = 0, continues = true) {
|
|
290
349
|
if (kind === "stop" && harness === "claude-code" && continues)
|
|
291
350
|
return JSON.stringify({ hookSpecificOutput: { hookEventName: "Stop", additionalContext: text } });
|
|
@@ -298,6 +357,12 @@ function hookReply(harness, kind, text, unread = 0, continues = true) {
|
|
|
298
357
|
return JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext: text } });
|
|
299
358
|
}
|
|
300
359
|
var DEFERS = /* @__PURE__ */ new Set(["claude-code", "codex"]);
|
|
360
|
+
var TLDR_EVERY_DEFAULT = 10;
|
|
361
|
+
var TLDR = `SCP: update your tl;dr with \`${CLI_NAME} set-now "..."\` or relay_set_now (what you are doing now).`;
|
|
362
|
+
function tldrEvery(env) {
|
|
363
|
+
const raw = env.SCP_TLDR_EVERY?.trim();
|
|
364
|
+
return raw && /^\d{1,6}$/.test(raw) ? Number(raw) : TLDR_EVERY_DEFAULT;
|
|
365
|
+
}
|
|
301
366
|
var NUDGE = `${CLI_NAME} (optional): a title and a one-line now (relay_set_title / relay_set_now) help peers find you; skip it when you only run workers.`;
|
|
302
367
|
async function runHook(args, options) {
|
|
303
368
|
const began = Date.now();
|
|
@@ -305,8 +370,10 @@ async function runHook(args, options) {
|
|
|
305
370
|
const takes = !args.includes(NO_INJECT);
|
|
306
371
|
const subagents = args.includes(SUBAGENT);
|
|
307
372
|
const waits = pushWait(args, options.env);
|
|
373
|
+
const wakeAt = args.indexOf(WAKE);
|
|
374
|
+
const wakeS = wakeAt < 0 ? 0 : Number(args[wakeAt + 1]);
|
|
308
375
|
const [harnessArg, eventArg] = args.filter(
|
|
309
|
-
(arg, i) => arg !== BLOCK_STOP && arg !== NO_INJECT && arg !== SUBAGENT && arg !== PUSH_WAIT && args[i - 1] !== PUSH_WAIT
|
|
376
|
+
(arg, i) => arg !== BLOCK_STOP && arg !== NO_INJECT && arg !== SUBAGENT && arg !== PUSH_WAIT && args[i - 1] !== PUSH_WAIT && arg !== WAKE && args[i - 1] !== WAKE
|
|
310
377
|
);
|
|
311
378
|
let payload;
|
|
312
379
|
try {
|
|
@@ -315,18 +382,32 @@ async function runHook(args, options) {
|
|
|
315
382
|
} catch {
|
|
316
383
|
payload = {};
|
|
317
384
|
}
|
|
385
|
+
if (Number.isFinite(wakeS) && wakeS > 0) return wakeHook(harnessArg, payload, wakeS, options);
|
|
318
386
|
const event = mapHookPayload(payload, eventArg);
|
|
319
387
|
if (!event || options.env.SCP_TRANSPORT === "cloud") return;
|
|
320
388
|
const sub = subagents && (event.kind === "tool" || event.kind === "stop") ? subagentOf(payload) : void 0;
|
|
321
389
|
if (subagents && !sub) return;
|
|
322
390
|
if (event.kind === "session_end") options.deadline?.(1e3);
|
|
323
391
|
if (event.kind === "session_start") options.deadline?.(1500);
|
|
392
|
+
const harness = normalizeHarness(hookHarness(harnessArg, options.env), "hook");
|
|
393
|
+
const pid = (options.harnessPid ?? ((h) => hookHarnessPid(options.env, h)))(harness);
|
|
394
|
+
const started = psStart(pid);
|
|
395
|
+
const joined = membership(
|
|
396
|
+
options.env,
|
|
397
|
+
sessionKey(
|
|
398
|
+
options.env,
|
|
399
|
+
processKey(pid, () => started)
|
|
400
|
+
)
|
|
401
|
+
);
|
|
402
|
+
if ("dark" in joined) return;
|
|
324
403
|
const claim = relayIdentity({
|
|
325
|
-
|
|
404
|
+
...joined.handle ? { as: joined.handle } : {},
|
|
405
|
+
harness,
|
|
326
406
|
fallbackHarness: "hook",
|
|
327
407
|
cwd: event.cwd ?? options.cwd,
|
|
328
408
|
env: options.env,
|
|
329
|
-
harnessPid:
|
|
409
|
+
harnessPid: () => pid,
|
|
410
|
+
startOf: () => started
|
|
330
411
|
});
|
|
331
412
|
if (!validHandle(claim.handle)) return;
|
|
332
413
|
const named = eventName(asString(field(payload, "hook_event_name", "hookEventName")) ?? eventArg);
|
|
@@ -345,7 +426,9 @@ async function runHook(args, options) {
|
|
|
345
426
|
const inject = takes && !event.agent && (stop || injectsAt(claim.harness, event.kind)) && (options.remaining?.() ?? Number.POSITIVE_INFINITY) >= INJECT_MIN_MS;
|
|
346
427
|
try {
|
|
347
428
|
const posted = [client.hello({ transient: true, claim })];
|
|
348
|
-
|
|
429
|
+
const every = tldrEvery(options.env);
|
|
430
|
+
const prompted = event.kind === "prompt" && named === "userpromptsubmit" ? client.request("prompted", every ? { tldr_every: every } : {}) : void 0;
|
|
431
|
+
if (prompted) posted.push(prompted);
|
|
349
432
|
if (event.kind !== "prompt") {
|
|
350
433
|
const session = event.agent ? void 0 : asString(field(payload, "session_id", "sessionId"));
|
|
351
434
|
const transcript = session && claim.harness === "claude-code" ? asString(field(payload, "transcript_path", "transcriptPath")) : void 0;
|
|
@@ -363,6 +446,8 @@ async function runHook(args, options) {
|
|
|
363
446
|
if (event.status) posted.push(client.request("status", { state: event.status }));
|
|
364
447
|
const turn = peekTurn(event, named, payload);
|
|
365
448
|
if (turn) posted.push(client.request("peek_turn", turn));
|
|
449
|
+
const title = inferredTitle(claim.harness, event, named, payload);
|
|
450
|
+
if (title) posted.push(client.request("title_infer", title));
|
|
366
451
|
const injected = inject ? client.request(
|
|
367
452
|
"inject",
|
|
368
453
|
counts ? { count: true, ...stop ? { stop: true, directives: true, ...blockStop ? { hold_unread: true } : {} } : {} } : {}
|
|
@@ -372,7 +457,8 @@ async function runHook(args, options) {
|
|
|
372
457
|
const untitled = hello?.status === "fulfilled" && hello.value.endpoint?.title === null;
|
|
373
458
|
return untitled && (claim.harness === "claude-code" || claim.harness === "codex") ? hookReply(claim.harness, event.kind, NUDGE) : void 0;
|
|
374
459
|
}
|
|
375
|
-
|
|
460
|
+
const tldr = (await prompted?.catch(() => void 0))?.tldr === true ? TLDR : void 0;
|
|
461
|
+
if (result.status !== "fulfilled" || !result.value) return tldr ? hookReply(claim.harness, event.kind, tldr) : void 0;
|
|
376
462
|
const continues = !stop || claim.harness !== "claude-code" || stopContinues(claudeVersion(payload, options.env));
|
|
377
463
|
if (stop && !result.value.block) {
|
|
378
464
|
if (waits === void 0 || !PUSHES.has(claim.harness) || !(result.value.workers || result.value.worker_unread) || result.value.pushes_left === 0) return;
|
|
@@ -384,7 +470,7 @@ async function runHook(args, options) {
|
|
|
384
470
|
const nudged = stop ? result.value.nudge ?? [] : [];
|
|
385
471
|
const intro = !stop && result.value.intro ? introText(result.value.intro, result.value.intro_key) : void 0;
|
|
386
472
|
const shown = stop || counts ? void 0 : injectionText({ ...result.value, more: await moreNow(client, result.value) }, claim.harness !== "agy");
|
|
387
|
-
const text = stop ? [directive?.text, [pointer, nudgeText(nudged, result.value.nudge_more)].filter(Boolean).join("\n")].filter(Boolean).join("\n\n") : [intro, counts ? unreadPointer(result.value.unread, result.value.awaiting) : shown].filter(Boolean).join("\n\n");
|
|
473
|
+
const text = stop ? [directive?.text, [pointer, nudgeText(nudged, result.value.nudge_more)].filter(Boolean).join("\n")].filter(Boolean).join("\n\n") : [intro, counts ? unreadPointer(result.value.unread, result.value.awaiting) : shown, tldr].filter(Boolean).join("\n\n");
|
|
388
474
|
if (!text || (options.remaining?.() ?? Number.POSITIVE_INFINITY) < CONFIRM_MIN_MS) return;
|
|
389
475
|
const reply = hookReply(claim.harness, event.kind, text, result.value.unread, continues);
|
|
390
476
|
const offered = result.value.offered ?? [];
|
|
@@ -409,6 +495,45 @@ async function runHook(args, options) {
|
|
|
409
495
|
client.close();
|
|
410
496
|
}
|
|
411
497
|
}
|
|
498
|
+
async function wakeHook(harnessArg, payload, waitS, options) {
|
|
499
|
+
if (options.env.SCP_TRANSPORT === "cloud" || field(payload, "agent_id", "agentId", "agent_type", "agentType", "subagentType")) return;
|
|
500
|
+
const claim = relayIdentity({
|
|
501
|
+
harness: hookHarness(harnessArg, options.env),
|
|
502
|
+
fallbackHarness: "hook",
|
|
503
|
+
cwd: asString(payload.cwd) ?? options.cwd,
|
|
504
|
+
env: options.env,
|
|
505
|
+
harnessPid: options.harnessPid ?? ((harness) => hookHarnessPid(options.env, harness))
|
|
506
|
+
});
|
|
507
|
+
if (!validHandle(claim.handle)) return;
|
|
508
|
+
const waitMs = Math.min(waitS, LIMITS.waitMaxS) * 1e3;
|
|
509
|
+
options.deadline?.(waitMs + PUSH_PRINT_MS + 1e3);
|
|
510
|
+
const client = await options.connect(false);
|
|
511
|
+
try {
|
|
512
|
+
await client.hello({ transient: true, claim });
|
|
513
|
+
const result = await client.request("inject", { wait_s: waitMs / 1e3 });
|
|
514
|
+
if (!result.offered.length) return;
|
|
515
|
+
const intro = result.intro ? introText(result.intro, result.intro_key) : void 0;
|
|
516
|
+
const shown = injectionText({ ...result, more: await moreNow(client, result) });
|
|
517
|
+
const text = [intro, shown].filter(Boolean).join("\n\n");
|
|
518
|
+
if (!text) return;
|
|
519
|
+
const reply = hookReply(claim.harness, "prompt", text);
|
|
520
|
+
if (options.stopping?.()) return;
|
|
521
|
+
if (options.print?.(reply) ?? true) {
|
|
522
|
+
const read = result.messages.flatMap((message) => message.seq === void 0 ? [] : [message.seq]);
|
|
523
|
+
await client.request("injected", {
|
|
524
|
+
seqs: result.offered,
|
|
525
|
+
hold: result.hold,
|
|
526
|
+
...read.length ? { read } : {},
|
|
527
|
+
deferred: true,
|
|
528
|
+
...shown && !result.brief && result.messages.length ? { preamble: true } : {},
|
|
529
|
+
...intro ? { intro: true } : {}
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return reply;
|
|
533
|
+
} finally {
|
|
534
|
+
client.close();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
412
537
|
var SUBAGENT_HARNESSES = /* @__PURE__ */ new Set(["claude-code"]);
|
|
413
538
|
function subagentOf(payload) {
|
|
414
539
|
const id = asString(field(payload, "agent_id", "agentId"));
|
|
@@ -481,6 +606,7 @@ async function relayHookCommand(args, startDaemon) {
|
|
|
481
606
|
const started = Date.now();
|
|
482
607
|
const agy = args[0] === "agy";
|
|
483
608
|
let printed = false;
|
|
609
|
+
let stopping = false;
|
|
484
610
|
const print = (text) => {
|
|
485
611
|
if (printed) return false;
|
|
486
612
|
printed = true;
|
|
@@ -497,6 +623,11 @@ async function relayHookCommand(args, startDaemon) {
|
|
|
497
623
|
print(void 0);
|
|
498
624
|
process.exit(0);
|
|
499
625
|
};
|
|
626
|
+
const terminate = () => {
|
|
627
|
+
stopping = true;
|
|
628
|
+
if (!printed) quit();
|
|
629
|
+
};
|
|
630
|
+
if (args.includes(WAKE)) process.on("SIGTERM", terminate);
|
|
500
631
|
let ends = started + (agy ? 900 : 1500);
|
|
501
632
|
let timer = setTimeout(quit, ends - Date.now());
|
|
502
633
|
const deadline = (ms) => {
|
|
@@ -515,6 +646,7 @@ async function relayHookCommand(args, startDaemon) {
|
|
|
515
646
|
deadline,
|
|
516
647
|
remaining: () => ends - Date.now(),
|
|
517
648
|
print,
|
|
649
|
+
stopping: () => stopping,
|
|
518
650
|
connect: (start) => start ? connectBroker(dir, () => startDaemon(dir), 1e3) : connectBroker(dir)
|
|
519
651
|
})
|
|
520
652
|
);
|
|
@@ -522,6 +654,7 @@ async function relayHookCommand(args, startDaemon) {
|
|
|
522
654
|
}
|
|
523
655
|
print(void 0);
|
|
524
656
|
clearTimeout(timer);
|
|
657
|
+
if (args.includes(WAKE)) process.off("SIGTERM", terminate);
|
|
525
658
|
return 0;
|
|
526
659
|
}
|
|
527
660
|
function stopArgs(options) {
|
|
@@ -626,6 +759,54 @@ const partId = (parts: Record<string, unknown>[]) => {
|
|
|
626
759
|
}
|
|
627
760
|
|
|
628
761
|
export const Scp = async ({ client, directory }: { client: any; directory: string }) => {
|
|
762
|
+
const WAKE = process.env.SCP_WAKE !== "0"
|
|
763
|
+
const busy = new Set<string>()
|
|
764
|
+
let target: string | undefined
|
|
765
|
+
let waiter: ReturnType<typeof spawn> | undefined
|
|
766
|
+
let looping = false
|
|
767
|
+
let armed = 0
|
|
768
|
+
const cancel = () => { waiter?.kill("SIGTERM") }
|
|
769
|
+
const wake = async () => {
|
|
770
|
+
if (looping) return
|
|
771
|
+
looping = true
|
|
772
|
+
let version = armed
|
|
773
|
+
try {
|
|
774
|
+
while (WAKE && target && busy.size === 0) {
|
|
775
|
+
const id = target
|
|
776
|
+
const began = Date.now()
|
|
777
|
+
let text: string | undefined
|
|
778
|
+
try {
|
|
779
|
+
const args = ["${CLI_NAME}", "hook", "opencode", "${WAKE}", "50"]
|
|
780
|
+
const child = spawn(args[0]!, args.slice(1), { env: { ...process.env }, stdio: ["pipe", "pipe", "ignore"] })
|
|
781
|
+
waiter = child
|
|
782
|
+
const ended = new Promise<void>((resolve, reject) => {
|
|
783
|
+
child.once("error", reject)
|
|
784
|
+
child.once("close", () => resolve())
|
|
785
|
+
})
|
|
786
|
+
child.stdin!.on("error", () => {})
|
|
787
|
+
child.stdin!.end(JSON.stringify({ hook_event_name: "Wake", session_id: id, cwd: directory }))
|
|
788
|
+
const [output] = await Promise.all([readAll(child.stdout!), ended])
|
|
789
|
+
text = output.trim() ? context(JSON.parse(output) as Reply) : undefined
|
|
790
|
+
} catch {} finally {
|
|
791
|
+
waiter = undefined
|
|
792
|
+
}
|
|
793
|
+
if (text) {
|
|
794
|
+
version = armed
|
|
795
|
+
await client.session.promptAsync({ path: { id }, body: { parts: [{ type: "text", text }] } }).catch(() => {})
|
|
796
|
+
break
|
|
797
|
+
}
|
|
798
|
+
if (Date.now() - began < 2000) await new Promise((resolve) => setTimeout(resolve, 5000))
|
|
799
|
+
}
|
|
800
|
+
} catch {} finally {
|
|
801
|
+
looping = false
|
|
802
|
+
if (armed !== version) void wake()
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const arm = (id: string) => {
|
|
806
|
+
target = id
|
|
807
|
+
armed++
|
|
808
|
+
void wake()
|
|
809
|
+
}
|
|
629
810
|
const children = new Map<string, Promise<boolean | undefined>>()
|
|
630
811
|
const failed = new Set<string>()
|
|
631
812
|
const isChild = async (id: string) => {
|
|
@@ -653,11 +834,14 @@ export const Scp = async ({ client, directory }: { client: any; directory: strin
|
|
|
653
834
|
},
|
|
654
835
|
"chat.message": async (input: { sessionID: string }, output: { message?: { id?: string }; parts: Record<string, unknown>[] }) => {
|
|
655
836
|
const messageID = output.message?.id
|
|
656
|
-
if (
|
|
837
|
+
if (await isChild(input.sessionID)) return
|
|
838
|
+
busy.add(input.sessionID)
|
|
839
|
+
cancel()
|
|
840
|
+
if (!messageID) return
|
|
657
841
|
const text = context(await hook({ hook_event_name: "UserPromptSubmit", session_id: input.sessionID, cwd: directory }, true))
|
|
658
842
|
if (text) output.parts.push({ id: partId(output.parts), sessionID: input.sessionID, messageID, type: "text", text, synthetic: true })
|
|
659
843
|
},
|
|
660
|
-
event: async ({ event }: { event: { type: string; properties?: { sessionID?: string; info?: { id?: string; parentID?: string; sessionID?: string; role?: string; error?: unknown } } } }) => {
|
|
844
|
+
event: async ({ event }: { event: { type: string; properties?: { sessionID?: string; status?: { type?: string }; info?: { id?: string; parentID?: string; sessionID?: string; role?: string; error?: unknown } } } }) => {
|
|
661
845
|
const info = event.properties?.info
|
|
662
846
|
if (event.type === "message.updated") {
|
|
663
847
|
if (info?.role === "assistant" && info.sessionID && info.error) failed.add(info.sessionID)
|
|
@@ -669,15 +853,39 @@ export const Scp = async ({ client, directory }: { client: any; directory: strin
|
|
|
669
853
|
if (!sessionID) return
|
|
670
854
|
if (event.type === "session.error") failed.add(sessionID)
|
|
671
855
|
if (event.type === "session.deleted") {
|
|
856
|
+
const child = await isChild(sessionID)
|
|
672
857
|
children.delete(sessionID)
|
|
673
858
|
failed.delete(sessionID)
|
|
859
|
+
if (!child) {
|
|
860
|
+
busy.delete(sessionID)
|
|
861
|
+
if (target === sessionID) target = undefined
|
|
862
|
+
cancel()
|
|
863
|
+
}
|
|
864
|
+
return
|
|
674
865
|
}
|
|
675
866
|
if (event.type === "session.created" && info) children.set(sessionID, Promise.resolve(Boolean(info.parentID)))
|
|
676
|
-
if (
|
|
867
|
+
if (await isChild(sessionID)) return
|
|
868
|
+
if (event.type === "session.status") {
|
|
869
|
+
const status = event.properties?.status?.type
|
|
870
|
+
if (status === "busy" || status === "retry") {
|
|
871
|
+
busy.add(sessionID)
|
|
872
|
+
cancel()
|
|
873
|
+
} else if (status === "idle") busy.delete(sessionID)
|
|
874
|
+
return
|
|
875
|
+
}
|
|
876
|
+
if (event.type === "session.idle") busy.delete(sessionID)
|
|
877
|
+
if (!name) return
|
|
677
878
|
const stop = name === "Stop" && !failed.has(sessionID)
|
|
678
|
-
const
|
|
879
|
+
const title = name === "Stop" ? await client.session.get({ path: { id: sessionID } }).then((found: any) => found?.data?.title).catch(() => undefined) : undefined
|
|
880
|
+
const reply = await hook(
|
|
881
|
+
{ hook_event_name: name, session_id: sessionID, cwd: directory, ...(name === "SessionStart" ? { source: "startup" } : {}), ...(typeof title === "string" ? { session_title: title } : {}) },
|
|
882
|
+
stop,
|
|
883
|
+
name !== "Stop" || stop,
|
|
884
|
+
stop && BLOCK_STOP,
|
|
885
|
+
)
|
|
679
886
|
const text = reply.decision === "block" && typeof reply.reason === "string" && !reply.reason.includes("<relay-message") ? reply.reason : undefined
|
|
680
887
|
if (text) await client.session.promptAsync({ path: { id: sessionID }, body: { parts: [{ type: "text", text }] } }).catch(() => {})
|
|
888
|
+
else arm(sessionID)
|
|
681
889
|
},
|
|
682
890
|
}
|
|
683
891
|
}
|
|
@@ -727,6 +935,8 @@ export {
|
|
|
727
935
|
BLOCK_STOP,
|
|
728
936
|
NO_INJECT,
|
|
729
937
|
PUSH_WAIT,
|
|
938
|
+
WAKE,
|
|
939
|
+
OPENCODE_WAKE,
|
|
730
940
|
PUSH_WAIT_DEFAULT_S,
|
|
731
941
|
AGY_UNVERIFIED,
|
|
732
942
|
summarize,
|
|
@@ -743,7 +953,13 @@ export {
|
|
|
743
953
|
nudgeText,
|
|
744
954
|
claudeVersionFile,
|
|
745
955
|
stopContinues,
|
|
956
|
+
titleText,
|
|
957
|
+
promptTitle,
|
|
958
|
+
transcriptTitle,
|
|
746
959
|
hookReply,
|
|
960
|
+
TLDR_EVERY_DEFAULT,
|
|
961
|
+
TLDR,
|
|
962
|
+
tldrEvery,
|
|
747
963
|
runHook,
|
|
748
964
|
subagentIntro,
|
|
749
965
|
relayHookCommand,
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
+
RelayUsage,
|
|
2
3
|
activeRelay,
|
|
3
4
|
checkRelayUrl,
|
|
4
5
|
configDir,
|
|
5
6
|
relayCredential,
|
|
6
7
|
writePrivateJson
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import {
|
|
9
|
-
RelayUsage
|
|
10
|
-
} from "./chunk-3V3DRXB2.js";
|
|
8
|
+
} from "./chunk-E7JG5PJB.js";
|
|
11
9
|
import {
|
|
12
10
|
note,
|
|
13
11
|
printTable
|
|
14
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-MNTIEWKF.js";
|
|
15
13
|
|
|
16
14
|
// src/remote.ts
|
|
17
15
|
import { existsSync, readFileSync, rmSync } from "fs";
|
|
@@ -3,16 +3,17 @@ import {
|
|
|
3
3
|
mcpFile,
|
|
4
4
|
mountedEntry,
|
|
5
5
|
whichOnPath
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-NOYMSD4F.js";
|
|
7
|
+
import "./chunk-F5QVHJIT.js";
|
|
8
|
+
import "./chunk-OTO6DBU6.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-E7JG5PJB.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
|
};
|