@oriro/orirocli 0.1.12 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -2
  3. package/dist/cli.js +1497 -183
  4. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -1250,37 +1250,37 @@ function registerVoiceSynth(fn) {
1250
1250
  function registerVoiceListen(fn) {
1251
1251
  listener = fn;
1252
1252
  }
1253
- function audioPlayers(file5) {
1254
- if (process.platform === "darwin") return [{ cmd: "afplay", args: [file5] }];
1253
+ function audioPlayers(file6) {
1254
+ if (process.platform === "darwin") return [{ cmd: "afplay", args: [file6] }];
1255
1255
  if (process.platform === "win32")
1256
1256
  return [
1257
- { cmd: "powershell", args: ["-NoProfile", "-c", `(New-Object Media.SoundPlayer '${file5}').PlaySync()`] }
1257
+ { cmd: "powershell", args: ["-NoProfile", "-c", `(New-Object Media.SoundPlayer '${file6}').PlaySync()`] }
1258
1258
  ];
1259
1259
  return [
1260
- { cmd: "aplay", args: ["-q", file5] },
1261
- { cmd: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", file5] },
1262
- { cmd: "paplay", args: [file5] }
1260
+ { cmd: "aplay", args: ["-q", file6] },
1261
+ { cmd: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", file6] },
1262
+ { cmd: "paplay", args: [file6] }
1263
1263
  ];
1264
1264
  }
1265
1265
  function playWav(wav) {
1266
- const file5 = join7(tmpdir(), `oriro-avatar-${process.pid}-${wav.length}.wav`);
1267
- writeFileSync5(file5, wav);
1268
- const players = audioPlayers(file5);
1266
+ const file6 = join7(tmpdir(), `oriro-avatar-${process.pid}-${wav.length}.wav`);
1267
+ writeFileSync5(file6, wav);
1268
+ const players = audioPlayers(file6);
1269
1269
  return new Promise((resolve3) => {
1270
1270
  const tryPlayer = (i) => {
1271
1271
  if (i >= players.length) {
1272
- rmSync(file5, { force: true });
1272
+ rmSync(file6, { force: true });
1273
1273
  return resolve3(false);
1274
1274
  }
1275
1275
  const p = players[i];
1276
1276
  if (!p) {
1277
- rmSync(file5, { force: true });
1277
+ rmSync(file6, { force: true });
1278
1278
  return resolve3(false);
1279
1279
  }
1280
1280
  const child = spawn(p.cmd, p.args, { stdio: "ignore" });
1281
1281
  child.on("error", () => tryPlayer(i + 1));
1282
1282
  child.on("close", (code) => {
1283
- rmSync(file5, { force: true });
1283
+ rmSync(file6, { force: true });
1284
1284
  resolve3(code === 0);
1285
1285
  });
1286
1286
  };
@@ -1317,9 +1317,9 @@ import { existsSync, readFileSync as readFileSync6, rmSync as rmSync2 } from "fs
1317
1317
  function tmpWav() {
1318
1318
  return join8(tmpdir2(), `oriro-tts-${process.pid}-${Date.now()}-${Math.floor(performance.now())}.wav`);
1319
1319
  }
1320
- function readAndClean(file5) {
1321
- const buf = readFileSync6(file5);
1322
- rmSync2(file5, { force: true });
1320
+ function readAndClean(file6) {
1321
+ const buf = readFileSync6(file6);
1322
+ rmSync2(file6, { force: true });
1323
1323
  return new Uint8Array(buf);
1324
1324
  }
1325
1325
  function winSapi(text, lang) {
@@ -1755,13 +1755,6 @@ var ROUTER_CATALOG = [
1755
1755
  freeModels: ["mistralai/Mistral-Small-Instruct"],
1756
1756
  obtainUrl: "https://berget.ai"
1757
1757
  }),
1758
- C4({
1759
- id: "huggingface",
1760
- displayName: "Hugging Face",
1761
- baseUrl: "https://router.huggingface.co/v1",
1762
- freeModels: ["meta-llama/Llama-3.2-3B-Instruct"],
1763
- obtainUrl: "https://huggingface.co/settings/tokens"
1764
- }),
1765
1758
  C4({
1766
1759
  id: "replicate",
1767
1760
  displayName: "Replicate",
@@ -1857,10 +1850,35 @@ var ROUTER_CATALOG = [
1857
1850
  obtainUrl: "https://platform.moonshot.ai",
1858
1851
  tier: "paid"
1859
1852
  }),
1860
- // ── ORIRO models — coming soon, greyed/"(free)", not selectable yet ──
1861
- C4({ id: "oriro-gauss", displayName: "ORIRO-Gauss", baseUrl: "", comingSoon: true }),
1862
- C4({ id: "oriro-avila", displayName: "ORIRO-Avila", baseUrl: "", comingSoon: true })
1853
+ // ── ORIRO's OWN models — LIVE, keyless, first-class racers (2026-07-04) ──
1854
+ // Served through the same-origin oriro.ai worker proxy, which injects the serve key server-side
1855
+ // so the CLI stays keyless (no bearer ever touches the client) the endpoints answer at
1856
+ // baseUrl + "/chat/completions" (race-{gauss,avila}.ts alias). ORIRO-Avila is V2.4 today
1857
+ // (AVILA_SERVE_URL set); ORIRO-Gauss races on the live serve and auto-upgrades to V2.4 the
1858
+ // moment GAUSS_SERVE_URL is flipped — no CLI change needed. Both are true GPU endpoints, so
1859
+ // they only race when the user opts them into the pool (`oriro routers add oriro-gauss`).
1860
+ C4({
1861
+ id: "oriro-gauss",
1862
+ displayName: "ORIRO-Gauss",
1863
+ baseUrl: "https://oriro.ai/api/race/gauss",
1864
+ freeModels: ["gauss"],
1865
+ obtainUrl: "https://oriro.ai",
1866
+ keyless: true,
1867
+ verified: true
1868
+ }),
1869
+ C4({
1870
+ id: "oriro-avila",
1871
+ displayName: "ORIRO-Avila",
1872
+ baseUrl: "https://oriro.ai/api/race/avila",
1873
+ freeModels: ["avila"],
1874
+ obtainUrl: "https://oriro.ai",
1875
+ keyless: true,
1876
+ verified: true
1877
+ })
1863
1878
  ];
1879
+ function selectableRouters() {
1880
+ return ROUTER_CATALOG.filter((r) => !r.comingSoon);
1881
+ }
1864
1882
  function routerById(id) {
1865
1883
  return ROUTER_CATALOG.find((r) => r.id === id);
1866
1884
  }
@@ -3371,7 +3389,7 @@ import {
3371
3389
  } from "@earendil-works/pi-coding-agent";
3372
3390
 
3373
3391
  // src/routers/mux-provider.ts
3374
- import { streamSimple as piStreamSimple, createAssistantMessageEventStream } from "@earendil-works/pi-ai";
3392
+ import { streamSimple as piStreamSimple2, createAssistantMessageEventStream } from "@earendil-works/pi-ai";
3375
3393
  import { register as registerOpenAICompletions } from "@earendil-works/pi-ai/openai-completions";
3376
3394
 
3377
3395
  // src/routers/mux.ts
@@ -3579,8 +3597,8 @@ function artifactsDir() {
3579
3597
  // src/scribe/digest.ts
3580
3598
  var DIGEST_CAP = 8192;
3581
3599
  var TIMELINE_DAY_CAP = 400;
3582
- function read(file5) {
3583
- return existsSync8(file5) ? readFileSync12(file5, "utf8") : "";
3600
+ function read(file6) {
3601
+ return existsSync8(file6) ? readFileSync12(file6, "utf8") : "";
3584
3602
  }
3585
3603
  function updateDigest(summary, context) {
3586
3604
  mkdirSync10(scribeDir(), { recursive: true });
@@ -4118,7 +4136,54 @@ function attachScribe(session) {
4118
4136
  });
4119
4137
  }
4120
4138
 
4121
- // src/routers/mux-provider.ts
4139
+ // src/context/project-md.ts
4140
+ import { existsSync as existsSync13, readFileSync as readFileSync18, statSync as statSync2 } from "fs";
4141
+ import { join as join21, dirname as dirname3, parse } from "path";
4142
+ var NAMES = ["AGENTS.md", "CLAUDE.md", ".oriro/ORIRO.md"];
4143
+ var MAX_BYTES = 32 * 1024;
4144
+ var MAX_LEVELS = 24;
4145
+ function isRoot(dir) {
4146
+ return existsSync13(join21(dir, ".git")) || existsSync13(join21(dir, ".oriro"));
4147
+ }
4148
+ function discoverProjectInstructions(cwd) {
4149
+ const chain = [];
4150
+ let dir = cwd;
4151
+ const rootOfDrive = parse(cwd).root;
4152
+ for (let i = 0; i < MAX_LEVELS; i++) {
4153
+ for (const name of NAMES) {
4154
+ const p = join21(dir, name);
4155
+ try {
4156
+ if (existsSync13(p) && statSync2(p).isFile()) {
4157
+ let text = readFileSync18(p, "utf8");
4158
+ if (text.length > MAX_BYTES) text = text.slice(0, MAX_BYTES) + "\n\u2026(truncated)";
4159
+ text = text.trim();
4160
+ if (text) chain.push({ path: p, text });
4161
+ break;
4162
+ }
4163
+ } catch {
4164
+ }
4165
+ }
4166
+ if (isRoot(dir)) break;
4167
+ const parent = dirname3(dir);
4168
+ if (parent === dir || dir === rootOfDrive) break;
4169
+ dir = parent;
4170
+ }
4171
+ return chain.reverse();
4172
+ }
4173
+ function buildProjectContext(cwd = process.cwd()) {
4174
+ let found;
4175
+ try {
4176
+ found = discoverProjectInstructions(cwd);
4177
+ } catch {
4178
+ return "";
4179
+ }
4180
+ if (!found.length) return "";
4181
+ const blocks = found.map((f) => `# Project instructions \u2014 ${f.path}
4182
+ ${f.text}`);
4183
+ return "The user's project ships these instructions. Treat them as authoritative for work in this repository; when two files conflict, the one listed LAST (nearest the working directory) wins.\n\n" + blocks.join("\n\n");
4184
+ }
4185
+
4186
+ // src/routers/mux-helpers.ts
4122
4187
  var MUX_PROVIDER = "oriro-mux";
4123
4188
  var MUX_MODEL = "oriro-free";
4124
4189
  function errToCallError(msg) {
@@ -4138,6 +4203,145 @@ function buildErrorMessage(message) {
4138
4203
  errorMessage: message
4139
4204
  };
4140
4205
  }
4206
+
4207
+ // src/routers/race.ts
4208
+ import { streamSimple as piStreamSimple } from "@earendil-works/pi-ai";
4209
+
4210
+ // src/routers/race-status.ts
4211
+ var listeners = /* @__PURE__ */ new Set();
4212
+ var current = { phase: "idle", racers: [], winner: null };
4213
+ function emitRaceStatus(s) {
4214
+ current = s;
4215
+ for (const l of listeners) {
4216
+ try {
4217
+ l(s);
4218
+ } catch {
4219
+ }
4220
+ }
4221
+ }
4222
+ function onRaceStatus(l) {
4223
+ listeners.add(l);
4224
+ try {
4225
+ l(current);
4226
+ } catch {
4227
+ }
4228
+ return () => {
4229
+ listeners.delete(l);
4230
+ };
4231
+ }
4232
+ function getRaceStatus() {
4233
+ return current;
4234
+ }
4235
+
4236
+ // src/routers/race.ts
4237
+ var DEFAULT_RACE_WIDTH = 3;
4238
+ var realStreamFactory = (router, context, options, signal) => piStreamSimple(routerModel(router), context, {
4239
+ ...options ?? {},
4240
+ apiKey: router.apiKey,
4241
+ signal
4242
+ });
4243
+ async function raceMux(out, mux, byId, context, options, opts = {}) {
4244
+ const width = opts.width ?? DEFAULT_RACE_WIDTH;
4245
+ const streamFactory = opts.streamFactory ?? realStreamFactory;
4246
+ const push = (ev) => out.push(ev);
4247
+ const ranked = mux.ranked().filter((id) => byId.has(id));
4248
+ if (ranked.length === 0) {
4249
+ const msg = buildErrorMessage("All selected routers are unavailable. Add a BYOK key, select more free routers, or retry shortly.");
4250
+ push({ type: "error", reason: "error", error: msg });
4251
+ out.end(msg);
4252
+ emitRaceStatus({ phase: "failed", racers: [], winner: null });
4253
+ return;
4254
+ }
4255
+ const racers = ranked.slice(0, Math.max(1, Math.min(width, ranked.length)));
4256
+ emitRaceStatus({ phase: "racing", racers, winner: null });
4257
+ const controllers = /* @__PURE__ */ new Map();
4258
+ for (const id of racers) controllers.set(id, new AbortController());
4259
+ const abortLosers = (keep) => {
4260
+ for (const [id, c] of controllers) if (id !== keep) {
4261
+ try {
4262
+ c.abort();
4263
+ } catch {
4264
+ }
4265
+ }
4266
+ };
4267
+ let winner = null;
4268
+ let settled2 = false;
4269
+ let lastError;
4270
+ let remaining = racers.length;
4271
+ return await new Promise((resolve3) => {
4272
+ const failAll = () => {
4273
+ if (settled2) return;
4274
+ settled2 = true;
4275
+ const msg = lastError ?? buildErrorMessage("All racers failed this request.");
4276
+ push({ type: "error", reason: "error", error: msg });
4277
+ out.end(msg);
4278
+ emitRaceStatus({ phase: "failed", racers, winner: null });
4279
+ resolve3();
4280
+ };
4281
+ for (const id of racers) {
4282
+ const router = byId.get(id);
4283
+ const ctrl = controllers.get(id);
4284
+ const t0 = Date.now();
4285
+ void (async () => {
4286
+ let iAmWinner = false;
4287
+ let lastPartial;
4288
+ try {
4289
+ for await (const ev of streamFactory(router, context, options, ctrl.signal)) {
4290
+ if (settled2 && !iAmWinner) return;
4291
+ if (ev.type === "error") {
4292
+ mux.recordFailure(id, ev.error ? errToCallError(ev.error) : {});
4293
+ if (iAmWinner && !settled2) {
4294
+ settled2 = true;
4295
+ push(ev);
4296
+ out.end(ev.error);
4297
+ resolve3();
4298
+ return;
4299
+ }
4300
+ lastError = ev.error ?? lastError;
4301
+ return;
4302
+ }
4303
+ if (!iAmWinner) {
4304
+ if (winner !== null || settled2) return;
4305
+ winner = id;
4306
+ iAmWinner = true;
4307
+ mux.recordSuccess(id, Date.now() - t0);
4308
+ emitRaceStatus({ phase: "won", racers, winner: id });
4309
+ abortLosers(id);
4310
+ }
4311
+ if (ev.type === "done") {
4312
+ if (!settled2) {
4313
+ settled2 = true;
4314
+ const clean = sanitizeMessageToolCalls(scrubMessageIdentity(ev.message));
4315
+ push({ type: "done", reason: ev.reason, message: clean });
4316
+ out.end(clean);
4317
+ resolve3();
4318
+ }
4319
+ return;
4320
+ }
4321
+ lastPartial = ev.partial ?? lastPartial;
4322
+ push(sanitizeEventToolCalls(ev));
4323
+ }
4324
+ if (iAmWinner && !settled2) {
4325
+ settled2 = true;
4326
+ out.end(lastPartial ? sanitizeMessageToolCalls(scrubMessageIdentity(lastPartial)) : void 0);
4327
+ resolve3();
4328
+ } else if (!iAmWinner) {
4329
+ mux.recordFailure(id, {});
4330
+ }
4331
+ } catch (e) {
4332
+ if (e?.name === "AbortError") return;
4333
+ mux.recordFailure(id, e);
4334
+ if (!iAmWinner) lastError ??= buildErrorMessage(e instanceof Error ? e.message : String(e));
4335
+ } finally {
4336
+ remaining -= 1;
4337
+ if (remaining === 0 && !settled2) failAll();
4338
+ }
4339
+ })();
4340
+ }
4341
+ });
4342
+ }
4343
+
4344
+ // src/routers/mux-provider.ts
4141
4345
  async function driveMux(out, mux, byId, context, options) {
4142
4346
  let lastError;
4143
4347
  for (const id of mux.ranked()) {
@@ -4147,7 +4351,7 @@ async function driveMux(out, mux, byId, context, options) {
4147
4351
  let committed = false;
4148
4352
  let lastPartial;
4149
4353
  try {
4150
- const inner = piStreamSimple(routerModel(router), context, {
4354
+ const inner = piStreamSimple2(routerModel(router), context, {
4151
4355
  ...options ?? {},
4152
4356
  apiKey: router.apiKey
4153
4357
  });
@@ -4196,13 +4400,16 @@ async function driveMux(out, mux, byId, context, options) {
4196
4400
  }
4197
4401
  function registerOriroMux(registry, opts = {}) {
4198
4402
  registerOpenAICompletions();
4199
- const pooled = resolvePool();
4200
- const routers = opts.routers ?? (pooled.length > 0 ? pooled : KEYLESS_FLOOR);
4201
- const byId = new Map(routers.map((r) => [r.id, r]));
4202
- const mux = new RouterMux(routers.map((r) => r.id));
4203
- try {
4204
- mux.load(loadMuxState(oriroDir()));
4205
- } catch {
4403
+ function resolveNow() {
4404
+ const pooled = resolvePool();
4405
+ const routers = opts.routers ?? (pooled.length > 0 ? pooled : KEYLESS_FLOOR);
4406
+ const byId = new Map(routers.map((r) => [r.id, r]));
4407
+ const mux = new RouterMux(routers.map((r) => r.id));
4408
+ try {
4409
+ mux.load(loadMuxState(oriroDir()));
4410
+ } catch {
4411
+ }
4412
+ return { routers, byId, mux };
4206
4413
  }
4207
4414
  registry.registerProvider(MUX_PROVIDER, {
4208
4415
  name: "ORIRO Free (keyless Mux)",
@@ -4226,12 +4433,16 @@ function registerOriroMux(registry, opts = {}) {
4226
4433
  ],
4227
4434
  streamSimple: (_model, context, options) => {
4228
4435
  const out = createAssistantMessageEventStream();
4436
+ const { routers, byId, mux } = resolveNow();
4229
4437
  const ctx = applyIdentity(context);
4438
+ const project = buildProjectContext();
4230
4439
  const memory = buildScribeContext();
4231
- const withMemory = memory ? { ...ctx, systemPrompt: `${ctx.systemPrompt}
4440
+ const extra = [project, memory].filter(Boolean).join("\n\n");
4441
+ const withMemory = extra ? { ...ctx, systemPrompt: `${ctx.systemPrompt}
4232
4442
 
4233
- ${memory}` } : ctx;
4234
- void driveMux(out, mux, byId, withMemory, options).finally(() => {
4443
+ ${extra}` } : ctx;
4444
+ const drive = routers.length > 1 ? raceMux(out, mux, byId, withMemory, options) : driveMux(out, mux, byId, withMemory, options);
4445
+ void drive.finally(() => {
4235
4446
  try {
4236
4447
  saveMuxState(oriroDir(), mux.snapshot());
4237
4448
  } catch {
@@ -4554,7 +4765,7 @@ async function comparePages(opts) {
4554
4765
 
4555
4766
  // src/head/run.ts
4556
4767
  import { writeFile } from "fs/promises";
4557
- import { join as join21 } from "path";
4768
+ import { join as join22 } from "path";
4558
4769
 
4559
4770
  // src/head/inspection-html.ts
4560
4771
  var PRIORITY_COLOR = {
@@ -5056,7 +5267,7 @@ async function runInspect(target, competitors, opts = {}) {
5056
5267
  const report = await comparePages({ targetUrl: target, competitorUrls: competitors.length ? competitors : [target] });
5057
5268
  const files = [];
5058
5269
  if (opts.html) {
5059
- const path = join21(opts.outDir ?? process.cwd(), `oriro-head-${hostSlug(target)}-inspect.html`);
5270
+ const path = join22(opts.outDir ?? process.cwd(), `oriro-head-${hostSlug(target)}-inspect.html`);
5060
5271
  await writeFile(path, buildInspectionHtml(report), "utf8");
5061
5272
  files.push(path);
5062
5273
  }
@@ -5072,7 +5283,7 @@ function parseHeadTargets(text, selfOrigin) {
5072
5283
  async function runUrlToCode(url, opts = {}) {
5073
5284
  try {
5074
5285
  const res = await urlToCode(url, headModels(), { goal: opts.goal, stack: opts.stack });
5075
- const codePath = join21(opts.outDir ?? process.cwd(), `oriro-head-${hostSlug(url)}${extForStack(opts.stack)}`);
5286
+ const codePath = join22(opts.outDir ?? process.cwd(), `oriro-head-${hostSlug(url)}${extForStack(opts.stack)}`);
5076
5287
  await writeFile(codePath, res.code, "utf8");
5077
5288
  return { summary: `Reverse-engineered ${url} into clean code (${res.code.length} chars) \u2192 ${codePath}`, files: [codePath] };
5078
5289
  } catch (e) {
@@ -5082,7 +5293,7 @@ async function runUrlToCode(url, opts = {}) {
5082
5293
  async function runUrlToSpec(url, opts = {}) {
5083
5294
  try {
5084
5295
  const res = await urlToSpec(url, headModels(), { goal: opts.goal });
5085
- const specPath = join21(opts.outDir ?? process.cwd(), `oriro-head-${hostSlug(url)}.spec.yaml`);
5296
+ const specPath = join22(opts.outDir ?? process.cwd(), `oriro-head-${hostSlug(url)}.spec.yaml`);
5086
5297
  await writeFile(specPath, res.spec, "utf8");
5087
5298
  return { summary: `Reverse-engineered ${url} into a YAML build spec \u2192 ${specPath}`, files: [specPath] };
5088
5299
  } catch (e) {
@@ -5094,7 +5305,7 @@ async function runCapture(urls, opts = {}) {
5094
5305
  const { captureScreens: captureScreens2, buildScreenshotFlowHtml: buildScreenshotFlowHtml2 } = await Promise.resolve().then(() => (init_screenshot_flow(), screenshot_flow_exports));
5095
5306
  const caps = await captureScreens2(urls, { video: opts.video });
5096
5307
  const html = buildScreenshotFlowHtml2([{ name: "Captured screens", captures: caps }]);
5097
- const flowPath = join21(opts.outDir ?? process.cwd(), "oriro-head-flow.html");
5308
+ const flowPath = join22(opts.outDir ?? process.cwd(), "oriro-head-flow.html");
5098
5309
  await writeFile(flowPath, html, "utf8");
5099
5310
  const ok2 = caps.filter((c) => c.ok).length;
5100
5311
  return { summary: `Captured ${ok2}/${caps.length} full-page screenshots \u2192 ${flowPath}`, files: [flowPath] };
@@ -5115,7 +5326,7 @@ async function runVideoToCode(videoPath, opts = {}) {
5115
5326
  { videoPath, frames, mimeType: mime, goal: opts.goal, stack: opts.stack },
5116
5327
  headVideoModels()
5117
5328
  );
5118
- const codePath = join21(opts.outDir ?? process.cwd(), `oriro-head-video${extForStack(opts.stack)}`);
5329
+ const codePath = join22(opts.outDir ?? process.cwd(), `oriro-head-video${extForStack(opts.stack)}`);
5119
5330
  await writeFile(codePath, res.code, "utf8");
5120
5331
  return { summary: `Watched ${videoPath} \u2192 built code (${res.code.length} chars) \u2192 ${codePath}
5121
5332
  (experimental on the free floor \u2014 add a vision-capable router for pixel-faithful results.)`, files: [codePath] };
@@ -5297,29 +5508,29 @@ function registerOrchestrator(pi) {
5297
5508
  import { Type as Type4 } from "typebox";
5298
5509
 
5299
5510
  // src/agents/store.ts
5300
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync16, readdirSync as readdirSync2, rmSync as rmSync3, existsSync as existsSync13 } from "fs";
5301
- import { join as join22 } from "path";
5511
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync19, writeFileSync as writeFileSync16, readdirSync as readdirSync2, rmSync as rmSync3, existsSync as existsSync14 } from "fs";
5512
+ import { join as join23 } from "path";
5302
5513
  var SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
5303
5514
  function isValidAgentName(name) {
5304
5515
  return SLUG.test(name);
5305
5516
  }
5306
5517
  function agentsDir() {
5307
- return join22(oriroDir(), "agents");
5518
+ return join23(oriroDir(), "agents");
5308
5519
  }
5309
5520
  function agentFile(name) {
5310
- return join22(agentsDir(), `${name}.json`);
5521
+ return join23(agentsDir(), `${name}.json`);
5311
5522
  }
5312
5523
  function stateFile() {
5313
- return join22(agentsDir(), ".state.json");
5524
+ return join23(agentsDir(), ".state.json");
5314
5525
  }
5315
5526
  function listAgents() {
5316
5527
  const dir = agentsDir();
5317
- if (!existsSync13(dir)) return [];
5528
+ if (!existsSync14(dir)) return [];
5318
5529
  const out = [];
5319
5530
  for (const f of readdirSync2(dir)) {
5320
5531
  if (!f.endsWith(".json") || f.startsWith(".")) continue;
5321
5532
  try {
5322
- const def = JSON.parse(readFileSync18(join22(dir, f), "utf8"));
5533
+ const def = JSON.parse(readFileSync19(join23(dir, f), "utf8"));
5323
5534
  if (def && typeof def.name === "string" && typeof def.task === "string") out.push(def);
5324
5535
  } catch {
5325
5536
  }
@@ -5328,7 +5539,7 @@ function listAgents() {
5328
5539
  }
5329
5540
  function loadAgent(name) {
5330
5541
  try {
5331
- return JSON.parse(readFileSync18(agentFile(name), "utf8"));
5542
+ return JSON.parse(readFileSync19(agentFile(name), "utf8"));
5332
5543
  } catch {
5333
5544
  return void 0;
5334
5545
  }
@@ -5341,9 +5552,9 @@ function saveAgent(def) {
5341
5552
  writeFileSync16(agentFile(def.name), JSON.stringify(def, null, 2), "utf8");
5342
5553
  }
5343
5554
  function removeAgent(name) {
5344
- const file5 = agentFile(name);
5345
- if (!existsSync13(file5)) return false;
5346
- rmSync3(file5, { force: true });
5555
+ const file6 = agentFile(name);
5556
+ if (!existsSync14(file6)) return false;
5557
+ rmSync3(file6, { force: true });
5347
5558
  const state = loadState();
5348
5559
  if (state[name]) {
5349
5560
  delete state[name];
@@ -5353,7 +5564,7 @@ function removeAgent(name) {
5353
5564
  }
5354
5565
  function loadState() {
5355
5566
  try {
5356
- return JSON.parse(readFileSync18(stateFile(), "utf8"));
5567
+ return JSON.parse(readFileSync19(stateFile(), "utf8"));
5357
5568
  } catch {
5358
5569
  return {};
5359
5570
  }
@@ -5479,6 +5690,218 @@ ${result.output.slice(0, 4e3)}` }],
5479
5690
  });
5480
5691
  }
5481
5692
 
5693
+ // src/connectors/mcp-client.ts
5694
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5695
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5696
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5697
+ var DISALLOWED_ENV = /* @__PURE__ */ new Set([
5698
+ "PATH",
5699
+ "LD_PRELOAD",
5700
+ "LD_LIBRARY_PATH",
5701
+ "DYLD_INSERT_LIBRARIES",
5702
+ "DYLD_LIBRARY_PATH",
5703
+ "NODE_OPTIONS",
5704
+ "PYTHONPATH",
5705
+ "PYTHONSTARTUP",
5706
+ "PERL5LIB",
5707
+ "RUBYOPT",
5708
+ "GEM_PATH",
5709
+ "APPINIT_DLLS",
5710
+ "COR_PROFILER",
5711
+ "BASH_ENV",
5712
+ "ENV",
5713
+ "IFS"
5714
+ ]);
5715
+ var HANDSHAKE_TIMEOUT_MS = 8e3;
5716
+ var CALL_TIMEOUT_MS = 3e4;
5717
+ function sanitizeName(name) {
5718
+ return (name || "").toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) || "x";
5719
+ }
5720
+ function assertSafeUrl(raw, allowLocal = false) {
5721
+ const u = new URL(raw);
5722
+ if (u.protocol !== "https:" && u.protocol !== "http:") throw new Error(`unsupported scheme: ${u.protocol}`);
5723
+ const host = u.hostname.toLowerCase();
5724
+ const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost");
5725
+ const isPrivate = /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || /^169\.254\./.test(host) || /^fe80:/i.test(host) || /^f[cd][0-9a-f]{2}:/i.test(host) || host === "169.254.169.254" || host === "metadata.google.internal";
5726
+ if ((isLoopback || isPrivate) && !allowLocal) {
5727
+ throw new Error(`blocked SSRF target ${host} (use --allow-local for loopback/LAN MCP servers)`);
5728
+ }
5729
+ if (u.protocol === "http:" && !isLoopback && !allowLocal) throw new Error(`refusing plaintext http to ${host} \u2014 use https`);
5730
+ return u;
5731
+ }
5732
+ function safeEnv(env) {
5733
+ const out = {};
5734
+ for (const [k, v] of Object.entries(env ?? {})) {
5735
+ if (DISALLOWED_ENV.has(k.toUpperCase())) continue;
5736
+ out[k] = v;
5737
+ }
5738
+ return out;
5739
+ }
5740
+ async function connectServer(name, config, opts = {}) {
5741
+ const client = new Client({ name: "oriro-cli", version: "0.1.0" }, { capabilities: {} });
5742
+ let transport;
5743
+ let stderr = "";
5744
+ if (config.type === "stdio") {
5745
+ const t = new StdioClientTransport({
5746
+ command: config.command,
5747
+ args: config.args ?? [],
5748
+ env: safeEnv(config.env),
5749
+ stderr: "pipe"
5750
+ });
5751
+ t.stderr?.on("data", (b) => {
5752
+ stderr += b.toString();
5753
+ });
5754
+ transport = t;
5755
+ } else {
5756
+ const url = assertSafeUrl(config.url, opts.allowLocal);
5757
+ transport = new StreamableHTTPClientTransport(url, {
5758
+ requestInit: { headers: { "User-Agent": "oriro-cli/0.1.0", ...config.headers ?? {} } }
5759
+ });
5760
+ }
5761
+ const timeoutMs = opts.timeoutMs ?? HANDSHAKE_TIMEOUT_MS;
5762
+ let timer;
5763
+ try {
5764
+ await Promise.race([
5765
+ client.connect(transport),
5766
+ new Promise((_, rej) => {
5767
+ timer = setTimeout(() => rej(new Error("handshake timed out")), timeoutMs);
5768
+ })
5769
+ ]);
5770
+ } catch (e) {
5771
+ const detail = stderr.trim() ? `
5772
+ server stderr:
5773
+ ${stderr.trim().slice(0, 800)}` : "";
5774
+ try {
5775
+ await transport.close();
5776
+ } catch {
5777
+ }
5778
+ throw new Error(`MCP connect failed (${name}): ${e instanceof Error ? e.message : String(e)}${detail}`);
5779
+ } finally {
5780
+ if (timer) clearTimeout(timer);
5781
+ }
5782
+ return {
5783
+ name,
5784
+ client,
5785
+ dispose: async () => {
5786
+ try {
5787
+ await client.close();
5788
+ } catch {
5789
+ }
5790
+ }
5791
+ };
5792
+ }
5793
+ async function listAllTools(client) {
5794
+ const tools = [];
5795
+ let cursor;
5796
+ do {
5797
+ const res = await client.listTools(cursor ? { cursor } : void 0, { timeout: CALL_TIMEOUT_MS });
5798
+ for (const t of res.tools) tools.push({ name: t.name, description: t.description, inputSchema: t.inputSchema });
5799
+ cursor = res.nextCursor;
5800
+ } while (cursor);
5801
+ return tools;
5802
+ }
5803
+
5804
+ // src/connectors/register.ts
5805
+ import { Type as Type5 } from "typebox";
5806
+ function registerToolList(pi, serverName, client, tools, seen = /* @__PURE__ */ new Set()) {
5807
+ const server = sanitizeName(serverName);
5808
+ const registered = [];
5809
+ for (const t of tools) {
5810
+ const publicName = `mcp__${server}__${sanitizeName(t.name)}`;
5811
+ if (seen.has(publicName)) continue;
5812
+ seen.add(publicName);
5813
+ const realName = t.name;
5814
+ pi.registerTool({
5815
+ name: publicName,
5816
+ label: `MCP: ${serverName}`,
5817
+ description: (t.description ?? `${t.name} (via ${serverName})`).slice(0, 1024),
5818
+ parameters: Type5.Object({}, { additionalProperties: true }),
5819
+ async execute(_id, params) {
5820
+ const details = { server: serverName, tool: realName };
5821
+ try {
5822
+ const res = await client.callTool(
5823
+ { name: realName, arguments: params ?? {} },
5824
+ void 0,
5825
+ { timeout: CALL_TIMEOUT_MS }
5826
+ );
5827
+ const text = (res.content ?? []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
5828
+ if (res.isError) {
5829
+ details.isError = true;
5830
+ return { content: [{ type: "text", text: `MCP tool error: ${text || "(no detail)"}` }], details };
5831
+ }
5832
+ return { content: [{ type: "text", text: text || "(no text content)" }], details };
5833
+ } catch (e) {
5834
+ details.isError = true;
5835
+ return { content: [{ type: "text", text: `MCP call failed: ${e instanceof Error ? e.message : String(e)}` }], details };
5836
+ }
5837
+ }
5838
+ });
5839
+ registered.push(publicName);
5840
+ }
5841
+ return registered;
5842
+ }
5843
+
5844
+ // src/connectors/custom.ts
5845
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync17 } from "fs";
5846
+ import { join as join24 } from "path";
5847
+ function file3() {
5848
+ return join24(oriroDir(), "mcp-custom.json");
5849
+ }
5850
+ function readCustomServers() {
5851
+ try {
5852
+ const v = JSON.parse(readFileSync20(file3(), "utf8"));
5853
+ return Array.isArray(v) ? v : [];
5854
+ } catch {
5855
+ return [];
5856
+ }
5857
+ }
5858
+ function saveCustomServer(server) {
5859
+ const rest = readCustomServers().filter((s) => s.name.toLowerCase() !== server.name.toLowerCase());
5860
+ writeFileSync17(join24(ensureOriroDir(), "mcp-custom.json"), JSON.stringify([...rest, server], null, 2), "utf8");
5861
+ }
5862
+ function removeCustomServer(name) {
5863
+ const before = readCustomServers();
5864
+ const after = before.filter((s) => s.name.toLowerCase() !== name.toLowerCase());
5865
+ if (after.length === before.length) return false;
5866
+ writeFileSync17(join24(ensureOriroDir(), "mcp-custom.json"), JSON.stringify(after, null, 2), "utf8");
5867
+ return true;
5868
+ }
5869
+ function trustedServerNames() {
5870
+ return readCustomServers().filter((s) => s.trusted).map((s) => s.name);
5871
+ }
5872
+ function isServerTrusted(name) {
5873
+ return trustedServerNames().some((n) => n.toLowerCase() === name.toLowerCase());
5874
+ }
5875
+
5876
+ // src/connectors/session-connect.ts
5877
+ var CONNECT_TIMEOUT_MS = 8e3;
5878
+ async function prepareConnectors() {
5879
+ const targets = [];
5880
+ for (const c of addedConnectors()) {
5881
+ if (c.mcpUrl) targets.push({ name: c.slug, config: { type: "http", url: c.mcpUrl } });
5882
+ }
5883
+ for (const s of readCustomServers()) {
5884
+ if (s.trusted) targets.push({ name: s.name, config: s.config, allowLocal: true });
5885
+ }
5886
+ const out = [];
5887
+ for (const t of targets) {
5888
+ try {
5889
+ const conn = await connectServer(t.name, t.config, {
5890
+ timeoutMs: CONNECT_TIMEOUT_MS,
5891
+ ...t.allowLocal ? { allowLocal: true } : {}
5892
+ });
5893
+ const tools = await listAllTools(conn.client);
5894
+ out.push({ name: t.name, client: conn.client, tools });
5895
+ } catch {
5896
+ }
5897
+ }
5898
+ return out;
5899
+ }
5900
+ function registerPreparedConnectors(pi, prepared) {
5901
+ const seen = /* @__PURE__ */ new Set();
5902
+ for (const p of prepared) registerToolList(pi, p.name, p.client, p.tools, seen);
5903
+ }
5904
+
5482
5905
  // src/onboarding/assemble.ts
5483
5906
  async function assembleOriroSession(opts = {}) {
5484
5907
  const cwd = opts.cwd ?? process.cwd();
@@ -5487,13 +5910,22 @@ async function assembleOriroSession(opts = {}) {
5487
5910
  const settingsManager = SettingsManager.create(cwd);
5488
5911
  const model = registerOriroMux(modelRegistry, opts.routers ? { routers: opts.routers } : {});
5489
5912
  if (!model) throw new Error("ORIRO keyless model unavailable");
5913
+ const preparedConnectors = await prepareConnectors();
5490
5914
  const resourceLoader = new DefaultResourceLoader({
5491
5915
  cwd,
5492
5916
  agentDir: getAgentDir(),
5493
5917
  settingsManager,
5494
5918
  additionalSkillPaths: skillRoots(),
5495
5919
  // bundled library + the user's own ~/.oriro/skills
5496
- extensionFactories: [registerGuardian, registerHead, registerScribe, registerOrchestrator, registerAgentRunner]
5920
+ extensionFactories: [
5921
+ registerGuardian,
5922
+ registerHead,
5923
+ registerScribe,
5924
+ registerOrchestrator,
5925
+ registerAgentRunner,
5926
+ (pi) => registerPreparedConnectors(pi, preparedConnectors)
5927
+ // MCP connectors → agent tools
5928
+ ]
5497
5929
  });
5498
5930
  await resourceLoader.reload();
5499
5931
  const { session, extensionsResult } = await createAgentSession2({
@@ -5682,14 +6114,14 @@ var MODE_META = {
5682
6114
  auto: { label: "Auto", indicator: "\u23F5\u23F5" },
5683
6115
  plan: { label: "Plan", indicator: "\u25A2" }
5684
6116
  };
5685
- var current = "manual";
6117
+ var current2 = "manual";
5686
6118
  function getMode() {
5687
- return current;
6119
+ return current2;
5688
6120
  }
5689
6121
  function cycleMode() {
5690
- const i = MODES.indexOf(current);
5691
- current = MODES[(i + 1) % MODES.length];
5692
- return current;
6122
+ const i = MODES.indexOf(current2);
6123
+ current2 = MODES[(i + 1) % MODES.length];
6124
+ return current2;
5693
6125
  }
5694
6126
  var thinking = false;
5695
6127
  function getThinking() {
@@ -5702,7 +6134,7 @@ function toggleThinking() {
5702
6134
  var THINKING_PRIMER = "Think step by step and plan your approach before acting. Reason carefully and check your work.";
5703
6135
 
5704
6136
  // src/repl-ui/verify-actions.ts
5705
- import { existsSync as existsSync14 } from "fs";
6137
+ import { existsSync as existsSync15 } from "fs";
5706
6138
  import { isAbsolute, resolve } from "path";
5707
6139
  var CLAIM = /\b(?:have|has)\s+been\s+created\b|\b(?:created|wrote|written|saved|generated)\b(?![ \t]*(?:by you|it yourself))/i;
5708
6140
  var SUGGESTION = /\byou\s+(?:can|could|should|may)\s+(?:create|add|save|make|put)\b/i;
@@ -5715,7 +6147,7 @@ function phantomFileWarning(reply, cwd = process.cwd()) {
5715
6147
  if (!p) continue;
5716
6148
  if (/^https?:|node_modules|<[^>]+>|your-|example\./i.test(p)) continue;
5717
6149
  const abs = isAbsolute(p) ? p : resolve(cwd, p.replace(/^[.][\\/]/, ""));
5718
- if (!existsSync14(abs)) missing.add(p);
6150
+ if (!existsSync15(abs)) missing.add(p);
5719
6151
  }
5720
6152
  if (missing.size === 0) return "";
5721
6153
  if (SUGGESTION.test(reply) && !/\b(?:have|has)\s+been\s+created\b/i.test(reply)) return "";
@@ -5725,6 +6157,212 @@ function phantomFileWarning(reply, cwd = process.cwd()) {
5725
6157
  \u26A0 ORIRO said it ${plural ? "created files" : "created a file"} (${list}), but ${plural ? "they're" : "it's"} not on disk \u2014 the free router may have described the write without actually running it. Retry, or add your own key with \`oriro routers\` for reliable coding.`;
5726
6158
  }
5727
6159
 
6160
+ // src/repl-ui/slash-routers.ts
6161
+ function isRouterSlash(cmd) {
6162
+ return /^\/(routers?|model)(\s|$)/i.test(cmd.trim());
6163
+ }
6164
+ function poolLine() {
6165
+ const pool = resolvePool();
6166
+ return pool.length ? `${dim("racing now:")} ${accent(pool.map((p) => p.id).join(", "))}` : dim("racing now: (empty) \u2192 keyless floor");
6167
+ }
6168
+ function catalogLines(head) {
6169
+ const lines = [];
6170
+ lines.push(
6171
+ head === "/model" ? dim(" ORIRO models & free routers \u2014 they race, best answer wins:") : dim(" Router catalog \u2014 they race, best answer wins:")
6172
+ );
6173
+ for (const r of selectableRouters()) {
6174
+ const tier = r.keyless ? fgHex(PALETTE.success, "keyless") : dim(r.tier);
6175
+ lines.push(` ${accent(r.id.padEnd(20))} ${r.displayName.padEnd(22)} ${tier}`);
6176
+ }
6177
+ lines.push(` ${poolLine()}`);
6178
+ lines.push(dim(" add: /routers add <id> \xB7 rotate: /routers use <id> [<id>\u2026]"));
6179
+ return lines;
6180
+ }
6181
+ async function handleRouterSlash(raw) {
6182
+ const parts = raw.trim().split(/\s+/);
6183
+ const head = (parts[0] ?? "").toLowerCase();
6184
+ const sub = (parts[1] ?? "").toLowerCase();
6185
+ try {
6186
+ if (sub === "add") {
6187
+ const id = parts[2];
6188
+ if (!id) return [dim(" usage: /routers add <id> (e.g. /routers add oriro-gauss)")];
6189
+ const entry = routerById(id);
6190
+ if (!entry) return [dim(` unknown router '${id}' \u2014 try /routers list`)];
6191
+ const res = await addRouter(entry, {});
6192
+ if (res.ok) {
6193
+ return [
6194
+ ` ${fgHex(PALETTE.success, "\u2713")} added ${accent(id)} (${res.validation.latencyMs}ms, model ${res.validation.model}) \u2192 ${fgHex(PALETTE.success, "now racing")}`,
6195
+ ` ${poolLine()}`
6196
+ ];
6197
+ }
6198
+ return [dim(` \u2717 could not add ${id}: ${res.validation.error ?? "validation failed"}`)];
6199
+ }
6200
+ const rotate = sub === "use" ? parts.slice(2) : head === "/model" && parts[1] && sub !== "list" ? parts.slice(1) : null;
6201
+ if (rotate) {
6202
+ if (!rotate.length) return [dim(" usage: /routers use <id> [<id>\u2026]")];
6203
+ const { applied, unknown } = useRouters(rotate);
6204
+ const out = [];
6205
+ if (applied.length) out.push(` ${fgHex(PALETTE.success, "\u2713")} now racing: ${accent(applied.join(", "))}`);
6206
+ if (unknown.length) out.push(dim(` not registered yet (add first): ${unknown.join(", ")}`));
6207
+ return out.length ? out : [dim(" nothing applied \u2014 add a router first: /routers add <id>")];
6208
+ }
6209
+ return catalogLines(head);
6210
+ } catch (e) {
6211
+ return [dim(` router command failed: ${e instanceof Error ? e.message : String(e)}`)];
6212
+ }
6213
+ }
6214
+
6215
+ // src/repl-ui/repl-state.ts
6216
+ var turns = 0;
6217
+ var trace = false;
6218
+ function bumpTurns() {
6219
+ turns += 1;
6220
+ }
6221
+ function getTurns() {
6222
+ return turns;
6223
+ }
6224
+ function getTrace() {
6225
+ return trace;
6226
+ }
6227
+ function toggleTrace() {
6228
+ trace = !trace;
6229
+ return trace;
6230
+ }
6231
+
6232
+ // src/repl-ui/slash-usage.ts
6233
+ function isUsageSlash(cmd) {
6234
+ return /^\/usage(\s|$)/i.test(cmd.trim());
6235
+ }
6236
+ function handleUsage() {
6237
+ const pool = resolvePool();
6238
+ const health = new Map(loadMuxState(oriroDir()).map((s) => [s.id, s]));
6239
+ const race = getRaceStatus();
6240
+ const lines = [];
6241
+ lines.push(dim(` turns this session: ${accent(String(getTurns()))} \xB7 thinking-trace: ${getTrace() ? fgHex(PALETTE.success, "on") : dim("off")}`));
6242
+ lines.push(dim(" racing pool (learned latency \xB7 health):"));
6243
+ if (!pool.length) {
6244
+ lines.push(dim(" (empty) \u2192 the keyless floor"));
6245
+ } else {
6246
+ const now = Date.now();
6247
+ for (const r of pool) {
6248
+ const s = health.get(r.id);
6249
+ const lat = s && Number.isFinite(s.latencyMs) ? `${Math.round(s.latencyMs)}ms` : "untried";
6250
+ const state = !s ? dim("new") : !s.healthy ? fgHex(PALETTE.error, "unhealthy") : s.cooldownUntil > now ? fgHex(PALETTE.error, "cooling") : fgHex(PALETTE.success, "healthy");
6251
+ lines.push(` ${accent(r.id.padEnd(20))} ${dim(lat.padEnd(9))} ${state}`);
6252
+ }
6253
+ }
6254
+ if (race.winner && race.racers.length > 1) {
6255
+ lines.push(dim(` last race: ${race.racers.join(" \xB7 ")} \u2192 won: `) + accent(race.winner));
6256
+ }
6257
+ return lines;
6258
+ }
6259
+
6260
+ // src/repl-ui/slash-artifacts.ts
6261
+ import { existsSync as existsSync16, writeFileSync as writeFileSync18 } from "fs";
6262
+
6263
+ // src/repl-ui/artifacts.ts
6264
+ var LANG_EXT = {
6265
+ python: "py",
6266
+ py: "py",
6267
+ javascript: "js",
6268
+ js: "js",
6269
+ typescript: "ts",
6270
+ ts: "ts",
6271
+ tsx: "tsx",
6272
+ jsx: "jsx",
6273
+ html: "html",
6274
+ css: "css",
6275
+ json: "json",
6276
+ yaml: "yaml",
6277
+ yml: "yml",
6278
+ bash: "sh",
6279
+ sh: "sh",
6280
+ shell: "sh",
6281
+ sql: "sql",
6282
+ go: "go",
6283
+ rust: "rs",
6284
+ rs: "rs",
6285
+ java: "java",
6286
+ c: "c",
6287
+ cpp: "cpp",
6288
+ "c++": "cpp",
6289
+ ruby: "rb",
6290
+ rb: "rb",
6291
+ php: "php",
6292
+ markdown: "md",
6293
+ md: "md",
6294
+ svg: "svg"
6295
+ };
6296
+ function extFor(lang) {
6297
+ return LANG_EXT[lang.toLowerCase()] ?? "txt";
6298
+ }
6299
+ function extractArtifacts(text) {
6300
+ const out = [];
6301
+ if (!text) return out;
6302
+ const fence = /```([\w+#.-]*)\n([\s\S]*?)```/g;
6303
+ let m;
6304
+ while ((m = fence.exec(text)) !== null) {
6305
+ const lang = (m[1] ?? "").trim();
6306
+ const content = (m[2] ?? "").replace(/\n$/, "");
6307
+ if (!content.trim()) continue;
6308
+ const isSvg = lang.toLowerCase() === "svg" || /^\s*<svg[\s>]/.test(content);
6309
+ out.push({
6310
+ kind: isSvg ? "svg" : "code",
6311
+ lang: lang || (isSvg ? "svg" : ""),
6312
+ content,
6313
+ suggestedName: `artifact-${out.length + 1}.${isSvg ? "svg" : extFor(lang)}`
6314
+ });
6315
+ }
6316
+ const svg = /<svg[\s>][\s\S]*?<\/svg>/gi;
6317
+ while ((m = svg.exec(text)) !== null) {
6318
+ const content = m[0];
6319
+ if (out.some((a) => a.content.includes(content))) continue;
6320
+ out.push({ kind: "svg", lang: "svg", content, suggestedName: `artifact-${out.length + 1}.svg` });
6321
+ }
6322
+ return out;
6323
+ }
6324
+ var current3 = [];
6325
+ function setArtifacts(a) {
6326
+ current3 = a;
6327
+ }
6328
+ function getArtifacts() {
6329
+ return current3;
6330
+ }
6331
+
6332
+ // src/repl-ui/slash-artifacts.ts
6333
+ function isArtifactSlash(cmd) {
6334
+ return /^\/(review|artifacts?|save)(\s|$)/i.test(cmd.trim());
6335
+ }
6336
+ function handleArtifactSlash(raw) {
6337
+ const parts = raw.trim().split(/\s+/);
6338
+ const head = (parts[0] ?? "").toLowerCase();
6339
+ const arts = getArtifacts();
6340
+ if (head === "/save") {
6341
+ const idx = parseInt(parts[1] ?? "", 10);
6342
+ if (!Number.isInteger(idx) || idx < 1 || idx > arts.length) {
6343
+ return [dim(" usage: /save <n> [path] \u2014 run /review to see the artifacts")];
6344
+ }
6345
+ const art = arts[idx - 1];
6346
+ if (!art) return [dim(" no such artifact")];
6347
+ const dest = parts[2] || art.suggestedName;
6348
+ if (existsSync16(dest)) return [dim(` \u2717 ${dest} already exists \u2014 give a different path: /save ${idx} <path>`)];
6349
+ try {
6350
+ writeFileSync18(dest, art.content, "utf8");
6351
+ } catch (e) {
6352
+ return [dim(` \u2717 could not write ${dest}: ${e instanceof Error ? e.message : String(e)}`)];
6353
+ }
6354
+ return [` ${fgHex(PALETTE.success, "\u2713")} saved artifact ${accent(String(idx))} \u2192 ${accent(dest)} ${dim(`(${art.content.length} bytes)`)}`];
6355
+ }
6356
+ if (!arts.length) return [dim(" no artifacts in the last reply \u2014 ask for code or an SVG, then /review")];
6357
+ const lines = [dim(" Artifacts from the last reply \u2014 save one with /save <n> [path]:")];
6358
+ arts.forEach((a, i) => {
6359
+ const nlines = a.content.split("\n").length;
6360
+ const preview = (a.content.split("\n")[0] ?? "").slice(0, 48).replace(/\s+/g, " ");
6361
+ lines.push(` ${accent(String(i + 1))}. ${a.kind}${a.lang ? `/${a.lang}` : ""} \xB7 ${nlines} lines \xB7 \u2192 ${dim(a.suggestedName)} ${dim(preview)}`);
6362
+ });
6363
+ return lines;
6364
+ }
6365
+
5728
6366
  // src/repl-ui/tui-repl.ts
5729
6367
  var editorTheme = {
5730
6368
  borderColor: (s) => dim(s),
@@ -5804,7 +6442,13 @@ async function runTuiRepl(session) {
5804
6442
  const slash = text.toLowerCase();
5805
6443
  if (slash === "/exit" || slash === "/quit") return cleanup();
5806
6444
  if (slash === "/help" || slash === "/?") {
5807
- chat.addChild(new Text(dim(" Just type to chat. Shift+Tab posture \xB7 Alt+Shift+T thinking \xB7 /voice to speak \xB7 /exit."), 0, 0));
6445
+ const help = [
6446
+ " Just type to chat \u2014 ORIRO writes and runs code for you (keyless, free).",
6447
+ ` ${accent("/routers")} pool add\xB7rotate ${accent("/model")} <id\u2026> switch ${accent("/usage")} health ${accent("/trace")} tool+router activity`,
6448
+ ` ${accent("/review")} artifacts from the last reply ${accent("/save")} <n> [path] ${accent("/skills")} ${accent("/connectors")} ${accent("/voice")}`,
6449
+ ` ${dim("Shift+Tab")} posture ${dim("Alt+Shift+T")} thinking ${accent("/help")} ${accent("/exit")}`
6450
+ ].join("\n");
6451
+ chat.addChild(new Text(help, 0, 0));
5808
6452
  editor.setText("");
5809
6453
  tui.requestRender();
5810
6454
  return;
@@ -5821,6 +6465,37 @@ async function runTuiRepl(session) {
5821
6465
  tui.requestRender();
5822
6466
  return;
5823
6467
  }
6468
+ if (isRouterSlash(slash)) {
6469
+ editor.setText("");
6470
+ const pending = new Text(dim(" \u2026"), 0, 0);
6471
+ chat.addChild(pending);
6472
+ tui.requestRender();
6473
+ void (async () => {
6474
+ const lines = await handleRouterSlash(text);
6475
+ pending.setText(lines.join("\n"));
6476
+ tui.requestRender();
6477
+ })();
6478
+ return;
6479
+ }
6480
+ if (isUsageSlash(slash)) {
6481
+ chat.addChild(new Text(handleUsage().join("\n"), 0, 0));
6482
+ editor.setText("");
6483
+ tui.requestRender();
6484
+ return;
6485
+ }
6486
+ if (slash === "/trace") {
6487
+ const on = toggleTrace();
6488
+ chat.addChild(new Text(dim(` trace ${on ? "ON \u2014 showing tool + router activity" : "off"}`), 0, 0));
6489
+ editor.setText("");
6490
+ tui.requestRender();
6491
+ return;
6492
+ }
6493
+ if (isArtifactSlash(slash)) {
6494
+ chat.addChild(new Text(handleArtifactSlash(text).join("\n"), 0, 0));
6495
+ editor.setText("");
6496
+ tui.requestRender();
6497
+ return;
6498
+ }
5824
6499
  if (slash === "/voice") {
5825
6500
  editor.setText("");
5826
6501
  const status = new Text(dim(" \u{1F399} listening\u2026 (needs ffmpeg + the transformers voice peer)"), 0, 0);
@@ -5841,10 +6516,23 @@ async function runTuiRepl(session) {
5841
6516
  editor.addToHistory(text);
5842
6517
  editor.setText("");
5843
6518
  chat.addChild(new Text(`${accent("\u203A")} ${text}`, 0, 1));
6519
+ const raceLine = new Text("", 0, 0);
6520
+ chat.addChild(raceLine);
5844
6521
  const streaming = new Text(dim("\u2026"), 0, 0);
5845
6522
  chat.addChild(streaming);
6523
+ const unsubRace = onRaceStatus((s) => {
6524
+ if (s.phase === "racing" && s.racers.length > 1) {
6525
+ raceLine.setText(dim(` \u23F1 racing: ${s.racers.join(" \xB7 ")}`));
6526
+ } else if (s.phase === "won" && s.winner && s.racers.length > 1) {
6527
+ raceLine.setText(dim(` \u23F1 ${s.racers.join(" \xB7 ")} \u2192 won: `) + accent(s.winner));
6528
+ } else {
6529
+ raceLine.setText("");
6530
+ }
6531
+ tui.requestRender();
6532
+ });
5846
6533
  tui.requestRender();
5847
6534
  busy = true;
6535
+ bumpTurns();
5848
6536
  void (async () => {
5849
6537
  let english = await translateIncoming(text);
5850
6538
  if (getThinking()) english = `${THINKING_PRIMER}
@@ -5860,6 +6548,9 @@ ${english}`;
5860
6548
  streaming.setText(out);
5861
6549
  tui.requestRender();
5862
6550
  }
6551
+ } else if (getTrace() && (e.type === "tool_start" || e.type === "tool_end" || e.type === "toolcall_start")) {
6552
+ chat.addChild(new Text(dim(` \u2699 ${e.type.replace("_", " ")}${e.toolName ? `: ${e.toolName}` : ""}`), 0, 0));
6553
+ tui.requestRender();
5863
6554
  }
5864
6555
  }
5865
6556
  );
@@ -5870,13 +6561,19 @@ ${english}`;
5870
6561
  tui.requestRender();
5871
6562
  busy = false;
5872
6563
  unsub();
6564
+ unsubRace();
5873
6565
  return;
5874
6566
  }
5875
6567
  unsub();
6568
+ unsubRace();
5876
6569
  const cleaned = scrubOutput(out);
5877
6570
  const finalText = isEnglish3 ? cleaned.trim() : await translateOutgoing(cleaned.trim());
5878
6571
  const warn = phantomFileWarning(finalText);
5879
- streaming.setText((finalText || dim("(no response)")) + (warn ? dim(warn) : ""));
6572
+ const arts = extractArtifacts(finalText);
6573
+ setArtifacts(arts);
6574
+ const hint = arts.length ? dim(`
6575
+ \u2398 ${arts.length} artifact${arts.length === 1 ? "" : "s"} \u2014 /review to save`) : "";
6576
+ streaming.setText((finalText || dim("(no response)")) + (warn ? dim(warn) : "") + hint);
5880
6577
  tui.requestRender();
5881
6578
  busy = false;
5882
6579
  })();
@@ -5890,8 +6587,8 @@ ${english}`;
5890
6587
  // src/voice/mic.ts
5891
6588
  import { spawn as spawn3 } from "child_process";
5892
6589
  import { tmpdir as tmpdir3 } from "os";
5893
- import { join as join23 } from "path";
5894
- import { existsSync as existsSync15, statSync as statSync2 } from "fs";
6590
+ import { join as join25 } from "path";
6591
+ import { existsSync as existsSync17, statSync as statSync3 } from "fs";
5895
6592
  function recorders(outFile, seconds) {
5896
6593
  const dur = String(seconds);
5897
6594
  if (process.platform === "darwin") {
@@ -5912,12 +6609,12 @@ function recorders(outFile, seconds) {
5912
6609
  ];
5913
6610
  }
5914
6611
  async function recordMic(seconds = 6) {
5915
- const outFile = join23(tmpdir3(), `oriro-voice-${process.pid}-${seconds}.wav`);
6612
+ const outFile = join25(tmpdir3(), `oriro-voice-${process.pid}-${seconds}.wav`);
5916
6613
  for (const r of recorders(outFile, seconds)) {
5917
6614
  const okFile = await new Promise((resolve3) => {
5918
6615
  const child = spawn3(r.cmd, r.args, { stdio: "ignore" });
5919
6616
  child.on("error", () => resolve3(false));
5920
- child.on("close", (code) => resolve3(code === 0 && existsSync15(outFile) && statSync2(outFile).size > 44));
6617
+ child.on("close", (code) => resolve3(code === 0 && existsSync17(outFile) && statSync3(outFile).size > 44));
5921
6618
  });
5922
6619
  if (okFile) return outFile;
5923
6620
  }
@@ -5981,9 +6678,13 @@ function replHelp() {
5981
6678
  ${accent("ORIRO terminal \u2014 help")}
5982
6679
  ${dim("Just type to chat; ORIRO writes and runs code for you (keyless, free).")}
5983
6680
 
5984
- ${accent("/help")} this help ${accent("/exit")} or ${accent("/quit")} leave ${dim("Ctrl-D / Ctrl-C also exit")}
5985
- ${dim("Run these OUTSIDE the chat (in your shell):")}
5986
- ${dim("oriro skills \xB7 routers \xB7 connectors \xB7 channels \xB7 scribe \xB7 language \xB7 avatar")}
6681
+ ${dim("Models & routers")} ${accent("/routers")} list\xB7add\xB7rotate the racing pool ${accent("/model")} <id\u2026> switch
6682
+ ${dim("This session")} ${accent("/usage")} pool health & turns ${accent("/trace")} show tool + router activity
6683
+ ${dim("Artifacts")} ${accent("/review")} code/SVG from the last reply ${accent("/save")} <n> [path] write one
6684
+ ${dim("Capabilities")} ${accent("/skills")} ${accent("/connectors")} ${accent("/voice")} speak a turn
6685
+ ${dim("General")} ${accent("/help")} this ${accent("/exit")} / ${accent("/quit")} leave ${dim("(Ctrl-D / Ctrl-C also exit)")}
6686
+
6687
+ ${dim("Full command list outside the chat:")} ${accent("oriro --help")}
5987
6688
 
5988
6689
  `;
5989
6690
  }
@@ -6042,6 +6743,24 @@ async function runReadlineRepl(session) {
6042
6743
  `);
6043
6744
  continue;
6044
6745
  }
6746
+ if (isRouterSlash(slash)) {
6747
+ stdout7.write((await handleRouterSlash(line)).join("\n") + "\n");
6748
+ continue;
6749
+ }
6750
+ if (isUsageSlash(slash)) {
6751
+ stdout7.write(handleUsage().join("\n") + "\n");
6752
+ continue;
6753
+ }
6754
+ if (slash === "/trace") {
6755
+ stdout7.write(` ${dim(`trace ${toggleTrace() ? "ON" : "off"}`)}
6756
+ `);
6757
+ continue;
6758
+ }
6759
+ if (isArtifactSlash(slash)) {
6760
+ stdout7.write(handleArtifactSlash(line).join("\n") + "\n");
6761
+ continue;
6762
+ }
6763
+ bumpTurns();
6045
6764
  const english = await translateIncoming(line);
6046
6765
  noteUserInput(line);
6047
6766
  let out = "";
@@ -6059,8 +6778,12 @@ async function runReadlineRepl(session) {
6059
6778
  }
6060
6779
  const cleaned = scrubOutput(out);
6061
6780
  const shown = isEnglish3 ? cleaned.trim() : await translateOutgoing(cleaned.trim());
6781
+ const arts = extractArtifacts(shown);
6782
+ setArtifacts(arts);
6783
+ const hint = arts.length ? ` ${dim(`\u2398 ${arts.length} artifact${arts.length === 1 ? "" : "s"} \u2014 /review to save`)}
6784
+ ` : "";
6062
6785
  stdout7.write(`${shown}${phantomFileWarning(shown)}
6063
-
6786
+ ${hint}
6064
6787
  `);
6065
6788
  }
6066
6789
  } finally {
@@ -6073,7 +6796,54 @@ async function runReadlineRepl(session) {
6073
6796
  }
6074
6797
  }
6075
6798
 
6799
+ // src/headless.ts
6800
+ function isOutputFormatMode(s) {
6801
+ return s === "text" || s === "json" || s === "stream-json";
6802
+ }
6803
+ async function runHeadless(prompt, format) {
6804
+ if (!prompt.trim()) {
6805
+ process.stderr.write('error: empty prompt \u2014 pass text after -p, e.g. oriro -p "summarise this repo"\n');
6806
+ process.exitCode = 1;
6807
+ return;
6808
+ }
6809
+ const { session } = await assembleOriroSession({});
6810
+ let text = "";
6811
+ const unsub = session.subscribe(
6812
+ (e) => {
6813
+ if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
6814
+ const d = e.assistantMessageEvent.delta ?? "";
6815
+ text += d;
6816
+ if (format === "stream-json" && d) process.stdout.write(JSON.stringify({ type: "text_delta", delta: d }) + "\n");
6817
+ }
6818
+ }
6819
+ );
6820
+ let error = "";
6821
+ try {
6822
+ await session.prompt(prompt);
6823
+ } catch (e) {
6824
+ error = e instanceof Error ? e.message : String(e);
6825
+ }
6826
+ unsub();
6827
+ const response = scrubOutput(text).trim();
6828
+ const ok2 = !error && response.length > 0;
6829
+ if (format === "json") {
6830
+ process.stdout.write(JSON.stringify({ ok: ok2, response, ...error ? { error } : {} }) + "\n");
6831
+ } else if (format === "stream-json") {
6832
+ process.stdout.write(JSON.stringify({ type: "done", ok: ok2, response, ...error ? { error } : {} }) + "\n");
6833
+ } else {
6834
+ process.stdout.write((response || (error ? `error: ${error}` : "(no response)")) + "\n");
6835
+ }
6836
+ process.exitCode = ok2 ? 0 : 1;
6837
+ try {
6838
+ session.dispose();
6839
+ } catch {
6840
+ }
6841
+ setTimeout(() => process.exit(ok2 ? 0 : 1), 400).unref();
6842
+ }
6843
+
6076
6844
  // src/commands/ui.ts
6845
+ import { createInterface as createInterface7 } from "readline/promises";
6846
+ import { stdin as stdin7, stdout as stdout8 } from "process";
6077
6847
  var ok = (s) => {
6078
6848
  process.stdout.write(`${fgHex(PALETTE.success, "\u2713")} ${s}
6079
6849
  `);
@@ -6098,11 +6868,159 @@ function die(msg) {
6098
6868
  process.exitCode = 1;
6099
6869
  throw new DieError(msg);
6100
6870
  }
6871
+ async function confirmDestructive(what, opts = {}) {
6872
+ if (opts.force) return true;
6873
+ if (!stdin7.isTTY || !stdout8.isTTY) {
6874
+ die(`refusing to ${what} without confirmation \u2014 re-run with --force in a non-interactive shell`);
6875
+ }
6876
+ const rl = createInterface7({ input: stdin7, output: stdout8 });
6877
+ try {
6878
+ const ans = (await rl.question(`${fgHex(PALETTE.error, "?")} ${what} \u2014 this cannot be undone. Proceed? [y/N] `)).trim().toLowerCase();
6879
+ return ans === "y" || ans === "yes";
6880
+ } finally {
6881
+ rl.close();
6882
+ }
6883
+ }
6884
+
6885
+ // src/config/store.ts
6886
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync19, mkdirSync as mkdirSync16 } from "fs";
6887
+ import { join as join26 } from "path";
6888
+ var KEYS = {
6889
+ output: {
6890
+ desc: "default output format for list commands: text | json | csv",
6891
+ validate: (v) => ["text", "json", "csv"].includes(v) ? null : "must be text | json | csv"
6892
+ },
6893
+ lang: { desc: "preferred UI language code (e.g. en, hi, es) \u2014 overrides terminal detection" },
6894
+ thinking: {
6895
+ desc: "default REPL thinking mode: on | off",
6896
+ validate: (v) => ["on", "off"].includes(v) ? null : "must be on | off"
6897
+ }
6898
+ };
6899
+ function configKeys() {
6900
+ return Object.keys(KEYS).map((key) => ({ key, desc: KEYS[key].desc }));
6901
+ }
6902
+ function isConfigKey(k) {
6903
+ return k in KEYS;
6904
+ }
6905
+ function validateConfig(key, value) {
6906
+ return KEYS[key].validate?.(value) ?? null;
6907
+ }
6908
+ function file4() {
6909
+ return join26(oriroDir(), "config.json");
6910
+ }
6911
+ var cache = null;
6912
+ function readAll() {
6913
+ if (cache) return cache;
6914
+ try {
6915
+ const v = JSON.parse(readFileSync21(file4(), "utf8"));
6916
+ cache = v && typeof v === "object" ? v : {};
6917
+ } catch {
6918
+ cache = {};
6919
+ }
6920
+ return cache;
6921
+ }
6922
+ function configGet(key) {
6923
+ return readAll()[key];
6924
+ }
6925
+ function configAll() {
6926
+ return { ...readAll() };
6927
+ }
6928
+ function configSet(key, value) {
6929
+ const all = { ...readAll(), [key]: value };
6930
+ mkdirSync16(oriroDir(), { recursive: true });
6931
+ writeFileSync19(file4(), JSON.stringify(all, null, 2), "utf8");
6932
+ cache = all;
6933
+ }
6934
+ function configUnset(key) {
6935
+ const all = readAll();
6936
+ if (!(key in all)) return false;
6937
+ const rest = { ...all };
6938
+ delete rest[key];
6939
+ writeFileSync19(file4(), JSON.stringify(rest, null, 2), "utf8");
6940
+ cache = rest;
6941
+ return true;
6942
+ }
6943
+
6944
+ // src/commands/output.ts
6945
+ function parseFormat(o) {
6946
+ const f = (o ?? configGet("output") ?? "text").toLowerCase();
6947
+ if (f === "json" || f === "csv" || f === "text") return f;
6948
+ throw new Error(`invalid --output '${o}'. Use: text | json | csv`);
6949
+ }
6950
+ function applyQuery(rows, query) {
6951
+ if (!query) return rows;
6952
+ const [filterPart, selectField] = query.split(":", 2);
6953
+ let out = rows;
6954
+ const fp = filterPart ?? "";
6955
+ if (fp.includes("=")) {
6956
+ const [field2, value] = fp.split("=", 2);
6957
+ out = rows.filter((r) => String(r[field2] ?? "") === value);
6958
+ } else if (fp && !selectField) {
6959
+ return rows.map((r) => r[fp]);
6960
+ }
6961
+ if (selectField) return out.map((r) => r[selectField]);
6962
+ return out;
6963
+ }
6964
+ function csvCell(v) {
6965
+ const s = v === null || v === void 0 ? "" : String(v);
6966
+ return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
6967
+ }
6968
+ function renderList2(rows, opts = {}) {
6969
+ const fmt = parseFormat(opts.output);
6970
+ const queried = applyQuery(rows, opts.query);
6971
+ if (fmt === "json") return JSON.stringify(queried, null, 2);
6972
+ if (!Array.isArray(queried) || queried.length === 0) return "";
6973
+ const first = queried[0];
6974
+ const scalar = typeof first !== "object" || first === null;
6975
+ if (scalar) return queried.map((v) => fmt === "csv" ? csvCell(v) : String(v)).join("\n");
6976
+ const objs = queried;
6977
+ const cols = opts.columns ?? [...new Set(objs.flatMap((r) => Object.keys(r)))];
6978
+ if (fmt === "csv") {
6979
+ return [cols.map(csvCell).join(","), ...objs.map((r) => cols.map((c) => csvCell(r[c])).join(","))].join("\n");
6980
+ }
6981
+ const widths = cols.map((c) => Math.max(c.length, ...objs.map((r) => String(r[c] ?? "").length)));
6982
+ const line = (cells) => cells.map((s, i) => s.padEnd(widths[i] ?? 0)).join(" ").trimEnd();
6983
+ return [line(cols), ...objs.map((r) => line(cols.map((c) => String(r[c] ?? ""))))].join("\n");
6984
+ }
6985
+ function isMachineOutput(opts) {
6986
+ return parseFormat(opts.output) !== "text";
6987
+ }
6988
+ function outputError(opts) {
6989
+ const f = (opts.output ?? configGet("output") ?? "text").toLowerCase();
6990
+ return f === "json" || f === "csv" || f === "text" ? null : `invalid --output '${opts.output}' \u2014 use text | json | csv`;
6991
+ }
6101
6992
 
6102
6993
  // src/commands/routers.ts
6103
6994
  function registerRoutersCommand(program2) {
6104
6995
  const routers = program2.command("routers").description("manage the free-router pool the model runs on");
6105
- routers.command("list").description("list the router catalog and the active pool").action(() => {
6996
+ routers.command("list").description("list the router catalog and the active pool").option("-o, --output <fmt>", "output format: text (default) | json | csv").option("-q, --query <expr>", "filter/select: 'field', 'field=value', or 'field=value:selectField'").action((opts) => {
6997
+ const oerr = outputError(opts);
6998
+ if (oerr) die(oerr);
6999
+ const pool = new Set(resolvePool().map((p) => p.id));
7000
+ if (isMachineOutput(opts) || opts.query) {
7001
+ const catalogRows = ROUTER_CATALOG.filter((r) => !r.comingSoon).map((r) => ({
7002
+ id: r.id,
7003
+ name: r.displayName,
7004
+ tier: r.keyless ? "keyless" : r.tier,
7005
+ keyless: Boolean(r.keyless),
7006
+ source: "catalog",
7007
+ active: pool.has(r.id)
7008
+ }));
7009
+ const customRows = registeredRouters().filter((r) => !ROUTER_CATALOG.some((c) => c.id === r.id)).map((r) => ({
7010
+ id: r.id,
7011
+ name: r.name,
7012
+ tier: r.apiKey && r.apiKey !== KEYLESS_SENTINEL ? "byok" : "keyless",
7013
+ keyless: !r.apiKey || r.apiKey === KEYLESS_SENTINEL,
7014
+ source: "custom",
7015
+ active: pool.has(r.id)
7016
+ }));
7017
+ process.stdout.write(renderList2([...catalogRows, ...customRows], {
7018
+ output: opts.output,
7019
+ query: opts.query,
7020
+ columns: ["id", "name", "tier", "keyless", "active", "source"]
7021
+ }) + "\n");
7022
+ return;
7023
+ }
6106
7024
  heading("Routers");
6107
7025
  for (const r of ROUTER_CATALOG) {
6108
7026
  if (r.comingSoon) {
@@ -6125,8 +7043,7 @@ function registerRoutersCommand(program2) {
6125
7043
  `);
6126
7044
  }
6127
7045
  }
6128
- const pool = resolvePool();
6129
- info(pool.length ? `active pool: ${pool.map((p) => p.id).join(", ")}` : "active pool: empty \u2192 using the keyless floor");
7046
+ info(pool.size ? `active pool: ${[...pool].join(", ")}` : "active pool: empty \u2192 using the keyless floor");
6130
7047
  });
6131
7048
  routers.command("add <name>").description("live-validate a router and add it to the pool \u2014 a catalog name, OR any custom endpoint via --url").option("-k, --key <key>", "API key (BYOK) \u2014 omit for a keyless free router").option("-m, --model <id>", "model id to run (REQUIRED for a custom --url router)").option("--url <baseUrl>", "add ANY custom free/BYOK router by its OpenAI-compatible base URL (the part BEFORE /chat/completions)").option("--api <api>", "custom router API: 'openai' (default) or 'google'", "openai").action(async (name, opts) => {
6132
7049
  let entry;
@@ -6162,10 +7079,10 @@ function registerRoutersCommand(program2) {
6162
7079
  }
6163
7080
 
6164
7081
  // src/commands/scribe.ts
6165
- import { readFileSync as readFileSync20 } from "fs";
7082
+ import { readFileSync as readFileSync23 } from "fs";
6166
7083
 
6167
7084
  // src/scribe/transcript.ts
6168
- import { existsSync as existsSync16, readFileSync as readFileSync19 } from "fs";
7085
+ import { existsSync as existsSync19, readFileSync as readFileSync22 } from "fs";
6169
7086
  function parseHookStdin(raw) {
6170
7087
  try {
6171
7088
  const j = JSON.parse(raw);
@@ -6198,8 +7115,8 @@ function isHumanUser(e) {
6198
7115
  }
6199
7116
  var FILE_KEYS = ["file_path", "path", "notebook_path", "filePath"];
6200
7117
  function lastTurnFromTranscript(path) {
6201
- if (!existsSync16(path)) return null;
6202
- const raw = readFileSync19(path, "utf8");
7118
+ if (!existsSync19(path)) return null;
7119
+ const raw = readFileSync22(path, "utf8");
6203
7120
  const entries = [];
6204
7121
  for (const line of raw.split("\n")) {
6205
7122
  if (!line.trim()) continue;
@@ -6260,7 +7177,7 @@ function lastTurnFromTranscript(path) {
6260
7177
  // src/commands/scribe.ts
6261
7178
  function readStdin() {
6262
7179
  try {
6263
- return readFileSync20(0, "utf8");
7180
+ return readFileSync23(0, "utf8");
6264
7181
  } catch {
6265
7182
  return "";
6266
7183
  }
@@ -6373,40 +7290,8 @@ function registerScribeCommand(program2) {
6373
7290
  }
6374
7291
 
6375
7292
  // src/commands/connectors.ts
6376
- import { createInterface as createInterface7 } from "readline/promises";
6377
- import { stdin as stdin7, stdout as stdout8 } from "process";
6378
-
6379
- // src/connectors/custom.ts
6380
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync17 } from "fs";
6381
- import { join as join24 } from "path";
6382
- function file3() {
6383
- return join24(oriroDir(), "mcp-custom.json");
6384
- }
6385
- function readCustomServers() {
6386
- try {
6387
- const v = JSON.parse(readFileSync21(file3(), "utf8"));
6388
- return Array.isArray(v) ? v : [];
6389
- } catch {
6390
- return [];
6391
- }
6392
- }
6393
- function saveCustomServer(server) {
6394
- const rest = readCustomServers().filter((s) => s.name.toLowerCase() !== server.name.toLowerCase());
6395
- writeFileSync17(join24(ensureOriroDir(), "mcp-custom.json"), JSON.stringify([...rest, server], null, 2), "utf8");
6396
- }
6397
- function removeCustomServer(name) {
6398
- const before = readCustomServers();
6399
- const after = before.filter((s) => s.name.toLowerCase() !== name.toLowerCase());
6400
- if (after.length === before.length) return false;
6401
- writeFileSync17(join24(ensureOriroDir(), "mcp-custom.json"), JSON.stringify(after, null, 2), "utf8");
6402
- return true;
6403
- }
6404
- function trustedServerNames() {
6405
- return readCustomServers().filter((s) => s.trusted).map((s) => s.name);
6406
- }
6407
- function isServerTrusted(name) {
6408
- return trustedServerNames().some((n) => n.toLowerCase() === name.toLowerCase());
6409
- }
7293
+ import { createInterface as createInterface8 } from "readline/promises";
7294
+ import { stdin as stdin8, stdout as stdout9 } from "process";
6410
7295
 
6411
7296
  // src/connectors/setup.ts
6412
7297
  function buildServerConfig(i) {
@@ -6437,39 +7322,41 @@ function parsePairs(s) {
6437
7322
  return out;
6438
7323
  }
6439
7324
 
6440
- // src/connectors/mcp-client.ts
6441
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6442
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
6443
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6444
- function assertSafeUrl(raw, allowLocal = false) {
6445
- const u = new URL(raw);
6446
- if (u.protocol !== "https:" && u.protocol !== "http:") throw new Error(`unsupported scheme: ${u.protocol}`);
6447
- const host = u.hostname.toLowerCase();
6448
- const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost");
6449
- const isPrivate = /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || /^169\.254\./.test(host) || /^fe80:/i.test(host) || /^f[cd][0-9a-f]{2}:/i.test(host) || host === "169.254.169.254" || host === "metadata.google.internal";
6450
- if ((isLoopback || isPrivate) && !allowLocal) {
6451
- throw new Error(`blocked SSRF target ${host} (use --allow-local for loopback/LAN MCP servers)`);
6452
- }
6453
- if (u.protocol === "http:" && !isLoopback && !allowLocal) throw new Error(`refusing plaintext http to ${host} \u2014 use https`);
6454
- return u;
6455
- }
6456
-
6457
7325
  // src/commands/connectors.ts
6458
7326
  function registerConnectorsCommand(program2) {
6459
7327
  const connectors = program2.command("connectors").description("MCP connectors \u2014 add external tools/services (inert until used)");
6460
- connectors.command("list [category]").description("list the connector catalog (optionally filtered by category)").action((category) => {
7328
+ connectors.command("list [category]").description("list the connector catalog (optionally filtered by category)").option("-o, --output <fmt>", "output format: text (default) | json | csv").option("-q, --query <expr>", "filter/select: 'field', 'field=value', or 'field=value:selectField'").action((category, opts) => {
7329
+ const oerr = outputError(opts);
7330
+ if (oerr) die(oerr);
6461
7331
  if (category && !connectorCategories().includes(category)) {
6462
7332
  die(`unknown category '${category}' \u2014 categories: ${connectorCategories().join(", ")}`);
6463
7333
  }
6464
7334
  const entries = listConnectors(category);
6465
7335
  const added = new Set(addedConnectors().map((c) => c.slug));
7336
+ if (isMachineOutput(opts) || opts.query) {
7337
+ const rows = entries.map((c) => ({
7338
+ slug: c.slug,
7339
+ name: c.name,
7340
+ category: c.category,
7341
+ addable: Boolean(c.mcpUrl),
7342
+ added: added.has(c.slug)
7343
+ }));
7344
+ process.stdout.write(renderList2(rows, {
7345
+ output: opts.output,
7346
+ query: opts.query,
7347
+ columns: ["slug", "name", "category", "addable", "added"]
7348
+ }) + "\n");
7349
+ return;
7350
+ }
6466
7351
  heading(category ? `Connectors \xB7 ${category}` : "Connectors");
6467
7352
  let addable = 0;
7353
+ const NAME_W = 34;
6468
7354
  for (const c of entries) {
6469
7355
  const canAdd = !!c.mcpUrl;
6470
7356
  if (canAdd) addable++;
6471
7357
  const mark = !canAdd ? dim("\xB7") : added.has(c.slug) ? accent("\u25CF") : dim("\u25CB");
6472
- const name = canAdd ? c.name.padEnd(22) : dim(`${c.name} (coming soon)`.padEnd(22));
7358
+ const label = (canAdd ? c.name : `${c.name} (coming soon)`).padEnd(NAME_W);
7359
+ const name = canAdd ? label : dim(label);
6473
7360
  process.stdout.write(` ${mark} ${(canAdd ? accent : dim)(c.slug.padEnd(20))} ${name} ${dim(c.category)}
6474
7361
  `);
6475
7362
  }
@@ -6484,12 +7371,20 @@ function registerConnectorsCommand(program2) {
6484
7371
  if (!res.ok) die(res.error ?? `could not add '${slug}'`);
6485
7372
  ok(`added ${accent(slug)} \u2014 recorded locally`);
6486
7373
  });
6487
- connectors.command("remove <slug>").description("remove a connector").action((slug) => {
7374
+ connectors.command("remove <slug>").description("remove a connector").option("-f, --force", "skip the confirmation prompt").action(async (slug, opts) => {
7375
+ if (!isConnectorAdded(slug)) {
7376
+ info(`'${slug}' is not in your added list \u2014 nothing to remove`);
7377
+ return;
7378
+ }
7379
+ if (!await confirmDestructive(`remove connector '${slug}'`, opts)) {
7380
+ info("cancelled");
7381
+ return;
7382
+ }
6488
7383
  if (removeConnector(slug)) ok(`removed ${accent(slug)}`);
6489
7384
  else info(`'${slug}' is not in your added list \u2014 nothing to remove`);
6490
7385
  });
6491
7386
  connectors.command("setup").description("guided setup of a CUSTOM MCP server \u2014 Guardian-vetted, no JSON").option("--name <name>", "a short name for the server").option("--command <cmd>", "stdio launch command, e.g. 'npx -y @scope/mcp'").option("--args <args>", "space-separated args for --command").option("--env <pairs>", "comma-separated KEY=VAL env vars").option("--url <url>", "http(s) MCP endpoint (instead of --command)").option("--header <pairs>", "comma-separated KEY=VAL headers (with --url)").option("--allow-local", "permit loopback/LAN URL targets").option("-y, --yes", "trust and save when Guardian says 'ask'").action(async (opts) => {
6492
- const interactive = !!stdin7.isTTY && !!stdout8.isTTY;
7387
+ const interactive = !!stdin8.isTTY && !!stdout9.isTTY;
6493
7388
  let { name, command, url } = opts;
6494
7389
  let argsStr = opts.args;
6495
7390
  let envStr = opts.env;
@@ -6508,7 +7403,7 @@ function registerConnectorsCommand(program2) {
6508
7403
  );
6509
7404
  return;
6510
7405
  }
6511
- const rl = createInterface7({ input: stdin7, output: stdout8 });
7406
+ const rl = createInterface8({ input: stdin8, output: stdout9 });
6512
7407
  try {
6513
7408
  name = name || (await rl.question("Server name: ")).trim();
6514
7409
  if (!command && !url) {
@@ -6550,7 +7445,7 @@ function registerConnectorsCommand(program2) {
6550
7445
  if (opts.yes) {
6551
7446
  trusted = true;
6552
7447
  } else if (interactive) {
6553
- const rl = createInterface7({ input: stdin7, output: stdout8 });
7448
+ const rl = createInterface8({ input: stdin8, output: stdout9 });
6554
7449
  try {
6555
7450
  const ans = (await rl.question(`Trust and save "${name}"? [y/N] `)).trim().toLowerCase();
6556
7451
  trusted = ans === "y" || ans === "yes";
@@ -6592,14 +7487,14 @@ function registerConnectorsCommand(program2) {
6592
7487
  }
6593
7488
 
6594
7489
  // src/channels/config.ts
6595
- import { readFileSync as readFileSync22, writeFileSync as writeFileSync18 } from "fs";
6596
- import { join as join25 } from "path";
6597
- function file4() {
6598
- return join25(oriroDir(), "channels.json");
7490
+ import { readFileSync as readFileSync24, writeFileSync as writeFileSync20 } from "fs";
7491
+ import { join as join27 } from "path";
7492
+ function file5() {
7493
+ return join27(oriroDir(), "channels.json");
6599
7494
  }
6600
7495
  function readChannels() {
6601
7496
  try {
6602
- const v = JSON.parse(readFileSync22(file4(), "utf8"));
7497
+ const v = JSON.parse(readFileSync24(file5(), "utf8"));
6603
7498
  return Array.isArray(v) ? v : [];
6604
7499
  } catch {
6605
7500
  return [];
@@ -6608,10 +7503,10 @@ function readChannels() {
6608
7503
  function saveChannel(cfg) {
6609
7504
  const all = readChannels().filter((c) => c.kind !== cfg.kind);
6610
7505
  all.push(cfg);
6611
- writeFileSync18(join25(ensureOriroDir(), "channels.json"), JSON.stringify(all, null, 2), "utf8");
7506
+ writeFileSync20(join27(ensureOriroDir(), "channels.json"), JSON.stringify(all, null, 2), "utf8");
6612
7507
  }
6613
7508
  function removeChannel(kind) {
6614
- writeFileSync18(join25(ensureOriroDir(), "channels.json"), JSON.stringify(readChannels().filter((c) => c.kind !== kind), null, 2), "utf8");
7509
+ writeFileSync20(join27(ensureOriroDir(), "channels.json"), JSON.stringify(readChannels().filter((c) => c.kind !== kind), null, 2), "utf8");
6615
7510
  }
6616
7511
 
6617
7512
  // src/channels/telegram.ts
@@ -6728,9 +7623,9 @@ async function startDiscord(token) {
6728
7623
  }
6729
7624
 
6730
7625
  // src/channels/whatsapp.ts
6731
- import { join as join26 } from "path";
7626
+ import { join as join28 } from "path";
6732
7627
  function whatsappAuthDir() {
6733
- return join26(oriroDir(), "whatsapp-auth");
7628
+ return join28(oriroDir(), "whatsapp-auth");
6734
7629
  }
6735
7630
  async function startWhatsApp() {
6736
7631
  let baileys;
@@ -6848,12 +7743,26 @@ function registerChannelsCommand(program2) {
6848
7743
  }
6849
7744
 
6850
7745
  // src/commands/skills.ts
6851
- import { existsSync as existsSync17, statSync as statSync3, mkdirSync as mkdirSync16, cpSync, rmSync as rmSync4 } from "fs";
6852
- import { resolve as resolve2, join as join27, basename, dirname as dirname3 } from "path";
7746
+ import { existsSync as existsSync20, statSync as statSync4, mkdirSync as mkdirSync17, cpSync, rmSync as rmSync4 } from "fs";
7747
+ import { resolve as resolve2, join as join29, basename, dirname as dirname4 } from "path";
6853
7748
  function registerSkillsCommand(program2) {
6854
7749
  const skills = program2.command("skills").description("the ORIRO skill library \u2014 bundled + your own");
6855
- skills.command("list").description("show CORE / TAIL skill counts (use --all to list names)").option("-a, --all", "list every skill name").action(async (opts) => {
7750
+ skills.command("list").description("show CORE / TAIL skill counts (use --all to list names)").option("-a, --all", "list every skill name").option("-o, --output <fmt>", "output format: text (default) | json | csv").option("-q, --query <expr>", "filter/select: 'field', 'field=value', or 'field=value:selectField'").action(async (opts) => {
7751
+ const oerr = outputError(opts);
7752
+ if (oerr) die(oerr);
6856
7753
  const s = await loadOriroSkills();
7754
+ if (isMachineOutput(opts) || opts.query) {
7755
+ const rows = s.all.map((sk) => ({
7756
+ name: sk.name,
7757
+ tier: sk.disableModelInvocation ? "TAIL" : "CORE"
7758
+ }));
7759
+ process.stdout.write(renderList2(rows, {
7760
+ output: opts.output,
7761
+ query: opts.query,
7762
+ columns: ["name", "tier"]
7763
+ }) + "\n");
7764
+ return;
7765
+ }
6857
7766
  heading("Skills");
6858
7767
  info(`${accent(String(s.all.length))} loaded \xB7 ${accent(String(s.core.length))} CORE (model-visible) \xB7 ${accent(String(s.tail.length))} TAIL (/name-only)`);
6859
7768
  if (opts.all) {
@@ -6867,38 +7776,42 @@ function registerSkillsCommand(program2) {
6867
7776
  });
6868
7777
  skills.command("add <path>").description("add your own skill \u2014 a folder containing SKILL.md, or a SKILL.md file").action((p) => {
6869
7778
  const src = resolve2(p);
6870
- if (!existsSync17(src)) die(`not found: ${src}`);
7779
+ if (!existsSync20(src)) die(`not found: ${src}`);
6871
7780
  const dest = userSkillsDir();
6872
- mkdirSync16(dest, { recursive: true });
6873
- const st = statSync3(src);
7781
+ mkdirSync17(dest, { recursive: true });
7782
+ const st = statSync4(src);
6874
7783
  if (st.isDirectory()) {
6875
- if (!existsSync17(join27(src, "SKILL.md"))) die(`no SKILL.md in ${src} \u2014 a skill folder must contain SKILL.md`);
7784
+ if (!existsSync20(join29(src, "SKILL.md"))) die(`no SKILL.md in ${src} \u2014 a skill folder must contain SKILL.md`);
6876
7785
  const name = basename(src);
6877
- cpSync(src, join27(dest, name), { recursive: true });
6878
- ok(`added skill ${accent(name)} \u2192 ${join27(dest, name)}`);
7786
+ cpSync(src, join29(dest, name), { recursive: true });
7787
+ ok(`added skill ${accent(name)} \u2192 ${join29(dest, name)}`);
6879
7788
  } else if (basename(src).toLowerCase() === "skill.md") {
6880
- const name = basename(dirname3(src)) || "custom-skill";
6881
- mkdirSync16(join27(dest, name), { recursive: true });
6882
- cpSync(src, join27(dest, name, "SKILL.md"));
6883
- ok(`added skill ${accent(name)} \u2192 ${join27(dest, name)}`);
7789
+ const name = basename(dirname4(src)) || "custom-skill";
7790
+ mkdirSync17(join29(dest, name), { recursive: true });
7791
+ cpSync(src, join29(dest, name, "SKILL.md"));
7792
+ ok(`added skill ${accent(name)} \u2192 ${join29(dest, name)}`);
6884
7793
  } else {
6885
7794
  die("expected a folder containing SKILL.md, or a SKILL.md file");
6886
7795
  }
6887
7796
  info("It loads on next launch \u2014 and is available in chat via /skill.");
6888
7797
  });
6889
- skills.command("remove <name>").description("remove a skill you added").action((name) => {
6890
- const target = join27(userSkillsDir(), name);
6891
- if (!existsSync17(target)) {
7798
+ skills.command("remove <name>").description("remove a skill you added").option("-f, --force", "skip the confirmation prompt").action(async (name, opts) => {
7799
+ const target = join29(userSkillsDir(), name);
7800
+ if (!existsSync20(target)) {
6892
7801
  info(`'${name}' is not a user-added skill \u2014 nothing to remove`);
6893
7802
  return;
6894
7803
  }
7804
+ if (!await confirmDestructive(`remove skill '${name}'`, opts)) {
7805
+ info("cancelled");
7806
+ return;
7807
+ }
6895
7808
  rmSync4(target, { recursive: true, force: true });
6896
7809
  ok(`removed ${accent(name)}`);
6897
7810
  });
6898
7811
  }
6899
7812
 
6900
7813
  // src/commands/language.ts
6901
- import { stdin as stdin8 } from "process";
7814
+ import { stdin as stdin9 } from "process";
6902
7815
  function resolveLanguage(input) {
6903
7816
  return languageByCode(input) ?? LANGUAGES.find((l) => l.name.toLowerCase() === input.trim().toLowerCase());
6904
7817
  }
@@ -6920,7 +7833,7 @@ function registerLanguageCommand(program2) {
6920
7833
  ok(`${accent(lang.name)} is now your terminal language.`);
6921
7834
  return;
6922
7835
  }
6923
- if (stdin8.isTTY) {
7836
+ if (stdin9.isTTY) {
6924
7837
  const lang = await selectLanguageInteractive();
6925
7838
  setTerminalLanguage(lang);
6926
7839
  ok(`${accent(lang.name)} is now your terminal language.`);
@@ -6933,7 +7846,7 @@ function registerLanguageCommand(program2) {
6933
7846
  }
6934
7847
 
6935
7848
  // src/commands/avatar.ts
6936
- import { stdin as stdin9 } from "process";
7849
+ import { stdin as stdin10 } from "process";
6937
7850
  function registerAvatarCommand(program2) {
6938
7851
  program2.command("avatar").description("show or change your terminal avatar").argument("[slug]", "set directly to this avatar slug").option("-l, --list", "list every avatar by category").action(async (slug, opts) => {
6939
7852
  if (opts.list) {
@@ -6951,7 +7864,7 @@ function registerAvatarCommand(program2) {
6951
7864
  ok(`${accent(avatar.slug)} is now your terminal face.`);
6952
7865
  return;
6953
7866
  }
6954
- if (stdin9.isTTY) {
7867
+ if (stdin10.isTTY) {
6955
7868
  const chosen = await selectAvatarInteractive();
6956
7869
  if (!chosen) {
6957
7870
  info("no change.");
@@ -7029,12 +7942,12 @@ function registerHeadCommand(program2) {
7029
7942
  }
7030
7943
 
7031
7944
  // src/commands/voice.ts
7032
- import { stdin as stdin10, stdout as stdout9 } from "process";
7945
+ import { stdin as stdin11, stdout as stdout10 } from "process";
7033
7946
  function registerVoiceCommand(program2) {
7034
- program2.command("voice").description("speech-to-text \u2014 transcribe an audio file or the mic (on-device Whisper, experimental)").argument("[file]", "audio file to transcribe (omit to record from the mic on a real terminal)").option("--translate", "translate speech to English (Whisper translate task)").option("--seconds <n>", "mic recording length in seconds", "6").action(async (file5, opts) => {
7035
- const interactive = !!stdin10.isTTY && !!stdout9.isTTY;
7947
+ program2.command("voice").description("speech-to-text \u2014 transcribe an audio file or the mic (on-device Whisper, experimental)").argument("[file]", "audio file to transcribe (omit to record from the mic on a real terminal)").option("--translate", "translate speech to English (Whisper translate task)").option("--seconds <n>", "mic recording length in seconds", "6").action(async (file6, opts) => {
7948
+ const interactive = !!stdin11.isTTY && !!stdout10.isTTY;
7036
7949
  heading("ORIRO voice \u{1F399}");
7037
- let audio = file5;
7950
+ let audio = file6;
7038
7951
  if (!audio) {
7039
7952
  if (!interactive) {
7040
7953
  info("On-device speech-to-text (experimental \u2014 needs ffmpeg + the transformers voice peer).");
@@ -7067,7 +7980,7 @@ function registerVoiceCommand(program2) {
7067
7980
  }
7068
7981
 
7069
7982
  // src/agents/catalog.ts
7070
- import { readFileSync as readFileSync23 } from "fs";
7983
+ import { readFileSync as readFileSync25 } from "fs";
7071
7984
  function parseAgentDef(raw, now) {
7072
7985
  if (!raw || typeof raw !== "object") return { ok: false, error: "not a JSON object" };
7073
7986
  const o = raw;
@@ -7094,7 +8007,7 @@ async function fetchAgentSource(pathOrUrl) {
7094
8007
  if (!res.ok) throw new Error(`fetch failed: HTTP ${res.status}`);
7095
8008
  return await res.json();
7096
8009
  }
7097
- return JSON.parse(readFileSync23(pathOrUrl, "utf8"));
8010
+ return JSON.parse(readFileSync25(pathOrUrl, "utf8"));
7098
8011
  }
7099
8012
  async function addAgentFromSource(pathOrUrl, now) {
7100
8013
  let raw;
@@ -7110,6 +8023,67 @@ async function addAgentFromSource(pathOrUrl, now) {
7110
8023
  return { ok: true, name: parsed.def.name, overwrote };
7111
8024
  }
7112
8025
 
8026
+ // src/commands/schedule.ts
8027
+ import { spawnSync } from "child_process";
8028
+ import { platform } from "process";
8029
+ var TASK_NAME = "ORIRO_Agents_Tick";
8030
+ function intervalMinutes(spec) {
8031
+ const m = /^(\d+)(m|h)$/.exec(spec.trim());
8032
+ if (!m) return null;
8033
+ const n = parseInt(m[1], 10);
8034
+ if (n <= 0) return null;
8035
+ return m[2] === "h" ? n * 60 : n;
8036
+ }
8037
+ function tickInvocation() {
8038
+ return { node: process.execPath, bin: process.argv[1] ?? "oriro" };
8039
+ }
8040
+ function buildCron(mins, remove) {
8041
+ const { node, bin } = tickInvocation();
8042
+ if (platform === "win32") {
8043
+ if (remove) return { cmd: `schtasks /Delete /TN ${TASK_NAME} /F`, note: "Windows Task Scheduler" };
8044
+ const sc = mins % 60 === 0 ? `/SC HOURLY /MO ${mins / 60}` : `/SC MINUTE /MO ${mins}`;
8045
+ return {
8046
+ cmd: `schtasks /Create /TN ${TASK_NAME} /TR "\\"${node}\\" \\"${bin}\\" agents tick" ${sc} /F`,
8047
+ note: "Windows Task Scheduler"
8048
+ };
8049
+ }
8050
+ const line = `*/${mins} * * * * "${node}" "${bin}" agents tick # ${TASK_NAME}`;
8051
+ if (remove) {
8052
+ return { cmd: `crontab -l 2>/dev/null | grep -v '# ${TASK_NAME}' | crontab -`, note: "crontab" };
8053
+ }
8054
+ return {
8055
+ cmd: `( crontab -l 2>/dev/null | grep -v '# ${TASK_NAME}'; echo '${line}' ) | crontab -`,
8056
+ note: "crontab"
8057
+ };
8058
+ }
8059
+ function runShell(cmd) {
8060
+ const r = platform === "win32" ? spawnSync("cmd", ["/c", cmd], { encoding: "utf8" }) : spawnSync("sh", ["-c", cmd], { encoding: "utf8" });
8061
+ if (r.status !== 0) {
8062
+ info(dim((r.stderr || r.stdout || "").trim().slice(0, 300)));
8063
+ return false;
8064
+ }
8065
+ return true;
8066
+ }
8067
+ function registerAgentsCron(agents) {
8068
+ agents.command("cron").description("install an OS scheduler that runs `agents tick` on an interval (fires scheduled agents)").option("--every <spec>", "interval: Nm | Nh", "5m").option("--remove", "remove the scheduler entry instead of installing it").option("--apply", "actually apply the change (default: just print the command to run)").action((opts) => {
8069
+ const mins = intervalMinutes(opts.every);
8070
+ if (!opts.remove && mins === null) die(`invalid --every '${opts.every}' \u2014 use Nm or Nh (e.g. 5m, 2h)`);
8071
+ const { cmd, note } = buildCron(mins ?? 5, Boolean(opts.remove));
8072
+ heading(opts.remove ? "Remove scheduled agents" : "Schedule agents");
8073
+ info(`${note}: runs ${accent("oriro agents tick")} ${opts.remove ? "" : `every ${accent(opts.every)}`}`);
8074
+ if (!opts.apply) {
8075
+ process.stdout.write(`
8076
+ ${cmd}
8077
+
8078
+ `);
8079
+ info(dim("printed only \u2014 re-run with --apply to make this change, or run the command yourself"));
8080
+ return;
8081
+ }
8082
+ if (runShell(cmd)) ok(opts.remove ? "scheduler entry removed" : `scheduled \u2014 agents tick will run every ${opts.every}`);
8083
+ else die("could not apply the schedule (see the message above) \u2014 you can run the printed command manually");
8084
+ });
8085
+ }
8086
+
7113
8087
  // src/commands/agents.ts
7114
8088
  function nowIso() {
7115
8089
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -7143,14 +8117,32 @@ function registerAgentsCommand(program2) {
7143
8117
  info(`make one: ${accent('oriro agents make <name> --task "\u2026" [--router <id>] [--schedule 1h]')}`);
7144
8118
  info(`then: ${accent("oriro agents run <name>")} ${dim("\xB7 or")} ${accent("oriro agents tick")} ${dim("for scheduled ones")}`);
7145
8119
  });
7146
- agents.command("list").description("list your saved agents").action(() => {
8120
+ agents.command("list").description("list your saved agents").option("-o, --output <fmt>", "output format: text (default) | json | csv").option("-q, --query <expr>", "filter/select: 'field', 'field=value', or 'field=value:selectField'").action((opts) => {
8121
+ const oerr = outputError(opts);
8122
+ if (oerr) die(oerr);
7147
8123
  const all = listAgents();
8124
+ const state = loadState();
8125
+ if (isMachineOutput(opts) || opts.query) {
8126
+ const rows = all.map((a) => ({
8127
+ name: a.name,
8128
+ brain: a.router ?? "pool",
8129
+ schedule: a.schedule ?? "manual",
8130
+ description: a.description ?? "",
8131
+ lastRun: state[a.name]?.lastRunAt ? new Date(state[a.name].lastRunAt).toISOString() : "",
8132
+ lastOk: state[a.name]?.lastOk ?? null
8133
+ }));
8134
+ process.stdout.write(renderList2(rows, {
8135
+ output: opts.output,
8136
+ query: opts.query,
8137
+ columns: ["name", "brain", "schedule", "lastRun", "lastOk"]
8138
+ }) + "\n");
8139
+ return;
8140
+ }
7148
8141
  heading("Agents");
7149
8142
  if (!all.length) {
7150
8143
  info(`no agents yet \u2014 make one: ${accent('oriro agents make my-agent --task "\u2026"')}`);
7151
8144
  return;
7152
8145
  }
7153
- const state = loadState();
7154
8146
  for (const a of all) {
7155
8147
  printAgent(a);
7156
8148
  const last = state[a.name]?.lastRunAt;
@@ -7200,13 +8192,22 @@ function registerAgentsCommand(program2) {
7200
8192
  ok(`${res.overwrote ? "updated" : "added"} agent ${accent(res.name ?? "")} ${dim("\u2192 ~/.oriro/agents")}`);
7201
8193
  info(`run it: ${accent(`oriro agents run ${res.name}`)}`);
7202
8194
  });
7203
- agents.command("remove <name>").description("delete an agent").action((name) => {
8195
+ agents.command("remove <name>").description("delete an agent").option("-f, --force", "skip the confirmation prompt").action(async (name, opts) => {
8196
+ if (!loadAgent(name)) {
8197
+ info(`'${name}' is not a saved agent \u2014 nothing to remove`);
8198
+ return;
8199
+ }
8200
+ if (!await confirmDestructive(`remove agent '${name}'`, opts)) {
8201
+ info("cancelled");
8202
+ return;
8203
+ }
7204
8204
  if (!removeAgent(name)) {
7205
8205
  info(`'${name}' is not a saved agent \u2014 nothing to remove`);
7206
8206
  return;
7207
8207
  }
7208
8208
  ok(`removed ${accent(name)}`);
7209
8209
  });
8210
+ registerAgentsCron(agents);
7210
8211
  agents.command("tick").description("run every DUE scheduled agent once, then exit (wire to OS cron / Task Scheduler)").action(async () => {
7211
8212
  const state = loadState();
7212
8213
  const now = Date.now();
@@ -7241,19 +8242,327 @@ function registerAgentsCommand(program2) {
7241
8242
  });
7242
8243
  }
7243
8244
 
8245
+ // src/commands/completion.ts
8246
+ function extractTree(program2) {
8247
+ const nodes = [];
8248
+ for (const c of program2.commands) {
8249
+ const name = c.name();
8250
+ if (name === "completion") continue;
8251
+ nodes.push({
8252
+ name,
8253
+ subs: c.commands.map((s) => s.name()),
8254
+ opts: c.options.map((o) => o.long).filter((l) => Boolean(l))
8255
+ });
8256
+ }
8257
+ return nodes;
8258
+ }
8259
+ var SHELLS = ["bash", "zsh", "fish", "pwsh"];
8260
+ function topNames(tree) {
8261
+ return [...tree.map((n) => n.name), "completion", "help"].join(" ");
8262
+ }
8263
+ function genBash(tree) {
8264
+ const cases = tree.map((n) => ` ${n.name}) COMPREPLY=( $(compgen -W "${n.subs.join(" ")} ${n.opts.join(" ")}" -- "$cur") );;`).join("\n");
8265
+ return `# ORIRO bash completion. Install: oriro completion bash > /etc/bash_completion.d/oriro
8266
+ # or (per-user): oriro completion bash >> ~/.bashrc
8267
+ _oriro_complete() {
8268
+ local cur prev cword
8269
+ cur="\${COMP_WORDS[COMP_CWORD]}"
8270
+ cword=$COMP_CWORD
8271
+ if [ "$cword" -eq 1 ]; then
8272
+ COMPREPLY=( $(compgen -W "${topNames(tree)}" -- "$cur") )
8273
+ return
8274
+ fi
8275
+ case "\${COMP_WORDS[1]}" in
8276
+ ${cases}
8277
+ *) COMPREPLY=();;
8278
+ esac
8279
+ }
8280
+ complete -F _oriro_complete oriro
8281
+ `;
8282
+ }
8283
+ function genZsh(tree) {
8284
+ const cases = tree.map((n) => ` ${n.name}) compadd ${n.subs.join(" ")} ${n.opts.join(" ")} ;;`).join("\n");
8285
+ return `#compdef oriro
8286
+ # ORIRO zsh completion. Install: oriro completion zsh > "\${fpath[1]}/_oriro" (then restart the shell)
8287
+ _oriro() {
8288
+ local -a words; words=("\${(@)words}")
8289
+ if (( CURRENT == 2 )); then
8290
+ compadd ${topNames(tree)}
8291
+ return
8292
+ fi
8293
+ case "\${words[2]}" in
8294
+ ${cases}
8295
+ esac
8296
+ }
8297
+ _oriro "$@"
8298
+ `;
8299
+ }
8300
+ function genFish(tree) {
8301
+ const lines = [
8302
+ "# ORIRO fish completion. Install: oriro completion fish > ~/.config/fish/completions/oriro.fish",
8303
+ `complete -c oriro -f -n __fish_use_subcommand -a "${topNames(tree)}"`
8304
+ ];
8305
+ for (const n of tree) {
8306
+ if (n.subs.length) {
8307
+ lines.push(`complete -c oriro -f -n "__fish_seen_subcommand_from ${n.name}" -a "${n.subs.join(" ")}"`);
8308
+ }
8309
+ }
8310
+ return lines.join("\n") + "\n";
8311
+ }
8312
+ function genPwsh(tree) {
8313
+ const cases = tree.map((n) => ` '${n.name}' { @(${[...n.subs, ...n.opts].map((s) => `'${s}'`).join(", ")}) }`).join("\n");
8314
+ const top = [...tree.map((n) => n.name), "completion", "help"].map((s) => `'${s}'`).join(", ");
8315
+ return `# ORIRO PowerShell completion. Install: oriro completion pwsh >> $PROFILE (then restart pwsh)
8316
+ Register-ArgumentCompleter -Native -CommandName oriro -ScriptBlock {
8317
+ param($wordToComplete, $commandAst, $cursorPosition)
8318
+ $tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
8319
+ $candidates = if ($tokens.Count -le 2) {
8320
+ @(${top})
8321
+ } else {
8322
+ switch ($tokens[1]) {
8323
+ ${cases}
8324
+ default { @() }
8325
+ }
8326
+ }
8327
+ $candidates | Where-Object { $_ -like "$wordToComplete*" } |
8328
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
8329
+ }
8330
+ `;
8331
+ }
8332
+ var GENERATORS = {
8333
+ bash: genBash,
8334
+ zsh: genZsh,
8335
+ fish: genFish,
8336
+ pwsh: genPwsh
8337
+ };
8338
+ function registerCompletionCommand(program2) {
8339
+ program2.command("completion <shell>").description("print a shell tab-completion script (bash | zsh | fish | pwsh)").action((shell) => {
8340
+ const s = shell.toLowerCase();
8341
+ if (!SHELLS.includes(s)) {
8342
+ die(`unsupported shell '${shell}'. Use one of: ${SHELLS.join(", ")}`);
8343
+ return;
8344
+ }
8345
+ process.stdout.write(GENERATORS[s](extractTree(program2)));
8346
+ });
8347
+ }
8348
+
8349
+ // src/commands/config.ts
8350
+ function registerConfigCommand(program2) {
8351
+ const config = program2.command("config").description("your durable CLI settings (defaults in ~/.oriro/config.json)");
8352
+ config.command("list").description("show every setting, its value, and what it does").action(() => {
8353
+ const all = configAll();
8354
+ heading("Config");
8355
+ for (const { key, desc } of configKeys()) {
8356
+ const val = all[key];
8357
+ process.stdout.write(` ${accent(key.padEnd(10))} ${val !== void 0 ? accent(val) : dim("(default)")} ${dim(desc)}
8358
+ `);
8359
+ }
8360
+ info(`set: ${accent("oriro config set <key> <value>")} \xB7 clear: ${accent("oriro config unset <key>")}`);
8361
+ });
8362
+ config.command("get <key>").description("print one setting's value").action((key) => {
8363
+ if (!isConfigKey(key)) die(`unknown key '${key}' \u2014 run \`oriro config list\``);
8364
+ const val = configGet(key);
8365
+ if (val === void 0) {
8366
+ info(`${key} is unset (using the built-in default)`);
8367
+ return;
8368
+ }
8369
+ process.stdout.write(`${val}
8370
+ `);
8371
+ });
8372
+ config.command("set <key> <value>").description("set a setting (validated)").action((key, value) => {
8373
+ if (!isConfigKey(key)) die(`unknown key '${key}' \u2014 run \`oriro config list\``);
8374
+ const err = validateConfig(key, value);
8375
+ if (err) die(`invalid value for '${key}': ${err}`);
8376
+ configSet(key, value);
8377
+ ok(`${accent(key)} = ${accent(value)}`);
8378
+ });
8379
+ config.command("unset <key>").description("clear a setting back to its built-in default").action((key) => {
8380
+ if (!isConfigKey(key)) die(`unknown key '${key}' \u2014 run \`oriro config list\``);
8381
+ if (configUnset(key)) ok(`cleared ${accent(key)}`);
8382
+ else info(`${key} was already at its default`);
8383
+ });
8384
+ }
8385
+
8386
+ // src/commands/setup.ts
8387
+ import { rmSync as rmSync5 } from "fs";
8388
+ import { join as join30 } from "path";
8389
+ import { stdin as stdin12, stdout as stdout11 } from "process";
8390
+ var MARKERS = [
8391
+ "language.json",
8392
+ "avatar.json",
8393
+ "skills-onboarded.json",
8394
+ "connectors-onboarded.json",
8395
+ "models-onboarded.json",
8396
+ join30("routers", "onboarded.json")
8397
+ ];
8398
+ function registerSetupCommand(program2) {
8399
+ program2.command("setup").description("run the guided setup wizard (language \xB7 routers \xB7 connectors \xB7 skills \xB7 avatar)").option("--reset", "clear your settled choices and re-ask every step").action(async (opts) => {
8400
+ if (opts.reset) {
8401
+ for (const m of MARKERS) {
8402
+ try {
8403
+ rmSync5(join30(oriroDir(), m), { force: true });
8404
+ } catch {
8405
+ }
8406
+ }
8407
+ ok("reset \u2014 every step will be asked again");
8408
+ }
8409
+ if (!stdin12.isTTY || !stdout11.isTTY) {
8410
+ heading("ORIRO setup");
8411
+ info(`ORIRO is ${accent("keyless")} \u2014 no login, no API keys. Run ${accent("oriro setup")} in a real terminal for the guided wizard.`);
8412
+ info(dim("or configure directly: oriro language <code> \xB7 oriro routers add <id> \xB7 oriro connectors add <slug> \xB7 oriro config set <k> <v>"));
8413
+ return;
8414
+ }
8415
+ await runOnboarding();
8416
+ });
8417
+ }
8418
+
8419
+ // src/commands/import.ts
8420
+ import { existsSync as existsSync21, readFileSync as readFileSync26, readdirSync as readdirSync3, statSync as statSync5, cpSync as cpSync2, mkdirSync as mkdirSync18 } from "fs";
8421
+ import { join as join31, basename as basename2 } from "path";
8422
+ function registerImportCommand(program2) {
8423
+ const imp = program2.command("import").description("migrate from another CLI (MCP servers, skills)");
8424
+ imp.command("mcp <file>").description("import MCP servers from a Claude-compatible mcp.json (Guardian-vetted)").action((file6) => {
8425
+ if (!existsSync21(file6)) die(`no such file: ${file6}`);
8426
+ let servers;
8427
+ try {
8428
+ const j = JSON.parse(readFileSync26(file6, "utf8"));
8429
+ servers = j.mcpServers ?? j.servers ?? {};
8430
+ } catch (e) {
8431
+ die(`could not parse ${file6}: ${e instanceof Error ? e.message : String(e)}`);
8432
+ return;
8433
+ }
8434
+ const names = Object.keys(servers);
8435
+ if (!names.length) die(`no "mcpServers" found in ${file6}`);
8436
+ heading(`Import MCP \xB7 ${names.length} server${names.length === 1 ? "" : "s"}`);
8437
+ let imported = 0, blocked2 = 0;
8438
+ for (const name of names) {
8439
+ const s = servers[name];
8440
+ const input = {
8441
+ name,
8442
+ ...s.command ? { command: s.command } : {},
8443
+ ...s.args ? { args: s.args } : {},
8444
+ ...s.env ? { env: s.env } : {},
8445
+ ...s.url ? { url: s.url } : {},
8446
+ ...s.headers ? { headers: s.headers } : {}
8447
+ };
8448
+ if (s.url) {
8449
+ try {
8450
+ assertSafeUrl(s.url);
8451
+ } catch (e) {
8452
+ process.stdout.write(` ${fgHex(PALETTE.error, "\u2717")} ${name} ${dim(`blocked: ${e instanceof Error ? e.message : String(e)}`)}
8453
+ `);
8454
+ blocked2++;
8455
+ continue;
8456
+ }
8457
+ }
8458
+ const outcome = vetServer(input);
8459
+ if (outcome.decision === "block") {
8460
+ process.stdout.write(` ${fgHex(PALETTE.error, "\u2717")} ${name} ${dim(`blocked: ${outcome.reason}`)}
8461
+ `);
8462
+ blocked2++;
8463
+ continue;
8464
+ }
8465
+ saveCustomServer({ name, config: buildServerConfig(input), trusted: outcome.decision === "allow" });
8466
+ const mark = outcome.decision === "allow" ? fgHex(PALETTE.success, "\u2713 trusted") : dim("\u25CB needs trust");
8467
+ process.stdout.write(` ${mark} ${accent(name)}
8468
+ `);
8469
+ imported++;
8470
+ }
8471
+ info(`${imported} imported \xB7 ${blocked2} blocked${imported ? ` \u2014 they connect in-session; see \`oriro connectors custom\`` : ""}`);
8472
+ });
8473
+ imp.command("skills <dir>").description("import SKILL.md skill folders from another CLI's skills directory").action((dir) => {
8474
+ if (!existsSync21(dir) || !statSync5(dir).isDirectory()) die(`no such directory: ${dir}`);
8475
+ const dest = userSkillsDir();
8476
+ mkdirSync18(dest, { recursive: true });
8477
+ heading("Import skills");
8478
+ const sources = existsSync21(join31(dir, "SKILL.md")) ? [dir] : readdirSync3(dir).map((e) => join31(dir, e)).filter((p) => statSync5(p).isDirectory() && existsSync21(join31(p, "SKILL.md")));
8479
+ let n = 0;
8480
+ for (const src of sources) {
8481
+ cpSync2(src, join31(dest, basename2(src)), { recursive: true });
8482
+ process.stdout.write(` ${fgHex(PALETTE.success, "\u2713")} ${accent(basename2(src))}
8483
+ `);
8484
+ n++;
8485
+ }
8486
+ if (n === 0) info(dim(`no SKILL.md skill folder found at or inside ${dir}`));
8487
+ else ok(`imported ${n} skill${n === 1 ? "" : "s"} \u2192 ${dim(dest)}`);
8488
+ });
8489
+ }
8490
+
8491
+ // src/commands/help-on-error.ts
8492
+ function lev(a, b) {
8493
+ const m = a.length, n = b.length;
8494
+ if (!m) return n;
8495
+ if (!n) return m;
8496
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
8497
+ let curr = new Array(n + 1);
8498
+ for (let i = 1; i <= m; i++) {
8499
+ curr[0] = i;
8500
+ for (let j = 1; j <= n; j++) {
8501
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
8502
+ curr[j] = Math.min((curr[j - 1] ?? 0) + 1, (prev[j] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
8503
+ }
8504
+ [prev, curr] = [curr, prev];
8505
+ }
8506
+ return prev[n] ?? n;
8507
+ }
8508
+ function didYouMean(input, candidates) {
8509
+ let best;
8510
+ let bestD = Infinity;
8511
+ for (const c of candidates) {
8512
+ const d = lev(input.toLowerCase(), c.toLowerCase());
8513
+ if (d < bestD) {
8514
+ bestD = d;
8515
+ best = c;
8516
+ }
8517
+ }
8518
+ return best !== void 0 && bestD <= Math.max(2, Math.floor(input.length * 0.4)) ? best : void 0;
8519
+ }
8520
+ function fullPath(cmd) {
8521
+ const parts = [];
8522
+ let c = cmd;
8523
+ while (c && c.name() !== "oriro") {
8524
+ parts.unshift(c.name());
8525
+ c = c.parent;
8526
+ }
8527
+ return parts.length ? `oriro ${parts.join(" ")}` : "oriro <command>";
8528
+ }
8529
+ function enableHelpOnError(program2) {
8530
+ const apply = (cmd) => {
8531
+ cmd.showHelpAfterError(`
8532
+ (run: ${fullPath(cmd)} --help for usage)`);
8533
+ cmd.showSuggestionAfterError(true);
8534
+ for (const sub of cmd.commands) apply(sub);
8535
+ };
8536
+ apply(program2);
8537
+ }
8538
+
7244
8539
  // src/cli.ts
7245
8540
  var version = createRequire(import.meta.url)("../package.json").version;
7246
8541
  var program = new Command();
7247
- program.name("oriro").description("ORIRO \u2014 a free, on-device-friendly terminal AI agent.").version(version, "-v, --version").action(async (_options, command) => {
8542
+ program.name("oriro").description("ORIRO \u2014 a free, on-device-friendly terminal AI agent.").version(version, "-v, --version").option("-p, --print <prompt>", "headless one-shot: run a single prompt, print the answer, exit (CI-friendly)").option("--output-format <fmt>", "with --print: text | json | stream-json", "text").action(async (options, command) => {
8543
+ if (options.print !== void 0) {
8544
+ const fmt = options.outputFormat ?? "text";
8545
+ if (!isOutputFormatMode(fmt)) {
8546
+ process.stderr.write(`error: --output-format must be text | json | stream-json
8547
+ `);
8548
+ process.exitCode = 1;
8549
+ return;
8550
+ }
8551
+ await runHeadless(options.print, fmt);
8552
+ return;
8553
+ }
7248
8554
  if (command.args.length > 0) {
7249
- const arg = command.args[0];
8555
+ const arg = command.args[0] ?? "";
7250
8556
  if (arg === "help") {
7251
8557
  command.outputHelp();
7252
8558
  return;
7253
8559
  }
7254
- process.stderr.write(`error: unknown command '${arg}'
7255
- Run 'oriro --help' to see available commands.
8560
+ const names = command.commands.map((c) => c.name());
8561
+ const guess = didYouMean(arg, names);
8562
+ process.stderr.write(`error: unknown command '${arg}'${guess ? ` \u2014 did you mean '${guess}'?` : ""}
8563
+
7256
8564
  `);
8565
+ command.outputHelp();
7257
8566
  process.exitCode = 1;
7258
8567
  return;
7259
8568
  }
@@ -7269,6 +8578,11 @@ registerAvatarCommand(program);
7269
8578
  registerHeadCommand(program);
7270
8579
  registerVoiceCommand(program);
7271
8580
  registerAgentsCommand(program);
8581
+ registerConfigCommand(program);
8582
+ registerSetupCommand(program);
8583
+ registerImportCommand(program);
8584
+ registerCompletionCommand(program);
8585
+ enableHelpOnError(program);
7272
8586
  program.parseAsync().catch((e) => {
7273
8587
  if (e instanceof DieError) return;
7274
8588
  process.stderr.write(`