salidium 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21651,7 +21651,7 @@ function explainerCwd() {
21651
21651
  mkdirSync(dir, { recursive: true, mode: 448 });
21652
21652
  return dir;
21653
21653
  }
21654
- function runProcess(invocation, timeoutMs) {
21654
+ function runProcess(invocation, timeoutMs, signal) {
21655
21655
  return new Promise((resolve2, reject) => {
21656
21656
  const path = trustedPathEntries().join(delimiter2);
21657
21657
  const child = spawn(invocation.command, invocation.args, {
@@ -21666,14 +21666,29 @@ function runProcess(invocation, timeoutMs) {
21666
21666
  let err = "";
21667
21667
  let outBytes = 0;
21668
21668
  let settled = false;
21669
+ let timer;
21670
+ const cleanup = () => {
21671
+ if (timer)
21672
+ clearTimeout(timer);
21673
+ signal?.removeEventListener("abort", abort);
21674
+ };
21669
21675
  const fail = (error51) => {
21670
21676
  if (settled)
21671
21677
  return;
21672
21678
  settled = true;
21673
- clearTimeout(timer);
21679
+ cleanup();
21674
21680
  reject(error51);
21675
21681
  };
21676
- const timer = setTimeout(() => {
21682
+ const abort = () => {
21683
+ child.kill("SIGKILL");
21684
+ fail(new Error("explainer canceled"));
21685
+ };
21686
+ signal?.addEventListener("abort", abort, { once: true });
21687
+ if (signal?.aborted) {
21688
+ abort();
21689
+ return;
21690
+ }
21691
+ timer = setTimeout(() => {
21677
21692
  child.kill("SIGKILL");
21678
21693
  fail(new Error(`explainer timed out after ${timeoutMs}ms`));
21679
21694
  }, timeoutMs);
@@ -21697,7 +21712,7 @@ function runProcess(invocation, timeoutMs) {
21697
21712
  if (settled)
21698
21713
  return;
21699
21714
  settled = true;
21700
- clearTimeout(timer);
21715
+ cleanup();
21701
21716
  if (code === 0)
21702
21717
  resolve2(out);
21703
21718
  else
@@ -21793,7 +21808,7 @@ function createClaudeExplainerBackend(resolvedCommand) {
21793
21808
  throw new Error("trusted claude command is unavailable");
21794
21809
  const invocation = buildClaudeInvocation(request, command);
21795
21810
  return {
21796
- output: await runProcess(invocation, request.timeoutMs),
21811
+ output: await runProcess(invocation, request.timeoutMs, request.signal),
21797
21812
  model: invocation.model
21798
21813
  };
21799
21814
  }
@@ -21811,7 +21826,7 @@ function createCodexExplainerBackend(resolvedCommand) {
21811
21826
  writeFileSync(schemaPath, JSON.stringify(request.schema), { mode: 384 });
21812
21827
  const invocation = buildCodexInvocation(request, schemaPath, command);
21813
21828
  return {
21814
- output: await runProcess(invocation, request.timeoutMs),
21829
+ output: await runProcess(invocation, request.timeoutMs, request.signal),
21815
21830
  model: invocation.model
21816
21831
  };
21817
21832
  }
@@ -22087,7 +22102,8 @@ async function explainWithStatus(state, opts = {}) {
22087
22102
  evidence,
22088
22103
  schema: SCHEMA,
22089
22104
  model,
22090
- timeoutMs
22105
+ timeoutMs,
22106
+ signal: opts.signal
22091
22107
  });
22092
22108
  raw = result.output;
22093
22109
  generatedBy = result.model;
@@ -23076,6 +23092,8 @@ var SessionCoordinator = class _SessionCoordinator {
23076
23092
  closed = false;
23077
23093
  flushFailures = 0;
23078
23094
  explanationStatus;
23095
+ /** Owns the one provider call this coordinator may have in flight. */
23096
+ explanationAbort;
23079
23097
  /** The stop in force for this session; the registry pushes a change to every live coordinator. */
23080
23098
  cadence;
23081
23099
  idleEndTimer;
@@ -23107,11 +23125,11 @@ var SessionCoordinator = class _SessionCoordinator {
23107
23125
  flushThreshold: 250,
23108
23126
  checkpointEvery: 500,
23109
23127
  explain: envAllows,
23110
- // The registry passes the stored stop; a coordinator loaded without one keeps the behaviour
23111
- // that shipped, which is a fresh explanation at every turn end.
23112
- cadence: envAllows ? "turn" : "off",
23128
+ // Fail closed when a caller does not pass a stored preference. Model work must always be an
23129
+ // explicit opt-in, including in future coordinator call sites that bypass the registry.
23130
+ cadence: "off",
23113
23131
  idleEndMs: IDLE_END_MS,
23114
- explainSession: explainWithStatus,
23132
+ explainSession: (state2, signal) => explainWithStatus(state2, { signal }),
23115
23133
  now: Date.now,
23116
23134
  ...args.options
23117
23135
  };
@@ -23282,6 +23300,7 @@ var SessionCoordinator = class _SessionCoordinator {
23282
23300
  this.cadence = cadence;
23283
23301
  if (cadence === "off") {
23284
23302
  this.clearIdleEnd();
23303
+ this.explanationAbort?.abort();
23285
23304
  this.explanationStatus = "disabled";
23286
23305
  } else if (wasOff) {
23287
23306
  this.explanationStatus = void 0;
@@ -23341,7 +23360,7 @@ var SessionCoordinator = class _SessionCoordinator {
23341
23360
  }
23342
23361
  if (this.state.internal)
23343
23362
  return;
23344
- if (this.explanationStatus === "generating")
23363
+ if (this.explanationAbort)
23345
23364
  return;
23346
23365
  if (this.explainedSeq === this.state.latestSeq)
23347
23366
  return;
@@ -23352,14 +23371,22 @@ var SessionCoordinator = class _SessionCoordinator {
23352
23371
  this.explanationStatus = "generating";
23353
23372
  this.scheduleSummary();
23354
23373
  const seq = this.state.latestSeq;
23355
- void this.opts.explainSession(this.state).then((result) => {
23374
+ const controller = new AbortController();
23375
+ this.explanationAbort = controller;
23376
+ void this.opts.explainSession(this.state, controller.signal).then((result) => {
23377
+ if (controller.signal.aborted)
23378
+ return;
23356
23379
  if (result.status === "generated")
23357
23380
  this.ingest([result.event]);
23358
23381
  this.explanationStatus = result.status;
23359
23382
  this.explainedSeq = result.status === "generated" ? this.state.latestSeq : seq;
23360
23383
  }).catch(() => {
23384
+ if (controller.signal.aborted)
23385
+ return;
23361
23386
  this.explanationStatus = "failed";
23362
23387
  }).finally(() => {
23388
+ if (this.explanationAbort === controller)
23389
+ this.explanationAbort = void 0;
23363
23390
  this.scheduleSummary();
23364
23391
  });
23365
23392
  }
@@ -23434,6 +23461,7 @@ var SessionCoordinator = class _SessionCoordinator {
23434
23461
  close() {
23435
23462
  this.closed = true;
23436
23463
  this.clearIdleEnd();
23464
+ this.explanationAbort?.abort();
23437
23465
  if (this.summaryTimer)
23438
23466
  clearTimeout(this.summaryTimer);
23439
23467
  this.checkpoint();
@@ -23465,7 +23493,7 @@ var SessionRegistry = class {
23465
23493
  * The stop every coordinator is loaded with. Held here rather than read from the store on each
23466
23494
  * load: it is one value for the whole daemon, and a coordinator is created on the ingest path.
23467
23495
  */
23468
- explainerCadence = "turn";
23496
+ explainerCadence = "off";
23469
23497
  /** Handed to every coordinator this registry loads; see `CoordinatorOptions.now`. */
23470
23498
  now;
23471
23499
  /** Reads the daemon's current helper routing at call time, so settings apply without a restart. */
@@ -25450,7 +25478,7 @@ var VERSION = (() => {
25450
25478
  }
25451
25479
  })();
25452
25480
  var DEFAULT_SETTINGS = {
25453
- explainerCadence: "turn",
25481
+ explainerCadence: "off",
25454
25482
  explainerBackend: "auto",
25455
25483
  explainerModel: null
25456
25484
  };
@@ -25548,9 +25576,9 @@ async function startDaemon(overrides = {}) {
25548
25576
  const activeExplainer = () => explainedConfiguration(stored.explainerBackend, stored.explainerModel, process.env);
25549
25577
  const registry2 = new SessionRegistry(store, {
25550
25578
  explainerCadence: effectiveCadence(stored.explainerCadence),
25551
- explainSession: (state) => {
25579
+ explainSession: (state, signal) => {
25552
25580
  const active = activeExplainer();
25553
- return explainWithStatus(state, { mode: active.mode, model: active.model });
25581
+ return explainWithStatus(state, { mode: active.mode, model: active.model, signal });
25554
25582
  },
25555
25583
  ...overrides.now ? { now: overrides.now } : {}
25556
25584
  });
@@ -25915,6 +25943,36 @@ function renderAudit(r, opts) {
25915
25943
  `;
25916
25944
  }
25917
25945
 
25946
+ // src/explanationMode.ts
25947
+ var LOCAL_ONLY = {
25948
+ value: "off",
25949
+ label: "Local only",
25950
+ detail: "No model calls"
25951
+ };
25952
+ var WHEN_DONE = {
25953
+ value: "session",
25954
+ label: "When done",
25955
+ detail: "One model call after a session ends"
25956
+ };
25957
+ var EACH_REPLY = {
25958
+ value: "turn",
25959
+ label: "Each reply",
25960
+ detail: "One model call after each agent reply"
25961
+ };
25962
+ var EXPLANATION_MODES = [LOCAL_ONLY, WHEN_DONE, EACH_REPLY];
25963
+ function explanationMode(cadence) {
25964
+ if (cadence === "session") return WHEN_DONE;
25965
+ if (cadence === "turn") return EACH_REPLY;
25966
+ return LOCAL_ONLY;
25967
+ }
25968
+ function parseExplanationMode(value) {
25969
+ const normalized2 = value?.trim().toLowerCase();
25970
+ if (normalized2 === "off" || normalized2 === "local" || normalized2 === "local-only") return "off";
25971
+ if (normalized2 === "session" || normalized2 === "when-done") return "session";
25972
+ if (normalized2 === "turn" || normalized2 === "each-reply") return "turn";
25973
+ return void 0;
25974
+ }
25975
+
25918
25976
  // src/integrations.ts
25919
25977
  import { existsSync as existsSync8 } from "node:fs";
25920
25978
  import { join as join11 } from "node:path";
@@ -26265,11 +26323,164 @@ function integrationById(id) {
26265
26323
  return providerIntegrationRegistry.get(id);
26266
26324
  }
26267
26325
 
26326
+ // src/terminalUi.ts
26327
+ var RESET = "\x1B[0m";
26328
+ var ANSI2 = {
26329
+ bold: "\x1B[1m",
26330
+ accent: "\x1B[38;2;143;166;255m",
26331
+ accentBackground: "\x1B[48;2;59;91;219m\x1B[38;2;255;255;255m",
26332
+ textMuted: "\x1B[38;2;142;142;139m",
26333
+ rail: "\x1B[38;2;91;91;88m",
26334
+ ok: "\x1B[38;2;95;207;136m",
26335
+ warn: "\x1B[38;2;224;168;60m",
26336
+ danger: "\x1B[38;2;240;115;106m",
26337
+ claude: "\x1B[38;2;231;155;125m",
26338
+ codex: "\x1B[38;2;79;196;163m"
26339
+ };
26340
+ function paint(enabled, code, value) {
26341
+ return enabled ? `${code}${value}${RESET}` : value;
26342
+ }
26343
+ var TerminalUi = class {
26344
+ color;
26345
+ constructor(color = false) {
26346
+ this.color = color;
26347
+ }
26348
+ tone(value, tone) {
26349
+ const code = tone === "muted" ? ANSI2.textMuted : tone === "claude" ? ANSI2.claude : tone === "codex" ? ANSI2.codex : ANSI2[tone];
26350
+ return paint(this.color, code, value);
26351
+ }
26352
+ bold(value) {
26353
+ return paint(this.color, ANSI2.bold, value);
26354
+ }
26355
+ rail(glyph) {
26356
+ return paint(this.color, ANSI2.rail, glyph);
26357
+ }
26358
+ header(firstRun) {
26359
+ const brand = paint(this.color, ANSI2.accentBackground, " SALIDIUM ");
26360
+ const mode = this.tone(firstRun ? "FIRST-RUN SETUP" : "AGENT SETUP", "muted");
26361
+ return `
26362
+ ${brand} ${mode}
26363
+ ${this.tone("Connect your coding agents", "muted")}
26364
+ `;
26365
+ }
26366
+ section(label) {
26367
+ return `
26368
+ ${this.tone("\u25C6", "accent")} ${this.bold(label.toUpperCase())}
26369
+ `;
26370
+ }
26371
+ copy(text) {
26372
+ return ` ${this.rail("\u2502")} ${text}
26373
+ `;
26374
+ }
26375
+ spacer() {
26376
+ return ` ${this.rail("\u2502")}
26377
+ `;
26378
+ }
26379
+ status(mark2, label, detail, tone) {
26380
+ return ` ${this.rail("\u2502")} ${this.tone(mark2, tone)} ${this.bold(label.padEnd(14))} ${this.tone(detail, "muted")}
26381
+ `;
26382
+ }
26383
+ path(provider, providerId, value) {
26384
+ const tone = providerId === "claude-code" ? "claude" : "codex";
26385
+ return [
26386
+ ` ${this.rail("\u2502")} ${this.tone(provider, tone)}
26387
+ `,
26388
+ ` ${this.rail("\u2502")} ${this.rail("\u2514\u2500")} ${this.tone(value, "muted")}
26389
+ `
26390
+ ].join("");
26391
+ }
26392
+ attention(text) {
26393
+ return ` ${this.rail("\u2502")} ${this.tone("!", "warn")} ${text}
26394
+ `;
26395
+ }
26396
+ item(mark2, text, tone) {
26397
+ return ` ${this.rail("\u2502")} ${this.tone(mark2, tone)} ${text}
26398
+ `;
26399
+ }
26400
+ failure(text) {
26401
+ return ` ${this.rail("\u2502")} ${this.tone("\xD7", "danger")} ${text}
26402
+ `;
26403
+ }
26404
+ choices(label, options, selectedIndex) {
26405
+ const selected = (value) => this.color ? paint(true, ANSI2.accentBackground, ` ${value} `) : `[ ${value} ]`;
26406
+ const idle = (value) => this.tone(` ${value} `, "muted");
26407
+ const rendered = options.map((option, index) => index === selectedIndex ? selected(option) : idle(option)).join(" ");
26408
+ return ` ${this.rail("\u251C\u2500")} ${this.bold(label)} ${rendered}`;
26409
+ }
26410
+ choice(label, yes) {
26411
+ return this.choices(label, ["No", "Yes"], yes ? 1 : 0);
26412
+ }
26413
+ close(mark2, text, tone = "muted") {
26414
+ return ` ${this.rail("\u2514\u2500")} ${this.tone(mark2, tone)} ${text}
26415
+ `;
26416
+ }
26417
+ open(url2, opened, firstRun = false) {
26418
+ const label = opened ? "OPENED" : "LOCAL URL";
26419
+ const mark2 = opened ? "\u2197" : "\u2192";
26420
+ const urlRail = firstRun ? "\u251C\u2500" : "\u2514\u2500";
26421
+ const next = firstRun ? ` ${this.rail("\u2514\u2500")} ${this.tone("NEXT", "muted")} ${this.bold("npx salidium")}
26422
+ ` : "";
26423
+ return `
26424
+ ${this.tone("\u25C6", "accent")} ${this.bold(label)}
26425
+ ${this.rail(urlRail)} ${this.tone(mark2, "accent")} ${this.tone(url2, "accent")}
26426
+ ${next}
26427
+ `;
26428
+ }
26429
+ running(explanations, detail) {
26430
+ return [
26431
+ this.section("Running"),
26432
+ this.status("\u2713", "Salidium", "Background service is active", "ok"),
26433
+ this.status(
26434
+ "\u25CF",
26435
+ "Explanations",
26436
+ `${explanations} \xB7 ${detail}`,
26437
+ explanations === "Local only" ? "ok" : "accent"
26438
+ ),
26439
+ this.item("\u2192", "Stop service: salidium stop", "muted"),
26440
+ this.close("\u2192", "Stop model calls: salidium explanations off")
26441
+ ].join("");
26442
+ }
26443
+ };
26444
+ function supportsTerminalColor(isTty, environment2 = process.env) {
26445
+ return isTty && !("NO_COLOR" in environment2) && environment2.TERM !== "dumb";
26446
+ }
26447
+ function homeRelative(path, userHome2) {
26448
+ if (path === userHome2) return "~";
26449
+ return path.startsWith(`${userHome2}/`) ? `~${path.slice(userHome2.length)}` : path;
26450
+ }
26451
+ function selectionKeyResult(key, selectedIndex, optionCount) {
26452
+ const last = Math.max(0, optionCount - 1);
26453
+ const selected = Math.max(0, Math.min(last, selectedIndex));
26454
+ if (key === "") return { selectedIndex: selected, aborted: true };
26455
+ if (key === "\r" || key === "\n") return { selectedIndex: selected, decision: selected };
26456
+ if (key === "\x1B") return { selectedIndex: 0, decision: 0 };
26457
+ if (key === "\x1B[D") return { selectedIndex: Math.max(0, selected - 1) };
26458
+ if (key === "\x1B[C" || key === " " || key === " ")
26459
+ return { selectedIndex: Math.min(last, selected + 1) };
26460
+ if (/^[1-9]$/.test(key)) {
26461
+ const direct = Number(key) - 1;
26462
+ if (direct <= last) return { selectedIndex: direct, decision: direct };
26463
+ }
26464
+ return { selectedIndex: selected };
26465
+ }
26466
+ function consentKeyResult(key, selected) {
26467
+ if (key === "y" || key === "Y") return { selected: true, decision: true };
26468
+ if (key === "n" || key === "N") return { selected: false, decision: false };
26469
+ if (key === " " || key === " ") return { selected: !selected };
26470
+ const result = selectionKeyResult(key, selected ? 1 : 0, 2);
26471
+ return {
26472
+ selected: result.selectedIndex === 1,
26473
+ ...result.decision === void 0 ? {} : { decision: result.decision === 1 },
26474
+ ...result.aborted ? { aborted: true } : {}
26475
+ };
26476
+ }
26477
+
26268
26478
  // src/onboarding.ts
26269
26479
  function names(providers) {
26270
26480
  return providers.map((provider) => provider.name).join(", ");
26271
26481
  }
26272
26482
  async function runFirstRunOnboarding(context, io, options = {}) {
26483
+ const ui = new TerminalUi(io.color);
26273
26484
  const integrations = options.integrations ?? providerIntegrations;
26274
26485
  const detected = integrations.filter((provider) => provider.detect(context).detected);
26275
26486
  const hookCapable = detected.filter((provider) => provider.liveHooksSupported(context));
@@ -26284,45 +26495,66 @@ async function runFirstRunOnboarding(context, io, options = {}) {
26284
26495
  const status = inspections.get(provider.id)?.status;
26285
26496
  return status === "not-configured" || status === "partial";
26286
26497
  });
26287
- const configured = hookCapable.filter(
26288
- (provider) => inspections.get(provider.id)?.status === "configured"
26289
- );
26290
26498
  const shouldDescribe = Boolean(options.firstRun || pending.length || invalid.length);
26291
26499
  if (shouldDescribe) {
26292
- io.write(
26293
- detected.length > 0 ? `Detected: ${names(detected)}.
26294
- ` : "Detected: no supported coding agents. Salidium will watch for Claude Code or Codex history when available.\n"
26295
- );
26500
+ io.write(ui.header(Boolean(options.firstRun)));
26501
+ io.write(ui.section("Agents"));
26502
+ if (detected.length === 0) {
26503
+ io.write(ui.item("\u25CB", "No supported coding agents detected", "muted"));
26504
+ io.write(ui.close("\u2192", "Start Claude Code or Codex, then run Salidium again", "accent"));
26505
+ } else {
26506
+ for (const provider of detected) {
26507
+ const inspection = inspections.get(provider.id);
26508
+ const detail = inspection?.status === "configured" ? "Connected" : provider.liveHooksSupported(context) ? "Detected" : "History only";
26509
+ io.write(
26510
+ ui.status(
26511
+ inspection?.status === "configured" ? "\u2713" : "\u25CF",
26512
+ provider.name,
26513
+ detail,
26514
+ provider.id === "claude-code" ? "claude" : "codex"
26515
+ )
26516
+ );
26517
+ }
26518
+ }
26296
26519
  if (historyOnly.length > 0) {
26520
+ io.write(ui.spacer());
26297
26521
  io.write(
26298
- `History-only on native Windows: ${names(historyOnly)} transcripts are imported, but POSIX live hooks are not installed.
26299
- `
26522
+ ui.attention(
26523
+ `Native Windows imports ${names(historyOnly)} history; live POSIX hooks are unavailable`
26524
+ )
26300
26525
  );
26301
26526
  }
26302
- if (configured.length > 0) io.write(`Already connected: ${names(configured)}.
26303
- `);
26304
26527
  for (const provider of invalid) {
26305
26528
  const inspection = inspections.get(provider.id);
26529
+ io.write(ui.spacer());
26306
26530
  io.write(
26307
- `Needs attention: ${provider.name} configuration was not changed because ${inspection?.issue ?? "it could not be read safely"}.
26308
- `
26531
+ ui.attention(
26532
+ `${provider.name} was not changed: ${inspection?.issue ?? "its settings could not be read safely"}`
26533
+ )
26309
26534
  );
26310
26535
  }
26311
26536
  }
26312
26537
  let consent = "not-needed";
26538
+ let explainerCadence;
26313
26539
  const changed = [];
26314
26540
  const guidance = [];
26315
26541
  if (pending.length > 0) {
26316
- io.write("Permission requested: add Salidium hooks while preserving existing settings in:\n");
26317
- for (const provider of pending) {
26318
- io.write(` ${provider.name}: ${inspections.get(provider.id)?.settingsPath}
26319
- `);
26320
- }
26542
+ io.write(ui.section("Permission"));
26543
+ io.write(ui.copy("Salidium will add its hooks. Existing settings stay intact."));
26544
+ io.write(ui.spacer());
26545
+ for (const [index, provider] of pending.entries()) {
26546
+ const settingsPath3 = inspections.get(provider.id)?.settingsPath;
26547
+ if (settingsPath3)
26548
+ io.write(ui.path(provider.name, provider.id, homeRelative(settingsPath3, context.userHome)));
26549
+ if (index < pending.length - 1) io.write(ui.spacer());
26550
+ }
26551
+ io.write(ui.spacer());
26321
26552
  let approved = Boolean(options.assumeYes);
26322
26553
  if (options.assumeYes) {
26323
26554
  consent = "approved";
26324
26555
  } else if (io.interactive) {
26325
- approved = await io.confirm(`Connect ${names(pending)}? [y/N] `);
26556
+ const question = pending.length === 1 ? `Connect ${pending[0]?.name}?` : "Connect both agents?";
26557
+ approved = await io.confirm(question);
26326
26558
  consent = approved ? "approved" : "declined";
26327
26559
  } else {
26328
26560
  consent = "non-interactive";
@@ -26333,36 +26565,80 @@ async function runFirstRunOnboarding(context, io, options = {}) {
26333
26565
  const result = provider.install(context);
26334
26566
  changed.push(result);
26335
26567
  io.write(
26336
- result.changed ? `Connected: ${provider.name}.
26337
- ` : `Connected: ${provider.name} (no changes needed).
26338
- `
26568
+ ui.status("\u2713", provider.name, result.changed ? "Connected" : "Already connected", "ok")
26339
26569
  );
26340
26570
  guidance.push(...provider.guidance(result));
26341
26571
  } catch (error51) {
26342
26572
  io.write(
26343
- `Needs attention: ${provider.name} could not be connected: ${error51 instanceof Error ? error51.message : String(error51)}.
26344
- `
26573
+ ui.failure(
26574
+ `${provider.name} could not be connected: ${error51 instanceof Error ? error51.message : String(error51)}`
26575
+ )
26345
26576
  );
26346
26577
  }
26347
26578
  }
26348
26579
  } else if (consent === "non-interactive") {
26349
26580
  io.write(
26350
- "No provider settings changed because this terminal is non-interactive. Re-run with --yes to approve setup, or use salidium install-hooks later.\n"
26581
+ ui.close(
26582
+ "\u25CB",
26583
+ "No changes made. Re-run with --yes, or use salidium install-hooks later.",
26584
+ "muted"
26585
+ )
26351
26586
  );
26352
26587
  } else {
26353
- io.write("No provider settings changed. Use salidium install-hooks when you are ready.\n");
26588
+ io.write(ui.close("\u25CB", "No changes made. Use salidium install-hooks when ready.", "muted"));
26589
+ }
26590
+ }
26591
+ if (options.firstRun) {
26592
+ io.write(ui.section("Explanations"));
26593
+ io.write(ui.copy("Reports, evidence, and quantities stay local."));
26594
+ io.write(ui.copy("Only the written Why and How can call a model."));
26595
+ io.write(ui.spacer());
26596
+ let selectedIndex = 0;
26597
+ if (io.interactive && !options.assumeYes) {
26598
+ selectedIndex = await io.select(
26599
+ "Written Why + How",
26600
+ EXPLANATION_MODES.map((mode2) => mode2.label),
26601
+ 0
26602
+ );
26354
26603
  }
26604
+ explainerCadence = EXPLANATION_MODES[selectedIndex]?.value ?? "off";
26605
+ const mode = explanationMode(explainerCadence);
26606
+ io.write(
26607
+ ui.close(
26608
+ explainerCadence === "off" ? "\u2713" : "\u25CF",
26609
+ `${mode.label} \xB7 ${mode.detail}`,
26610
+ explainerCadence === "off" ? "ok" : "accent"
26611
+ )
26612
+ );
26355
26613
  }
26356
26614
  const validations = detected.flatMap((provider) => provider.validate(context));
26357
26615
  const attention = validations.filter((validation) => validation.level === "attention");
26358
26616
  if (shouldDescribe || changed.length > 0) {
26359
- if (attention.length === 0) io.write("Setup checks passed.\n");
26360
- else for (const validation of attention) io.write(`Needs attention: ${validation.message}.
26361
- `);
26617
+ const ready = [];
26618
+ if (detected.length === 0) {
26619
+ ready.push({ mark: "\u25CB", text: "Waiting for Claude Code or Codex", tone: "muted" });
26620
+ } else if (attention.length === 0) {
26621
+ ready.push({ mark: "\u2713", text: "Setup checks passed", tone: "ok" });
26622
+ } else {
26623
+ for (const validation of attention)
26624
+ ready.push({ mark: "!", text: validation.message, tone: "warn" });
26625
+ }
26626
+ for (const instruction of guidance)
26627
+ ready.push({ mark: "!", text: `Codex: ${instruction}`, tone: "warn" });
26628
+ io.write(ui.section("Ready"));
26629
+ for (const row of ready.slice(0, -1)) io.write(ui.item(row.mark, row.text, row.tone));
26630
+ const last = ready.at(-1);
26631
+ if (last) io.write(ui.close(last.mark, last.text, last.tone));
26362
26632
  }
26363
- for (const instruction of guidance) io.write(`Codex requires one more action: ${instruction}
26364
- `);
26365
- return { detected, changed, guidance, validations, consent };
26633
+ return {
26634
+ detected,
26635
+ changed,
26636
+ guidance,
26637
+ validations,
26638
+ consent,
26639
+ ...explainerCadence ? { explainerCadence } : {},
26640
+ presented: shouldDescribe || changed.length > 0
26641
+ };
26366
26642
  }
26367
26643
 
26368
26644
  // src/render.ts
@@ -26555,6 +26831,9 @@ Usage:
26555
26831
  salidium stop Stop the background daemon
26556
26832
  salidium restart Stop it, start it again, and open the UI (--no-open to skip)
26557
26833
  salidium status Show daemon status
26834
+ salidium explanations Show whether written explanations can call a model
26835
+ salidium explanations off|when-done|each-reply
26836
+ Change model-call frequency without stopping local reports
26558
26837
  salidium open Open the UI in your browser
26559
26838
  salidium show [session] Print the report for a session as text (default: most recent)
26560
26839
  --detail=summary|detail|source, --width=N
@@ -26631,35 +26910,70 @@ async function main(argv) {
26631
26910
  }
26632
26911
  case "start": {
26633
26912
  const running = await ensureDaemon();
26913
+ const explanations = await currentExplanationState(running, "reachable");
26634
26914
  process.stdout.write(
26635
26915
  `daemon running on http://127.0.0.1:${running.port} (pid ${running.pid})
26916
+ Explanations: ${explanationStateLabel(explanations)}
26636
26917
  `
26637
26918
  );
26638
26919
  return 0;
26639
26920
  }
26640
26921
  case "up": {
26641
26922
  const context = { userHome, salidiumHome };
26642
- await runFirstRunOnboarding(
26923
+ const color = supportsTerminalColor(Boolean(process.stdout.isTTY));
26924
+ const firstRun = !existsSync9(daemonPaths(salidiumHome).db);
26925
+ const onboarding = await runFirstRunOnboarding(
26643
26926
  context,
26644
26927
  {
26645
26928
  interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
26646
- confirm: confirmSetup,
26929
+ color,
26930
+ confirm: (question) => confirmSetup(question, color),
26931
+ select: (question, options, selectedIndex) => selectTerminalOption(question, options, selectedIndex, color),
26647
26932
  write: (text) => process.stdout.write(text)
26648
26933
  },
26649
26934
  {
26650
26935
  assumeYes,
26651
- firstRun: !existsSync9(daemonPaths(salidiumHome).db)
26936
+ firstRun
26652
26937
  }
26653
26938
  );
26654
- for (const validation of essentialValidations()) {
26655
- if (validation.level === "attention")
26939
+ if (onboarding.explainerCadence) {
26940
+ writeSettings(salidiumHome, {
26941
+ ...readSettings(salidiumHome),
26942
+ explainerCadence: onboarding.explainerCadence
26943
+ });
26944
+ }
26945
+ const systemAttention = essentialValidations().filter(
26946
+ (validation) => validation.level === "attention"
26947
+ );
26948
+ if (systemAttention.length > 0 && onboarding.presented) {
26949
+ const ui2 = new TerminalUi(color);
26950
+ process.stdout.write(ui2.section("System"));
26951
+ for (const validation of systemAttention.slice(0, -1))
26952
+ process.stdout.write(ui2.item("!", validation.message, "warn"));
26953
+ const last = systemAttention.at(-1);
26954
+ if (last) process.stdout.write(ui2.close("!", last.message, "warn"));
26955
+ } else {
26956
+ for (const validation of systemAttention)
26656
26957
  process.stdout.write(`Needs attention: ${validation.message}.
26657
26958
  `);
26658
26959
  }
26659
26960
  const running = await ensureDaemon();
26660
26961
  if (!noOpen && process.stdout.isTTY) openBrowser(uiUrl(running));
26661
- process.stdout.write(`${uiUrl(running)}
26962
+ const url2 = uiUrl(running);
26963
+ const ui = new TerminalUi(color);
26964
+ const state = await currentExplanationState(running, "reachable");
26965
+ if (onboarding.presented) {
26966
+ process.stdout.write(
26967
+ ui.open(url2, !noOpen && Boolean(process.stdout.isTTY), Boolean(firstRun))
26968
+ );
26969
+ } else if (process.stdout.isTTY) {
26970
+ const mode = explanationMode(state.effective);
26971
+ process.stdout.write(ui.running(mode.label, mode.detail));
26972
+ process.stdout.write(ui.open(url2, !noOpen));
26973
+ } else {
26974
+ process.stdout.write(`${url2}
26662
26975
  `);
26976
+ }
26663
26977
  return 0;
26664
26978
  }
26665
26979
  case "open": {
@@ -26729,6 +27043,13 @@ async function main(argv) {
26729
27043
  ` : `daemon (pid ${stopped.pid}) was asked to stop and is still running
26730
27044
  `
26731
27045
  );
27046
+ const storedMode = readSettings(salidiumHome).explainerCadence;
27047
+ if (storedMode !== "off") {
27048
+ process.stdout.write(
27049
+ `Explanations remain set to ${explanationMode(storedMode).label} for the next start. Disable them with: salidium explanations off
27050
+ `
27051
+ );
27052
+ }
26732
27053
  return stopped === void 0 || stopped.signaled && stopped.exited ? 0 : 1;
26733
27054
  }
26734
27055
  /*
@@ -26761,9 +27082,13 @@ async function main(argv) {
26761
27082
  `);
26762
27083
  const running = await ensureDaemon();
26763
27084
  if (!noOpen) openBrowser(uiUrl(running));
26764
- process.stdout.write(`daemon running (pid ${running.pid})
27085
+ const explanations = await currentExplanationState(running, "reachable");
27086
+ process.stdout.write(
27087
+ `daemon running (pid ${running.pid})
27088
+ Explanations: ${explanationStateLabel(explanations)}
26765
27089
  ${uiUrl(running)}
26766
- `);
27090
+ `
27091
+ );
26767
27092
  return 0;
26768
27093
  }
26769
27094
  case "status": {
@@ -26773,6 +27098,9 @@ ${uiUrl(running)}
26773
27098
  d && presence !== "absent" ? `running: pid ${d.pid}, port ${d.port}, since ${d.startedAt}${presence === "unresponsive" ? "; not answering" : ""}
26774
27099
  ` : "not running\n"
26775
27100
  );
27101
+ const explanations = await currentExplanationState(d, presence);
27102
+ process.stdout.write(`Explanations: ${explanationStateLabel(explanations)}
27103
+ `);
26776
27104
  const context = { userHome, salidiumHome };
26777
27105
  for (const provider of providerIntegrations) {
26778
27106
  const detection = provider.detect(context);
@@ -26782,6 +27110,75 @@ ${uiUrl(running)}
26782
27110
  }
26783
27111
  return presence === "reachable" ? 0 : 1;
26784
27112
  }
27113
+ case "explanations": {
27114
+ const d = readDaemonJson(salidiumHome);
27115
+ const presence = await presenceOf(d);
27116
+ if (!arg) {
27117
+ const state2 = await currentExplanationState(d, presence);
27118
+ const mode = explanationMode(state2.effective);
27119
+ process.stdout.write(
27120
+ `Explanations: ${explanationStateLabel(state2)}
27121
+ ${mode.detail}. Reports, evidence, and quantities stay local.
27122
+ Change with: salidium explanations off|when-done|each-reply
27123
+ `
27124
+ );
27125
+ return 0;
27126
+ }
27127
+ const cadence = parseExplanationMode(arg);
27128
+ if (!cadence) {
27129
+ process.stderr.write(
27130
+ "Choose off, when-done, or each-reply. Example: salidium explanations off\n"
27131
+ );
27132
+ return 2;
27133
+ }
27134
+ if (presence === "unresponsive") {
27135
+ process.stderr.write(
27136
+ `daemon pid ${d?.pid ?? "unknown"} is running but did not answer; the setting was not changed
27137
+ `
27138
+ );
27139
+ return 1;
27140
+ }
27141
+ let state;
27142
+ if (d && presence === "reachable") {
27143
+ try {
27144
+ const response = await fetch(`http://127.0.0.1:${d.port}/api/settings/explainer`, {
27145
+ method: "PUT",
27146
+ headers: {
27147
+ Authorization: `Bearer ${d.token}`,
27148
+ "Content-Type": "application/json"
27149
+ },
27150
+ body: JSON.stringify({ cadence }),
27151
+ signal: AbortSignal.timeout(2e3)
27152
+ });
27153
+ if (!response.ok) {
27154
+ process.stderr.write(`daemon refused the explanation setting (${response.status})
27155
+ `);
27156
+ return 1;
27157
+ }
27158
+ const settings = ExplainerSettingsSchema.parse(await response.json());
27159
+ state = explanationStateFromApi(settings);
27160
+ } catch {
27161
+ process.stderr.write(
27162
+ "daemon stopped answering; the explanation setting was not changed\n"
27163
+ );
27164
+ return 1;
27165
+ }
27166
+ } else {
27167
+ writeSettings(salidiumHome, {
27168
+ ...readSettings(salidiumHome),
27169
+ explainerCadence: cadence
27170
+ });
27171
+ state = localExplanationState();
27172
+ }
27173
+ const chosen = explanationMode(cadence);
27174
+ process.stdout.write(`Saved: ${chosen.label} \xB7 ${chosen.detail}
27175
+ `);
27176
+ if (state.effective !== cadence)
27177
+ process.stdout.write(
27178
+ "Local only is active because the daemon environment prevents model calls.\n"
27179
+ );
27180
+ return 0;
27181
+ }
26785
27182
  case "install-hooks":
26786
27183
  case "uninstall-hooks": {
26787
27184
  const remove = cmd === "uninstall-hooks";
@@ -27096,6 +27493,36 @@ function formatBytes(bytes) {
27096
27493
  function uiUrl(d) {
27097
27494
  return `http://127.0.0.1:${d.port}/#token=${d.token}`;
27098
27495
  }
27496
+ function explanationStateFromApi(settings) {
27497
+ return {
27498
+ stored: settings.cadence,
27499
+ effective: settings.envOff ? "off" : settings.cadence,
27500
+ envOff: settings.envOff
27501
+ };
27502
+ }
27503
+ function localExplanationState() {
27504
+ const stored = readSettings(salidiumHome).explainerCadence;
27505
+ const effective = effectiveCadence(stored);
27506
+ return { stored, effective, envOff: effective !== stored };
27507
+ }
27508
+ async function currentExplanationState(daemon, presence) {
27509
+ if (daemon && presence === "reachable") {
27510
+ try {
27511
+ const response = await fetch(`http://127.0.0.1:${daemon.port}/api/settings/explainer`, {
27512
+ headers: { Authorization: `Bearer ${daemon.token}` },
27513
+ signal: AbortSignal.timeout(2e3)
27514
+ });
27515
+ if (response.ok)
27516
+ return explanationStateFromApi(ExplainerSettingsSchema.parse(await response.json()));
27517
+ } catch {
27518
+ }
27519
+ }
27520
+ return localExplanationState();
27521
+ }
27522
+ function explanationStateLabel(state) {
27523
+ const mode = explanationMode(state.effective);
27524
+ return `${mode.label}${state.envOff && state.stored !== "off" ? " (forced by environment)" : ""} \xB7 ${mode.detail}`;
27525
+ }
27099
27526
  async function alive(d) {
27100
27527
  try {
27101
27528
  const res = await fetch(`http://127.0.0.1:${d.port}/api/info`, {
@@ -27263,14 +27690,94 @@ function openBrowser(url2) {
27263
27690
  } catch {
27264
27691
  }
27265
27692
  }
27266
- async function confirmSetup(question) {
27267
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
27268
- try {
27269
- const answer = await prompt.question(question);
27270
- return /^(y|yes)$/i.test(answer.trim());
27271
- } finally {
27272
- prompt.close();
27693
+ async function confirmSetup(question, color) {
27694
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
27695
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
27696
+ try {
27697
+ const answer = await prompt.question(`${question} [y/N] `);
27698
+ return /^(y|yes)$/i.test(answer.trim());
27699
+ } finally {
27700
+ prompt.close();
27701
+ }
27273
27702
  }
27703
+ const ui = new TerminalUi(color);
27704
+ const input = process.stdin;
27705
+ const output = process.stdout;
27706
+ const wasRaw = Boolean(input.isRaw);
27707
+ let selected = false;
27708
+ const render = () => output.write(`\x1B[?25l\r\x1B[2K${ui.choice(question, selected)}`);
27709
+ return new Promise((resolve2, reject) => {
27710
+ const finish = (decision) => {
27711
+ input.off("data", onData);
27712
+ input.setRawMode(wasRaw);
27713
+ if (!wasRaw) input.pause();
27714
+ render();
27715
+ output.write("\x1B[?25h\n");
27716
+ resolve2(decision);
27717
+ };
27718
+ const onData = (data) => {
27719
+ const result = consentKeyResult(String(data), selected);
27720
+ selected = result.selected;
27721
+ if (result.aborted) {
27722
+ input.off("data", onData);
27723
+ input.setRawMode(wasRaw);
27724
+ if (!wasRaw) input.pause();
27725
+ output.write("\x1B[?25h\n");
27726
+ reject(new Error("Aborted with Ctrl+C"));
27727
+ return;
27728
+ }
27729
+ if (result.decision !== void 0) {
27730
+ finish(result.decision);
27731
+ return;
27732
+ }
27733
+ render();
27734
+ };
27735
+ input.setRawMode(true);
27736
+ input.resume();
27737
+ input.on("data", onData);
27738
+ render();
27739
+ });
27740
+ }
27741
+ async function selectTerminalOption(question, options, initialIndex, color) {
27742
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return initialIndex;
27743
+ const ui = new TerminalUi(color);
27744
+ const input = process.stdin;
27745
+ const output = process.stdout;
27746
+ const wasRaw = Boolean(input.isRaw);
27747
+ let selectedIndex = initialIndex;
27748
+ const render = () => output.write(`\x1B[?25l\r\x1B[2K${ui.choices(question, options, selectedIndex)}`);
27749
+ return new Promise((resolve2, reject) => {
27750
+ const finish = (decision) => {
27751
+ input.off("data", onData);
27752
+ input.setRawMode(wasRaw);
27753
+ if (!wasRaw) input.pause();
27754
+ selectedIndex = decision;
27755
+ render();
27756
+ output.write("\x1B[?25h\n");
27757
+ resolve2(decision);
27758
+ };
27759
+ const onData = (data) => {
27760
+ const result = selectionKeyResult(String(data), selectedIndex, options.length);
27761
+ selectedIndex = result.selectedIndex;
27762
+ if (result.aborted) {
27763
+ input.off("data", onData);
27764
+ input.setRawMode(wasRaw);
27765
+ if (!wasRaw) input.pause();
27766
+ output.write("\x1B[?25h\n");
27767
+ reject(new Error("Aborted with Ctrl+C"));
27768
+ return;
27769
+ }
27770
+ if (result.decision !== void 0) {
27771
+ finish(result.decision);
27772
+ return;
27773
+ }
27774
+ render();
27775
+ };
27776
+ input.setRawMode(true);
27777
+ input.resume();
27778
+ input.on("data", onData);
27779
+ render();
27780
+ });
27274
27781
  }
27275
27782
  function essentialValidations() {
27276
27783
  const validations = [];
@@ -27333,6 +27840,7 @@ async function doctor() {
27333
27840
  lines.push("settings file is invalid; optional explanations are safely off until it is fixed");
27334
27841
  problems++;
27335
27842
  }
27843
+ lines.push(`explanations ${explanationStateLabel(localExplanationState())}`);
27336
27844
  const context = { userHome, salidiumHome };
27337
27845
  for (const provider of providerIntegrations) {
27338
27846
  const detection = provider.detect(context);