@centralu/darwin-arm64 0.1.0-beta.13 → 0.1.0-beta.15

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.
@@ -42886,7 +42886,7 @@ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync10, writeSync
42886
42886
 
42887
42887
  // packages/protocol/src/brand.ts
42888
42888
  var APP_NAME = "Centralu";
42889
- var APP_VERSION = "0.1.0-beta.13";
42889
+ var APP_VERSION = "0.1.0-beta.15";
42890
42890
  var APP_SLUG = "centralu";
42891
42891
  var CLIENT_INFO = { name: APP_SLUG, title: APP_NAME, version: APP_VERSION };
42892
42892
  var DATA_DIR = ".centralu";
@@ -42926,7 +42926,10 @@ function isNewerVersion(a, b2) {
42926
42926
  }
42927
42927
  return false;
42928
42928
  }
42929
- function split(v) {
42929
+ function split(raw) {
42930
+ let v = raw.trim().replace(/^v/i, "");
42931
+ const plus = v.indexOf("+");
42932
+ if (plus !== -1) v = v.slice(0, plus);
42930
42933
  const dash = v.indexOf("-");
42931
42934
  const core = (dash === -1 ? v : v.slice(0, dash)).split(".").map(Number);
42932
42935
  const pre2 = dash === -1 ? null : v.slice(dash + 1);
@@ -58125,7 +58128,7 @@ function parseUiPreferences(raw) {
58125
58128
  // packages/protocol/src/machines.ts
58126
58129
  var MACHINE_ID_RE = /^[a-z][a-z0-9-]{0,31}$/;
58127
58130
  var MachineId = external_exports.string().regex(MACHINE_ID_RE, "Not a machine id");
58128
- var MachineStatus = external_exports.enum(["connecting", "connected", "unreachable", "not_running", "versions_differ", "refused", "starting"]);
58131
+ var MachineStatus = external_exports.enum(["connecting", "connected", "unreachable", "not_running", "versions_differ", "refused", "starting", "updating"]);
58129
58132
  var MachineSide = external_exports.object({
58130
58133
  version: external_exports.string(),
58131
58134
  protocolVersion: external_exports.number().int(),
@@ -58145,6 +58148,19 @@ var MachineVersions = external_exports.object({
58145
58148
  /** The person chose to connect without aligning (`machines.acceptVersions`); only possible when compatible */
58146
58149
  accepted: external_exports.boolean().default(false)
58147
58150
  });
58151
+ var InstalledVersion = external_exports.object({ version: external_exports.string(), node: external_exports.string() });
58152
+ var MachineOperation = external_exports.object({
58153
+ kind: external_exports.enum(["install", "update", "rollback", "uninstall"]).catch("update"),
58154
+ step: external_exports.string(),
58155
+ /** The version it brings the machine to, when there is one */
58156
+ target: external_exports.string().nullable().default(null),
58157
+ at: external_exports.number()
58158
+ });
58159
+ var MachineAutostart = external_exports.object({
58160
+ on: external_exports.boolean().catch(false),
58161
+ how: external_exports.enum(["systemd", "task"]).nullable().catch(null),
58162
+ linger: external_exports.boolean().nullable().catch(null).default(null)
58163
+ });
58148
58164
  var MachineInfo = external_exports.object({
58149
58165
  id: MachineId,
58150
58166
  /** What the person calls it; shown as the group header in the sidebar */
@@ -58177,7 +58193,42 @@ var MachineInfo = external_exports.object({
58177
58193
  * because that machine blocks starting a process through WMI, and ends when the link does (`note`
58178
58194
  * says why). Null when the host was already running. Additive; an older hub sends nothing
58179
58195
  */
58180
- hostStarted: external_exports.object({ how: external_exports.enum(["detached", "link_bound"]).catch("detached"), at: external_exports.number(), note: external_exports.string().nullable().default(null) }).nullable().catch(null).default(null)
58196
+ hostStarted: external_exports.object({ how: external_exports.enum(["detached", "link_bound"]).catch("detached"), at: external_exports.number(), note: external_exports.string().nullable().default(null) }).nullable().catch(null).default(null),
58197
+ /**
58198
+ * What the hub installed there, as the remote's connection line last said (docs/plans/remote-hub.md
58199
+ * §10.5, S12): `managed` when the running `centralu` is that install, `current` and `previous` (the
58200
+ * rollback target) from its pointer files. Null when the machine has not answered, or predates the
58201
+ * field. Additive
58202
+ */
58203
+ install: external_exports.object({ managed: external_exports.boolean().catch(false), current: InstalledVersion.nullable().catch(null), previous: InstalledVersion.nullable().catch(null) }).nullable().catch(null).default(null),
58204
+ /**
58205
+ * An install, update, rollback or uninstall the hub is running there, and its step, while it runs
58206
+ * (`machines.install` / `update` / `rollback` / `uninstall`). Null otherwise. Additive
58207
+ */
58208
+ operation: MachineOperation.nullable().catch(null).default(null),
58209
+ /**
58210
+ * Whether the machine starts its host at boot, from its connection line and the last
58211
+ * `machines.autostart` (`MachineAutostart`). Null when the machine has not answered, or predates it.
58212
+ * Additive
58213
+ */
58214
+ autostart: MachineAutostart.nullable().catch(null).default(null)
58215
+ });
58216
+ var MachineInstallResult = external_exports.object({
58217
+ machine: MachineInfo,
58218
+ current: InstalledVersion,
58219
+ previous: InstalledVersion.nullable(),
58220
+ removed: external_exports.array(external_exports.string()).default([]),
58221
+ left: external_exports.array(external_exports.string()).default([])
58222
+ });
58223
+ var MachineUninstallResult = external_exports.object({ machine: MachineInfo, stopped: external_exports.boolean() });
58224
+ var MachineAutostartResult = external_exports.object({ machine: MachineInfo, autostart: MachineAutostart });
58225
+ var HostActivity = external_exports.object({
58226
+ working: external_exports.number().int().default(0),
58227
+ approvals: external_exports.number().int().default(0),
58228
+ questions: external_exports.number().int().default(0),
58229
+ background: external_exports.number().int().default(0),
58230
+ terminals: external_exports.number().int().default(0),
58231
+ commandRuns: external_exports.number().int().default(0)
58181
58232
  });
58182
58233
 
58183
58234
  // packages/protocol/src/events.ts
@@ -60754,6 +60805,51 @@ var RpcMethods = {
60754
60805
  * question comes again. Refused when the protocols differ.
60755
60806
  */
60756
60807
  "machines.acceptVersions": { params: external_exports.object({ machineId: MachineId }), result: MachineInfo },
60808
+ /**
60809
+ * Installs this hub's version on a linked machine over its ssh (docs/plans/remote-hub.md §10.2):
60810
+ * the pinned Node and the npm packages, checked against the release's pin and the registry's
60811
+ * signed metadata, beside what is there, with `current` switched to it and `previous` kept. A
60812
+ * host running there is not touched; the next start runs the new version (`machines.update` stops
60813
+ * and starts it in order). Refused by a development hub (its version is not on npm) and for
60814
+ * a machine that runs a command of its own; answers when the install is done or refused
60815
+ */
60816
+ "machines.install": { params: external_exports.object({ machineId: MachineId }), result: MachineInstallResult },
60817
+ /**
60818
+ * Updates a linked machine to this hub's version (docs/plans/remote-hub.md §10.5): installs it beside
60819
+ * the running one, stops the running host in order (`centralu serve --stop`; without a keeper its
60820
+ * agents end and resume on the new host, a turn in progress is lost), switches `current`, starts it
60821
+ * (`serve --detach`) and waits at most 30 s for a hello from the new version. When none comes, the
60822
+ * old version is put back and started, and the call fails with the end of the new host's log. On
60823
+ * success only the current and previous versions are kept. Progress is the machine's `operation`
60824
+ * in `machine_status`. Refused like `machines.install`
60825
+ */
60826
+ "machines.update": { params: external_exports.object({ machineId: MachineId }), result: MachineInstallResult },
60827
+ /**
60828
+ * Switches a machine back to the version the hub installed before (`previous`), stopping and starting
60829
+ * its host as `machines.update` does and checking the same way. One step only: `previous` is cleared,
60830
+ * so a second rollback is not offered (a store migrated twice may not read on the older build)
60831
+ */
60832
+ "machines.rollback": { params: external_exports.object({ machineId: MachineId }), result: MachineInstallResult },
60833
+ /**
60834
+ * Stops the host there and removes what the hub installed (`<data>/remote/`), keeping the machine's
60835
+ * data and an npm install of its own. The machine stays linked
60836
+ */
60837
+ "machines.uninstall": { params: external_exports.object({ machineId: MachineId }), result: MachineUninstallResult },
60838
+ /**
60839
+ * Starts a linked machine's host at boot, or stops doing that, or (without `on`) reads whether it
60840
+ * does (docs/plans/remote-hub.md §10.4, owner decision 4: per machine, off by default). The
60841
+ * Centralu there writes its own entry (`centralu serve --autostart`): a systemd user unit with
60842
+ * lingering on Linux, a scheduled task at sign-in on Windows and for WSL. A host running there is
60843
+ * neither started nor stopped. Refused by a machine that predates it or offers none (macOS)
60844
+ */
60845
+ "machines.autostart": { params: external_exports.object({ machineId: MachineId, on: external_exports.boolean().optional() }), result: MachineAutostartResult },
60846
+ /**
60847
+ * What would stop on a linked machine if its host stopped now (`host.activity`, asked of it), for the
60848
+ * "Update <machine>" prompt. Null when the machine cannot say (unreachable, other protocol, older)
60849
+ */
60850
+ "machines.activity": { params: external_exports.object({ machineId: MachineId }), result: external_exports.object({ activity: HostActivity.nullable() }) },
60851
+ /** What would stop if this host stopped now (the `hostBusy` rule); for a hub's `machines.activity` */
60852
+ "host.activity": { params: external_exports.object({}), result: HostActivity },
60757
60853
  /**
60758
60854
  * Stops this host the way a signal would: agents, terminals, command runs and app processes
60759
60855
  * stopped in order, the store closed, then exit. For `centralu serve --stop` (docs/agent-host.md
@@ -60935,6 +61031,17 @@ function sessionBusy(s) {
60935
61031
  function hostBusy(s) {
60936
61032
  return s.terminals > 0 || s.commandRuns > 0 || s.sessions.some(sessionBusy);
60937
61033
  }
61034
+ function activityCounts(s) {
61035
+ const c = { working: 0, approvals: 0, questions: 0, background: 0, terminals: s.terminals, commandRuns: s.commandRuns };
61036
+ for (const x of s.sessions) {
61037
+ if (!sessionBusy(x)) continue;
61038
+ if (x.pendingApproval || x.state === "waiting_approval") c.approvals++;
61039
+ else if ((x.pendingQuestions?.length ?? 0) > 0) c.questions++;
61040
+ else if (BUSY_STATES.has(x.state)) c.working++;
61041
+ else c.background++;
61042
+ }
61043
+ return c;
61044
+ }
60938
61045
  function sessionIdle(s, process4) {
60939
61046
  if (s.pendingApproval) return { idle: false, reason: "approval" };
60940
61047
  if (s.pendingQuestions.length > 0) return { idle: false, reason: "question" };
@@ -62006,12 +62113,7 @@ var HostServer = class _HostServer {
62006
62113
  this.listenError = null;
62007
62114
  if (err.code === "EADDRINUSE") {
62008
62115
  reject(
62009
- new Error(
62010
- `Port ${this.opts.port} is already in use.
62011
- \xB7 If a host is already running, just use it (start only the UI)
62012
- \xB7 To clean up a leftover process: lsof -ti:${this.opts.port} | xargs kill
62013
- \xB7 To use another port: pnpm host --port ${this.opts.port + 1}`
62014
- )
62116
+ new Error(portInUseMessage(this.opts.port, this.opts.servedBy))
62015
62117
  );
62016
62118
  return;
62017
62119
  }
@@ -62246,6 +62348,17 @@ function errorCode(raw) {
62246
62348
  const known = ProtocolErrorCode.safeParse(raw);
62247
62349
  return known.success ? known.data : "internal";
62248
62350
  }
62351
+ function portInUseMessage(port2, servedBy) {
62352
+ if (servedBy === "serve") {
62353
+ return `Port ${port2} is already in use.
62354
+ \xB7 Another program listens on 127.0.0.1:${port2}; on Windows this is often WSL (wslrelay.exe) relaying a Centralu inside a distro
62355
+ \xB7 To use another port: centralu serve --port <n> (it is kept for the next start)`;
62356
+ }
62357
+ return `Port ${port2} is already in use.
62358
+ \xB7 If a host is already running, just use it (start only the UI)
62359
+ \xB7 To clean up a leftover process: lsof -ti:${port2} | xargs kill
62360
+ \xB7 To use another port: pnpm host --port ${port2 + 1}`;
62361
+ }
62249
62362
 
62250
62363
  // packages/agent-host/src/view-handover.ts
62251
62364
  var VIEW_HANDOVER_KEY = "views.handover";
@@ -62278,8 +62391,9 @@ function restoreViewHandover(settings, views2, inline, now = Date.now()) {
62278
62391
  if (!(age2 >= 0 && age2 <= VIEW_HANDOVER_MAX_AGE_MS)) return { restored: 0, skipped: list.length };
62279
62392
  const opened = new Set(views2.restore(list));
62280
62393
  for (const v of list) {
62394
+ if (typeof v !== "object" || v === null || !opened.has(v.id)) continue;
62281
62395
  const o = v.inline;
62282
- if (!opened.has(v.id) || !o || typeof o.sessionId !== "string" || typeof o.callId !== "string" || typeof o.tool !== "string") continue;
62396
+ if (!o || typeof o.sessionId !== "string" || typeof o.callId !== "string" || typeof o.tool !== "string") continue;
62283
62397
  inline?.adopt({ sessionId: o.sessionId, callId: o.callId, ref: v.app, tool: o.tool, uri: v.uri, instanceId: v.id });
62284
62398
  }
62285
62399
  return { restored: opened.size, skipped: list.length - opened.size };
@@ -94949,7 +95063,8 @@ var AppVersions2 = class {
94949
95063
  for (const name of names) {
94950
95064
  if (name.startsWith(".")) continue;
94951
95065
  try {
94952
- out.push(JSON.parse(readFileSync7(join13(dir, name, "meta.json"), "utf8")));
95066
+ const meta3 = JSON.parse(readFileSync7(join13(dir, name, "meta.json"), "utf8"));
95067
+ if (meta3 && typeof meta3 === "object" && typeof meta3.id === "string" && typeof meta3.at === "number") out.push(meta3);
94953
95068
  } catch {
94954
95069
  }
94955
95070
  }
@@ -98082,19 +98197,21 @@ ${summary}
98082
98197
  ` : "";
98083
98198
  const verbatimMark = "\u2500\u2500 verbatim from here \u2500\u2500\n\n";
98084
98199
  const fixed = bytes(instructions + touchedLine + summarySection + verbatimMark) + 200;
98085
- const size = () => rendered.reduce((n, e) => n + bytes(e.text) + 2, fixed);
98200
+ const sizes = rendered.map((e) => bytes(e.text) + 2);
98201
+ let size = sizes.reduce((n, s) => n + s, fixed);
98086
98202
  let dropped = 0;
98087
- while (size() > cap && rendered.length > 1) {
98088
- rendered.shift();
98203
+ while (size > cap && rendered.length - dropped > 1) {
98204
+ size -= sizes[dropped];
98089
98205
  dropped++;
98090
98206
  }
98091
- const from = rendered.length ? rendered[0].seq : lastSeq;
98207
+ const kept = rendered.slice(dropped);
98208
+ const from = kept.length ? kept[0].seq : lastSeq;
98092
98209
  const covers = `covers seq ${num(from)}\u2013${num(lastSeq)} of ${num(lastSeq)} \xB7 ${(cap / 1e6).toFixed(1)} MB cap` + (dropped > 0 || folded ? " \xB7 earlier material stays in the app's records" : "");
98093
98210
  const header = `# Handoff \xB7 ${opts.name} \xB7 ${opts.tool}${opts.toTool ? ` \u2192 ${opts.toTool}` : ""}
98094
98211
  ${covers}
98095
98212
  ${touchedLine}
98096
98213
  `;
98097
- return header + instructions + summarySection + verbatimMark + rendered.map((e) => e.text).join("\n\n") + "\n";
98214
+ return header + instructions + summarySection + verbatimMark + kept.map((e) => e.text).join("\n\n") + "\n";
98098
98215
  }
98099
98216
 
98100
98217
  // packages/agent-host/src/sessions/orchestrator-proposals.ts
@@ -98105,6 +98222,19 @@ var SKILL_PROPOSALS_KEY = "orchestrator_skill_proposals";
98105
98222
  var SKILLS_KEY = "orchestrator_skills";
98106
98223
  var SKILL_MAX_COUNT = 10;
98107
98224
  var SKILL_MAX_CHARS = 2e3;
98225
+ var isText = (v) => typeof v === "string";
98226
+ var isTextList = (v) => Array.isArray(v) && v.every(isText);
98227
+ function storedList(raw, has) {
98228
+ if (!raw) return [];
98229
+ let v;
98230
+ try {
98231
+ v = JSON.parse(raw);
98232
+ } catch {
98233
+ return [];
98234
+ }
98235
+ if (!Array.isArray(v)) return [];
98236
+ return v.filter((x) => typeof x === "object" && x !== null && !Array.isArray(x) && has(x));
98237
+ }
98108
98238
  var OrchestratorProposals = class {
98109
98239
  constructor(store2, apps) {
98110
98240
  this.store = store2;
@@ -98114,12 +98244,7 @@ var OrchestratorProposals = class {
98114
98244
  apps;
98115
98245
  /** MCP server proposals waiting on the person's approval */
98116
98246
  mcpProposals() {
98117
- try {
98118
- const raw = this.store.appSetting(MCP_PROPOSALS_KEY);
98119
- return raw ? JSON.parse(raw) : [];
98120
- } catch {
98121
- return [];
98122
- }
98247
+ return storedList(this.store.appSetting(MCP_PROPOSALS_KEY), (x) => isText(x.name) && isText(x.command) && isTextList(x.args));
98123
98248
  }
98124
98249
  /** Saves an MCP server proposal (propose_mcp_server) — validation and persistence only */
98125
98250
  proposeMcpServer(spec) {
@@ -98219,21 +98344,11 @@ var OrchestratorProposals = class {
98219
98344
  }
98220
98345
  /** Skill proposals waiting on the person's approval (#71) */
98221
98346
  skillProposals() {
98222
- try {
98223
- const raw = this.store.appSetting(SKILL_PROPOSALS_KEY);
98224
- return raw ? JSON.parse(raw) : [];
98225
- } catch {
98226
- return [];
98227
- }
98347
+ return storedList(this.store.appSetting(SKILL_PROPOSALS_KEY), (x) => isText(x.name) && isText(x.content));
98228
98348
  }
98229
98349
  /** Skills that have been approved and loaded into the orchestrator's role prompt (#71) */
98230
98350
  orchestratorSkills() {
98231
- try {
98232
- const raw = this.store.appSetting(SKILLS_KEY);
98233
- return raw ? JSON.parse(raw) : [];
98234
- } catch {
98235
- return [];
98236
- }
98351
+ return storedList(this.store.appSetting(SKILLS_KEY), (x) => isText(x.name) && isText(x.content));
98237
98352
  }
98238
98353
  /**
98239
98354
  * Turns approved skills into a block appended to the role prompt (#71). This is tool-agnostic text
@@ -101914,11 +102029,17 @@ var Store = class _Store {
101914
102029
  forgetProjectConsent(fromProjectId, toProjectId, kind) {
101915
102030
  return this.db.prepare(`DELETE FROM project_consents WHERE from_project_id = ? AND to_project_id = ? AND kind = ?`).run(fromProjectId, toProjectId, kind).changes > 0;
101916
102031
  }
102032
+ /**
102033
+ * Only the kinds this build knows (#384). `kind` has no CHECK, so a newer build can add one without a breaking step;
102034
+ * a row of a kind this build cannot name is left in place for that build rather than listed here, where the settings
102035
+ * screen would show it without a label and could not revoke it (`projectConsents.revoke` takes known kinds only).
102036
+ */
101917
102037
  listProjectConsents() {
101918
- return this.db.prepare(
102038
+ const rows = this.db.prepare(
101919
102039
  `SELECT from_project_id as fromProjectId, to_project_id as toProjectId, kind, decided_at as decidedAt
101920
102040
  FROM project_consents ORDER BY decided_at DESC`
101921
102041
  ).all();
102042
+ return rows.filter((r) => ProjectConsentKind.safeParse(r.kind).success);
101922
102043
  }
101923
102044
  // ── Linked machines (#82, v46) ─────────────────────────────────────────────────────────────
101924
102045
  listLinkedMachines() {
@@ -104128,6 +104249,7 @@ var SessionManager = class _SessionManager {
104128
104249
  if (m.projectId) project2 = this.store.listProjects().find((p) => p.id === m.projectId);
104129
104250
  this.running.delete(sessionId);
104130
104251
  this.restartAfterTurn.delete(sessionId);
104252
+ this.prCheckedAt.delete(sessionId);
104131
104253
  this.meta.delete(sessionId);
104132
104254
  await this.store.trashSession(sessionId, {
104133
104255
  projectId: m.projectId,
@@ -104334,9 +104456,11 @@ var SessionManager = class _SessionManager {
104334
104456
  * location is no longer ours — it is never looked at.
104335
104457
  */
104336
104458
  async sweepOrphanHandoffNotes(projectId) {
104337
- const claimed = this.store.handoffPredecessors();
104338
- const trashed = this.store.trashedIds();
104339
- await sweepHandoffNotes((owner) => this.meta.has(owner) || claimed.has(owner) || trashed.has(owner), projectId);
104459
+ await sweepHandoffNotes(
104460
+ (owner) => this.meta.has(owner) || // A session in the trash still owns its note (#204) — restoring it must find it; only purging lets it go
104461
+ this.store.trashedIds().has(owner) || this.store.handoffPredecessors().has(owner),
104462
+ projectId
104463
+ );
104340
104464
  }
104341
104465
  /**
104342
104466
  * Changes model or permissions (FR-7). If the adapter supports it, it applies starting next turn;
@@ -104491,7 +104615,7 @@ var SessionManager = class _SessionManager {
104491
104615
  ` : ending.type === "turn_complete" ? `[Centralu] The assigned work finished.
104492
104616
  ` : `[Centralu] The assigned work stopped before finishing.
104493
104617
  `;
104494
- const failure2 = ending.type === "error" ? `Error: ${ending.error.message.slice(0, 600)}
104618
+ const failure3 = ending.type === "error" ? `Error: ${ending.error.message.slice(0, 600)}
104495
104619
 
104496
104620
  ` : "";
104497
104621
  try {
@@ -104501,7 +104625,7 @@ var SessionManager = class _SessionManager {
104501
104625
  id: ${sessionId}
104502
104626
  Project: ${frameField(project2)}
104503
104627
 
104504
- ` + failure2 + `Last response:
104628
+ ` + failure3 + `Last response:
104505
104629
  ${preview || "(no content)"}
104506
104630
 
104507
104631
  If you need more, you can read that session's recent conversation with read_session.`,
@@ -105429,33 +105553,53 @@ ${String(stderr).slice(0, 2e3)}`
105429
105553
  * Screen preferences (UiPreferences) — read as one blob, and written back accepting **only what
105430
105554
  * changed.**
105431
105555
  *
105432
- * Corrupted JSON is treated the same as if it did not exist (parseUiPreferences). The app failing
105433
- * to start because of a single broken setting is worse than losing one value the person had chosen.
105556
+ * Corrupted JSON is treated the same as if it did not exist (parseUiPreferences), and said once on
105557
+ * stderr with the row it came from. The app failing to start because of a single broken setting is
105558
+ * worse than losing one value the person had chosen.
105434
105559
  */
105435
105560
  uiPreferences() {
105436
- const raw = this.store.appSetting(UI_PREFS_KEY);
105437
- let stored;
105438
- try {
105439
- stored = raw === null ? void 0 : JSON.parse(raw);
105440
- } catch {
105441
- stored = void 0;
105442
- }
105561
+ const stored = this.storedUiPreferences();
105443
105562
  const prefs = parseUiPreferences(stored);
105444
- const hasTextSize = typeof stored === "object" && stored !== null && "textSize" in stored;
105445
- if (!hasTextSize) {
105563
+ if (!("textSize" in stored)) {
105446
105564
  const legacy = textSizeFromLegacyStep(this.loadWorkspace()?.textScale);
105447
105565
  if (legacy !== null) {
105448
105566
  prefs.textSize = legacy;
105449
- this.store.setAppSetting(UI_PREFS_KEY, JSON.stringify(prefs));
105567
+ this.store.setAppSetting(UI_PREFS_KEY, JSON.stringify({ ...stored, textSize: legacy }));
105450
105568
  }
105451
105569
  }
105452
105570
  return prefs;
105453
105571
  }
105572
+ /**
105573
+ * Writes the patch **over the stored record, not over this build's reading of it** (#384). Another
105574
+ * build reads and writes the same row: during a host swap, and after a person goes back a release.
105575
+ * A field this build does not know, or a value it cannot read (a theme mode a newer build added),
105576
+ * stays as it was unless the patch sets it, so changing the text size in an older build does not
105577
+ * reset what a newer one chose.
105578
+ */
105454
105579
  setUiPreferences(patch) {
105455
- const next = { ...this.uiPreferences(), ...patch };
105456
- this.store.setAppSetting(UI_PREFS_KEY, JSON.stringify(next));
105457
- return next;
105580
+ const current = this.uiPreferences();
105581
+ this.store.setAppSetting(UI_PREFS_KEY, JSON.stringify({ ...this.storedUiPreferences(), ...patch }));
105582
+ if (patch.sessionTools !== void 0 && patch.sessionTools !== current.sessionTools) {
105583
+ for (const id2 of this.meta.keys()) this.appsHub?.sessionAppsChanged(id2);
105584
+ }
105585
+ return { ...current, ...patch };
105586
+ }
105587
+ /** The stored record as written, every field kept; `{}` for none or for one that is not a JSON object */
105588
+ storedUiPreferences() {
105589
+ const raw = this.store.appSetting(UI_PREFS_KEY);
105590
+ if (raw === null) return {};
105591
+ try {
105592
+ const v = JSON.parse(raw);
105593
+ if (typeof v === "object" && v !== null && !Array.isArray(v)) return v;
105594
+ } catch {
105595
+ }
105596
+ if (!this.brokenPrefsSaid) {
105597
+ this.brokenPrefsSaid = true;
105598
+ console.error(`[agent-host] app_settings.${UI_PREFS_KEY} is not a JSON object; screen preferences read as their defaults until the next change`);
105599
+ }
105600
+ return {};
105458
105601
  }
105602
+ brokenPrefsSaid = false;
105459
105603
  /** The settings screen must be able to see and delete rules (FR-3: make the outcome visible) */
105460
105604
  listApprovalRules() {
105461
105605
  return this.store.listApprovalRules().filter((r) => r.matcher).map((r) => ({
@@ -106595,7 +106739,12 @@ ${String(stderr).slice(0, 2e3)}`
106595
106739
  ...opts,
106596
106740
  onDemand: {
106597
106741
  attached: (id2) => this.appAccess?.attached(id2) ?? [],
106598
- allowed: (session2, app) => this.appAccess?.allowed(session2, app) ?? false
106742
+ /*
106743
+ * Only while the session holds the reader set (#382): attach_app and detach_app are part of it, so a
106744
+ * session the person turned the set off for (or that became a manager) would keep tools it can no
106745
+ * longer detach. The records stay; they count again once the set is back.
106746
+ */
106747
+ allowed: (session2, app) => this.toolProfileOf(session2.id) === "reader" && (this.appAccess?.allowed(session2, app) ?? false)
106599
106748
  }
106600
106749
  });
106601
106750
  this.appAccess = new AppAccess({
@@ -106801,7 +106950,7 @@ ${String(stderr).slice(0, 2e3)}`
106801
106950
  let merged = await gitBranchMerged(cwd, m.worktree.branch, m.worktree.base, trunk).catch(() => false);
106802
106951
  if (!merged && this.ghAvailable) {
106803
106952
  const now = Date.now();
106804
- if (now - (this.prCheckedAt.get(m.id) ?? 0) >= this.prPollMs) {
106953
+ if (this.meta.get(m.id) === m && now - (this.prCheckedAt.get(m.id) ?? 0) >= this.prPollMs) {
106805
106954
  this.prCheckedAt.set(m.id, now);
106806
106955
  const pr = await this.prLookup(cwd, m.worktree.branch).catch(() => null);
106807
106956
  if (pr === "unavailable") {
@@ -106818,6 +106967,7 @@ ${String(stderr).slice(0, 2e3)}`
106818
106967
  }
106819
106968
  }
106820
106969
  if (!merged || this.meta.get(m.id) !== m) continue;
106970
+ this.prCheckedAt.delete(m.id);
106821
106971
  m.worktreeMerged = true;
106822
106972
  this.emit({ type: "worktree_merged", sessionId: m.id });
106823
106973
  console.error(`[worktree] branch merged into trunk: ${m.worktree.branch} (${m.id.slice(0, 8)})`);
@@ -107454,6 +107604,29 @@ async function readUsage(q2) {
107454
107604
  if (typeof fn2 !== "function") throw new Error(UNSUPPORTED2);
107455
107605
  return toSnapshot(await fn2.call(q2));
107456
107606
  }
107607
+ var USAGE_ASK_MS = 4e3;
107608
+ async function readUsageFromAny(queries, { timeoutMs = USAGE_ASK_MS, log = (line) => console.error(line) } = {}) {
107609
+ let first = null;
107610
+ for (const [i, { query, sessionId }] of queries.entries()) {
107611
+ let timer;
107612
+ try {
107613
+ return await Promise.race([
107614
+ readUsage(query),
107615
+ new Promise((_, reject) => {
107616
+ timer = setTimeout(() => reject(new Error(`no answer within ${timeoutMs} ms`)), timeoutMs);
107617
+ })
107618
+ ]);
107619
+ } catch (e) {
107620
+ first ??= e;
107621
+ const why = e instanceof Error ? e.message : String(e);
107622
+ log(`[usage] claude: session ${sessionId ?? "unknown"} could not read usage (${i + 1} of ${queries.length}): ${why}`);
107623
+ if (why === UNSUPPORTED2) break;
107624
+ } finally {
107625
+ clearTimeout(timer);
107626
+ }
107627
+ }
107628
+ throw first instanceof Error ? first : new Error("A running session is required to read usage");
107629
+ }
107457
107630
 
107458
107631
  // packages/agent-host/src/adapters/claude/orchestrator-mcp.ts
107459
107632
  init_sdk();
@@ -108736,22 +108909,22 @@ var ClaudeSession = class {
108736
108909
  * Bash caught by an `ask` rule in the settings file reaches the callback → answering allow runs it
108737
108910
  * the same request, with no callback (old-style auto) denied — "Claude requested permissions to use Bash, but you haven't granted it yet."
108738
108911
  *
108739
- * So auto's own callback only accepts question choices, and **denies everything else the
108740
- * way it used to.** Allowing it here would let our own callback approve an `ask` rule
108912
+ * So auto's own callback only accepts question choices and Centralu's own tools (which every
108913
+ * preset lets through, #382), and **denies everything else the way it used to.** Allowing it here would let our own callback approve an `ask` rule
108741
108914
  * written into a trusted project's own `.claude/settings.json` — the opposite direction
108742
108915
  * from #92, which exists so files from the repo cannot decide approvals. (The SDK leaves a
108743
108916
  * `CLAUDE_SDK_CAN_USE_TOOL_SHADOWED` warning line for this combination.)
108744
108917
  */
108745
- canUseTool: async (toolName, toolInput) => {
108918
+ canUseTool: async (toolName, toolInput, options) => {
108919
+ if (isOrchestratorTool(toolName) && self2.orchestratorServer) {
108920
+ return { behavior: "allow", updatedInput: toolInput };
108921
+ }
108746
108922
  if (preset === "auto" && toolName !== "AskUserQuestion") {
108747
108923
  return {
108748
108924
  behavior: "deny",
108749
108925
  message: `Permission to use ${toolName} was not granted (a settings "ask" rule matched; the auto preset does not ask)`
108750
108926
  };
108751
108927
  }
108752
- if (isOrchestratorTool(toolName)) {
108753
- return { behavior: "allow", updatedInput: toolInput };
108754
- }
108755
108928
  const appTool = appToolOf(toolName);
108756
108929
  if (appTool && self2.opts.apps?.readOnly(appTool.server, appTool.tool)) {
108757
108930
  return { behavior: "allow", updatedInput: toolInput };
@@ -108767,6 +108940,7 @@ var ClaudeSession = class {
108767
108940
  self2.emit({ type: "question_request", sessionId: self2.sessionId, requestId: requestId2, questions });
108768
108941
  return new Promise((resolve10) => {
108769
108942
  self2.questions.set(requestId2, { resolve: resolve10, input: toolInput });
108943
+ self2.releaseOnCancel(requestId2, options?.signal);
108770
108944
  });
108771
108945
  }
108772
108946
  const detail = approvalDetail(toolName, toolInput, self2.opts.cwd);
@@ -108776,11 +108950,13 @@ var ClaudeSession = class {
108776
108950
  self2.emit({ type: "approval_request", sessionId: self2.sessionId, requestId, detail });
108777
108951
  return new Promise((resolve10) => {
108778
108952
  self2.pending.set(requestId, { resolve: resolve10, input: toolInput });
108953
+ self2.releaseOnCancel(requestId, options?.signal);
108779
108954
  });
108780
108955
  }
108781
108956
  }
108782
108957
  });
108783
108958
  ClaudeAdapter.liveQueries.add(q2);
108959
+ ClaudeAdapter.querySessions.set(q2, this.sessionId);
108784
108960
  this.releaseLostCalls(q2);
108785
108961
  this.goalKnown = new Promise((resolve10) => this.settleGoalKnown = resolve10);
108786
108962
  q2.supportedCommands().then((list) => {
@@ -109238,6 +109414,32 @@ var ClaudeSession = class {
109238
109414
  releaseAgents(why) {
109239
109415
  for (const e of this.stream.release(why)) this.emit(e);
109240
109416
  }
109417
+ /**
109418
+ * Closes a card when the CLI withdraws its question. The SDK aborts the callback's `signal` when the CLI sends
109419
+ * `control_cancel_request` for that permission request (the tool was aborted, or the turn ended without our Stop)
109420
+ * and when the query closes. Nothing else tells us: without this the card stayed open forever, and answering it
109421
+ * resolved a promise nobody was waiting on. A card already answered or released is no longer in its map, so this
109422
+ * closes nothing twice.
109423
+ */
109424
+ releaseOnCancel(requestId, signal) {
109425
+ if (!signal) return;
109426
+ const release = () => {
109427
+ const approval = this.pending.get(requestId);
109428
+ if (approval) {
109429
+ this.pending.delete(requestId);
109430
+ approval.resolve({ behavior: "deny", message: "Cancelled by Claude Code" });
109431
+ this.emit({ type: "approval_resolved", sessionId: this.sessionId, requestId, decision: "deny" });
109432
+ }
109433
+ const question = this.questions.get(requestId);
109434
+ if (question) {
109435
+ this.questions.delete(requestId);
109436
+ question.resolve({ behavior: "deny", message: "Cancelled by Claude Code" });
109437
+ this.emit({ type: "question_resolved", sessionId: this.sessionId, requestId });
109438
+ }
109439
+ };
109440
+ if (signal.aborted) release();
109441
+ else signal.addEventListener("abort", release, { once: true });
109442
+ }
109241
109443
  /** Releases a choice that was still waiting on an answer — **never released silently**, for the same reason as approval. */
109242
109444
  releaseQuestions(why) {
109243
109445
  for (const [id2, open5] of this.questions) {
@@ -109306,6 +109508,8 @@ var ClaudeAdapter = class _ClaudeAdapter {
109306
109508
  * older session was still alive. Insertion order is the same as start order.
109307
109509
  */
109308
109510
  static liveQueries = /* @__PURE__ */ new Set();
109511
+ /** Which session each live query belongs to, for the log line when one cannot answer (#481) */
109512
+ static querySessions = /* @__PURE__ */ new WeakMap();
109309
109513
  /** The most recently started query that is still alive. */
109310
109514
  static get lastQuery() {
109311
109515
  let last = null;
@@ -109383,9 +109587,9 @@ var ClaudeAdapter = class _ClaudeAdapter {
109383
109587
  * a Query. It throws when there is no session at all, and the manager degrades with a reason.
109384
109588
  */
109385
109589
  async listUsage() {
109386
- const q2 = _ClaudeAdapter.lastQuery;
109387
- if (!q2) throw new Error("A running session is required to read usage");
109388
- return readUsage(q2);
109590
+ const live = [..._ClaudeAdapter.liveQueries].reverse();
109591
+ if (live.length === 0) throw new Error("A running session is required to read usage");
109592
+ return readUsageFromAny(live.map((query) => ({ query, sessionId: _ClaudeAdapter.querySessions.get(query) ?? null })));
109389
109593
  }
109390
109594
  async listModels() {
109391
109595
  const q2 = _ClaudeAdapter.lastQuery;
@@ -109543,10 +109747,19 @@ var CodexClient = class {
109543
109747
  const id2 = `${this.idPrefix}${this.nextId++}`;
109544
109748
  this.write({ id: id2, method, params });
109545
109749
  return new Promise((resolve10, reject) => {
109546
- this.pending.set(id2, { resolve: resolve10, reject });
109547
- setTimeout(() => {
109750
+ const timer = setTimeout(() => {
109548
109751
  if (this.pending.delete(id2)) reject(new Error(`No response for ${method}`));
109549
109752
  }, timeoutMs);
109753
+ this.pending.set(id2, {
109754
+ resolve: (v) => {
109755
+ clearTimeout(timer);
109756
+ resolve10(v);
109757
+ },
109758
+ reject: (e) => {
109759
+ clearTimeout(timer);
109760
+ reject(e);
109761
+ }
109762
+ });
109550
109763
  });
109551
109764
  }
109552
109765
  notify(method, params) {
@@ -109565,7 +109778,7 @@ var CodexClient = class {
109565
109778
  this.closed = true;
109566
109779
  this.proc.stdin.end();
109567
109780
  const exited = await new Promise((resolve10) => {
109568
- if (this.proc.exitCode !== null || this.finished) return resolve10(true);
109781
+ if (this.proc.exitCode !== null || this.proc.signalCode != null || this.finished) return resolve10(true);
109569
109782
  const t = setTimeout(() => resolve10(false), 2e3);
109570
109783
  this.proc.once("exit", () => {
109571
109784
  clearTimeout(t);
@@ -110225,6 +110438,8 @@ var CODEX_KNOWN_NOTIFICATIONS = /* @__PURE__ */ new Set([
110225
110438
  "item/reasoning/summaryPartAdded",
110226
110439
  "thread/compacted",
110227
110440
  "account/rateLimits/updated",
110441
+ // read in index.ts: closes the approval card of a request Codex ended without our answer
110442
+ "serverRequest/resolved",
110228
110443
  // mapped by #290 for a child thread (CodexChildTracker); the parent's own status is still not read
110229
110444
  "thread/status/changed",
110230
110445
  // mapped by #304: notices, model switches and MCP servers that failed to start (thread/settings/updated in index.ts)
@@ -110242,7 +110457,6 @@ var CODEX_KNOWN_NOTIFICATIONS = /* @__PURE__ */ new Set([
110242
110457
  "rawResponseItem/completed",
110243
110458
  "rawResponse/completed",
110244
110459
  // correctly ignored
110245
- "serverRequest/resolved",
110246
110460
  "thread/started",
110247
110461
  "thread/archived",
110248
110462
  "thread/unarchived",
@@ -110428,16 +110642,16 @@ function normalizeNotification(sessionId, n) {
110428
110642
  */
110429
110643
  case "turn/completed": {
110430
110644
  const turn = obj(p.turn);
110431
- const failure2 = obj(turn.error);
110432
- if (str7(turn.status) !== "failed" && !failure2.message) return [{ type: "turn_complete", sessionId }];
110433
- const detail = str7(failure2.additionalDetails);
110645
+ const failure3 = obj(turn.error);
110646
+ if (str7(turn.status) !== "failed" && !failure3.message) return [{ type: "turn_complete", sessionId }];
110647
+ const detail = str7(failure3.additionalDetails);
110434
110648
  return [
110435
110649
  {
110436
110650
  type: "error",
110437
110651
  sessionId,
110438
110652
  error: {
110439
110653
  code: "internal",
110440
- message: [str7(failure2.message) || "The turn failed", detail].filter(Boolean).join("\n"),
110654
+ message: [str7(failure3.message) || "The turn failed", detail].filter(Boolean).join("\n"),
110441
110655
  retryable: true
110442
110656
  }
110443
110657
  }
@@ -110886,7 +111100,18 @@ var CodexSession = class {
110886
111100
  * if stop is pressed very quickly, the response can arrive before the notification.
110887
111101
  */
110888
111102
  turnId = null;
110889
- /** Our requestId to the Codex server's request id */
111103
+ /**
111104
+ * Turns already reported ended (`turn/completed`/`turn/failed`), the latest few. The `turn/start` response can
111105
+ * arrive after its own turn's `turn/completed`; taking the id from that late response would bring a finished
111106
+ * turn back as the target of Stop, and the next Stop would send `turn/interrupt` for a dead turn and say
111107
+ * "Could not stop". Bounded: only a response still in flight can name one of these.
111108
+ */
111109
+ endedTurns = [];
111110
+ /**
111111
+ * Our requestId to the Codex server's request, with the thread and turn it was asked in. Codex ends a request
111112
+ * itself when its turn ends or is interrupted (and says so with `serverRequest/resolved`); the card has to close
111113
+ * with it, or an answer goes to a request id nobody is waiting on.
111114
+ */
110890
111115
  approvals = /* @__PURE__ */ new Map();
110891
111116
  /**
110892
111117
  * Cards raised for app tool approval (M4 A-5) — the answer has a different shape (elicitation's
@@ -110895,6 +111120,8 @@ var CodexSession = class {
110895
111120
  elicitations = /* @__PURE__ */ new Set();
110896
111121
  /** App servers loaded onto this thread through the bridge — only elicitations under those names go to our cards */
110897
111122
  appServers = /* @__PURE__ */ new Set();
111123
+ /** Whether this thread was given Centralu's own server — only then is an elicitation under that name ours to accept (#382) */
111124
+ ownServer = false;
110898
111125
  reqCounter = 0;
110899
111126
  alwaysAllow = new AlwaysAllowRules();
110900
111127
  /**
@@ -111045,7 +111272,7 @@ var CodexSession = class {
111045
111272
  }
111046
111273
  this.threadId = threadIdOf(res) ?? this.opts.resumeExternalId;
111047
111274
  this.threadSettings = threadSettingsOf(res);
111048
- if (adopted) this.turnId ??= runningTurnOf(res) ?? await this.latestRunningTurn(res);
111275
+ if (adopted) this.takeStartedTurn(runningTurnOf(res) ?? await this.latestRunningTurn(res));
111049
111276
  void this.client.request("thread/goal/get", { threadId: this.threadId }).then((r) => {
111050
111277
  const g5 = r.goal ? goalFromCodex(r.goal) : null;
111051
111278
  if (g5) this.emit({ type: "goal", sessionId: this.sessionId, goal: g5 });
@@ -111118,6 +111345,7 @@ var CodexSession = class {
111118
111345
  const bridge = this.opts.orchestratorBridge;
111119
111346
  const servers = {};
111120
111347
  const orchestrator = !!(this.opts.orchestratorTools && bridge);
111348
+ this.ownServer = orchestrator;
111121
111349
  if (orchestrator) {
111122
111350
  servers[ORCHESTRATOR_MCP_NAME] = {
111123
111351
  command: process.execPath,
@@ -111166,6 +111394,7 @@ var CodexSession = class {
111166
111394
  }
111167
111395
  onNotification(n) {
111168
111396
  this.noteFileChange(n);
111397
+ this.noteRequestEnded(n);
111169
111398
  this.unmapped.note(n.method);
111170
111399
  const from = n.params?.threadId;
111171
111400
  if (typeof from === "string" && this.threadId !== null && from !== this.threadId) {
@@ -111175,7 +111404,11 @@ var CodexSession = class {
111175
111404
  return;
111176
111405
  }
111177
111406
  if (n.method === "turn/started") this.turnId = turnIdOf(n.params);
111178
- if (n.method === "turn/completed" || n.method === "turn/failed") this.turnId = null;
111407
+ if (n.method === "turn/completed" || n.method === "turn/failed") {
111408
+ this.turnId = null;
111409
+ const ended = turnIdOf(n.params);
111410
+ if (ended) this.endedTurns = [...this.endedTurns.slice(-(ENDED_TURNS_KEPT - 1)), ended];
111411
+ }
111179
111412
  if (this.retrying && n.method.startsWith("item/")) {
111180
111413
  this.retrying = false;
111181
111414
  this.emit({ type: "activity", sessionId: this.sessionId, activity: this.activityBefore });
@@ -111277,6 +111510,34 @@ var CodexSession = class {
111277
111510
  for (const early of held3) this.onChildNotification(thread, early);
111278
111511
  }
111279
111512
  }
111513
+ /**
111514
+ * Closes the cards of server requests Codex ended without our answer. `serverRequest/resolved` names one request
111515
+ * (generated `ServerRequestResolvedNotification`, codex-cli 0.160.0: `{ threadId, requestId }`); a turn that ends
111516
+ * takes every request asked in it, whether or not that notification comes. A request we answered is already out of
111517
+ * `approvals`, so its own `serverRequest/resolved` finds nothing and closes nothing twice.
111518
+ */
111519
+ noteRequestEnded(n) {
111520
+ const p = n.params ?? {};
111521
+ if (n.method === "serverRequest/resolved") {
111522
+ if (typeof p.requestId !== "string" && typeof p.requestId !== "number") return;
111523
+ const serverId = String(p.requestId);
111524
+ this.closeApprovals((a) => String(a.serverId) === serverId);
111525
+ return;
111526
+ }
111527
+ if (n.method !== "turn/completed" && n.method !== "turn/failed") return;
111528
+ const turn = turnIdOf(n.params);
111529
+ if (typeof p.threadId !== "string" || !turn) return;
111530
+ this.closeApprovals((a) => a.threadId === p.threadId && a.turnId === turn);
111531
+ }
111532
+ /** Closes the matching cards as denied, without answering Codex: the request is already gone on its side */
111533
+ closeApprovals(match) {
111534
+ for (const [requestId, a] of this.approvals) {
111535
+ if (!match(a)) continue;
111536
+ this.approvals.delete(requestId);
111537
+ this.elicitations.delete(requestId);
111538
+ this.emit({ type: "approval_resolved", sessionId: this.sessionId, requestId, decision: "deny" });
111539
+ }
111540
+ }
111280
111541
  /** Keeps a file-change item's changes until it completes — see `fileChanges` */
111281
111542
  noteFileChange(n) {
111282
111543
  const p = n.params ?? {};
@@ -111316,12 +111577,12 @@ var CodexSession = class {
111316
111577
  const p = typeof r.params === "object" && r.params !== null ? r.params : {};
111317
111578
  if (typeof p.serverName === "string" && this.appServers.has(p.serverName) && approvalKindOf(p._meta) === "mcp_tool_call") {
111318
111579
  const requestId2 = `codex-req-${++this.reqCounter}`;
111319
- this.approvals.set(requestId2, r.id);
111580
+ this.approvals.set(requestId2, requestOrigin(r));
111320
111581
  this.elicitations.add(requestId2);
111321
111582
  this.emit({ type: "approval_request", sessionId: this.sessionId, requestId: requestId2, detail: appApprovalDetail(p.serverName, p.message, p._meta) });
111322
111583
  return;
111323
111584
  }
111324
- const ours = p.serverName === ORCHESTRATOR_MCP_NAME;
111585
+ const ours = p.serverName === ORCHESTRATOR_MCP_NAME && this.ownServer;
111325
111586
  this.client.respond(r.id, { action: ours ? "accept" : "decline", content: null, _meta: null });
111326
111587
  return;
111327
111588
  }
@@ -111344,7 +111605,7 @@ var CodexSession = class {
111344
111605
  return;
111345
111606
  }
111346
111607
  const requestId = `codex-req-${++this.reqCounter}`;
111347
- this.approvals.set(requestId, r.id);
111608
+ this.approvals.set(requestId, requestOrigin(r));
111348
111609
  this.emit({ type: "approval_request", sessionId: this.sessionId, requestId, detail });
111349
111610
  }
111350
111611
  applyRules(matchers) {
@@ -111419,9 +111680,7 @@ var CodexSession = class {
111419
111680
  */
111420
111681
  ...this.opts.effort ? { effort: this.opts.effort } : {},
111421
111682
  ...this.outputSchemaParam()
111422
- }).then((res) => {
111423
- this.turnId ??= turnIdOf(res);
111424
- });
111683
+ }).then((res) => this.takeStartedTurn(turnIdOf(res)));
111425
111684
  }).catch((e) => {
111426
111685
  if (this.startFailed) return;
111427
111686
  this.emit({
@@ -111441,9 +111700,7 @@ var CodexSession = class {
111441
111700
  input,
111442
111701
  ...this.opts.effort ? { effort: this.opts.effort } : {},
111443
111702
  ...this.outputSchemaParam()
111444
- }).then((res) => {
111445
- this.turnId ??= turnIdOf(res);
111446
- }).catch((e) => {
111703
+ }).then((res) => this.takeStartedTurn(turnIdOf(res))).catch((e) => {
111447
111704
  this.emit({
111448
111705
  type: "error",
111449
111706
  sessionId: this.sessionId,
@@ -111451,6 +111708,14 @@ var CodexSession = class {
111451
111708
  });
111452
111709
  });
111453
111710
  }
111711
+ /**
111712
+ * Takes a running turn's id from a response, unless the notifications already named one or already ended it. The
111713
+ * response and the notifications race: `turn/started` can come after the response (Stop pressed very quickly), and
111714
+ * `turn/completed` can come before it (a short turn), so a late response must not bring an ended turn back.
111715
+ */
111716
+ takeStartedTurn(id2) {
111717
+ if (id2 && !this.endedTurns.includes(id2)) this.turnId ??= id2;
111718
+ }
111454
111719
  /**
111455
111720
  * A turn belonging to an agent that an app gave a schema and asked for (M4 D-1). Codex receives
111456
111721
  * the schema **per turn** — the installed 0.153.4's generated type is
@@ -111464,9 +111729,10 @@ var CodexSession = class {
111464
111729
  return this.opts.outputSchema ? { outputSchema: this.opts.outputSchema } : {};
111465
111730
  }
111466
111731
  respondApproval(requestId, decision, _scope, matcher) {
111467
- const serverId = this.approvals.get(requestId);
111468
- if (serverId === void 0) return false;
111732
+ const open5 = this.approvals.get(requestId);
111733
+ if (open5 === void 0) return false;
111469
111734
  this.approvals.delete(requestId);
111735
+ const serverId = open5.serverId;
111470
111736
  if (this.elicitations.delete(requestId)) {
111471
111737
  this.client.respond(serverId, {
111472
111738
  action: decision === "deny" ? "decline" : "accept",
@@ -111515,6 +111781,10 @@ var CodexSession = class {
111515
111781
  this.emit({ type: "state_change", sessionId: this.sessionId, state: "waiting_input", reason: "interrupted" });
111516
111782
  return;
111517
111783
  }
111784
+ for (const [requestId, a] of this.approvals) {
111785
+ if (a.threadId !== null && a.threadId !== this.threadId) continue;
111786
+ this.respondApproval(requestId, "deny");
111787
+ }
111518
111788
  if (!this.threadId || !this.turnId) return;
111519
111789
  void this.client.request("turn/interrupt", { threadId: this.threadId, turnId: this.turnId }).catch((err) => {
111520
111790
  this.emit({
@@ -111657,6 +111927,15 @@ function runningTurnOf(res) {
111657
111927
  }
111658
111928
  return null;
111659
111929
  }
111930
+ var ENDED_TURNS_KEPT = 8;
111931
+ function requestOrigin(r) {
111932
+ const p = typeof r.params === "object" && r.params !== null ? r.params : {};
111933
+ return {
111934
+ serverId: r.id,
111935
+ threadId: typeof p.threadId === "string" ? p.threadId : null,
111936
+ turnId: typeof p.turnId === "string" ? p.turnId : null
111937
+ };
111938
+ }
111660
111939
  function turnIdOf(payload) {
111661
111940
  const turn = payload?.turn;
111662
111941
  return typeof turn?.id === "string" ? turn.id : null;
@@ -112181,7 +112460,7 @@ var toInfo = (h) => ({
112181
112460
  function sessionIdsOf(panels) {
112182
112461
  return panels.flatMap((p) => p.kind === "session" ? [p.sessionId] : []);
112183
112462
  }
112184
- function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: updates2, commands, externalApps: externalApps2, views: views2, inlineViews: inlineViews2, heldPids, themes: themes2, agentVersions: agentVersions2, machines, stopHost } = {}) {
112463
+ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: updates2, commands, externalApps: externalApps2, views: views2, inlineViews: inlineViews2, heldPids, themes: themes2, agentVersions: agentVersions2, machines, stopHost, activity: activity2 } = {}) {
112185
112464
  const requireThemes = () => {
112186
112465
  if (!themes2) throw Object.assign(new Error("Theme files are unavailable"), { code: "internal" });
112187
112466
  return themes2;
@@ -112768,6 +113047,19 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
112768
113047
  return { ok: true };
112769
113048
  },
112770
113049
  "machines.reconnect": async (p) => requireMachines().reconnect(RpcMethods["machines.reconnect"].params.parse(p).machineId),
113050
+ "machines.install": async (p) => requireMachines().install(RpcMethods["machines.install"].params.parse(p).machineId),
113051
+ "machines.update": async (p) => requireMachines().update(RpcMethods["machines.update"].params.parse(p).machineId),
113052
+ "machines.rollback": async (p) => requireMachines().rollback(RpcMethods["machines.rollback"].params.parse(p).machineId),
113053
+ "machines.uninstall": async (p) => requireMachines().uninstall(RpcMethods["machines.uninstall"].params.parse(p).machineId),
113054
+ "machines.autostart": async (p) => {
113055
+ const { machineId, on: on2 } = RpcMethods["machines.autostart"].params.parse(p);
113056
+ return requireMachines().autostart(machineId, on2);
113057
+ },
113058
+ "machines.activity": async (p) => ({ activity: await requireMachines().activity(RpcMethods["machines.activity"].params.parse(p).machineId) }),
113059
+ "host.activity": async () => {
113060
+ if (!activity2) throw Object.assign(new Error("This host does not report its activity"), { code: "internal" });
113061
+ return activity2();
113062
+ },
112771
113063
  "machines.acceptVersions": async (p) => requireMachines().acceptVersions(RpcMethods["machines.acceptVersions"].params.parse(p).machineId),
112772
113064
  "updates.status": async (p) => requireUpdates().check(RpcMethods["updates.status"].params.parse(p).force),
112773
113065
  "updates.setAuto": async (p) => requireUpdates().setAuto(RpcMethods["updates.setAuto"].params.parse(p).enabled),
@@ -114046,6 +114338,868 @@ async function runDrain(bound, deps) {
114046
114338
  return report;
114047
114339
  }
114048
114340
 
114341
+ // packages/agent-host/src/links/install.ts
114342
+ import { existsSync as existsSync20, readFileSync as readFileSync13 } from "node:fs";
114343
+ import { hostname as hostname3 } from "node:os";
114344
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
114345
+ import { gzipSync } from "node:zlib";
114346
+
114347
+ // packages/agent-host/src/links/registry.ts
114348
+ import { createPublicKey, verify } from "node:crypto";
114349
+ var NPM_REGISTRY = "https://registry.npmjs.org";
114350
+ var PACKAGE_NAME = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
114351
+ var VERSION = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/;
114352
+ var NotPublished = class extends Error {
114353
+ constructor(id2) {
114354
+ super(`${id2} is not on the npm registry`);
114355
+ this.id = id2;
114356
+ }
114357
+ id;
114358
+ };
114359
+ async function getJson(url2, f, id2) {
114360
+ const res = await f(url2, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(3e4) });
114361
+ if (res.status === 404 && id2) throw new NotPublished(id2);
114362
+ if (!res.ok) throw new Error(`The npm registry answered ${res.status} for ${url2}`);
114363
+ return res.json();
114364
+ }
114365
+ async function verifiedPackage(name, version2, opts = {}) {
114366
+ if (!PACKAGE_NAME.test(name) || !VERSION.test(version2)) throw new Error(`Not a package to install: ${name}@${version2}`);
114367
+ const registry2 = (opts.registry ?? NPM_REGISTRY).replace(/\/+$/, "");
114368
+ const f = opts.fetch ?? globalThis.fetch;
114369
+ const now = opts.now?.() ?? Date.now();
114370
+ const [meta3, keys] = await Promise.all([getJson(`${registry2}/${name}/${version2}`, f, `${name}@${version2}`), getJson(`${registry2}/-/npm/v1/keys`, f)]);
114371
+ const m = meta3;
114372
+ if (m?.name !== name || m.version !== version2) throw new Error(`The npm registry answered for another package than ${name}@${version2}`);
114373
+ const tarball = m.dist?.tarball;
114374
+ const integrity = m.dist?.integrity;
114375
+ if (typeof integrity !== "string" || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity)) {
114376
+ throw new Error(`${name}@${version2} has no sha512 integrity in the registry`);
114377
+ }
114378
+ if (typeof tarball !== "string" || new URL(tarball).origin !== new URL(registry2).origin) {
114379
+ throw new Error(`${name}@${version2}'s tarball is not served by the registry (${String(tarball)})`);
114380
+ }
114381
+ const known = /* @__PURE__ */ new Map();
114382
+ for (const k2 of keys?.keys ?? []) {
114383
+ if (k2 && typeof k2.keyid === "string" && typeof k2.key === "string") known.set(k2.keyid, k2);
114384
+ }
114385
+ const signed = `${name}@${version2}:${integrity}`;
114386
+ const sigs = Array.isArray(m.dist?.signatures) ? m.dist.signatures : [];
114387
+ const ok = sigs.some((s) => {
114388
+ const key = typeof s?.keyid === "string" ? known.get(s.keyid) : void 0;
114389
+ if (!key || typeof s.sig !== "string") return false;
114390
+ if (key.expires && Date.parse(key.expires) <= now) return false;
114391
+ try {
114392
+ const pub = createPublicKey({ key: Buffer.from(key.key, "base64"), format: "der", type: "spki" });
114393
+ return verify("sha256", Buffer.from(signed), pub, Buffer.from(s.sig, "base64"));
114394
+ } catch {
114395
+ return false;
114396
+ }
114397
+ });
114398
+ if (!ok) throw new Error(`The npm registry's signature on ${name}@${version2} does not check out; nothing was installed`);
114399
+ return { name, version: version2, tarball, integrity };
114400
+ }
114401
+
114402
+ // packages/agent-host/src/links/tunnel.ts
114403
+ import { spawn as spawn5 } from "node:child_process";
114404
+ import { createConnection as createConnection2, createServer as createServer3 } from "node:net";
114405
+ function parseConnectionLine(stdout) {
114406
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
114407
+ for (let i = lines.length - 1; i >= 0; i--) {
114408
+ let raw;
114409
+ try {
114410
+ raw = JSON.parse(lines[i]);
114411
+ } catch {
114412
+ continue;
114413
+ }
114414
+ const o = raw;
114415
+ if (o && typeof o === "object" && typeof o.v === "number") {
114416
+ if (o.v !== 1) throw new Error(`The remote's Centralu speaks connection format ${o.v}; update Centralu on this computer`);
114417
+ if (typeof o.port !== "number" || typeof o.token !== "string" || typeof o.protocolVersion !== "number") {
114418
+ throw new Error("The remote answered with an incomplete connection line");
114419
+ }
114420
+ const install = parseInstall(o.install);
114421
+ const autostart = parseAutostart(o.autostart);
114422
+ return {
114423
+ v: 1,
114424
+ port: o.port,
114425
+ token: o.token,
114426
+ version: typeof o.version === "string" ? o.version : "unknown",
114427
+ protocolVersion: o.protocolVersion,
114428
+ dataDir: typeof o.dataDir === "string" ? o.dataDir : "",
114429
+ hostRunning: o.hostRunning === true,
114430
+ ...install ? { install } : {},
114431
+ ...autostart ? { autostart } : {}
114432
+ };
114433
+ }
114434
+ }
114435
+ throw new Error("The remote did not print a connection line; its Centralu may predate `centralu serve` (update it there: npm i -g centralu)");
114436
+ }
114437
+ function parseInstall(raw) {
114438
+ if (!raw || typeof raw !== "object") return null;
114439
+ const o = raw;
114440
+ const pointer = (v) => {
114441
+ const p = v;
114442
+ return p && typeof p.version === "string" && typeof p.node === "string" ? { version: p.version, node: p.node } : null;
114443
+ };
114444
+ return { managed: o.managed === true, current: pointer(o.current), previous: pointer(o.previous), node: typeof o.node === "string" ? o.node : null };
114445
+ }
114446
+ function parseAutostart(raw) {
114447
+ if (!raw || typeof raw !== "object") return null;
114448
+ const o = raw;
114449
+ if (typeof o.on !== "boolean") return null;
114450
+ const how = o.how === "systemd" || o.how === "task" ? o.how : null;
114451
+ return { on: o.on && how !== null, how, linger: typeof o.linger === "boolean" ? o.linger : null };
114452
+ }
114453
+ function parseDetachLine(stdout) {
114454
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
114455
+ for (let i = lines.length - 1; i >= 0; i--) {
114456
+ let raw;
114457
+ try {
114458
+ raw = JSON.parse(lines[i]);
114459
+ } catch {
114460
+ continue;
114461
+ }
114462
+ const d2 = raw?.detach;
114463
+ if (!d2 || typeof d2 !== "object") continue;
114464
+ if (d2.ok === true) return { ok: true, line: parseConnectionLine(lines[i]), how: typeof d2.how === "string" ? d2.how : null };
114465
+ return { ok: false, reason: typeof d2.reason === "string" ? d2.reason : "failed", message: typeof d2.message === "string" ? d2.message : "centralu serve --detach failed" };
114466
+ }
114467
+ throw new Error("The remote did not say whether it started");
114468
+ }
114469
+ var WSL_DISTRO_RE = /^[A-Za-z0-9._-]{1,64}$/;
114470
+ function powershell(script) {
114471
+ const full = `[Console]::OutputEncoding=[Text.Encoding]::UTF8
114472
+ ${script}`;
114473
+ return `powershell -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(full, "utf16le").toString("base64")}`;
114474
+ }
114475
+ var WINDOWS_INLINE_MAX = 7900;
114476
+ var PS_FROM_STDIN = "$s=[Console]::In.ReadToEnd(); $c=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(($s -replace '[^A-Za-z0-9+/=]',''))); & ([ScriptBlock]::Create($c))";
114477
+ function powershellScript(script) {
114478
+ const inline = powershell(script);
114479
+ if (inline.length <= WINDOWS_INLINE_MAX) return inline;
114480
+ const full = `[Console]::OutputEncoding=[Text.Encoding]::UTF8
114481
+ ${script}`;
114482
+ return { command: powershell(PS_FROM_STDIN), stdin: Buffer.from(full, "utf8").toString("base64") };
114483
+ }
114484
+ function wslKeepAlive(spec) {
114485
+ if (spec?.shell !== "wsl") return null;
114486
+ const distro = spec.wslDistro ?? "";
114487
+ if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
114488
+ return `wsl.exe -d ${distro} --exec sleep infinity`;
114489
+ }
114490
+ var MANAGED_SH = 'm="${CC_DATA_DIR:-$HOME/.centralu}/remote/bin/centralu"';
114491
+ var MANAGED_PS = "$d = if ($env:CC_DATA_DIR) { $env:CC_DATA_DIR } else { Join-Path $env:USERPROFILE '.centralu' }; $m = Join-Path $d 'remote\\bin\\centralu.cmd'";
114492
+ var NOT_FOUND2 = "CENTRALU-NOT-FOUND";
114493
+ function connectionCommand(spec) {
114494
+ return remoteCommand(spec, "serve --connection");
114495
+ }
114496
+ function detachCommand(spec) {
114497
+ return remoteCommand(spec, "serve --detach");
114498
+ }
114499
+ function stopCommand(spec) {
114500
+ return remoteCommand(spec, "serve --stop");
114501
+ }
114502
+ function serveCommand(spec) {
114503
+ return remoteCommand(spec, "serve");
114504
+ }
114505
+ function autostartCommand(spec, action) {
114506
+ return remoteCommand(spec, `serve --autostart ${action}`);
114507
+ }
114508
+ function remoteCommand(spec, args) {
114509
+ const custom2 = spec.command?.trim() || null;
114510
+ if (spec.shell === "powershell") {
114511
+ if (custom2) return powershell(`${custom2} ${args}`);
114512
+ return powershell(
114513
+ `${MANAGED_PS}; if (Test-Path $m) { & $m ${args} } elseif (Get-Command centralu -ErrorAction SilentlyContinue) { & centralu ${args} } else { $l = Join-Path $env:USERPROFILE '.centralu\\bin\\centralu.cmd'; if (Test-Path $l) { & $l ${args} } else { '${NOT_FOUND2}' } }`
114514
+ );
114515
+ }
114516
+ const lookup = `${MANAGED_SH}; if [ -x "$m" ]; then exec "$m" ${args}; elif command -v centralu >/dev/null 2>&1; then exec centralu ${args}; elif [ -x "$HOME/.centralu/bin/centralu" ]; then exec "$HOME/.centralu/bin/centralu" ${args}; else echo ${NOT_FOUND2}; exit 127; fi`;
114517
+ if (spec.shell === "posix") return custom2 ? `${custom2} ${args}` : `sh -c '${lookup}'`;
114518
+ const distro = spec.wslDistro ?? "";
114519
+ if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
114520
+ const script = custom2 ? `${custom2} ${args}` : `PATH=$(printf %s "$PATH" | tr : '\\n' | grep -v '^/mnt/' | paste -sd: -); ${lookup}`;
114521
+ const b64 = Buffer.from(script, "utf8").toString("base64");
114522
+ return powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
114523
+ }
114524
+ function remoteScript(spec, script) {
114525
+ if (spec.shell === "powershell") return powershellScript(script.ps);
114526
+ const b64 = Buffer.from(script.sh, "utf8").toString("base64");
114527
+ if (spec.shell === "posix") return `printf %s ${b64} | base64 -d | sh`;
114528
+ const distro = spec.wslDistro ?? "";
114529
+ if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
114530
+ const inline = powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
114531
+ if (inline.length <= WINDOWS_INLINE_MAX) return inline;
114532
+ return { command: powershell(`$s=[Console]::In.ReadToEnd(); $s | wsl.exe -d '${distro}' -- bash -lc 'base64 -di | bash -l'`), stdin: b64 };
114533
+ }
114534
+ var SshTunnel = class {
114535
+ constructor(opts) {
114536
+ this.opts = opts;
114537
+ if (!opts.target.trim() || opts.target.startsWith("-")) throw new Error("Not an ssh target");
114538
+ }
114539
+ opts;
114540
+ child = null;
114541
+ /** The asking ssh runs, while they run */
114542
+ asking = /* @__PURE__ */ new Set();
114543
+ downListeners = [];
114544
+ closing = false;
114545
+ /**
114546
+ * Forwards this tunnel ended on purpose (a reopen, a forward that did not come up in time): their
114547
+ * exit is not the link going down. Reported as down, a reopen of a live link made the link retry,
114548
+ * and that retry's reopen ended the new forward again, every few seconds without end
114549
+ */
114550
+ ended = /* @__PURE__ */ new WeakSet();
114551
+ /** The remote cannot start a host outside a session (WMI blocked): the forward runs it (`startHost`) */
114552
+ linkBound = false;
114553
+ async open() {
114554
+ this.closing = false;
114555
+ await this.stopChild();
114556
+ this.ifClosed();
114557
+ const line = await this.connectionLine().catch((err) => {
114558
+ this.ifClosed();
114559
+ throw err;
114560
+ });
114561
+ this.ifClosed();
114562
+ if (!line.hostRunning && !this.linkBound) {
114563
+ return { url: "", token: line.token, line, localPort: 0 };
114564
+ }
114565
+ const hosting = !line.hostRunning;
114566
+ let localPort = await portFree(line.port) ? line.port : await freePort();
114567
+ try {
114568
+ await this.forward(localPort, line.port, hosting);
114569
+ } catch (err) {
114570
+ if (localPort !== line.port) throw err;
114571
+ this.ifClosed();
114572
+ localPort = await freePort();
114573
+ await this.forward(localPort, line.port, hosting);
114574
+ }
114575
+ if (this.closing) {
114576
+ await this.stopChild();
114577
+ this.ifClosed();
114578
+ }
114579
+ const served = hosting ? await this.untilServing() : line;
114580
+ return { url: `ws://127.0.0.1:${localPort}`, token: served.token, line: served, localPort };
114581
+ }
114582
+ /**
114583
+ * Where Centralu runs, for a sentence: "on <target>", or "in WSL (<distro>) on <target>" (#482).
114584
+ * A Windows laptop can be linked twice, as Windows and as a distro, and both rows used to say
114585
+ * "on <target>" alike.
114586
+ */
114587
+ get where() {
114588
+ const remote = this.opts.remote;
114589
+ if (remote?.shell !== "wsl") return `on ${this.opts.target}`;
114590
+ return `in WSL (${remote.wslDistro || "its default distro"}) on ${this.opts.target}`;
114591
+ }
114592
+ /**
114593
+ * Starts the remote's host with `centralu serve --detach` (plan §10.4, owner decision 7): it then
114594
+ * runs outside any ssh session and outlives this link. When the remote says WMI process creation
114595
+ * is blocked there (decision 3), every `open` from then on runs `centralu serve` as the forward's
114596
+ * own remote command instead, so the host lives as long as the link.
114597
+ */
114598
+ async startHost() {
114599
+ this.ifClosed();
114600
+ const remote = this.opts.remote ?? { shell: "posix" };
114601
+ const r = await this.run([...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target, detachCommand(remote)]);
114602
+ this.ifClosed();
114603
+ if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
114604
+ if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
114605
+ throw new Error(`Centralu is not installed ${this.where} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
114606
+ }
114607
+ let answer;
114608
+ try {
114609
+ answer = parseDetachLine(r.stdout);
114610
+ } catch (err) {
114611
+ if (/unknown option for serve: --detach/.test(r.stderr)) {
114612
+ throw new Error(`Centralu on ${this.opts.target} is too old to be started from here: run \`centralu serve\` there, or update it (npm i -g centralu)`);
114613
+ }
114614
+ const why = lastLine(r.stderr);
114615
+ throw new Error(`${err.message}${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
114616
+ }
114617
+ if (answer.ok) {
114618
+ this.linkBound = false;
114619
+ return { how: "detached", note: null };
114620
+ }
114621
+ if (answer.reason === "wmi_blocked") {
114622
+ this.linkBound = true;
114623
+ return { how: "link_bound", note: answer.message };
114624
+ }
114625
+ throw new Error(answer.message);
114626
+ }
114627
+ /**
114628
+ * Runs one command there the way the connection line is asked for (batch mode, `--` before the
114629
+ * target), and answers what it printed. For the installer (install.ts): the command is already in
114630
+ * the remote shell's terms. `close()` ends it like any asking ssh
114631
+ */
114632
+ async exec(command) {
114633
+ this.ifClosed();
114634
+ const [line, stdin] = typeof command === "string" ? [command, void 0] : [command.command, command.stdin];
114635
+ const r = await this.run([...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target, line], stdin);
114636
+ if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
114637
+ return r;
114638
+ }
114639
+ /** The connection line again until the link-bound host answers, or the forward that runs it ends */
114640
+ async untilServing() {
114641
+ const deadline = Date.now() + (this.opts.startTimeoutMs ?? 3e4);
114642
+ for (; ; ) {
114643
+ this.ifClosed();
114644
+ if (!this.child) throw new Error(`The ssh session that runs Centralu on ${this.opts.target} ended before it served`);
114645
+ const line = await this.connectionLine();
114646
+ if (line.hostRunning) return line;
114647
+ if (Date.now() > deadline) {
114648
+ await this.stopChild();
114649
+ throw new Error(`Centralu on ${this.opts.target} did not start serving in time`);
114650
+ }
114651
+ await new Promise((r) => setTimeout(r, 500));
114652
+ }
114653
+ }
114654
+ onDown(listener) {
114655
+ this.downListeners.push(listener);
114656
+ }
114657
+ async close() {
114658
+ this.closing = true;
114659
+ for (const c of this.asking) c.kill("SIGTERM");
114660
+ await this.stopChild();
114661
+ }
114662
+ ifClosed() {
114663
+ if (this.closing) throw new Error(`The link to ${this.opts.target} was closed`);
114664
+ }
114665
+ run(args, stdin) {
114666
+ return new Promise((resolve10, reject) => {
114667
+ const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"] });
114668
+ if (stdin !== void 0) {
114669
+ child.stdin.on("error", () => {
114670
+ });
114671
+ child.stdin.end(stdin);
114672
+ }
114673
+ this.asking.add(child);
114674
+ child.once("exit", () => this.asking.delete(child));
114675
+ let stdout = "";
114676
+ let stderr = "";
114677
+ child.stdout.on("data", (d2) => stdout += String(d2));
114678
+ child.stderr.on("data", (d2) => stderr += String(d2));
114679
+ child.once("error", (e) => {
114680
+ this.asking.delete(child);
114681
+ reject(e);
114682
+ });
114683
+ child.once("close", (code2) => resolve10({ code: code2, stdout, stderr }));
114684
+ });
114685
+ }
114686
+ config() {
114687
+ return this.opts.configFile ? ["-F", this.opts.configFile] : [];
114688
+ }
114689
+ async connectionLine() {
114690
+ const remote = this.opts.remote ?? { shell: "posix" };
114691
+ const base2 = [...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target];
114692
+ const r = await this.run([...base2, connectionCommand(remote)]);
114693
+ if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
114694
+ if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
114695
+ throw new Error(`Centralu is not installed ${this.where} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
114696
+ }
114697
+ try {
114698
+ return parseConnectionLine(r.stdout);
114699
+ } catch (err) {
114700
+ const why = lastLine(r.stderr);
114701
+ throw new Error(`${err.message}${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
114702
+ }
114703
+ }
114704
+ forward(localPort, remotePort, hosting = false) {
114705
+ this.ifClosed();
114706
+ const keepAlive = hosting ? serveCommand(this.opts.remote ?? { shell: "posix" }) : wslKeepAlive(this.opts.remote);
114707
+ const args = [
114708
+ ...this.config(),
114709
+ ...keepAlive ? ["-T"] : ["-N"],
114710
+ "-o",
114711
+ "BatchMode=yes",
114712
+ "-o",
114713
+ "ExitOnForwardFailure=yes",
114714
+ "-o",
114715
+ "ServerAliveInterval=15",
114716
+ "-o",
114717
+ "ServerAliveCountMax=3",
114718
+ "-o",
114719
+ "GatewayPorts=no",
114720
+ "-L",
114721
+ `127.0.0.1:${localPort}:127.0.0.1:${remotePort}`,
114722
+ "--",
114723
+ this.opts.target,
114724
+ ...keepAlive ? [keepAlive] : []
114725
+ ];
114726
+ const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "ignore", "pipe"] });
114727
+ this.child = child;
114728
+ let stderr = "";
114729
+ child.stderr.on("data", (d2) => stderr += String(d2));
114730
+ return new Promise((resolve10, reject) => {
114731
+ let settled = false;
114732
+ const deadline = Date.now() + (this.opts.forwardTimeoutMs ?? 2e4);
114733
+ const fail4 = (message) => {
114734
+ if (settled) return;
114735
+ settled = true;
114736
+ reject(new Error(message));
114737
+ };
114738
+ child.once("error", (e) => fail4(`Could not start ssh: ${e.message}`));
114739
+ child.once("exit", (code2, signal) => {
114740
+ if (this.child === child) this.child = null;
114741
+ if (!settled) return fail4(`The ssh forward to ${this.opts.target} ended (${signal ?? code2}): ${lastLine(stderr) || "no reason given"}`);
114742
+ if (!this.closing && !this.ended.has(child)) for (const l of this.downListeners) l(`ssh exited (${signal ?? code2})${stderr ? `: ${lastLine(stderr)}` : ""}`);
114743
+ });
114744
+ const poll = async () => {
114745
+ while (!settled) {
114746
+ if (await accepts(localPort)) {
114747
+ settled = true;
114748
+ this.opts.log?.(`[links] forward up: 127.0.0.1:${localPort} -> ${this.opts.target}:127.0.0.1:${remotePort}`);
114749
+ return resolve10();
114750
+ }
114751
+ if (Date.now() > deadline) {
114752
+ this.ended.add(child);
114753
+ child.kill("SIGTERM");
114754
+ return fail4(`The ssh forward to ${this.opts.target} did not come up in time`);
114755
+ }
114756
+ await new Promise((r) => setTimeout(r, 100));
114757
+ }
114758
+ };
114759
+ void poll();
114760
+ });
114761
+ }
114762
+ async stopChild() {
114763
+ const child = this.child;
114764
+ this.child = null;
114765
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
114766
+ this.ended.add(child);
114767
+ await new Promise((resolve10) => {
114768
+ child.once("exit", () => resolve10());
114769
+ child.kill("SIGTERM");
114770
+ setTimeout(() => {
114771
+ child.kill("SIGKILL");
114772
+ resolve10();
114773
+ }, 2e3).unref();
114774
+ });
114775
+ }
114776
+ };
114777
+ function lastLine(s) {
114778
+ const lines = s.replace(/#< CLIXML[\s\S]*$/, "").split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("**") && !/^Node\.js v\d/.test(l) && !/^at /.test(l));
114779
+ return [...lines].reverse().find((l) => /error|denied|refused|timed out|not found|cannot|could not/i.test(l)) ?? lines.at(-1) ?? "";
114780
+ }
114781
+ function accepts(port2) {
114782
+ return new Promise((resolve10) => {
114783
+ const s = createConnection2({ host: "127.0.0.1", port: port2 });
114784
+ s.once("connect", () => {
114785
+ s.destroy();
114786
+ resolve10(true);
114787
+ });
114788
+ s.once("error", () => resolve10(false));
114789
+ });
114790
+ }
114791
+ function portFree(port2) {
114792
+ return new Promise((resolve10) => {
114793
+ const probe = createServer3();
114794
+ probe.once("error", () => resolve10(false));
114795
+ probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve10(true)));
114796
+ });
114797
+ }
114798
+ function freePort() {
114799
+ return new Promise((resolve10, reject) => {
114800
+ const probe = createServer3();
114801
+ probe.once("error", reject);
114802
+ probe.listen(0, "127.0.0.1", () => {
114803
+ const { port: port2 } = probe.address();
114804
+ probe.close(() => resolve10(port2));
114805
+ });
114806
+ });
114807
+ }
114808
+
114809
+ // packages/agent-host/src/links/install.ts
114810
+ var PREFLIGHT = "CENTRALU-PREFLIGHT";
114811
+ var STEP = "CENTRALU-INSTALL";
114812
+ var MIN_GLIBC = [2, 34];
114813
+ var NEED_MB = { linux: 600, win32: 300 };
114814
+ var DATA_SH = 'd="${CC_DATA_DIR:-$HOME/.centralu}"; r="$d/remote"';
114815
+ var DATA_PS = "$d = if ($env:CC_DATA_DIR) { $env:CC_DATA_DIR } else { Join-Path $env:USERPROFILE '.centralu' }; $r = Join-Path $d 'remote'";
114816
+ var PREFLIGHT_SH = `${DATA_SH}
114817
+ p="$d"; while [ ! -d "$p" ]; do p=$(dirname "$p"); done
114818
+ musl=0; if ldd --version 2>&1 | grep -qi musl; then musl=1; fi
114819
+ for f in /lib/ld-musl-*; do [ -e "$f" ] && musl=1; done
114820
+ has() { command -v "$1" >/dev/null 2>&1 && echo 1 || echo 0; }
114821
+ fetch=; if command -v curl >/dev/null 2>&1; then fetch=curl; elif command -v wget >/dev/null 2>&1; then fetch=wget; fi
114822
+ sha=; if command -v sha256sum >/dev/null 2>&1; then sha=sha256sum; elif command -v shasum >/dev/null 2>&1; then sha=shasum; fi
114823
+ echo "${PREFLIGHT} os=$(uname -s) arch=$(uname -m) glibc=$(getconf GNU_LIBC_VERSION 2>/dev/null | sed -n 's/^glibc //p') musl=$musl freeKb=$(df -Pk "$p" 2>/dev/null | awk 'NR==2 {print $4}') tar=$(has tar) gzip=$(has gzip) fetch=$fetch sha=$sha"
114824
+ `;
114825
+ var PREFLIGHT_PS = `${DATA_PS}
114826
+ $arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
114827
+ $p = $d; while (-not (Test-Path $p)) { $p = Split-Path $p }
114828
+ $free = try { [math]::Floor((New-Object IO.DriveInfo ([IO.Path]::GetPathRoot((Resolve-Path $p).Path))).AvailableFreeSpace / 1024) } catch { '' }
114829
+ $tar = [int](Test-Path (Join-Path $env:SystemRoot 'System32\\tar.exe'))
114830
+ $fetch = if (Test-Path (Join-Path $env:SystemRoot 'System32\\curl.exe')) { 'curl' } else { 'iwr' }
114831
+ "${PREFLIGHT} os=Windows arch=$arch glibc= musl=0 freeKb=$free tar=$tar gzip=1 fetch=$fetch sha=Get-FileHash"
114832
+ `;
114833
+ function preflightCommand(spec) {
114834
+ return remoteScript(spec, { sh: PREFLIGHT_SH, ps: PREFLIGHT_PS });
114835
+ }
114836
+ function parsePreflight(stdout) {
114837
+ const line = stdout.split(/\r?\n/).reverse().find((l) => l.startsWith(`${PREFLIGHT} `));
114838
+ if (!line) throw new Error("The remote did not answer the install check");
114839
+ const kv2 = new Map(line.slice(PREFLIGHT.length + 1).trim().split(/\s+/).map((w) => [w.slice(0, w.indexOf("=")), w.slice(w.indexOf("=") + 1)]));
114840
+ const word = (k2) => kv2.get(k2) || null;
114841
+ const free = Number(kv2.get("freeKb"));
114842
+ return {
114843
+ os: word("os") ?? "",
114844
+ arch: word("arch") ?? "",
114845
+ glibc: word("glibc"),
114846
+ musl: kv2.get("musl") === "1",
114847
+ freeKb: kv2.get("freeKb") && Number.isFinite(free) ? free : null,
114848
+ tar: kv2.get("tar") === "1",
114849
+ gzip: kv2.get("gzip") === "1",
114850
+ fetch: word("fetch"),
114851
+ sha: word("sha")
114852
+ };
114853
+ }
114854
+ function preflight(f, runtime) {
114855
+ const no2 = (reason) => ({ ok: false, reason });
114856
+ let platform;
114857
+ if (f.os === "Linux") {
114858
+ if (f.musl) return no2("This Linux uses musl (Alpine and the like); Centralu runs on glibc 2.34 or later only");
114859
+ const arch = f.arch === "x86_64" || f.arch === "amd64" ? "x64" : f.arch === "aarch64" || f.arch === "arm64" ? "arm64" : null;
114860
+ if (!arch) return no2(`Centralu has no build for Linux on ${f.arch || "this processor"}`);
114861
+ const [maj = 0, min = 0] = (f.glibc ?? "").split(".").map(Number);
114862
+ if (!f.glibc || maj < MIN_GLIBC[0] || maj === MIN_GLIBC[0] && min < MIN_GLIBC[1]) {
114863
+ return no2(`Centralu needs glibc ${MIN_GLIBC.join(".")} or later (Ubuntu 22.04, Debian 12, RHEL 9); this machine has ${f.glibc ? `glibc ${f.glibc}` : "none that it reports"}`);
114864
+ }
114865
+ platform = `linux-${arch}`;
114866
+ } else if (f.os === "Windows") {
114867
+ if (f.arch !== "AMD64") return no2(`Centralu has no build for Windows on ${f.arch || "this processor"}`);
114868
+ platform = "win32-x64";
114869
+ } else {
114870
+ return no2(`Installing from here is for Linux and Windows; on ${f.os || "this system"}, install Centralu there (npm i -g centralu)`);
114871
+ }
114872
+ if (!runtime.node.archives[platform]) return no2(`This Centralu pins no Node for ${platform}`);
114873
+ if (!f.tar) return no2(platform === "win32-x64" ? "tar.exe is missing (it comes with Windows 10 1803 and later)" : "tar is missing there; install it and try again");
114874
+ if (!f.gzip) return no2("gzip is missing there; install it and try again");
114875
+ if (!f.fetch) return no2("Neither curl nor wget is installed there; install one and try again");
114876
+ if (!f.sha) return no2("No SHA-256 tool (sha256sum) is installed there; install coreutils and try again");
114877
+ const need = NEED_MB[platform === "win32-x64" ? "win32" : "linux"];
114878
+ if (f.freeKb !== null && f.freeKb < need * 1024) return no2(`Installing needs about ${need} MB free there; ${Math.floor(f.freeKb / 1024)} MB is`);
114879
+ return { ok: true, platform };
114880
+ }
114881
+ var PLAIN = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/;
114882
+ var SAFE_URL = /^https?:\/\/[0-9A-Za-z.:-]+(\/[0-9A-Za-z._~%/-]*)?$/;
114883
+ function nodeCommand(spec, n) {
114884
+ if (!PLAIN.test(n.version) || !SAFE_URL.test(n.url) || !/^[0-9a-f]{64}$/.test(n.sha256)) throw new Error("Not a Node archive to install");
114885
+ const sh = `${DATA_SH}; V="${n.version}"; n="$r/node/v$V"
114886
+ say() { echo "${STEP} $*"; }
114887
+ if [ -x "$n/bin/node" ] && "$n/bin/node" --version >/dev/null 2>&1; then say node ok; exit 0; fi
114888
+ t="$r/.partial-node-$$"; trap 'rm -rf "$t"' EXIT
114889
+ mkdir -p "$t/x" || { say fail mkdir; exit 1; }
114890
+ if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 2 -o "$t/a" "${n.url}" || { say fail download node; exit 1; }
114891
+ else wget -q -O "$t/a" "${n.url}" || { say fail download node; exit 1; }; fi
114892
+ if command -v sha256sum >/dev/null 2>&1; then h=$(sha256sum "$t/a" | cut -d" " -f1); else h=$(shasum -a 256 "$t/a" | cut -d" " -f1); fi
114893
+ [ "$h" = "${n.sha256}" ] || { say fail node_hash "$h"; exit 1; }
114894
+ tar -xzf "$t/a" -C "$t/x" --strip-components 1 || { say fail unpack node; exit 1; }
114895
+ mkdir -p "$t/keep/bin" && mv "$t/x/bin/node" "$t/keep/bin/node" || { say fail unpack node; exit 1; }
114896
+ [ ! -f "$t/x/LICENSE" ] || mv "$t/x/LICENSE" "$t/keep/LICENSE"
114897
+ "$t/keep/bin/node" --version >/dev/null 2>&1 || { say fail node_runs; exit 1; }
114898
+ mkdir -p "$r/node" && rm -rf "$n" && mv "$t/keep" "$n" || { say fail place node; exit 1; }
114899
+ say node ok
114900
+ `;
114901
+ const ps2 = `$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue'
114902
+ ${DATA_PS}; $n = Join-Path $r 'node\\v${n.version}'
114903
+ function Say($s) { "${STEP} $s" }
114904
+ if ((Test-Path (Join-Path $n 'node.exe'))) { Say 'node ok'; return }
114905
+ $t = Join-Path $r ".partial-node-$PID"
114906
+ try {
114907
+ New-Item -ItemType Directory -Force (Join-Path $t 'x') | Out-Null
114908
+ $a = Join-Path $t 'a.zip'
114909
+ $curl = Join-Path $env:SystemRoot 'System32\\curl.exe'
114910
+ if (Test-Path $curl) { & $curl -fsSL --retry 2 -o $a '${n.url}'; if ($LASTEXITCODE -ne 0) { throw 'download node' } }
114911
+ else { try { Invoke-WebRequest -UseBasicParsing '${n.url}' -OutFile $a } catch { throw 'download node' } }
114912
+ $h = (Get-FileHash -Algorithm SHA256 $a).Hash.ToLower()
114913
+ if ($h -ne '${n.sha256}') { throw "node_hash $h" }
114914
+ & (Join-Path $env:SystemRoot 'System32\\tar.exe') -xf $a -C (Join-Path $t 'x') --strip-components 1
114915
+ if ($LASTEXITCODE -ne 0) { throw 'unpack node' }
114916
+ $k = Join-Path $t 'keep'; New-Item -ItemType Directory -Force $k | Out-Null
114917
+ Move-Item (Join-Path $t 'x\\node.exe') $k
114918
+ if (Test-Path (Join-Path $t 'x\\LICENSE')) { Move-Item (Join-Path $t 'x\\LICENSE') $k }
114919
+ & (Join-Path $k 'node.exe') --version | Out-Null
114920
+ if ($LASTEXITCODE -ne 0) { throw 'node_runs' }
114921
+ New-Item -ItemType Directory -Force (Join-Path $r 'node') | Out-Null
114922
+ if (Test-Path $n) { Remove-Item -Recurse -Force $n }
114923
+ Move-Item $k $n
114924
+ Say 'node ok'
114925
+ } catch {
114926
+ $m = if ($_.Exception.Message) { $_.Exception.Message } else { "$_" }
114927
+ Say "fail $($m -replace '\\s+', ' ')"
114928
+ } finally {
114929
+ if (Test-Path $t) { Remove-Item -Recurse -Force $t -ErrorAction SilentlyContinue }
114930
+ }
114931
+ `;
114932
+ return remoteScript(spec, { sh, ps: ps2 });
114933
+ }
114934
+ function installCommand(spec, script, params) {
114935
+ if (!PLAIN.test(params.version) || !PLAIN.test(params.node)) throw new Error("Not a version to install");
114936
+ const gz2 = gzipSync(Buffer.from(script, "utf8"), { level: 9 }).toString("base64");
114937
+ const arg = Buffer.from(JSON.stringify(params), "utf8").toString("base64");
114938
+ const sh = `${DATA_SH}; f="$r/.install-$$.mjs"
114939
+ mkdir -p "$r" && printf %s "${gz2}" | base64 -d | gzip -dc > "$f" || { echo "${STEP} fail script"; exit 1; }
114940
+ "$r/node/v${params.node}/bin/node" "$f" "${arg}"; c=$?; rm -f "$f"; exit $c
114941
+ `;
114942
+ const ps2 = `${DATA_PS}; $f = Join-Path $r ".install-$PID.mjs"
114943
+ try {
114944
+ New-Item -ItemType Directory -Force $r | Out-Null
114945
+ $z = New-Object IO.Compression.GZipStream((New-Object IO.MemoryStream(,[Convert]::FromBase64String('${gz2}'))), [IO.Compression.CompressionMode]::Decompress)
114946
+ $o = [IO.File]::Create($f); $z.CopyTo($o); $o.Close(); $z.Close()
114947
+ } catch { "${STEP} fail script"; return }
114948
+ & (Join-Path $r 'node\\v${params.node}\\node.exe') $f '${arg}'
114949
+ Remove-Item -Force $f -ErrorAction SilentlyContinue
114950
+ `;
114951
+ return remoteScript(spec, { sh, ps: ps2 });
114952
+ }
114953
+ function failure2(target, code2, detail) {
114954
+ switch (code2) {
114955
+ case "node_hash":
114956
+ return `The Node downloaded on ${target} does not match the SHA-256 this Centralu release pinned; nothing was installed`;
114957
+ case "integrity":
114958
+ return `${detail || "A package"} downloaded on ${target} does not match the integrity the npm registry signed; nothing was installed`;
114959
+ case "download":
114960
+ return `${target} could not download ${detail || "what it needs"}; check that it can reach nodejs.org and registry.npmjs.org`;
114961
+ case "busy":
114962
+ return `Another install is running on ${target}`;
114963
+ case "no_host":
114964
+ return `The package for ${detail} carries no host; this version cannot be installed on ${target}`;
114965
+ case "no_launcher":
114966
+ return `Centralu ${detail} predates installs from the app; install a newer version`;
114967
+ case "node_runs":
114968
+ return `The Node downloaded on ${target} does not run there`;
114969
+ case "missing":
114970
+ return `Centralu ${detail} is no longer installed on ${target}`;
114971
+ case "remove":
114972
+ return `What this computer installed on ${target} could not be removed: ${detail || "no reason given"}`;
114973
+ default:
114974
+ return `Installing on ${target} failed (${[code2, detail].filter(Boolean).join(": ")})`;
114975
+ }
114976
+ }
114977
+ function readStep(run, ok, target) {
114978
+ const line = run.stdout.split(/\r?\n/).reverse().find((l) => l.startsWith(`${STEP} `));
114979
+ if (!line) {
114980
+ const why = lastLine(run.stderr);
114981
+ throw new Error(`Installing on ${target} stopped without saying why${why ? `: ${why}` : run.code ? ` (exit ${run.code})` : ""}`);
114982
+ }
114983
+ const rest = line.slice(STEP.length + 1).trim();
114984
+ if (rest.startsWith("fail")) {
114985
+ const [, code2 = "error", ...detail] = rest.split(" ");
114986
+ throw new Error(failure2(target, code2, detail.join(" ")));
114987
+ }
114988
+ if (!rest.startsWith(ok)) throw new Error(`Installing on ${target} answered something unexpected: ${rest.slice(0, 120)}`);
114989
+ return rest.slice(ok.length).trim();
114990
+ }
114991
+ var hostOnlyPackage = (platform) => `@centralu/host-${platform}`;
114992
+ async function hostPackage(platform, version2, registry2) {
114993
+ try {
114994
+ return await verifiedPackage(hostOnlyPackage(platform), version2, registry2);
114995
+ } catch (err) {
114996
+ if (err instanceof NotPublished) return verifiedPackage(`@centralu/${platform}`, version2, registry2);
114997
+ throw err;
114998
+ }
114999
+ }
115000
+ async function installRemote(o) {
115001
+ o.onStep?.("preflight");
115002
+ const pf2 = preflight(parsePreflight((await o.exec(preflightCommand(o.spec))).stdout), o.runtime);
115003
+ if (!pf2.ok) throw new Error(`Cannot install on ${o.target}: ${pf2.reason}`);
115004
+ o.onStep?.("registry");
115005
+ const packages = await Promise.all([verifiedPackage("centralu", o.version, o.registry), hostPackage(pf2.platform, o.version, o.registry)]);
115006
+ const archive = o.runtime.node.archives[pf2.platform];
115007
+ const nodeVersion = o.runtime.node.version;
115008
+ o.onStep?.("node");
115009
+ const url2 = `${(o.nodeDist ?? "https://nodejs.org/dist").replace(/\/+$/, "")}/v${nodeVersion}/${archive.file}`;
115010
+ readStep(await o.exec(nodeCommand(o.spec, { version: nodeVersion, url: url2, sha256: archive.sha256 })), "node ok", o.target);
115011
+ o.onStep?.("centralu");
115012
+ const params = { version: o.version, node: nodeVersion, platform: pf2.platform, hub: `${hostname3()} (Centralu ${o.version})`, packages: packages.map(({ name, tarball, integrity }) => ({ name, tarball, integrity })), ...o.activate === false ? { activate: false } : {} };
115013
+ const done = readStep(await o.exec(installCommand(o.spec, o.script, params)), "done", o.target);
115014
+ const r = JSON.parse(done);
115015
+ return { current: r.current, previous: r.previous ?? null, removed: r.removed ?? [], left: r.left ?? [], ...r.installed ? { installed: r.installed } : {} };
115016
+ }
115017
+ function bundled(name, source) {
115018
+ for (const url2 of [new URL(`./${name}`, import.meta.url), new URL(source, import.meta.url)]) {
115019
+ const file2 = fileURLToPath6(url2);
115020
+ if (existsSync20(file2)) return readFileSync13(file2, "utf8");
115021
+ }
115022
+ throw new Error(`${name} is missing from this host`);
115023
+ }
115024
+ function installScript() {
115025
+ return bundled("remote-install.mjs", "./remote-install.mjs");
115026
+ }
115027
+ function remoteRuntime() {
115028
+ return JSON.parse(bundled("remote-runtime.json", "../../../../packaging/remote-runtime.json"));
115029
+ }
115030
+
115031
+ // packages/agent-host/src/links/autostart.ts
115032
+ function parseAutostartLine(stdout) {
115033
+ for (const line of stdout.split(/\r?\n/).reverse()) {
115034
+ const t = line.trim();
115035
+ if (!t.startsWith("{")) continue;
115036
+ try {
115037
+ const o = JSON.parse(t);
115038
+ const state = o.v === 1 ? parseAutostart(o.autostart) : null;
115039
+ if (state && typeof o.autostart?.ok === "boolean") {
115040
+ return { ok: o.autostart.ok, state, message: typeof o.autostart.message === "string" ? o.autostart.message : null };
115041
+ }
115042
+ } catch {
115043
+ }
115044
+ }
115045
+ return null;
115046
+ }
115047
+ var FAILED = {
115048
+ on: "could not be set to start at boot",
115049
+ off: "could not stop starting at boot",
115050
+ status: "could not say whether it starts at boot"
115051
+ };
115052
+ async function autostartRemote(c, action) {
115053
+ const r = await c.exec(autostartCommand(c.spec, action));
115054
+ const unasked = (reason, message) => Object.assign(new Error(message), { reason });
115055
+ if (r.stdout.includes(NOT_FOUND2)) throw unasked("not_found", `Centralu is not installed on ${c.target}`);
115056
+ const answer = parseAutostartLine(r.stdout);
115057
+ if (!answer) {
115058
+ if (/unknown option for serve: --autostart/.test(r.stderr)) {
115059
+ throw unasked("too_old", `The Centralu on ${c.target} is too old to start at boot; update it first`);
115060
+ }
115061
+ const why = lastLine(r.stderr);
115062
+ throw new Error(`Centralu on ${c.target} gave no answer about starting at boot${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
115063
+ }
115064
+ if (!answer.ok) throw new Error(`Centralu on ${c.target} ${FAILED[action]}: ${answer.message ?? "no reason given"}`);
115065
+ return answer.state;
115066
+ }
115067
+ async function removeAutostart(c) {
115068
+ try {
115069
+ await autostartRemote(c, "off");
115070
+ return true;
115071
+ } catch (err) {
115072
+ if (err.reason) return false;
115073
+ throw err;
115074
+ }
115075
+ }
115076
+
115077
+ // packages/agent-host/src/links/update.ts
115078
+ var CHECK_MS = 3e4;
115079
+ function parseStopLine(stdout) {
115080
+ for (const line of stdout.split(/\r?\n/).reverse()) {
115081
+ const t = line.trim();
115082
+ if (!t.startsWith("{")) continue;
115083
+ try {
115084
+ const o = JSON.parse(t);
115085
+ if (o.v === 1 && o.stop && typeof o.stop.ok === "boolean") {
115086
+ return { ok: o.stop.ok, wasRunning: o.stop.wasRunning === true, message: typeof o.stop.message === "string" ? o.stop.message : null };
115087
+ }
115088
+ } catch {
115089
+ }
115090
+ }
115091
+ return null;
115092
+ }
115093
+ async function stopRemoteHost(c) {
115094
+ const r = await c.exec(stopCommand(c.spec));
115095
+ if (r.stdout.includes(NOT_FOUND2)) return { wasRunning: false };
115096
+ const answer = parseStopLine(r.stdout);
115097
+ if (!answer) {
115098
+ if (/unknown option for serve: --stop/.test(r.stderr)) {
115099
+ throw new Error(`The Centralu running on ${c.target} is too old to be stopped from here: stop \`centralu serve\` there, then try again`);
115100
+ }
115101
+ const why = lastLine(r.stderr);
115102
+ throw new Error(`Stopping Centralu on ${c.target} gave no answer${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
115103
+ }
115104
+ if (!answer.ok) throw new Error(`Centralu on ${c.target} could not be stopped: ${answer.message ?? "no reason given"}`);
115105
+ return { wasRunning: answer.wasRunning };
115106
+ }
115107
+ async function setPointers(c, run, current, previous) {
115108
+ const out = readStep(await c.exec(installCommand(c.spec, c.script, { action: "pointers", version: run.version, node: run.node, current, previous })), "done", c.target);
115109
+ return JSON.parse(out);
115110
+ }
115111
+ async function pruneThere(c, run) {
115112
+ const out = JSON.parse(readStep(await c.exec(installCommand(c.spec, c.script, { action: "prune", version: run.version, node: run.node })), "done", c.target));
115113
+ return { removed: out.removed ?? [], left: out.left ?? [] };
115114
+ }
115115
+ var TAIL_SH = `${DATA_SH}; tail -n 12 "$d/host.log" 2>/dev/null; true
115116
+ `;
115117
+ var TAIL_PS = `${DATA_PS}; Get-Content -Tail 12 (Join-Path $d 'host.log') -ErrorAction SilentlyContinue
115118
+ `;
115119
+ async function hostLogTail(c) {
115120
+ const r = await c.exec(remoteScript(c.spec, { sh: TAIL_SH, ps: TAIL_PS }));
115121
+ const lines = r.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(-4);
115122
+ const text2 = lines.join(" / ");
115123
+ return text2.length > 600 ? `\u2026${text2.slice(-600)}` : text2;
115124
+ }
115125
+ async function startAndCheck(c, version2) {
115126
+ c.step("start");
115127
+ try {
115128
+ await c.start();
115129
+ } catch (err) {
115130
+ return err.message;
115131
+ }
115132
+ c.step("check");
115133
+ return await c.answers(version2, CHECK_MS) ? null : `it did not answer within ${CHECK_MS / 1e3} s`;
115134
+ }
115135
+ async function putBack(c, to, why, was) {
115136
+ c.step("roll_back");
115137
+ const tail = await hostLogTail(c).catch(() => "");
115138
+ const log = tail ? ` The end of its host.log: ${tail}` : "";
115139
+ const back = was.current ? `Centralu ${was.current.version}` : "the Centralu that was there";
115140
+ try {
115141
+ await stopRemoteHost(c).catch(() => void 0);
115142
+ await setPointers(c, to, was.current, was.previous);
115143
+ if (was.running) await c.start();
115144
+ } catch (err) {
115145
+ throw new Error(`Centralu ${to.version} did not start on ${c.target} (${why}), and putting back ${back} failed too: ${err.message}.${log}`);
115146
+ }
115147
+ throw new Error(`Centralu ${to.version} did not start on ${c.target} (${why}); ${was.running ? `it runs ${back} again` : `${back} is back in place`}.${log}`);
115148
+ }
115149
+ async function updateRemote(c, o) {
115150
+ const placed = await installRemote({ ...o, exec: c.exec, spec: c.spec, target: c.target, script: c.script, activate: false, onStep: c.step });
115151
+ const next = placed.installed;
115152
+ if (!next) throw new Error(`Installing on ${c.target} answered without saying what it placed; the Centralu there may predate updates from the app`);
115153
+ const was = { current: placed.current ?? null, previous: placed.previous ?? null };
115154
+ await c.hold();
115155
+ c.step("stop");
115156
+ const { wasRunning } = await stopRemoteHost(c);
115157
+ c.step("switch");
115158
+ const keep = was.current && was.current.version !== next.version ? was.current : was.previous;
115159
+ await setPointers(c, next, next, keep);
115160
+ const why = await startAndCheck(c, next.version);
115161
+ if (why) return putBack(c, next, why, { ...was, running: wasRunning });
115162
+ c.step("prune");
115163
+ const pruned = await pruneThere(c, next).catch(() => ({ removed: [], left: [] }));
115164
+ return { current: next, previous: keep, ...pruned };
115165
+ }
115166
+ async function rollbackRemote(c, there) {
115167
+ await c.hold();
115168
+ c.step("stop");
115169
+ const { wasRunning } = await stopRemoteHost(c);
115170
+ c.step("switch");
115171
+ await setPointers(c, there.previous, there.previous, null);
115172
+ const why = await startAndCheck(c, there.previous.version);
115173
+ if (why) return putBack(c, there.previous, why, { current: there.current, previous: there.previous, running: wasRunning });
115174
+ return { current: there.previous, previous: null, removed: [], left: [] };
115175
+ }
115176
+ var UNINSTALL_SH = `${DATA_SH}
115177
+ if [ -d "$r" ]; then rm -rf "$r" || { echo "${STEP} fail remove $r"; exit 1; }; fi
115178
+ echo "${STEP} uninstalled"
115179
+ `;
115180
+ var UNINSTALL_PS = `$ErrorActionPreference = 'Stop'
115181
+ ${DATA_PS}
115182
+ try {
115183
+ if (Test-Path $r) { Remove-Item -Recurse -Force $r }
115184
+ "${STEP} uninstalled"
115185
+ } catch {
115186
+ "${STEP} fail remove $($_.Exception.Message -replace '\\s+', ' ')"
115187
+ }
115188
+ `;
115189
+ function uninstallCommand(spec) {
115190
+ return remoteScript(spec, { sh: UNINSTALL_SH, ps: UNINSTALL_PS });
115191
+ }
115192
+ async function uninstallRemote(c) {
115193
+ await c.hold();
115194
+ c.step("stop");
115195
+ const { wasRunning } = await stopRemoteHost(c);
115196
+ c.step("autostart");
115197
+ await removeAutostart(c);
115198
+ c.step("remove");
115199
+ readStep(await c.exec(uninstallCommand(c.spec)), "uninstalled", c.target);
115200
+ return { stopped: wasRunning };
115201
+ }
115202
+
114049
115203
  // packages/agent-host/src/links/machine-ids.ts
114050
115204
  import { randomBytes as randomBytes5 } from "node:crypto";
114051
115205
  function isMachineId(value) {
@@ -114514,6 +115668,13 @@ function channel(version2) {
114514
115668
 
114515
115669
  // packages/agent-host/src/links/links.ts
114516
115670
  var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
115671
+ var linkError2 = (message) => Object.assign(new Error(message), { code: "internal" });
115672
+ function installOf(i) {
115673
+ return i ? { managed: i.managed, current: i.current, previous: i.previous } : null;
115674
+ }
115675
+ function autostartOf(a) {
115676
+ return a ? { on: a.on, how: a.how, linger: a.linger } : null;
115677
+ }
114517
115678
  function hiddenKind(s) {
114518
115679
  return isObj2(s) && (s.kind === "orchestrator" || s.kind === "coordinator");
114519
115680
  }
@@ -114547,6 +115708,21 @@ var LinkedMachine = class {
114547
115708
  opening = null;
114548
115709
  /** A refusal was already answered by asking the remote again; the next one is reported and backed off */
114549
115710
  refusedOnce = false;
115711
+ /** One install, update, rollback or uninstall at a time per machine (the remote also holds `install.lock`) */
115712
+ busy = false;
115713
+ /** That operation and its step, for the row (`MachineInfo.operation`) */
115714
+ operation = null;
115715
+ /**
115716
+ * While an update stops, switches and starts the host there, the link neither reconnects nor
115717
+ * starts a host of its own: it would start the old version between the stop and the switch
115718
+ */
115719
+ held = false;
115720
+ /** Called with the version of every hello, while an update waits for the new one */
115721
+ helloWaiters = /* @__PURE__ */ new Set();
115722
+ /** What the hub installed there, from the last connection line (plan §10.5, S12) */
115723
+ installed = null;
115724
+ /** Whether it starts its host at boot, from the last connection line or `machines.autostart` (plan §10.4) */
115725
+ bootStart = null;
114550
115726
  get id() {
114551
115727
  return this.record.id;
114552
115728
  }
@@ -114570,7 +115746,10 @@ var LinkedMachine = class {
114570
115746
  lastConnectedAt: this.lastConnectedAt,
114571
115747
  localPort: this.endpoint && this.endpoint.localPort > 0 ? this.endpoint.localPort : null,
114572
115748
  sameLocalPort: this.endpoint ? this.endpoint.localPort === this.endpoint.line.port : false,
114573
- hostStarted: this.hostStarted
115749
+ hostStarted: this.hostStarted,
115750
+ install: this.installed,
115751
+ operation: this.operation,
115752
+ autostart: this.bootStart
114574
115753
  };
114575
115754
  }
114576
115755
  // ── RoutedMachine ───────────────────────────────────────────────────────────────────────
@@ -114614,6 +115793,201 @@ var LinkedMachine = class {
114614
115793
  this.versions = { ...this.versions, accepted: true };
114615
115794
  this.reconnect();
114616
115795
  }
115796
+ /**
115797
+ * Installs the hub's version there (`machines.install`, install.ts, plan §10.2). A host running
115798
+ * there keeps running; a link that is not connected (nothing was installed, or nothing runs) opens
115799
+ * again at once, and finds the managed launcher first
115800
+ */
115801
+ install() {
115802
+ return this.operate("install", this.deps.hub.version, async (c, installer) => {
115803
+ const r = await installRemote({
115804
+ exec: c.exec,
115805
+ spec: c.spec,
115806
+ target: c.target,
115807
+ version: this.deps.hub.version,
115808
+ runtime: installer.runtime(),
115809
+ script: c.script,
115810
+ registry: installer.registry,
115811
+ nodeDist: installer.nodeDist,
115812
+ onStep: c.step
115813
+ });
115814
+ this.deps.log?.(`[links] ${this.name}: installed Centralu ${r.current.version} on Node ${r.current.node}${r.previous ? `; ${r.previous.version} kept to roll back to` : ""}`);
115815
+ return this.installedNow(r);
115816
+ });
115817
+ }
115818
+ /** `machines.update` (update.ts, plan §10.5): install beside, stop, switch, start, check, or put back */
115819
+ update() {
115820
+ return this.operate("update", this.deps.hub.version, async (c, installer) => {
115821
+ const r = await updateRemote(c, {
115822
+ version: this.deps.hub.version,
115823
+ runtime: installer.runtime(),
115824
+ registry: installer.registry,
115825
+ nodeDist: installer.nodeDist
115826
+ });
115827
+ this.deps.log?.(`[links] ${this.name}: updated to Centralu ${r.current.version}${r.previous ? `; ${r.previous.version} kept to roll back to` : ""}`);
115828
+ return this.installedNow(r);
115829
+ });
115830
+ }
115831
+ /** `machines.rollback`: back to `previous`, one step */
115832
+ rollback() {
115833
+ const current = this.installed?.current;
115834
+ const previous = this.installed?.previous;
115835
+ if (!current || !previous) return Promise.reject(linkError2(`There is no earlier version installed on ${this.name} to go back to`));
115836
+ return this.operate("rollback", previous.version, async (c) => {
115837
+ const r = await rollbackRemote(c, { current, previous });
115838
+ this.deps.log?.(`[links] ${this.name}: rolled back to Centralu ${r.current.version}`);
115839
+ return this.installedNow(r);
115840
+ });
115841
+ }
115842
+ /** `machines.uninstall`: stops the host there and removes what the hub installed; the data stays */
115843
+ uninstall() {
115844
+ if (!this.installed?.current) return Promise.reject(linkError2(`Nothing this computer installed is on ${this.name}`));
115845
+ return this.operate("uninstall", null, async (c) => {
115846
+ const r = await uninstallRemote(c);
115847
+ this.installed = { managed: false, current: null, previous: null };
115848
+ if (this.bootStart) this.bootStart = { ...this.bootStart, on: false, linger: null };
115849
+ this.deps.log?.(`[links] ${this.name}: removed the Centralu this computer installed; its data stays`);
115850
+ return { machine: this.info(), stopped: r.stopped };
115851
+ });
115852
+ }
115853
+ /**
115854
+ * `machines.autostart` (plan §10.4, owner decision 4): the machine's host started at boot, or not, or
115855
+ * (without `on`) whether it is. The Centralu there writes its own entry; nothing starts or stops now.
115856
+ * Not while an install, update, rollback or uninstall runs there: the entry names a launcher that one
115857
+ * may be removing
115858
+ */
115859
+ async autostart(on2) {
115860
+ const exec8 = this.tunnel.exec?.bind(this.tunnel);
115861
+ if (!exec8) throw linkError2(`This computer has no shell on ${this.name} to change how it starts`);
115862
+ if (this.busy) throw linkError2(`${this.name} is busy with ${this.operation?.kind ?? "another change"}; wait until it is done`);
115863
+ this.busy = true;
115864
+ try {
115865
+ const a = await autostartRemote({ exec: exec8, spec: this.record.remote, target: this.record.sshTarget }, on2 === void 0 ? "status" : on2 ? "on" : "off");
115866
+ const state = { on: a.on, how: a.how, linger: a.linger };
115867
+ this.bootStart = state;
115868
+ if (on2 !== void 0) {
115869
+ this.deps.log?.(`[links] ${this.name}: ${a.on ? `starts Centralu at boot (${a.how}${a.how === "systemd" && a.linger === false ? ", at login until lingering is enabled" : ""})` : "no longer starts Centralu at boot"}`);
115870
+ }
115871
+ this.deps.broadcast({ type: "machine_status", machine: this.info() });
115872
+ return { machine: this.info(), autostart: state };
115873
+ } catch (err) {
115874
+ this.deps.log?.(`[links] ${this.name}: autostart ${on2 === void 0 ? "status" : on2 ? "on" : "off"} failed: ${err.message}`);
115875
+ throw linkError2(err.message);
115876
+ } finally {
115877
+ this.busy = false;
115878
+ }
115879
+ }
115880
+ /**
115881
+ * What would stop there if its host stopped now (`host.activity` on the remote), for the "Update"
115882
+ * prompt. Through the link when connected; while the link is held at `versions_differ` on one
115883
+ * protocol, through a short connection of its own. Null when the machine cannot say
115884
+ */
115885
+ activity() {
115886
+ const read = (r) => {
115887
+ const p = HostActivity.safeParse(r);
115888
+ return p.success ? p.data : null;
115889
+ };
115890
+ if (this.client?.connected) return this.client.call("host.activity", {}).then(read, () => null);
115891
+ const e = this.endpoint;
115892
+ if (!e?.url || this.versions?.compatible === false || this.held) return Promise.resolve(null);
115893
+ return new Promise((resolve10) => {
115894
+ let settled = false;
115895
+ const done = (v) => {
115896
+ if (settled) return;
115897
+ settled = true;
115898
+ clearTimeout(timer);
115899
+ client.close();
115900
+ resolve10(v);
115901
+ };
115902
+ const client = new RemoteClient(
115903
+ { ...this.deps.client, url: e.url, token: e.token, handshakeTimeoutMs: 5e3, callTimeoutMs: 5e3 },
115904
+ { hello: () => void client.call("host.activity", {}).then(read, () => null).then(done), refused: () => done(null) }
115905
+ );
115906
+ const timer = setTimeout(() => done(null), 8e3);
115907
+ client.connect();
115908
+ });
115909
+ }
115910
+ installedNow(r) {
115911
+ this.installed = { managed: true, current: r.current, previous: r.previous };
115912
+ return { machine: this.info(), current: r.current, previous: r.previous, removed: r.removed, left: r.left };
115913
+ }
115914
+ /**
115915
+ * Runs one install, update, rollback or uninstall: refused where this hub cannot install (no
115916
+ * installer or shell, a development build, a command of the person's own), one at a time, with its
115917
+ * steps in the row. Afterwards the link opens again, also after a failure
115918
+ */
115919
+ async operate(kind, target, fn2) {
115920
+ const installer = this.deps.installer;
115921
+ const tunnelExec = this.tunnel.exec?.bind(this.tunnel);
115922
+ if (!installer || !tunnelExec) throw linkError2(`This computer cannot install on ${this.name}`);
115923
+ if (this.deps.hub.dev && (kind === "install" || kind === "update")) {
115924
+ throw linkError2(`A development build is not published, so it cannot be installed on ${this.name}; install the latest release there (npm i -g centralu)`);
115925
+ }
115926
+ if (this.record.remote.command) throw linkError2(`${this.name} runs a command of its own in place of centralu; clear it to use an installed one`);
115927
+ if (this.busy) throw linkError2(`${this.name} is busy with ${this.operation?.kind ?? "another change"}; wait until it is done`);
115928
+ this.busy = true;
115929
+ const at2 = Date.now();
115930
+ const step = (s) => {
115931
+ this.deps.log?.(`[links] ${this.name}: ${kind}${target ? ` ${target}` : ""}: ${s}`);
115932
+ this.operation = { kind, step: s, target, at: at2 };
115933
+ this.deps.broadcast({ type: "machine_status", machine: this.info() });
115934
+ };
115935
+ let result;
115936
+ try {
115937
+ result = await fn2(this.control(tunnelExec, installer.script(), step), installer);
115938
+ } catch (err) {
115939
+ this.deps.log?.(`[links] ${this.name}: ${kind} failed: ${err.message}`);
115940
+ throw linkError2(err.message);
115941
+ } finally {
115942
+ this.busy = false;
115943
+ const wasHeld = this.held;
115944
+ this.held = false;
115945
+ this.operation = null;
115946
+ if (!this.stopped && (wasHeld || this.status !== "connected")) this.reconnect();
115947
+ this.deps.broadcast({ type: "machine_status", machine: this.info() });
115948
+ }
115949
+ return result && typeof result === "object" && "machine" in result ? { ...result, machine: this.info() } : result;
115950
+ }
115951
+ /** What update.ts runs things there with */
115952
+ control(exec8, script, step) {
115953
+ const hold = async () => {
115954
+ this.held = true;
115955
+ clearTimeout(this.retryTimer);
115956
+ await this.opening?.catch(() => void 0);
115957
+ clearTimeout(this.retryTimer);
115958
+ this.client?.close();
115959
+ this.client = null;
115960
+ this.setStatus("updating", null);
115961
+ };
115962
+ return {
115963
+ exec: exec8,
115964
+ spec: this.record.remote,
115965
+ target: this.record.sshTarget,
115966
+ script,
115967
+ hold,
115968
+ step,
115969
+ start: async () => {
115970
+ if (!this.tunnel.startHost) throw new Error(`This computer cannot start Centralu on ${this.name}`);
115971
+ const started = await this.tunnel.startHost();
115972
+ this.hostStarted = { ...started, at: Date.now() };
115973
+ },
115974
+ answers: (version2, ms3) => new Promise((resolve10) => {
115975
+ const waiter = (v) => {
115976
+ if (v !== version2) return;
115977
+ clearTimeout(timer);
115978
+ this.helloWaiters.delete(waiter);
115979
+ resolve10(true);
115980
+ };
115981
+ const timer = setTimeout(() => {
115982
+ this.helloWaiters.delete(waiter);
115983
+ void hold().then(() => resolve10(false));
115984
+ }, this.deps.checkMs ?? ms3);
115985
+ this.helloWaiters.add(waiter);
115986
+ this.held = false;
115987
+ this.reconnect();
115988
+ })
115989
+ };
115990
+ }
114617
115991
  async stop() {
114618
115992
  this.stopped = true;
114619
115993
  clearTimeout(this.retryTimer);
@@ -114622,6 +115996,7 @@ var LinkedMachine = class {
114622
115996
  await this.tunnel.close();
114623
115997
  }
114624
115998
  open() {
115999
+ if (this.held) return Promise.resolve();
114625
116000
  this.opening ??= this.doOpen().finally(() => {
114626
116001
  this.opening = null;
114627
116002
  });
@@ -114634,7 +116009,10 @@ var LinkedMachine = class {
114634
116009
  this.setStatus("connecting", null);
114635
116010
  let endpoint = await this.openTunnel();
114636
116011
  if (!endpoint) return;
116012
+ this.installed = installOf(endpoint.line.install);
116013
+ this.bootStart = autostartOf(endpoint.line.autostart);
114637
116014
  if (!endpoint.line.hostRunning && this.tunnel.startHost) {
116015
+ if (this.held) return;
114638
116016
  this.setStatus("starting", null);
114639
116017
  this.deps.log?.(`[links] ${this.name}: no Centralu host is running there; starting one (centralu serve --detach)`);
114640
116018
  let started;
@@ -114653,12 +116031,16 @@ var LinkedMachine = class {
114653
116031
  this.setStatus("starting", null);
114654
116032
  endpoint = await this.openTunnel();
114655
116033
  if (!endpoint) return;
116034
+ this.installed = installOf(endpoint.line.install);
116035
+ this.bootStart = autostartOf(endpoint.line.autostart);
114656
116036
  }
116037
+ if (this.held) return;
114657
116038
  this.endpoint = endpoint;
114658
116039
  if (!endpoint.line.hostRunning) {
114659
116040
  this.setStatus("not_running", `Centralu is installed on ${this.name}, but no \`centralu serve\` is running there`);
114660
116041
  return this.retryLater();
114661
116042
  }
116043
+ for (const w of [...this.helloWaiters]) w(endpoint.line.version);
114662
116044
  const versions = this.check({ version: endpoint.line.version, protocolVersion: endpoint.line.protocolVersion, dev: false });
114663
116045
  if (!mayConnect(versions)) return;
114664
116046
  this.connectClient(endpoint);
@@ -114727,6 +116109,7 @@ var LinkedMachine = class {
114727
116109
  }
114728
116110
  async onHello(client, h) {
114729
116111
  if (this.client !== client) return;
116112
+ for (const w of [...this.helloWaiters]) w(h.build?.version ?? null);
114730
116113
  if (h.build) {
114731
116114
  const v = this.check({ version: h.build.version ?? this.versions?.remote.version ?? "unknown", protocolVersion: h.build.protocolVersion, dev: h.build.commit === "dev" });
114732
116115
  if (!mayConnect(v)) return;
@@ -114775,11 +116158,12 @@ var LinkedMachine = class {
114775
116158
  if (this.stopped) return;
114776
116159
  this.client?.close();
114777
116160
  this.client = null;
116161
+ if (this.held) return;
114778
116162
  this.setStatus("unreachable", `The connection to ${this.name} ended (${reason}); reconnecting`);
114779
116163
  this.retryLater();
114780
116164
  }
114781
116165
  retryLater(fixedMs) {
114782
- if (this.stopped) return;
116166
+ if (this.stopped || this.held) return;
114783
116167
  clearTimeout(this.retryTimer);
114784
116168
  const [first, max] = this.deps.retryMs ?? [2e3, 6e4];
114785
116169
  const delay = fixedMs ?? Math.min(max, first * 2 ** this.attempt++);
@@ -114846,6 +116230,24 @@ var Links = class {
114846
116230
  m.acceptVersions();
114847
116231
  return m.info();
114848
116232
  }
116233
+ install(id2) {
116234
+ return this.need(id2).install();
116235
+ }
116236
+ update(id2) {
116237
+ return this.need(id2).update();
116238
+ }
116239
+ rollback(id2) {
116240
+ return this.need(id2).rollback();
116241
+ }
116242
+ uninstall(id2) {
116243
+ return this.need(id2).uninstall();
116244
+ }
116245
+ autostart(id2, on2) {
116246
+ return this.need(id2).autostart(on2);
116247
+ }
116248
+ activity(id2) {
116249
+ return this.need(id2).activity();
116250
+ }
114849
116251
  need(id2) {
114850
116252
  const m = this.machines.get(id2);
114851
116253
  if (!m) throw Object.assign(new Error(`No linked machine ${id2}`), { code: "internal" });
@@ -115067,6 +116469,14 @@ var ROUTES = {
115067
116469
  // Stopping a remote host is `centralu serve --stop` over ssh, never a call through the link
115068
116470
  "host.stop": HUB,
115069
116471
  "machines.acceptVersions": HUB,
116472
+ "machines.install": HUB,
116473
+ "machines.update": HUB,
116474
+ "machines.rollback": HUB,
116475
+ "machines.uninstall": HUB,
116476
+ "machines.autostart": HUB,
116477
+ "machines.activity": HUB,
116478
+ // The hub's own answer; a hub asks a remote's through `machines.activity`
116479
+ "host.activity": HUB,
115070
116480
  // ── Callbacks of the hub's own agents ───────────────────────────────────────────────────
115071
116481
  "orchestrator.tools": INTERNAL,
115072
116482
  "orchestrator.tool": INTERNAL,
@@ -115201,7 +116611,11 @@ var Router = class {
115201
116611
  if (kind === "projects") return list.map((p) => mark(m.q.project(p)));
115202
116612
  return list.filter((s) => !m.hides(s)).map((s) => mark(m.q.session(s)));
115203
116613
  }
115204
- /** The person's project order, split per machine: each machine keeps the order of its own */
116614
+ /**
116615
+ * The person's project order, split per machine: each machine keeps the order of its own. A
116616
+ * machine that cannot take it does not fail the rest, which has already been saved: its projects
116617
+ * come back from the mirror as they were, marked `unreachable`, the way `projects.list` gives them
116618
+ */
115205
116619
  async reorderProjects(machines, params) {
115206
116620
  const ids = isObj3(params) && Array.isArray(params.orderedIds) ? params.orderedIds : [];
115207
116621
  const perMachine = /* @__PURE__ */ new Map();
@@ -115213,7 +116627,13 @@ var Router = class {
115213
116627
  }
115214
116628
  const [mine, ...theirs] = await Promise.all([
115215
116629
  this.opts.local("projects.reorder", { ...isObj3(params) ? params : {}, orderedIds: local }),
115216
- ...[...perMachine].map(async ([m, orderedIds]) => m.q.projects(await this.forward(m, "projects.reorder", { orderedIds })))
116630
+ ...[...perMachine].map(async ([m, orderedIds]) => {
116631
+ try {
116632
+ return m.q.projects(await this.forward(m, "projects.reorder", { orderedIds }));
116633
+ } catch {
116634
+ return this.present(m, "projects", m.lastKnown("projects") ?? [], true);
116635
+ }
116636
+ })
115217
116637
  ]);
115218
116638
  return [...mine, ...theirs.flat()];
115219
116639
  }
@@ -115280,338 +116700,6 @@ function combine(method, acc, r, q2) {
115280
116700
  }
115281
116701
  }
115282
116702
 
115283
- // packages/agent-host/src/links/tunnel.ts
115284
- import { spawn as spawn5 } from "node:child_process";
115285
- import { createConnection as createConnection2, createServer as createServer3 } from "node:net";
115286
- function parseConnectionLine(stdout) {
115287
- const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
115288
- for (let i = lines.length - 1; i >= 0; i--) {
115289
- let raw;
115290
- try {
115291
- raw = JSON.parse(lines[i]);
115292
- } catch {
115293
- continue;
115294
- }
115295
- const o = raw;
115296
- if (o && typeof o === "object" && typeof o.v === "number") {
115297
- if (o.v !== 1) throw new Error(`The remote's Centralu speaks connection format ${o.v}; update Centralu on this computer`);
115298
- if (typeof o.port !== "number" || typeof o.token !== "string" || typeof o.protocolVersion !== "number") {
115299
- throw new Error("The remote answered with an incomplete connection line");
115300
- }
115301
- const install = parseInstall(o.install);
115302
- return {
115303
- v: 1,
115304
- port: o.port,
115305
- token: o.token,
115306
- version: typeof o.version === "string" ? o.version : "unknown",
115307
- protocolVersion: o.protocolVersion,
115308
- dataDir: typeof o.dataDir === "string" ? o.dataDir : "",
115309
- hostRunning: o.hostRunning === true,
115310
- ...install ? { install } : {}
115311
- };
115312
- }
115313
- }
115314
- throw new Error("The remote did not print a connection line; its Centralu may predate `centralu serve` (update it there: npm i -g centralu)");
115315
- }
115316
- function parseInstall(raw) {
115317
- if (!raw || typeof raw !== "object") return null;
115318
- const o = raw;
115319
- const pointer = (v) => {
115320
- const p = v;
115321
- return p && typeof p.version === "string" && typeof p.node === "string" ? { version: p.version, node: p.node } : null;
115322
- };
115323
- return { managed: o.managed === true, current: pointer(o.current), previous: pointer(o.previous), node: typeof o.node === "string" ? o.node : null };
115324
- }
115325
- function parseDetachLine(stdout) {
115326
- const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
115327
- for (let i = lines.length - 1; i >= 0; i--) {
115328
- let raw;
115329
- try {
115330
- raw = JSON.parse(lines[i]);
115331
- } catch {
115332
- continue;
115333
- }
115334
- const d2 = raw?.detach;
115335
- if (!d2 || typeof d2 !== "object") continue;
115336
- if (d2.ok === true) return { ok: true, line: parseConnectionLine(lines[i]), how: typeof d2.how === "string" ? d2.how : null };
115337
- return { ok: false, reason: typeof d2.reason === "string" ? d2.reason : "failed", message: typeof d2.message === "string" ? d2.message : "centralu serve --detach failed" };
115338
- }
115339
- throw new Error("The remote did not say whether it started");
115340
- }
115341
- var WSL_DISTRO_RE = /^[A-Za-z0-9._-]{1,64}$/;
115342
- function powershell(script) {
115343
- const full = `[Console]::OutputEncoding=[Text.Encoding]::UTF8
115344
- ${script}`;
115345
- return `powershell -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(full, "utf16le").toString("base64")}`;
115346
- }
115347
- function wslKeepAlive(spec) {
115348
- if (spec?.shell !== "wsl") return null;
115349
- const distro = spec.wslDistro ?? "";
115350
- if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
115351
- return `wsl.exe -d ${distro} --exec sleep infinity`;
115352
- }
115353
- var NOT_FOUND2 = "CENTRALU-NOT-FOUND";
115354
- function connectionCommand(spec) {
115355
- return remoteCommand(spec, "serve --connection");
115356
- }
115357
- function detachCommand(spec) {
115358
- return remoteCommand(spec, "serve --detach");
115359
- }
115360
- function serveCommand(spec) {
115361
- return remoteCommand(spec, "serve");
115362
- }
115363
- function remoteCommand(spec, args) {
115364
- const custom2 = spec.command?.trim() || null;
115365
- if (spec.shell === "powershell") {
115366
- if (custom2) return powershell(`${custom2} ${args}`);
115367
- return powershell(
115368
- `if (Get-Command centralu -ErrorAction SilentlyContinue) { & centralu ${args} } else { $l = Join-Path $env:USERPROFILE '.centralu\\bin\\centralu.cmd'; if (Test-Path $l) { & $l ${args} } else { '${NOT_FOUND2}' } }`
115369
- );
115370
- }
115371
- const lookup = `if command -v centralu >/dev/null 2>&1; then exec centralu ${args}; elif [ -x "$HOME/.centralu/bin/centralu" ]; then exec "$HOME/.centralu/bin/centralu" ${args}; else echo ${NOT_FOUND2}; exit 127; fi`;
115372
- if (spec.shell === "posix") return custom2 ? `${custom2} ${args}` : `sh -c '${lookup}'`;
115373
- const distro = spec.wslDistro ?? "";
115374
- if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
115375
- const script = custom2 ? `${custom2} ${args}` : `PATH=$(printf %s "$PATH" | tr : '\\n' | grep -v '^/mnt/' | paste -sd: -); ${lookup}`;
115376
- const b64 = Buffer.from(script, "utf8").toString("base64");
115377
- return powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
115378
- }
115379
- var SshTunnel = class {
115380
- constructor(opts) {
115381
- this.opts = opts;
115382
- if (!opts.target.trim() || opts.target.startsWith("-")) throw new Error("Not an ssh target");
115383
- }
115384
- opts;
115385
- child = null;
115386
- /** The asking ssh runs, while they run */
115387
- asking = /* @__PURE__ */ new Set();
115388
- downListeners = [];
115389
- closing = false;
115390
- /** The remote cannot start a host outside a session (WMI blocked): the forward runs it (`startHost`) */
115391
- linkBound = false;
115392
- async open() {
115393
- await this.stopChild();
115394
- this.closing = false;
115395
- const line = await this.connectionLine().catch((err) => {
115396
- this.ifClosed();
115397
- throw err;
115398
- });
115399
- this.ifClosed();
115400
- if (!line.hostRunning && !this.linkBound) {
115401
- return { url: "", token: line.token, line, localPort: 0 };
115402
- }
115403
- const hosting = !line.hostRunning;
115404
- let localPort = await portFree(line.port) ? line.port : await freePort();
115405
- try {
115406
- await this.forward(localPort, line.port, hosting);
115407
- } catch (err) {
115408
- if (localPort !== line.port) throw err;
115409
- this.ifClosed();
115410
- localPort = await freePort();
115411
- await this.forward(localPort, line.port, hosting);
115412
- }
115413
- if (this.closing) {
115414
- await this.stopChild();
115415
- this.ifClosed();
115416
- }
115417
- const served = hosting ? await this.untilServing() : line;
115418
- return { url: `ws://127.0.0.1:${localPort}`, token: served.token, line: served, localPort };
115419
- }
115420
- /**
115421
- * Starts the remote's host with `centralu serve --detach` (plan §10.4, owner decision 7): it then
115422
- * runs outside any ssh session and outlives this link. When the remote says WMI process creation
115423
- * is blocked there (decision 3), every `open` from then on runs `centralu serve` as the forward's
115424
- * own remote command instead, so the host lives as long as the link.
115425
- */
115426
- async startHost() {
115427
- this.ifClosed();
115428
- const remote = this.opts.remote ?? { shell: "posix" };
115429
- const r = await this.run([...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target, detachCommand(remote)]);
115430
- this.ifClosed();
115431
- if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
115432
- if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
115433
- throw new Error(`Centralu is not installed on ${this.opts.target} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
115434
- }
115435
- let answer;
115436
- try {
115437
- answer = parseDetachLine(r.stdout);
115438
- } catch (err) {
115439
- if (/unknown option for serve: --detach/.test(r.stderr)) {
115440
- throw new Error(`Centralu on ${this.opts.target} is too old to be started from here: run \`centralu serve\` there, or update it (npm i -g centralu)`);
115441
- }
115442
- const why = lastLine(r.stderr);
115443
- throw new Error(`${err.message}${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
115444
- }
115445
- if (answer.ok) {
115446
- this.linkBound = false;
115447
- return { how: "detached", note: null };
115448
- }
115449
- if (answer.reason === "wmi_blocked") {
115450
- this.linkBound = true;
115451
- return { how: "link_bound", note: answer.message };
115452
- }
115453
- throw new Error(answer.message);
115454
- }
115455
- /** The connection line again until the link-bound host answers, or the forward that runs it ends */
115456
- async untilServing() {
115457
- const deadline = Date.now() + (this.opts.startTimeoutMs ?? 3e4);
115458
- for (; ; ) {
115459
- this.ifClosed();
115460
- if (!this.child) throw new Error(`The ssh session that runs Centralu on ${this.opts.target} ended before it served`);
115461
- const line = await this.connectionLine();
115462
- if (line.hostRunning) return line;
115463
- if (Date.now() > deadline) {
115464
- await this.stopChild();
115465
- throw new Error(`Centralu on ${this.opts.target} did not start serving in time`);
115466
- }
115467
- await new Promise((r) => setTimeout(r, 500));
115468
- }
115469
- }
115470
- onDown(listener) {
115471
- this.downListeners.push(listener);
115472
- }
115473
- async close() {
115474
- this.closing = true;
115475
- for (const c of this.asking) c.kill("SIGTERM");
115476
- await this.stopChild();
115477
- }
115478
- ifClosed() {
115479
- if (this.closing) throw new Error(`The link to ${this.opts.target} was closed`);
115480
- }
115481
- run(args) {
115482
- return new Promise((resolve10, reject) => {
115483
- const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
115484
- this.asking.add(child);
115485
- child.once("exit", () => this.asking.delete(child));
115486
- let stdout = "";
115487
- let stderr = "";
115488
- child.stdout.on("data", (d2) => stdout += String(d2));
115489
- child.stderr.on("data", (d2) => stderr += String(d2));
115490
- child.once("error", reject);
115491
- child.once("close", (code2) => resolve10({ code: code2, stdout, stderr }));
115492
- });
115493
- }
115494
- config() {
115495
- return this.opts.configFile ? ["-F", this.opts.configFile] : [];
115496
- }
115497
- async connectionLine() {
115498
- const remote = this.opts.remote ?? { shell: "posix" };
115499
- const base2 = [...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target];
115500
- const r = await this.run([...base2, connectionCommand(remote)]);
115501
- if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
115502
- if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
115503
- throw new Error(`Centralu is not installed on ${this.opts.target} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
115504
- }
115505
- try {
115506
- return parseConnectionLine(r.stdout);
115507
- } catch (err) {
115508
- const why = lastLine(r.stderr);
115509
- throw new Error(`${err.message}${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
115510
- }
115511
- }
115512
- forward(localPort, remotePort, hosting = false) {
115513
- this.ifClosed();
115514
- const keepAlive = hosting ? serveCommand(this.opts.remote ?? { shell: "posix" }) : wslKeepAlive(this.opts.remote);
115515
- const args = [
115516
- ...this.config(),
115517
- ...keepAlive ? ["-T"] : ["-N"],
115518
- "-o",
115519
- "BatchMode=yes",
115520
- "-o",
115521
- "ExitOnForwardFailure=yes",
115522
- "-o",
115523
- "ServerAliveInterval=15",
115524
- "-o",
115525
- "ServerAliveCountMax=3",
115526
- "-o",
115527
- "GatewayPorts=no",
115528
- "-L",
115529
- `127.0.0.1:${localPort}:127.0.0.1:${remotePort}`,
115530
- "--",
115531
- this.opts.target,
115532
- ...keepAlive ? [keepAlive] : []
115533
- ];
115534
- const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "ignore", "pipe"] });
115535
- this.child = child;
115536
- let stderr = "";
115537
- child.stderr.on("data", (d2) => stderr += String(d2));
115538
- return new Promise((resolve10, reject) => {
115539
- let settled = false;
115540
- const deadline = Date.now() + (this.opts.forwardTimeoutMs ?? 2e4);
115541
- const fail4 = (message) => {
115542
- if (settled) return;
115543
- settled = true;
115544
- reject(new Error(message));
115545
- };
115546
- child.once("error", (e) => fail4(`Could not start ssh: ${e.message}`));
115547
- child.once("exit", (code2, signal) => {
115548
- if (this.child === child) this.child = null;
115549
- if (!settled) return fail4(`The ssh forward to ${this.opts.target} ended (${signal ?? code2}): ${lastLine(stderr) || "no reason given"}`);
115550
- if (!this.closing) for (const l of this.downListeners) l(`ssh exited (${signal ?? code2})${stderr ? `: ${lastLine(stderr)}` : ""}`);
115551
- });
115552
- const poll = async () => {
115553
- while (!settled) {
115554
- if (await accepts(localPort)) {
115555
- settled = true;
115556
- this.opts.log?.(`[links] forward up: 127.0.0.1:${localPort} -> ${this.opts.target}:127.0.0.1:${remotePort}`);
115557
- return resolve10();
115558
- }
115559
- if (Date.now() > deadline) {
115560
- child.kill("SIGTERM");
115561
- return fail4(`The ssh forward to ${this.opts.target} did not come up in time`);
115562
- }
115563
- await new Promise((r) => setTimeout(r, 100));
115564
- }
115565
- };
115566
- void poll();
115567
- });
115568
- }
115569
- async stopChild() {
115570
- const child = this.child;
115571
- this.child = null;
115572
- if (!child || child.exitCode !== null || child.signalCode !== null) return;
115573
- await new Promise((resolve10) => {
115574
- child.once("exit", () => resolve10());
115575
- child.kill("SIGTERM");
115576
- setTimeout(() => {
115577
- child.kill("SIGKILL");
115578
- resolve10();
115579
- }, 2e3).unref();
115580
- });
115581
- }
115582
- };
115583
- function lastLine(s) {
115584
- const lines = s.replace(/#< CLIXML[\s\S]*$/, "").split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("**") && !/^Node\.js v\d/.test(l) && !/^at /.test(l));
115585
- return [...lines].reverse().find((l) => /error|denied|refused|timed out|not found|cannot|could not/i.test(l)) ?? lines.at(-1) ?? "";
115586
- }
115587
- function accepts(port2) {
115588
- return new Promise((resolve10) => {
115589
- const s = createConnection2({ host: "127.0.0.1", port: port2 });
115590
- s.once("connect", () => {
115591
- s.destroy();
115592
- resolve10(true);
115593
- });
115594
- s.once("error", () => resolve10(false));
115595
- });
115596
- }
115597
- function portFree(port2) {
115598
- return new Promise((resolve10) => {
115599
- const probe = createServer3();
115600
- probe.once("error", () => resolve10(false));
115601
- probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve10(true)));
115602
- });
115603
- }
115604
- function freePort() {
115605
- return new Promise((resolve10, reject) => {
115606
- const probe = createServer3();
115607
- probe.once("error", reject);
115608
- probe.listen(0, "127.0.0.1", () => {
115609
- const { port: port2 } = probe.address();
115610
- probe.close(() => resolve10(port2));
115611
- });
115612
- });
115613
- }
115614
-
115615
116703
  // packages/agent-host/src/links/stored.ts
115616
116704
  function storeRegistry(store2) {
115617
116705
  return {
@@ -115639,7 +116727,7 @@ function storeMirror(store2) {
115639
116727
  }
115640
116728
 
115641
116729
  // packages/agent-host/src/main.ts
115642
- var BUILD = true ? "b974cd2" : "dev";
116730
+ var BUILD = true ? "34635b7" : "dev";
115643
116731
  var underKeeper = process.env.CC_KEEPER === "1";
115644
116732
  var keeperSource = process.env.CC_HOST_SOURCE;
115645
116733
  var frontDoor = process.env.CC_FRONT_DOOR;
@@ -115879,6 +116967,7 @@ var links = new Links({
115879
116967
  mirror: storeMirror(store),
115880
116968
  registry: storeRegistry(store),
115881
116969
  tunnelFor: (record3) => new SshTunnel({ target: record3.sshTarget, remote: record3.remote, log: (line) => console.error(line) }),
116970
+ installer: { runtime: remoteRuntime, script: installScript },
115882
116971
  log: (line) => console.error(line)
115883
116972
  });
115884
116973
  mgr.useLinkedSessions((id2) => links.knowsSession(id2));
@@ -115894,12 +116983,14 @@ var router = new Router({
115894
116983
  inlineViews,
115895
116984
  heldPids: held2?.heldPids,
115896
116985
  machines: links,
115897
- stopHost: startedByServe && !underKeeper ? () => stopFromRequest() : void 0
116986
+ stopHost: startedByServe && !underKeeper ? () => stopFromRequest() : void 0,
116987
+ activity: () => activityCounts(activity())
115898
116988
  }),
115899
116989
  machines: () => links.all()
115900
116990
  });
115901
116991
  var server = new HostServer({
115902
116992
  port: Number(values.port),
116993
+ ...startedByServe ? { servedBy: "serve" } : {},
115903
116994
  token,
115904
116995
  allowedOrigins,
115905
116996
  onRpc: router.handle,