@scp-dev/cli 0.2.2 → 0.2.4

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.
@@ -3,10 +3,10 @@ import {
3
3
  agySettingsFile,
4
4
  mcpFile,
5
5
  mountedEntry
6
- } from "./chunk-NOYMSD4F.js";
6
+ } from "./chunk-W3VNCRBX.js";
7
7
  import {
8
8
  steerPrompt
9
- } from "./chunk-OTO6DBU6.js";
9
+ } from "./chunk-X4ZLEM47.js";
10
10
  import {
11
11
  OPTIONS,
12
12
  RelayUsage,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  hooksTarget
3
- } from "./chunk-F5QVHJIT.js";
3
+ } from "./chunk-ZMVFN6MU.js";
4
4
  import {
5
5
  AGY_HOOK_SET,
6
6
  BLOCK_STOP,
@@ -12,7 +12,7 @@ import {
12
12
  mergeHooks,
13
13
  musePlugin,
14
14
  opencodePlugin
15
- } from "./chunk-OTO6DBU6.js";
15
+ } from "./chunk-X4ZLEM47.js";
16
16
  import {
17
17
  RelayUsage,
18
18
  gitToplevel,
@@ -35,6 +35,7 @@ import {
35
35
  mkdtempSync,
36
36
  readFileSync,
37
37
  readlinkSync,
38
+ realpathSync,
38
39
  renameSync,
39
40
  rmSync,
40
41
  statSync,
@@ -611,6 +612,23 @@ function museInstalled(env) {
611
612
  }
612
613
  }
613
614
  var installedFrom = (record) => record && isObject(record.source) ? String(record.source.path) : void 0;
615
+ function samePath(a, b) {
616
+ if (a === void 0) return false;
617
+ if (a === b) return true;
618
+ try {
619
+ return realpathSync(a) === realpathSync(b);
620
+ } catch {
621
+ return false;
622
+ }
623
+ }
624
+ function museReviewNeeded(cli, run) {
625
+ try {
626
+ const caps = JSON.parse(run([cli, "plugins", "inspect", SERVER, "--json"]).output).runtime_capabilities;
627
+ return Array.isArray(caps) && caps.some((cap) => isObject(cap) && cap.status === "review_needed");
628
+ } catch {
629
+ return false;
630
+ }
631
+ }
614
632
  function pluginDir(manifest) {
615
633
  return dirname(dirname(manifest));
616
634
  }
@@ -655,19 +673,24 @@ function planHooks(plan, harness, ctx) {
655
673
  const from = installedFrom(record);
656
674
  const current = content ?? readFileSync(resolved(file), "utf8");
657
675
  const disabled = record?.enabled === false;
658
- const install = content !== void 0 || from !== dir || record?.manifest_sha256 !== sha256(current);
659
- if (!install && !disabled) {
676
+ const install = content !== void 0 || !samePath(from, dir) || record?.manifest_sha256 !== sha256(current);
677
+ const cli = ctx.found.get("muse");
678
+ const review = install || museReviewNeeded(cli, run);
679
+ if (!install && !disabled && !review) {
660
680
  note(plan, harness, `hooks: up to date in ${file}${held}`);
661
681
  return;
662
682
  }
663
- const cli = ctx.found.get("muse");
664
- const commands = [...install ? [[cli, "plugins", "install", dir, "--scope", "user"]] : [], ...disabled ? [[cli, "plugins", "enable", SERVER]] : []];
665
- const head = install ? `${content ? "write" : "install"} the ${CLI_NAME} plugin ${file}${held}${from && from !== dir ? `, replacing the scp plugin installed from ${from}` : ""}${disabled ? ", and enable it, which Muse has disabled" : ""}` : `enable the ${CLI_NAME} plugin ${file}, which Muse has disabled`;
683
+ const commands = [
684
+ ...install ? [[cli, "plugins", "install", dir, "--scope", "user"]] : [],
685
+ ...disabled ? [[cli, "plugins", "enable", SERVER]] : [],
686
+ ...review ? [[cli, "plugins", "approve", SERVER]] : []
687
+ ];
688
+ const head = install ? `${content ? "write" : "install"} the ${CLI_NAME} plugin ${file}${held}${from && !samePath(from, dir) ? `, replacing the scp plugin installed from ${from}` : ""}${disabled ? ", and enable it, which Muse has disabled" : ""}` : disabled ? `enable the ${CLI_NAME} plugin ${file}, which Muse has disabled` : `approve the hooks of the ${CLI_NAME} plugin ${file}, which Muse holds for review`;
666
689
  addStep(plan, {
667
690
  harness,
668
691
  label: "hooks",
669
692
  file,
670
- plan: [head, ...commands.map(shown), ...install ? ["Muse asks you to review a plugin's hooks before it runs them"] : []],
693
+ plan: [head, ...commands.map(shown), ...review ? ["approves its hooks: a headless muse exec runs no hook Muse holds for review"] : []],
671
694
  apply: () => {
672
695
  const next = install ? desiredHooks(kind, file, blockStop) : void 0;
673
696
  const saved = next === void 0 ? void 0 : writeFile(file, next, now, { mode: 420 });
@@ -741,7 +764,7 @@ function planUnhook(plan, harness, kind, file, items, ctx) {
741
764
  const gone = { harness, label: "hooks", file, plan: [`forget ${file}, which is gone`], apply: () => void 0, manifest: forgotten };
742
765
  if (kind === "muse") {
743
766
  const cli = ctx.found.get("muse");
744
- const installed = installedFrom(museInstalled(ctx.env)) === pluginDir(file);
767
+ const installed = samePath(installedFrom(museInstalled(ctx.env)), pluginDir(file));
745
768
  if (!existsSync(target) && !installed) {
746
769
  addStep(plan, gone);
747
770
  return;
@@ -936,7 +959,7 @@ function verifyMusePlugin(cli, dir, run, manifest) {
936
959
  } catch {
937
960
  return { ok: false, line: `unknown: muse plugins list --json exited ${result.code}: ${lastLine(result.output)}` };
938
961
  }
939
- const ours = installedFrom(record) === dir;
962
+ const ours = samePath(installedFrom(record), dir);
940
963
  if (manifest === void 0) return { ok: !ours, line: ours ? "still installed (muse plugins list)" : "removed (muse plugins list)" };
941
964
  if (!ours) return { ok: false, line: "failed: muse plugins list does not show the scp plugin from setup" };
942
965
  const current = record.manifest_sha256 === sha256(manifest) && record.enabled !== false;
@@ -345,6 +345,15 @@ function inferredTitle(harness, event, named, payload) {
345
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
346
  return text ? { text, from: "summary" } : void 0;
347
347
  }
348
+ function launchedSubagent(harness, event, payload) {
349
+ if (harness !== "claude-code" || event.kind !== "tool" || event.agent || !/^(agent|task)$/i.test(event.tool ?? "")) return void 0;
350
+ const input = field(payload, "tool_input", "toolInput");
351
+ const response = field(payload, "tool_response", "toolResponse");
352
+ if (typeof input !== "object" || typeof response !== "object" || Array.isArray(response)) return void 0;
353
+ const id = asString(response.agentId);
354
+ const title = titleText(asString(input.description));
355
+ return id && title && /^[A-Za-z0-9_-]{1,64}$/.test(id) ? { id, title } : void 0;
356
+ }
348
357
  function hookReply(harness, kind, text, unread = 0, continues = true) {
349
358
  if (kind === "stop" && harness === "claude-code" && continues)
350
359
  return JSON.stringify({ hookSpecificOutput: { hookEventName: "Stop", additionalContext: text } });
@@ -448,6 +457,8 @@ async function runHook(args, options) {
448
457
  if (turn) posted.push(client.request("peek_turn", turn));
449
458
  const title = inferredTitle(claim.harness, event, named, payload);
450
459
  if (title) posted.push(client.request("title_infer", title));
460
+ const launched = launchedSubagent(claim.harness, event, payload);
461
+ if (launched) posted.push(client.request("subagent_title", launched));
451
462
  const injected = inject ? client.request(
452
463
  "inject",
453
464
  counts ? { count: true, ...stop ? { stop: true, directives: true, ...blockStop ? { hold_unread: true } : {} } : {} } : {}
@@ -510,6 +521,8 @@ async function wakeHook(harnessArg, payload, waitS, options) {
510
521
  const client = await options.connect(false);
511
522
  try {
512
523
  await client.hello({ transient: true, claim });
524
+ await client.request("wake", { via: OPENCODE_WAKE }).catch(() => {
525
+ });
513
526
  const result = await client.request("inject", { wait_s: waitMs / 1e3 });
514
527
  if (!result.offered.length) return;
515
528
  const intro = result.intro ? introText(result.intro, result.intro_key) : void 0;
@@ -10,7 +10,7 @@ import {
10
10
  mergeHooks,
11
11
  musePlugin,
12
12
  opencodePlugin
13
- } from "./chunk-OTO6DBU6.js";
13
+ } from "./chunk-X4ZLEM47.js";
14
14
  import {
15
15
  RelayUsage,
16
16
  gitToplevel
@@ -166,7 +166,7 @@ function injectionNotes(harness, scope, target, blockStop, wait) {
166
166
  ];
167
167
  case "muse":
168
168
  return [
169
- `Muse loads hooks only from plugins: muse plugins install ${dirname(dirname(target))} --scope ${scope}`,
169
+ `Muse loads hooks only from plugins: muse plugins install ${dirname(dirname(target))} --scope ${scope}, then muse plugins approve scp (a headless muse exec runs no hook Muse holds for review)`,
170
170
  `new relay messages reach the model after each tool call and with each prompt; ${held}`,
171
171
  pushed,
172
172
  "Muse hands plugin hooks no SCP_* variables: they key the session by the harness process, so set no SCP_SESSION for Muse's shim"
@@ -3,9 +3,9 @@ import {
3
3
  mcpFile,
4
4
  mountedEntry,
5
5
  whichOnPath
6
- } from "./chunk-NOYMSD4F.js";
7
- import "./chunk-F5QVHJIT.js";
8
- import "./chunk-OTO6DBU6.js";
6
+ } from "./chunk-W3VNCRBX.js";
7
+ import "./chunk-ZMVFN6MU.js";
8
+ import "./chunk-X4ZLEM47.js";
9
9
  import "./chunk-YYSLYVNZ.js";
10
10
  import {
11
11
  CHANNELS_ENV,
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-YN3J6MGW.js");
20
+ const { run } = await import("./main-MCNGHJP2.js");
21
21
  await run(process.argv.slice(2));
22
22
  }
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  codexMount,
3
3
  codexOverrides
4
- } from "./chunk-G2SQPY6E.js";
4
+ } from "./chunk-IXCHAWOL.js";
5
5
  import "./chunk-KN7WEIE4.js";
6
- import "./chunk-NOYMSD4F.js";
7
- import "./chunk-F5QVHJIT.js";
8
- import "./chunk-OTO6DBU6.js";
6
+ import "./chunk-W3VNCRBX.js";
7
+ import "./chunk-ZMVFN6MU.js";
8
+ import "./chunk-X4ZLEM47.js";
9
9
  import "./chunk-YYSLYVNZ.js";
10
10
  import {
11
11
  RelayUsage,
@@ -486,6 +486,7 @@ ${stderr.toString().trim()}` : ""}`
486
486
  },
487
487
  claim,
488
488
  channels: true,
489
+ wake: "codex-app-server",
489
490
  input,
490
491
  env: launch.env,
491
492
  output: (chunk) => {
@@ -38,7 +38,7 @@ import {
38
38
  tldrEvery,
39
39
  transcriptTitle,
40
40
  unreadPointer
41
- } from "./chunk-OTO6DBU6.js";
41
+ } from "./chunk-X4ZLEM47.js";
42
42
  import {
43
43
  SUBAGENT
44
44
  } from "./chunk-YYSLYVNZ.js";
@@ -2,8 +2,8 @@ import {
2
2
  hooksTarget,
3
3
  installHooks,
4
4
  mcpInstall
5
- } from "./chunk-F5QVHJIT.js";
6
- import "./chunk-OTO6DBU6.js";
5
+ } from "./chunk-ZMVFN6MU.js";
6
+ import "./chunk-X4ZLEM47.js";
7
7
  import "./chunk-YYSLYVNZ.js";
8
8
  import "./chunk-E7JG5PJB.js";
9
9
  import "./chunk-MNTIEWKF.js";
@@ -33,7 +33,7 @@ import { parseArgs } from "util";
33
33
  // package.json
34
34
  var package_default = {
35
35
  name: "@scp-dev/cli",
36
- version: "0.2.2",
36
+ version: "0.2.4",
37
37
  description: "The swarm command line tool.",
38
38
  license: "Apache-2.0",
39
39
  homepage: "https://scp.dev",
@@ -80,11 +80,13 @@ import {
80
80
  deliveryText,
81
81
  LIMITS,
82
82
  PREAMBLE,
83
+ presenceText,
83
84
  reachLines,
84
85
  receiptLines,
85
86
  RelayError,
86
87
  shortId,
87
88
  stripControl,
89
+ wakeText,
88
90
  widenText,
89
91
  wrapMessage,
90
92
  CLI_NAME
@@ -99,7 +101,7 @@ results back, as with native subagents.
99
101
  ${CLI_NAME} reply <id> [text|-] answer a message in its thread: ${CLI_NAME} reply 5af1f2 "done"
100
102
  ${CLI_NAME} ask <to> [text|-] [--wait S] send a request and wait for its reply: ${CLI_NAME} ask lead "which branch?"
101
103
  ${CLI_NAME} inbox [--wait S] your unread messages, then marked read: ${CLI_NAME} inbox
102
- ${CLI_NAME} list [--all] agents on the relay: address, title, presence, status; --all adds offline ones
104
+ ${CLI_NAME} list [--all] agents on the relay: address, title, presence, wake, status; --all adds offline ones
103
105
  ${CLI_NAME} join [name] put this session on the relay, named once: ${CLI_NAME} join lead (${CLI_NAME} name renames it)
104
106
  ${CLI_NAME} join --under <parent> link this session under an orchestrator, as a spawned worker sits: ${CLI_NAME} join --under lead
105
107
  ${CLI_NAME} status [id] the fleet in short; with the id of a message you sent: who read it when
@@ -122,7 +124,7 @@ More: ${CLI_NAME} help --all (the full reference), ${CLI_NAME} <verb> --help (on
122
124
  var RELAY_HELP = `${CLI_NAME}: messages between the agent sessions on this machine, and a live view of the fleet
123
125
 
124
126
  usage:
125
- ${CLI_NAME} list [--all] [--roots] [--tree] agents on the relay: address, title, presence, status, cwd; offline ones only with --all;
127
+ ${CLI_NAME} list [--all] [--roots] [--tree] agents on the relay: address, title, presence, wake, status, cwd; offline only with --all;
126
128
  sessions nobody spawned that spawned nobody fold into one line, --roots (or --all) lists
127
129
  them; --tree nests them by parent; (you) marks your own
128
130
  ${CLI_NAME} list --contacts the endpoints of other accounts your contacts let you reach: address, harness, presence
@@ -230,7 +232,7 @@ scopes (selectors are comma-separated, e.g. tree:parent,tree:children,group:team
230
232
  messages with the relay MCP tools (relay_send, relay_inbox, relay_reply). A
231
233
  ${CLI_NAME} command from codex's workspace-write sandbox also needs
232
234
  -c sandbox_workspace_write.network_access=true (the relay socket counts as network)
233
- ${CLI_NAME} pi [--name N] [--] [pi args...] runs pi with the @scp-dev/pi extension: the relay tools, and each new
235
+ ${CLI_NAME} pi [--name N] [--] [pi args...] runs pi with the bundled SCP extension: the relay tools, and each new
234
236
  message as a turn (steering a running one, waking an idle session)
235
237
  ${CLI_NAME} codex [--name N] [--headless] [--] [codex args...]
236
238
  runs codex with relay messages waking an idle thread or steering a running turn
@@ -263,7 +265,7 @@ agent sessions:
263
265
  ${CLI_NAME} hook <harness> [event] what those hooks run: one event from the hook JSON on stdin; prints only the reply
264
266
  A session's shim and hooks share one endpoint, keyed by $SCP_SESSION, else the harness process's pid and start time.
265
267
  Without --as or $SCP_NAME the handle is <harness>-<worktree>; labels gain machine=<short hostname>.
266
- A Claude Code subagent's hooks give it a child endpoint, <session handle>-<type>-<first 6 of its id>, whose messages
268
+ A Claude Code subagent's hooks give it a child endpoint, <session handle>-<short type>-<first 4 of its id>, whose messages
267
269
  reach it after its tools; its shell is the session's, so it acts as that child with --as.
268
270
  Grok runs the Claude Code hooks, labelled grok; Muse takes them as a plugin; Codex runs new hooks after /hooks review.
269
271
  ${CLI_NAME} claude [--name N] [--] [claude args...] start Claude Code with the scp channel loaded, so peer messages wake
@@ -464,15 +466,16 @@ function printEndpoints(endpoints, me, host = shortHostname()) {
464
466
  const row = (e, indent) => [
465
467
  `${indent}${addressCell(e, me)}`,
466
468
  e.title ?? "",
467
- e.presence,
469
+ presenceText(e),
470
+ wakeText(e),
468
471
  e.status ? statusText(e.status) : e.kind === "human" ? "" : "-",
469
472
  Object.entries(e.labels).filter(([k]) => k !== "machine").map(([k, v]) => `${k}=${v}`).join(","),
470
473
  shortCwd(e.cwd),
471
474
  ...peek ? [e.peekable ?? ""] : []
472
475
  ];
473
- const blank = (first) => [first, "", "", "", "", "", ...peek ? [""] : []];
476
+ const blank = (first) => [first, "", "", "", "", "", "", ...peek ? [""] : []];
474
477
  const rows = [
475
- ["ADDRESS", "TITLE", "PRESENCE", "STATUS", "LABELS", "CWD", ...peek ? ["PEEK"] : []],
478
+ ["ADDRESS", "TITLE", "PRESENCE", "WAKE", "STATUS", "LABELS", "CWD", ...peek ? ["PEEK"] : []],
476
479
  ...endpoints.filter((e) => e.kind === "human").map((e) => row(e, ""))
477
480
  ];
478
481
  const machines = /* @__PURE__ */ new Map();
@@ -535,13 +538,13 @@ async function relayMain(argv, cwd = process.cwd()) {
535
538
  out(package_default.version);
536
539
  return 0;
537
540
  }
538
- if (verb === "hook") return (await import("./hooks-EKGY5O5N.js")).relayHookCommand(rest, startDaemon);
539
- if (verb === "setup") return (await import("./setup-H7GGUID2.js")).relaySetupCommand(rest, cwd, out);
540
- if (verb === "claude") return (await import("./claude-CVWECMRQ.js")).claudeCommand(rest);
541
- if (verb === "pi") return (await import("./pi-R23XR4AC.js")).piCommand(rest);
542
- if (verb === "codex") return (await import("./codex-Y7SGLZ64.js")).codexCommand(rest);
541
+ if (verb === "hook") return (await import("./hooks-P4J6OZCO.js")).relayHookCommand(rest, startDaemon);
542
+ if (verb === "setup") return (await import("./setup-RO7356M4.js")).relaySetupCommand(rest, cwd, out);
543
+ if (verb === "claude") return (await import("./claude-JD7WXMAT.js")).claudeCommand(rest);
544
+ if (verb === "pi") return (await import("./pi-JZ4DXY7V.js")).piCommand(rest);
545
+ if (verb === "codex") return (await import("./codex-4IIM6ZE2.js")).codexCommand(rest);
543
546
  if (verb === "spawn") {
544
- const { splitSpawn, spawnCommand } = await import("./spawn-AJAYD6CW.js");
547
+ const { splitSpawn, spawnCommand } = await import("./spawn-BYU2X46L.js");
545
548
  const { flags } = splitSpawn(rest);
546
549
  if (flags.includes("--help") || flags.includes("-h")) {
547
550
  out(flags.includes("--all") ? RELAY_HELP : SPAWN_HELP);
@@ -569,11 +572,11 @@ async function relayMain(argv, cwd = process.cwd()) {
569
572
  return daemon(values.idle, values.policy);
570
573
  }
571
574
  if (verb === "mcp")
572
- return (await import("./mcp-BVC4PLZ7.js")).relayMcpCommand(values, positionals);
575
+ return (await import("./mcp-TUWMLGE4.js")).relayMcpCommand(values, positionals);
573
576
  if (verb === "hooks") {
574
577
  if (positionals[0] !== "install" || positionals.length > 1) throw new RelayUsage(`${CLI_NAME} hooks takes install --harness claude|codex|opencode|agy|muse`);
575
578
  if (values.write && values.print) throw new RelayUsage("--print and --write: pick one");
576
- return (await import("./install-4VBSX6RI.js")).installHooks({
579
+ return (await import("./install-FKGGPT6L.js")).installHooks({
577
580
  harness: values.harness,
578
581
  scope: values.scope,
579
582
  write: values.write === true,
@@ -585,7 +588,7 @@ async function relayMain(argv, cwd = process.cwd()) {
585
588
  });
586
589
  }
587
590
  if (verb === "relay") return (await import("./relay-U634RHIJ.js")).relayVerb(positionals, values.json === true);
588
- if (verb === "up") return (await import("./up-HZKGURPI.js")).up(positionals, values, cwd, relinkBroker);
591
+ if (verb === "up") return (await import("./up-GDD24GPL.js")).up(positionals, values, cwd, relinkBroker);
589
592
  if (verb === "login") {
590
593
  const code = await (await import("./relay-U634RHIJ.js")).loginMain(positionals, values);
591
594
  if (code === 0) await relinkRunning();
@@ -679,7 +682,7 @@ async function runVerb({ client, me, broker, where, bound, harnessPid }, verb, v
679
682
  out(`${me.address} (${me.kind}${me.harness ? `, ${me.harness}` : ""})`);
680
683
  const how = bound === "as" ? "--as" : bound === "lineage" ? "this shell's harness process" : bound === "human" ? "human inbox" : "-";
681
684
  const fields = [
682
- ["presence", me.presence],
685
+ ["presence", presenceText(me)],
683
686
  ["title", me.title ?? "-"],
684
687
  ...me.kind === "human" ? [] : [
685
688
  ["status", me.status?.state ?? "-"],
@@ -1223,7 +1226,7 @@ function printTree(endpoints, me) {
1223
1226
  if (e.parent && byAddress.has(e.parent) && e.parent !== e.address) children.set(e.parent, [...children.get(e.parent) ?? [], e]);
1224
1227
  else roots.push(e);
1225
1228
  }
1226
- const rows = [["ADDRESS", "SCOPE", "TITLE", "PRESENCE", "STATUS", "LABELS", "CWD"]];
1229
+ const rows = [["ADDRESS", "SCOPE", "TITLE", "PRESENCE", "WAKE", "STATUS", "LABELS", "CWD"]];
1227
1230
  const seen = /* @__PURE__ */ new Set();
1228
1231
  const walk = (e, depth) => {
1229
1232
  if (seen.has(e.address)) return;
@@ -1233,7 +1236,8 @@ function printTree(endpoints, me) {
1233
1236
  `${" ".repeat(depth)}${addressCell(e, me)}`,
1234
1237
  e.kind === "human" ? "" : e.scope ?? "open",
1235
1238
  e.title ?? "",
1236
- e.presence,
1239
+ presenceText(e),
1240
+ wakeText(e),
1237
1241
  e.status ? statusText(e.status) : e.kind === "human" ? "" : "-",
1238
1242
  labels,
1239
1243
  shortCwd(e.cwd)
@@ -1247,7 +1251,7 @@ var STATES = ["working", "waiting", "blocked", "done"];
1247
1251
  function printSummary(snapshot, me, all) {
1248
1252
  if (me)
1249
1253
  out(
1250
- `you: ${me.address} (${me.kind}${me.harness ? `, ${me.harness}` : ""}) ${me.presence}${me.title ? ` ${quoted(me.title)}` : ""}${me.kind === "human" ? "" : `, status ${me.status ? statusText(me.status) : "-"}`}`
1254
+ `you: ${me.address} (${me.kind}${me.harness ? `, ${me.harness}` : ""}) ${presenceText(me)}${me.title ? ` ${quoted(me.title)}` : ""}${me.kind === "human" ? "" : `, status ${me.status ? statusText(me.status) : "-"}`}`
1251
1255
  );
1252
1256
  const agents = snapshot.endpoints.filter((e) => e.kind !== "human");
1253
1257
  const count = (presence) => agents.filter((e) => e.presence === presence).length;
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  spawnDetached
3
- } from "./chunk-G2SQPY6E.js";
3
+ } from "./chunk-IXCHAWOL.js";
4
4
  import "./chunk-KN7WEIE4.js";
5
- import "./chunk-NOYMSD4F.js";
5
+ import "./chunk-W3VNCRBX.js";
6
6
  import {
7
7
  mcpInstall
8
- } from "./chunk-F5QVHJIT.js";
9
- import "./chunk-OTO6DBU6.js";
8
+ } from "./chunk-ZMVFN6MU.js";
9
+ import "./chunk-X4ZLEM47.js";
10
10
  import "./chunk-YYSLYVNZ.js";
11
11
  import {
12
12
  CHANNELS_ENV,
@@ -5,10 +5,11 @@ import "./chunk-MNTIEWKF.js";
5
5
 
6
6
  // src/pi.ts
7
7
  import { spawn } from "child_process";
8
+ import { existsSync } from "fs";
8
9
  import { fileURLToPath } from "url";
9
10
  import { CLI_NAME, validHandle } from "@scp-dev/core";
10
11
  var PI_HELP = `${CLI_NAME} pi [--name NAME | --as NAME] [--] [pi args...]
11
- runs pi with the @scp-dev/pi extension: the relay tools, and each new message as a turn, waking the session when idle.
12
+ runs pi with the bundled SCP extension: the relay tools, and each new message as a turn, waking the session when idle.
12
13
  $SCP_PI_BIN picks the pi binary (default pi on PATH), $SCP_PI_EXTENSION the extension file.`;
13
14
  function piLaunch(args, env, extension) {
14
15
  let name;
@@ -32,13 +33,13 @@ function piLaunch(args, env, extension) {
32
33
  env: { ...env, SCP_HARNESS: "pi", ...name !== void 0 ? { SCP_NAME: name } : {} }
33
34
  };
34
35
  }
35
- function extensionPath(env) {
36
+ function extensionPath(env, base = import.meta.url) {
36
37
  if (env.SCP_PI_EXTENSION) return env.SCP_PI_EXTENSION;
37
- try {
38
- return fileURLToPath(import.meta.resolve("@scp-dev/pi"));
39
- } catch {
40
- throw new RelayUsage(`${CLI_NAME} pi needs the @scp-dev/pi extension: install it next to ${CLI_NAME}, or set $SCP_PI_EXTENSION to its file`);
38
+ for (const candidate of ["./pi-extension.js", "../../pi/src/index.ts"]) {
39
+ const file = fileURLToPath(new URL(candidate, base));
40
+ if (existsSync(file)) return file;
41
41
  }
42
+ throw new RelayUsage(`${CLI_NAME} pi found no pi extension next to ${CLI_NAME}: reinstall it, or set $SCP_PI_EXTENSION to the extension file`);
42
43
  }
43
44
  async function piCommand(args) {
44
45
  const own = args.includes("--") ? args.slice(0, args.indexOf("--")) : args;
@@ -66,6 +67,7 @@ async function piCommand(args) {
66
67
  }
67
68
  export {
68
69
  PI_HELP,
70
+ extensionPath,
69
71
  piCommand,
70
72
  piLaunch
71
73
  };
@@ -0,0 +1,137 @@
1
+ // ../pi/src/index.ts
2
+ import { spawn } from "child_process";
3
+ import { createInterface } from "readline";
4
+ var PROTOCOL = "2025-06-18";
5
+ var CHANNEL = "notifications/claude/channel";
6
+ function swarmWords(env) {
7
+ const raw = env.SCP_PI_SWARM;
8
+ if (!raw) return ["swarm"];
9
+ const words = JSON.parse(raw);
10
+ if (!Array.isArray(words) || !words.length || !words.every((word) => typeof word === "string"))
11
+ throw new Error("SCP_PI_SWARM must be a JSON array of strings");
12
+ return words;
13
+ }
14
+ function shimArgv(env) {
15
+ return [...swarmWords(env), "mcp", "--harness", "pi", "--channels", ...env.SCP_NAME ? ["--as", env.SCP_NAME] : []];
16
+ }
17
+ var McpLink = class {
18
+ child;
19
+ pending = /* @__PURE__ */ new Map();
20
+ next = 1;
21
+ closed = false;
22
+ constructor(argv, env, onPush, onExit) {
23
+ this.child = spawn(argv[0], argv.slice(1), { env, stdio: ["pipe", "pipe", "ignore"] });
24
+ this.child.stdin.on("error", () => {
25
+ });
26
+ createInterface({ input: this.child.stdout }).on("line", (line) => {
27
+ let message;
28
+ try {
29
+ message = JSON.parse(line);
30
+ } catch {
31
+ return;
32
+ }
33
+ if (typeof message.id === "number" && this.pending.has(message.id)) {
34
+ const waiter = this.pending.get(message.id);
35
+ this.pending.delete(message.id);
36
+ const error = message.error;
37
+ if (error) waiter.reject(new Error(error.message ?? "mcp error"));
38
+ else waiter.resolve(message.result ?? {});
39
+ return;
40
+ }
41
+ if (message.method === CHANNEL) {
42
+ const params = message.params ?? {};
43
+ if (typeof params.content === "string") onPush(params.content, params.meta ?? {});
44
+ }
45
+ });
46
+ this.child.once("error", () => this.fail(null, onExit));
47
+ this.child.once("exit", (code) => this.fail(code, onExit));
48
+ }
49
+ fail(code, onExit) {
50
+ if (this.closed) return;
51
+ this.closed = true;
52
+ for (const waiter of this.pending.values()) waiter.reject(new Error("the scp shim exited"));
53
+ this.pending.clear();
54
+ onExit(code);
55
+ }
56
+ request(method, params = {}) {
57
+ if (this.closed) return Promise.reject(new Error("the scp shim exited"));
58
+ const id = this.next++;
59
+ return new Promise((resolve, reject) => {
60
+ this.pending.set(id, { resolve, reject });
61
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
62
+ `);
63
+ });
64
+ }
65
+ notify(method, params = {}) {
66
+ if (!this.closed) this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
67
+ `);
68
+ }
69
+ close() {
70
+ if (this.closed) return;
71
+ this.closed = true;
72
+ this.child.stdin.end();
73
+ this.child.kill();
74
+ }
75
+ };
76
+ function toolResult(result) {
77
+ const content = (result.content ?? []).flatMap(
78
+ (part) => part.type === "text" && typeof part.text === "string" ? [{ type: "text", text: part.text }] : []
79
+ );
80
+ if (result.isError) throw new Error(content.map((part) => part.text).join("\n") || "the relay tool failed");
81
+ return { content, details: {} };
82
+ }
83
+ function scp(pi) {
84
+ let link;
85
+ const warn = (ctx, text) => {
86
+ if (ctx.hasUI) ctx.ui.notify(text, "warning");
87
+ };
88
+ pi.on("session_start", async (_event, ctx) => {
89
+ if (process.env.SCP_PI === "0") return;
90
+ link?.close();
91
+ const current = new McpLink(
92
+ shimArgv(process.env),
93
+ process.env,
94
+ (content, meta) => {
95
+ pi.sendMessage({ customType: "scp", content, display: true, details: meta }, { triggerTurn: true, deliverAs: "steer" });
96
+ },
97
+ (code) => {
98
+ if (link === current) warn(ctx, `scp: the relay shim exited (${code ?? "error"}); relay tools are unavailable`);
99
+ }
100
+ );
101
+ link = current;
102
+ try {
103
+ await current.request("initialize", {
104
+ protocolVersion: PROTOCOL,
105
+ capabilities: { experimental: { "claude/channel": {} } },
106
+ clientInfo: { name: "pi", version: "1" }
107
+ });
108
+ const { tools } = await current.request("tools/list");
109
+ for (const tool of tools ?? []) {
110
+ pi.registerTool({
111
+ name: tool.name,
112
+ label: tool.name,
113
+ description: tool.description ?? tool.name,
114
+ parameters: tool.inputSchema ?? { type: "object", properties: {} },
115
+ async execute(_id, params) {
116
+ if (link !== current) throw new Error("this scp session ended");
117
+ return toolResult(await current.request("tools/call", { name: tool.name, arguments: params ?? {} }));
118
+ }
119
+ });
120
+ }
121
+ current.notify("notifications/initialized");
122
+ } catch (error) {
123
+ warn(ctx, `scp: could not start the relay shim: ${error instanceof Error ? error.message : String(error)}`);
124
+ }
125
+ });
126
+ pi.on("session_shutdown", async () => {
127
+ link?.close();
128
+ link = void 0;
129
+ });
130
+ }
131
+ export {
132
+ McpLink,
133
+ scp as default,
134
+ shimArgv,
135
+ swarmWords,
136
+ toolResult
137
+ };
@@ -15,9 +15,9 @@ import {
15
15
  verifyMount,
16
16
  verifyMusePlugin,
17
17
  whichOnPath
18
- } from "./chunk-NOYMSD4F.js";
19
- import "./chunk-F5QVHJIT.js";
20
- import "./chunk-OTO6DBU6.js";
18
+ } from "./chunk-W3VNCRBX.js";
19
+ import "./chunk-ZMVFN6MU.js";
20
+ import "./chunk-X4ZLEM47.js";
21
21
  import "./chunk-YYSLYVNZ.js";
22
22
  import "./chunk-E7JG5PJB.js";
23
23
  import "./chunk-MNTIEWKF.js";
@@ -29,13 +29,13 @@ import {
29
29
  steerArgv,
30
30
  transcriptReplies,
31
31
  workerName
32
- } from "./chunk-G2SQPY6E.js";
32
+ } from "./chunk-IXCHAWOL.js";
33
33
  import {
34
34
  SPAWN_HELP
35
35
  } from "./chunk-KN7WEIE4.js";
36
- import "./chunk-NOYMSD4F.js";
37
- import "./chunk-F5QVHJIT.js";
38
- import "./chunk-OTO6DBU6.js";
36
+ import "./chunk-W3VNCRBX.js";
37
+ import "./chunk-ZMVFN6MU.js";
38
+ import "./chunk-X4ZLEM47.js";
39
39
  import "./chunk-YYSLYVNZ.js";
40
40
  import "./chunk-E7JG5PJB.js";
41
41
  import "./chunk-MNTIEWKF.js";
@@ -23,7 +23,7 @@ async function up(positionals, values, cwd, brokerStatus) {
23
23
  } else out(`already logged in to ${url}`);
24
24
  const enabled = await (await import("./remote-NTHRHVEP.js")).cloudMain(["enable"], values.machine !== void 0 ? { machine: values.machine } : {}, brokerStatus);
25
25
  if (enabled !== 0) return enabled;
26
- const setup = (await import("./setup-H7GGUID2.js")).relaySetupCommand(["--write"], cwd, out);
26
+ const setup = (await import("./setup-RO7356M4.js")).relaySetupCommand(["--write"], cwd, out);
27
27
  if (setup !== 0) return setup;
28
28
  const broker = await brokerStatus();
29
29
  if (!broker.enabled) out(`the broker is still local-only; run \`${CLI_NAME} daemon restart\` to link it to ${url}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scp-dev/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "The swarm command line tool.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://scp.dev",
@@ -24,15 +24,15 @@
24
24
  "typecheck": "tsc -p ."
25
25
  },
26
26
  "dependencies": {
27
- "@scp-dev/core": "0.1.3",
28
- "@scp-dev/broker": "0.1.4",
29
- "@scp-dev/hub": "0.3.0",
30
- "@scp-dev/mcp": "0.1.4",
27
+ "@scp-dev/core": "0.1.4",
28
+ "@scp-dev/broker": "0.1.6",
29
+ "@scp-dev/hub": "0.4.0",
30
+ "@scp-dev/mcp": "0.1.6",
31
31
  "smol-toml": "^1.9.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@openai/codex": "0.159.0",
35
- "@scp-dev/mesh": "0.0.1",
35
+ "@scp-dev/mesh": "0.0.3",
36
36
  "opencode-ai": "1.18.34",
37
37
  "tsup": "^8.5.1"
38
38
  },