@oriro/orirocli 0.1.12 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -2
  3. package/dist/cli.js +1553 -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,252 @@ 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
+
6366
+ // src/repl-ui/slash-compact.ts
6367
+ function isCompactSlash(cmd) {
6368
+ return /^\/compact(\s|$)/i.test(cmd.trim());
6369
+ }
6370
+ function compactInstructions(cmd) {
6371
+ const rest = cmd.trim().replace(/^\/compact\s*/i, "").trim();
6372
+ return rest.length ? rest : void 0;
6373
+ }
6374
+ function formatCompactionResult(result) {
6375
+ const before = result.tokensBefore;
6376
+ const after = result.estimatedTokensAfter;
6377
+ const lines = [];
6378
+ if (typeof after === "number" && before > 0) {
6379
+ const freed = Math.max(0, before - after);
6380
+ const pct = Math.round(freed / before * 100);
6381
+ lines.push(
6382
+ ` ${fgHex(PALETTE.success, "\u2713 compacted")} ${dim(`${before.toLocaleString()} \u2192 ${after.toLocaleString()} tokens`)} ${accent(`(${pct}% freed)`)}`
6383
+ );
6384
+ } else {
6385
+ lines.push(` ${fgHex(PALETTE.success, "\u2713 compacted")} ${dim(`${before.toLocaleString()} tokens summarized`)}`);
6386
+ }
6387
+ lines.push(dim(" history summarized; the summary is kept, raw turns dropped. Keep going."));
6388
+ return lines;
6389
+ }
6390
+ async function handleCompact(session, cmd) {
6391
+ if (session.isCompacting) {
6392
+ return [dim(" compaction already in progress \u2014 hold on\u2026")];
6393
+ }
6394
+ if (session.messages.length < 4) {
6395
+ return [dim(" not much to compact yet \u2014 keep chatting, then /compact frees context.")];
6396
+ }
6397
+ try {
6398
+ const result = await session.compact(compactInstructions(cmd));
6399
+ if (!result) return [dim(" nothing to compact right now.")];
6400
+ return formatCompactionResult(result);
6401
+ } catch (e) {
6402
+ return [` ${fgHex(PALETTE.error, "compaction failed")}: ${dim(e instanceof Error ? e.message : String(e))}`];
6403
+ }
6404
+ }
6405
+
5728
6406
  // src/repl-ui/tui-repl.ts
5729
6407
  var editorTheme = {
5730
6408
  borderColor: (s) => dim(s),
@@ -5804,7 +6482,13 @@ async function runTuiRepl(session) {
5804
6482
  const slash = text.toLowerCase();
5805
6483
  if (slash === "/exit" || slash === "/quit") return cleanup();
5806
6484
  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));
6485
+ const help = [
6486
+ " Just type to chat \u2014 ORIRO writes and runs code for you (keyless, free).",
6487
+ ` ${accent("/routers")} pool add\xB7rotate ${accent("/model")} <id\u2026> switch ${accent("/usage")} health ${accent("/trace")} tool+router activity ${accent("/compact")} free context`,
6488
+ ` ${accent("/review")} artifacts from the last reply ${accent("/save")} <n> [path] ${accent("/skills")} ${accent("/connectors")} ${accent("/voice")}`,
6489
+ ` ${dim("Shift+Tab")} posture ${dim("Alt+Shift+T")} thinking ${accent("/help")} ${accent("/exit")}`
6490
+ ].join("\n");
6491
+ chat.addChild(new Text(help, 0, 0));
5808
6492
  editor.setText("");
5809
6493
  tui.requestRender();
5810
6494
  return;
@@ -5821,6 +6505,49 @@ async function runTuiRepl(session) {
5821
6505
  tui.requestRender();
5822
6506
  return;
5823
6507
  }
6508
+ if (isRouterSlash(slash)) {
6509
+ editor.setText("");
6510
+ const pending = new Text(dim(" \u2026"), 0, 0);
6511
+ chat.addChild(pending);
6512
+ tui.requestRender();
6513
+ void (async () => {
6514
+ const lines = await handleRouterSlash(text);
6515
+ pending.setText(lines.join("\n"));
6516
+ tui.requestRender();
6517
+ })();
6518
+ return;
6519
+ }
6520
+ if (isUsageSlash(slash)) {
6521
+ chat.addChild(new Text(handleUsage().join("\n"), 0, 0));
6522
+ editor.setText("");
6523
+ tui.requestRender();
6524
+ return;
6525
+ }
6526
+ if (slash === "/trace") {
6527
+ const on = toggleTrace();
6528
+ chat.addChild(new Text(dim(` trace ${on ? "ON \u2014 showing tool + router activity" : "off"}`), 0, 0));
6529
+ editor.setText("");
6530
+ tui.requestRender();
6531
+ return;
6532
+ }
6533
+ if (isArtifactSlash(slash)) {
6534
+ chat.addChild(new Text(handleArtifactSlash(text).join("\n"), 0, 0));
6535
+ editor.setText("");
6536
+ tui.requestRender();
6537
+ return;
6538
+ }
6539
+ if (isCompactSlash(slash)) {
6540
+ editor.setText("");
6541
+ const pending = new Text(dim(" compacting\u2026"), 0, 0);
6542
+ chat.addChild(pending);
6543
+ tui.requestRender();
6544
+ void (async () => {
6545
+ const lines = await handleCompact(session, text);
6546
+ pending.setText(lines.join("\n"));
6547
+ tui.requestRender();
6548
+ })();
6549
+ return;
6550
+ }
5824
6551
  if (slash === "/voice") {
5825
6552
  editor.setText("");
5826
6553
  const status = new Text(dim(" \u{1F399} listening\u2026 (needs ffmpeg + the transformers voice peer)"), 0, 0);
@@ -5841,10 +6568,23 @@ async function runTuiRepl(session) {
5841
6568
  editor.addToHistory(text);
5842
6569
  editor.setText("");
5843
6570
  chat.addChild(new Text(`${accent("\u203A")} ${text}`, 0, 1));
6571
+ const raceLine = new Text("", 0, 0);
6572
+ chat.addChild(raceLine);
5844
6573
  const streaming = new Text(dim("\u2026"), 0, 0);
5845
6574
  chat.addChild(streaming);
6575
+ const unsubRace = onRaceStatus((s) => {
6576
+ if (s.phase === "racing" && s.racers.length > 1) {
6577
+ raceLine.setText(dim(` \u23F1 racing: ${s.racers.join(" \xB7 ")}`));
6578
+ } else if (s.phase === "won" && s.winner && s.racers.length > 1) {
6579
+ raceLine.setText(dim(` \u23F1 ${s.racers.join(" \xB7 ")} \u2192 won: `) + accent(s.winner));
6580
+ } else {
6581
+ raceLine.setText("");
6582
+ }
6583
+ tui.requestRender();
6584
+ });
5846
6585
  tui.requestRender();
5847
6586
  busy = true;
6587
+ bumpTurns();
5848
6588
  void (async () => {
5849
6589
  let english = await translateIncoming(text);
5850
6590
  if (getThinking()) english = `${THINKING_PRIMER}
@@ -5860,6 +6600,9 @@ ${english}`;
5860
6600
  streaming.setText(out);
5861
6601
  tui.requestRender();
5862
6602
  }
6603
+ } else if (getTrace() && (e.type === "tool_start" || e.type === "tool_end" || e.type === "toolcall_start")) {
6604
+ chat.addChild(new Text(dim(` \u2699 ${e.type.replace("_", " ")}${e.toolName ? `: ${e.toolName}` : ""}`), 0, 0));
6605
+ tui.requestRender();
5863
6606
  }
5864
6607
  }
5865
6608
  );
@@ -5870,13 +6613,19 @@ ${english}`;
5870
6613
  tui.requestRender();
5871
6614
  busy = false;
5872
6615
  unsub();
6616
+ unsubRace();
5873
6617
  return;
5874
6618
  }
5875
6619
  unsub();
6620
+ unsubRace();
5876
6621
  const cleaned = scrubOutput(out);
5877
6622
  const finalText = isEnglish3 ? cleaned.trim() : await translateOutgoing(cleaned.trim());
5878
6623
  const warn = phantomFileWarning(finalText);
5879
- streaming.setText((finalText || dim("(no response)")) + (warn ? dim(warn) : ""));
6624
+ const arts = extractArtifacts(finalText);
6625
+ setArtifacts(arts);
6626
+ const hint = arts.length ? dim(`
6627
+ \u2398 ${arts.length} artifact${arts.length === 1 ? "" : "s"} \u2014 /review to save`) : "";
6628
+ streaming.setText((finalText || dim("(no response)")) + (warn ? dim(warn) : "") + hint);
5880
6629
  tui.requestRender();
5881
6630
  busy = false;
5882
6631
  })();
@@ -5890,8 +6639,8 @@ ${english}`;
5890
6639
  // src/voice/mic.ts
5891
6640
  import { spawn as spawn3 } from "child_process";
5892
6641
  import { tmpdir as tmpdir3 } from "os";
5893
- import { join as join23 } from "path";
5894
- import { existsSync as existsSync15, statSync as statSync2 } from "fs";
6642
+ import { join as join25 } from "path";
6643
+ import { existsSync as existsSync17, statSync as statSync3 } from "fs";
5895
6644
  function recorders(outFile, seconds) {
5896
6645
  const dur = String(seconds);
5897
6646
  if (process.platform === "darwin") {
@@ -5912,12 +6661,12 @@ function recorders(outFile, seconds) {
5912
6661
  ];
5913
6662
  }
5914
6663
  async function recordMic(seconds = 6) {
5915
- const outFile = join23(tmpdir3(), `oriro-voice-${process.pid}-${seconds}.wav`);
6664
+ const outFile = join25(tmpdir3(), `oriro-voice-${process.pid}-${seconds}.wav`);
5916
6665
  for (const r of recorders(outFile, seconds)) {
5917
6666
  const okFile = await new Promise((resolve3) => {
5918
6667
  const child = spawn3(r.cmd, r.args, { stdio: "ignore" });
5919
6668
  child.on("error", () => resolve3(false));
5920
- child.on("close", (code) => resolve3(code === 0 && existsSync15(outFile) && statSync2(outFile).size > 44));
6669
+ child.on("close", (code) => resolve3(code === 0 && existsSync17(outFile) && statSync3(outFile).size > 44));
5921
6670
  });
5922
6671
  if (okFile) return outFile;
5923
6672
  }
@@ -5981,9 +6730,13 @@ function replHelp() {
5981
6730
  ${accent("ORIRO terminal \u2014 help")}
5982
6731
  ${dim("Just type to chat; ORIRO writes and runs code for you (keyless, free).")}
5983
6732
 
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")}
6733
+ ${dim("Models & routers")} ${accent("/routers")} list\xB7add\xB7rotate the racing pool ${accent("/model")} <id\u2026> switch
6734
+ ${dim("This session")} ${accent("/usage")} pool health & turns ${accent("/trace")} show tool + router activity ${accent("/compact")} free context
6735
+ ${dim("Artifacts")} ${accent("/review")} code/SVG from the last reply ${accent("/save")} <n> [path] write one
6736
+ ${dim("Capabilities")} ${accent("/skills")} ${accent("/connectors")} ${accent("/voice")} speak a turn
6737
+ ${dim("General")} ${accent("/help")} this ${accent("/exit")} / ${accent("/quit")} leave ${dim("(Ctrl-D / Ctrl-C also exit)")}
6738
+
6739
+ ${dim("Full command list outside the chat:")} ${accent("oriro --help")}
5987
6740
 
5988
6741
  `;
5989
6742
  }
@@ -6042,6 +6795,28 @@ async function runReadlineRepl(session) {
6042
6795
  `);
6043
6796
  continue;
6044
6797
  }
6798
+ if (isRouterSlash(slash)) {
6799
+ stdout7.write((await handleRouterSlash(line)).join("\n") + "\n");
6800
+ continue;
6801
+ }
6802
+ if (isUsageSlash(slash)) {
6803
+ stdout7.write(handleUsage().join("\n") + "\n");
6804
+ continue;
6805
+ }
6806
+ if (slash === "/trace") {
6807
+ stdout7.write(` ${dim(`trace ${toggleTrace() ? "ON" : "off"}`)}
6808
+ `);
6809
+ continue;
6810
+ }
6811
+ if (isCompactSlash(slash)) {
6812
+ stdout7.write((await handleCompact(session, line)).join("\n") + "\n");
6813
+ continue;
6814
+ }
6815
+ if (isArtifactSlash(slash)) {
6816
+ stdout7.write(handleArtifactSlash(line).join("\n") + "\n");
6817
+ continue;
6818
+ }
6819
+ bumpTurns();
6045
6820
  const english = await translateIncoming(line);
6046
6821
  noteUserInput(line);
6047
6822
  let out = "";
@@ -6059,8 +6834,12 @@ async function runReadlineRepl(session) {
6059
6834
  }
6060
6835
  const cleaned = scrubOutput(out);
6061
6836
  const shown = isEnglish3 ? cleaned.trim() : await translateOutgoing(cleaned.trim());
6837
+ const arts = extractArtifacts(shown);
6838
+ setArtifacts(arts);
6839
+ const hint = arts.length ? ` ${dim(`\u2398 ${arts.length} artifact${arts.length === 1 ? "" : "s"} \u2014 /review to save`)}
6840
+ ` : "";
6062
6841
  stdout7.write(`${shown}${phantomFileWarning(shown)}
6063
-
6842
+ ${hint}
6064
6843
  `);
6065
6844
  }
6066
6845
  } finally {
@@ -6073,7 +6852,54 @@ async function runReadlineRepl(session) {
6073
6852
  }
6074
6853
  }
6075
6854
 
6855
+ // src/headless.ts
6856
+ function isOutputFormatMode(s) {
6857
+ return s === "text" || s === "json" || s === "stream-json";
6858
+ }
6859
+ async function runHeadless(prompt, format) {
6860
+ if (!prompt.trim()) {
6861
+ process.stderr.write('error: empty prompt \u2014 pass text after -p, e.g. oriro -p "summarise this repo"\n');
6862
+ process.exitCode = 1;
6863
+ return;
6864
+ }
6865
+ const { session } = await assembleOriroSession({});
6866
+ let text = "";
6867
+ const unsub = session.subscribe(
6868
+ (e) => {
6869
+ if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
6870
+ const d = e.assistantMessageEvent.delta ?? "";
6871
+ text += d;
6872
+ if (format === "stream-json" && d) process.stdout.write(JSON.stringify({ type: "text_delta", delta: d }) + "\n");
6873
+ }
6874
+ }
6875
+ );
6876
+ let error = "";
6877
+ try {
6878
+ await session.prompt(prompt);
6879
+ } catch (e) {
6880
+ error = e instanceof Error ? e.message : String(e);
6881
+ }
6882
+ unsub();
6883
+ const response = scrubOutput(text).trim();
6884
+ const ok2 = !error && response.length > 0;
6885
+ if (format === "json") {
6886
+ process.stdout.write(JSON.stringify({ ok: ok2, response, ...error ? { error } : {} }) + "\n");
6887
+ } else if (format === "stream-json") {
6888
+ process.stdout.write(JSON.stringify({ type: "done", ok: ok2, response, ...error ? { error } : {} }) + "\n");
6889
+ } else {
6890
+ process.stdout.write((response || (error ? `error: ${error}` : "(no response)")) + "\n");
6891
+ }
6892
+ process.exitCode = ok2 ? 0 : 1;
6893
+ try {
6894
+ session.dispose();
6895
+ } catch {
6896
+ }
6897
+ setTimeout(() => process.exit(ok2 ? 0 : 1), 400).unref();
6898
+ }
6899
+
6076
6900
  // src/commands/ui.ts
6901
+ import { createInterface as createInterface7 } from "readline/promises";
6902
+ import { stdin as stdin7, stdout as stdout8 } from "process";
6077
6903
  var ok = (s) => {
6078
6904
  process.stdout.write(`${fgHex(PALETTE.success, "\u2713")} ${s}
6079
6905
  `);
@@ -6098,11 +6924,159 @@ function die(msg) {
6098
6924
  process.exitCode = 1;
6099
6925
  throw new DieError(msg);
6100
6926
  }
6927
+ async function confirmDestructive(what, opts = {}) {
6928
+ if (opts.force) return true;
6929
+ if (!stdin7.isTTY || !stdout8.isTTY) {
6930
+ die(`refusing to ${what} without confirmation \u2014 re-run with --force in a non-interactive shell`);
6931
+ }
6932
+ const rl = createInterface7({ input: stdin7, output: stdout8 });
6933
+ try {
6934
+ const ans = (await rl.question(`${fgHex(PALETTE.error, "?")} ${what} \u2014 this cannot be undone. Proceed? [y/N] `)).trim().toLowerCase();
6935
+ return ans === "y" || ans === "yes";
6936
+ } finally {
6937
+ rl.close();
6938
+ }
6939
+ }
6940
+
6941
+ // src/config/store.ts
6942
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync19, mkdirSync as mkdirSync16 } from "fs";
6943
+ import { join as join26 } from "path";
6944
+ var KEYS = {
6945
+ output: {
6946
+ desc: "default output format for list commands: text | json | csv",
6947
+ validate: (v) => ["text", "json", "csv"].includes(v) ? null : "must be text | json | csv"
6948
+ },
6949
+ lang: { desc: "preferred UI language code (e.g. en, hi, es) \u2014 overrides terminal detection" },
6950
+ thinking: {
6951
+ desc: "default REPL thinking mode: on | off",
6952
+ validate: (v) => ["on", "off"].includes(v) ? null : "must be on | off"
6953
+ }
6954
+ };
6955
+ function configKeys() {
6956
+ return Object.keys(KEYS).map((key) => ({ key, desc: KEYS[key].desc }));
6957
+ }
6958
+ function isConfigKey(k) {
6959
+ return k in KEYS;
6960
+ }
6961
+ function validateConfig(key, value) {
6962
+ return KEYS[key].validate?.(value) ?? null;
6963
+ }
6964
+ function file4() {
6965
+ return join26(oriroDir(), "config.json");
6966
+ }
6967
+ var cache = null;
6968
+ function readAll() {
6969
+ if (cache) return cache;
6970
+ try {
6971
+ const v = JSON.parse(readFileSync21(file4(), "utf8"));
6972
+ cache = v && typeof v === "object" ? v : {};
6973
+ } catch {
6974
+ cache = {};
6975
+ }
6976
+ return cache;
6977
+ }
6978
+ function configGet(key) {
6979
+ return readAll()[key];
6980
+ }
6981
+ function configAll() {
6982
+ return { ...readAll() };
6983
+ }
6984
+ function configSet(key, value) {
6985
+ const all = { ...readAll(), [key]: value };
6986
+ mkdirSync16(oriroDir(), { recursive: true });
6987
+ writeFileSync19(file4(), JSON.stringify(all, null, 2), "utf8");
6988
+ cache = all;
6989
+ }
6990
+ function configUnset(key) {
6991
+ const all = readAll();
6992
+ if (!(key in all)) return false;
6993
+ const rest = { ...all };
6994
+ delete rest[key];
6995
+ writeFileSync19(file4(), JSON.stringify(rest, null, 2), "utf8");
6996
+ cache = rest;
6997
+ return true;
6998
+ }
6999
+
7000
+ // src/commands/output.ts
7001
+ function parseFormat(o) {
7002
+ const f = (o ?? configGet("output") ?? "text").toLowerCase();
7003
+ if (f === "json" || f === "csv" || f === "text") return f;
7004
+ throw new Error(`invalid --output '${o}'. Use: text | json | csv`);
7005
+ }
7006
+ function applyQuery(rows, query) {
7007
+ if (!query) return rows;
7008
+ const [filterPart, selectField] = query.split(":", 2);
7009
+ let out = rows;
7010
+ const fp = filterPart ?? "";
7011
+ if (fp.includes("=")) {
7012
+ const [field2, value] = fp.split("=", 2);
7013
+ out = rows.filter((r) => String(r[field2] ?? "") === value);
7014
+ } else if (fp && !selectField) {
7015
+ return rows.map((r) => r[fp]);
7016
+ }
7017
+ if (selectField) return out.map((r) => r[selectField]);
7018
+ return out;
7019
+ }
7020
+ function csvCell(v) {
7021
+ const s = v === null || v === void 0 ? "" : String(v);
7022
+ return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
7023
+ }
7024
+ function renderList2(rows, opts = {}) {
7025
+ const fmt = parseFormat(opts.output);
7026
+ const queried = applyQuery(rows, opts.query);
7027
+ if (fmt === "json") return JSON.stringify(queried, null, 2);
7028
+ if (!Array.isArray(queried) || queried.length === 0) return "";
7029
+ const first = queried[0];
7030
+ const scalar = typeof first !== "object" || first === null;
7031
+ if (scalar) return queried.map((v) => fmt === "csv" ? csvCell(v) : String(v)).join("\n");
7032
+ const objs = queried;
7033
+ const cols = opts.columns ?? [...new Set(objs.flatMap((r) => Object.keys(r)))];
7034
+ if (fmt === "csv") {
7035
+ return [cols.map(csvCell).join(","), ...objs.map((r) => cols.map((c) => csvCell(r[c])).join(","))].join("\n");
7036
+ }
7037
+ const widths = cols.map((c) => Math.max(c.length, ...objs.map((r) => String(r[c] ?? "").length)));
7038
+ const line = (cells) => cells.map((s, i) => s.padEnd(widths[i] ?? 0)).join(" ").trimEnd();
7039
+ return [line(cols), ...objs.map((r) => line(cols.map((c) => String(r[c] ?? ""))))].join("\n");
7040
+ }
7041
+ function isMachineOutput(opts) {
7042
+ return parseFormat(opts.output) !== "text";
7043
+ }
7044
+ function outputError(opts) {
7045
+ const f = (opts.output ?? configGet("output") ?? "text").toLowerCase();
7046
+ return f === "json" || f === "csv" || f === "text" ? null : `invalid --output '${opts.output}' \u2014 use text | json | csv`;
7047
+ }
6101
7048
 
6102
7049
  // src/commands/routers.ts
6103
7050
  function registerRoutersCommand(program2) {
6104
7051
  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(() => {
7052
+ 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) => {
7053
+ const oerr = outputError(opts);
7054
+ if (oerr) die(oerr);
7055
+ const pool = new Set(resolvePool().map((p) => p.id));
7056
+ if (isMachineOutput(opts) || opts.query) {
7057
+ const catalogRows = ROUTER_CATALOG.filter((r) => !r.comingSoon).map((r) => ({
7058
+ id: r.id,
7059
+ name: r.displayName,
7060
+ tier: r.keyless ? "keyless" : r.tier,
7061
+ keyless: Boolean(r.keyless),
7062
+ source: "catalog",
7063
+ active: pool.has(r.id)
7064
+ }));
7065
+ const customRows = registeredRouters().filter((r) => !ROUTER_CATALOG.some((c) => c.id === r.id)).map((r) => ({
7066
+ id: r.id,
7067
+ name: r.name,
7068
+ tier: r.apiKey && r.apiKey !== KEYLESS_SENTINEL ? "byok" : "keyless",
7069
+ keyless: !r.apiKey || r.apiKey === KEYLESS_SENTINEL,
7070
+ source: "custom",
7071
+ active: pool.has(r.id)
7072
+ }));
7073
+ process.stdout.write(renderList2([...catalogRows, ...customRows], {
7074
+ output: opts.output,
7075
+ query: opts.query,
7076
+ columns: ["id", "name", "tier", "keyless", "active", "source"]
7077
+ }) + "\n");
7078
+ return;
7079
+ }
6106
7080
  heading("Routers");
6107
7081
  for (const r of ROUTER_CATALOG) {
6108
7082
  if (r.comingSoon) {
@@ -6125,8 +7099,7 @@ function registerRoutersCommand(program2) {
6125
7099
  `);
6126
7100
  }
6127
7101
  }
6128
- const pool = resolvePool();
6129
- info(pool.length ? `active pool: ${pool.map((p) => p.id).join(", ")}` : "active pool: empty \u2192 using the keyless floor");
7102
+ info(pool.size ? `active pool: ${[...pool].join(", ")}` : "active pool: empty \u2192 using the keyless floor");
6130
7103
  });
6131
7104
  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
7105
  let entry;
@@ -6162,10 +7135,10 @@ function registerRoutersCommand(program2) {
6162
7135
  }
6163
7136
 
6164
7137
  // src/commands/scribe.ts
6165
- import { readFileSync as readFileSync20 } from "fs";
7138
+ import { readFileSync as readFileSync23 } from "fs";
6166
7139
 
6167
7140
  // src/scribe/transcript.ts
6168
- import { existsSync as existsSync16, readFileSync as readFileSync19 } from "fs";
7141
+ import { existsSync as existsSync19, readFileSync as readFileSync22 } from "fs";
6169
7142
  function parseHookStdin(raw) {
6170
7143
  try {
6171
7144
  const j = JSON.parse(raw);
@@ -6198,8 +7171,8 @@ function isHumanUser(e) {
6198
7171
  }
6199
7172
  var FILE_KEYS = ["file_path", "path", "notebook_path", "filePath"];
6200
7173
  function lastTurnFromTranscript(path) {
6201
- if (!existsSync16(path)) return null;
6202
- const raw = readFileSync19(path, "utf8");
7174
+ if (!existsSync19(path)) return null;
7175
+ const raw = readFileSync22(path, "utf8");
6203
7176
  const entries = [];
6204
7177
  for (const line of raw.split("\n")) {
6205
7178
  if (!line.trim()) continue;
@@ -6260,7 +7233,7 @@ function lastTurnFromTranscript(path) {
6260
7233
  // src/commands/scribe.ts
6261
7234
  function readStdin() {
6262
7235
  try {
6263
- return readFileSync20(0, "utf8");
7236
+ return readFileSync23(0, "utf8");
6264
7237
  } catch {
6265
7238
  return "";
6266
7239
  }
@@ -6373,40 +7346,8 @@ function registerScribeCommand(program2) {
6373
7346
  }
6374
7347
 
6375
7348
  // 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
- }
7349
+ import { createInterface as createInterface8 } from "readline/promises";
7350
+ import { stdin as stdin8, stdout as stdout9 } from "process";
6410
7351
 
6411
7352
  // src/connectors/setup.ts
6412
7353
  function buildServerConfig(i) {
@@ -6437,39 +7378,41 @@ function parsePairs(s) {
6437
7378
  return out;
6438
7379
  }
6439
7380
 
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
7381
  // src/commands/connectors.ts
6458
7382
  function registerConnectorsCommand(program2) {
6459
7383
  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) => {
7384
+ 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) => {
7385
+ const oerr = outputError(opts);
7386
+ if (oerr) die(oerr);
6461
7387
  if (category && !connectorCategories().includes(category)) {
6462
7388
  die(`unknown category '${category}' \u2014 categories: ${connectorCategories().join(", ")}`);
6463
7389
  }
6464
7390
  const entries = listConnectors(category);
6465
7391
  const added = new Set(addedConnectors().map((c) => c.slug));
7392
+ if (isMachineOutput(opts) || opts.query) {
7393
+ const rows = entries.map((c) => ({
7394
+ slug: c.slug,
7395
+ name: c.name,
7396
+ category: c.category,
7397
+ addable: Boolean(c.mcpUrl),
7398
+ added: added.has(c.slug)
7399
+ }));
7400
+ process.stdout.write(renderList2(rows, {
7401
+ output: opts.output,
7402
+ query: opts.query,
7403
+ columns: ["slug", "name", "category", "addable", "added"]
7404
+ }) + "\n");
7405
+ return;
7406
+ }
6466
7407
  heading(category ? `Connectors \xB7 ${category}` : "Connectors");
6467
7408
  let addable = 0;
7409
+ const NAME_W = 34;
6468
7410
  for (const c of entries) {
6469
7411
  const canAdd = !!c.mcpUrl;
6470
7412
  if (canAdd) addable++;
6471
7413
  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));
7414
+ const label = (canAdd ? c.name : `${c.name} (coming soon)`).padEnd(NAME_W);
7415
+ const name = canAdd ? label : dim(label);
6473
7416
  process.stdout.write(` ${mark} ${(canAdd ? accent : dim)(c.slug.padEnd(20))} ${name} ${dim(c.category)}
6474
7417
  `);
6475
7418
  }
@@ -6484,12 +7427,20 @@ function registerConnectorsCommand(program2) {
6484
7427
  if (!res.ok) die(res.error ?? `could not add '${slug}'`);
6485
7428
  ok(`added ${accent(slug)} \u2014 recorded locally`);
6486
7429
  });
6487
- connectors.command("remove <slug>").description("remove a connector").action((slug) => {
7430
+ connectors.command("remove <slug>").description("remove a connector").option("-f, --force", "skip the confirmation prompt").action(async (slug, opts) => {
7431
+ if (!isConnectorAdded(slug)) {
7432
+ info(`'${slug}' is not in your added list \u2014 nothing to remove`);
7433
+ return;
7434
+ }
7435
+ if (!await confirmDestructive(`remove connector '${slug}'`, opts)) {
7436
+ info("cancelled");
7437
+ return;
7438
+ }
6488
7439
  if (removeConnector(slug)) ok(`removed ${accent(slug)}`);
6489
7440
  else info(`'${slug}' is not in your added list \u2014 nothing to remove`);
6490
7441
  });
6491
7442
  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;
7443
+ const interactive = !!stdin8.isTTY && !!stdout9.isTTY;
6493
7444
  let { name, command, url } = opts;
6494
7445
  let argsStr = opts.args;
6495
7446
  let envStr = opts.env;
@@ -6508,7 +7459,7 @@ function registerConnectorsCommand(program2) {
6508
7459
  );
6509
7460
  return;
6510
7461
  }
6511
- const rl = createInterface7({ input: stdin7, output: stdout8 });
7462
+ const rl = createInterface8({ input: stdin8, output: stdout9 });
6512
7463
  try {
6513
7464
  name = name || (await rl.question("Server name: ")).trim();
6514
7465
  if (!command && !url) {
@@ -6550,7 +7501,7 @@ function registerConnectorsCommand(program2) {
6550
7501
  if (opts.yes) {
6551
7502
  trusted = true;
6552
7503
  } else if (interactive) {
6553
- const rl = createInterface7({ input: stdin7, output: stdout8 });
7504
+ const rl = createInterface8({ input: stdin8, output: stdout9 });
6554
7505
  try {
6555
7506
  const ans = (await rl.question(`Trust and save "${name}"? [y/N] `)).trim().toLowerCase();
6556
7507
  trusted = ans === "y" || ans === "yes";
@@ -6592,14 +7543,14 @@ function registerConnectorsCommand(program2) {
6592
7543
  }
6593
7544
 
6594
7545
  // 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");
7546
+ import { readFileSync as readFileSync24, writeFileSync as writeFileSync20 } from "fs";
7547
+ import { join as join27 } from "path";
7548
+ function file5() {
7549
+ return join27(oriroDir(), "channels.json");
6599
7550
  }
6600
7551
  function readChannels() {
6601
7552
  try {
6602
- const v = JSON.parse(readFileSync22(file4(), "utf8"));
7553
+ const v = JSON.parse(readFileSync24(file5(), "utf8"));
6603
7554
  return Array.isArray(v) ? v : [];
6604
7555
  } catch {
6605
7556
  return [];
@@ -6608,10 +7559,10 @@ function readChannels() {
6608
7559
  function saveChannel(cfg) {
6609
7560
  const all = readChannels().filter((c) => c.kind !== cfg.kind);
6610
7561
  all.push(cfg);
6611
- writeFileSync18(join25(ensureOriroDir(), "channels.json"), JSON.stringify(all, null, 2), "utf8");
7562
+ writeFileSync20(join27(ensureOriroDir(), "channels.json"), JSON.stringify(all, null, 2), "utf8");
6612
7563
  }
6613
7564
  function removeChannel(kind) {
6614
- writeFileSync18(join25(ensureOriroDir(), "channels.json"), JSON.stringify(readChannels().filter((c) => c.kind !== kind), null, 2), "utf8");
7565
+ writeFileSync20(join27(ensureOriroDir(), "channels.json"), JSON.stringify(readChannels().filter((c) => c.kind !== kind), null, 2), "utf8");
6615
7566
  }
6616
7567
 
6617
7568
  // src/channels/telegram.ts
@@ -6728,9 +7679,9 @@ async function startDiscord(token) {
6728
7679
  }
6729
7680
 
6730
7681
  // src/channels/whatsapp.ts
6731
- import { join as join26 } from "path";
7682
+ import { join as join28 } from "path";
6732
7683
  function whatsappAuthDir() {
6733
- return join26(oriroDir(), "whatsapp-auth");
7684
+ return join28(oriroDir(), "whatsapp-auth");
6734
7685
  }
6735
7686
  async function startWhatsApp() {
6736
7687
  let baileys;
@@ -6848,12 +7799,26 @@ function registerChannelsCommand(program2) {
6848
7799
  }
6849
7800
 
6850
7801
  // 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";
7802
+ import { existsSync as existsSync20, statSync as statSync4, mkdirSync as mkdirSync17, cpSync, rmSync as rmSync4 } from "fs";
7803
+ import { resolve as resolve2, join as join29, basename, dirname as dirname4 } from "path";
6853
7804
  function registerSkillsCommand(program2) {
6854
7805
  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) => {
7806
+ 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) => {
7807
+ const oerr = outputError(opts);
7808
+ if (oerr) die(oerr);
6856
7809
  const s = await loadOriroSkills();
7810
+ if (isMachineOutput(opts) || opts.query) {
7811
+ const rows = s.all.map((sk) => ({
7812
+ name: sk.name,
7813
+ tier: sk.disableModelInvocation ? "TAIL" : "CORE"
7814
+ }));
7815
+ process.stdout.write(renderList2(rows, {
7816
+ output: opts.output,
7817
+ query: opts.query,
7818
+ columns: ["name", "tier"]
7819
+ }) + "\n");
7820
+ return;
7821
+ }
6857
7822
  heading("Skills");
6858
7823
  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
7824
  if (opts.all) {
@@ -6867,38 +7832,42 @@ function registerSkillsCommand(program2) {
6867
7832
  });
6868
7833
  skills.command("add <path>").description("add your own skill \u2014 a folder containing SKILL.md, or a SKILL.md file").action((p) => {
6869
7834
  const src = resolve2(p);
6870
- if (!existsSync17(src)) die(`not found: ${src}`);
7835
+ if (!existsSync20(src)) die(`not found: ${src}`);
6871
7836
  const dest = userSkillsDir();
6872
- mkdirSync16(dest, { recursive: true });
6873
- const st = statSync3(src);
7837
+ mkdirSync17(dest, { recursive: true });
7838
+ const st = statSync4(src);
6874
7839
  if (st.isDirectory()) {
6875
- if (!existsSync17(join27(src, "SKILL.md"))) die(`no SKILL.md in ${src} \u2014 a skill folder must contain SKILL.md`);
7840
+ if (!existsSync20(join29(src, "SKILL.md"))) die(`no SKILL.md in ${src} \u2014 a skill folder must contain SKILL.md`);
6876
7841
  const name = basename(src);
6877
- cpSync(src, join27(dest, name), { recursive: true });
6878
- ok(`added skill ${accent(name)} \u2192 ${join27(dest, name)}`);
7842
+ cpSync(src, join29(dest, name), { recursive: true });
7843
+ ok(`added skill ${accent(name)} \u2192 ${join29(dest, name)}`);
6879
7844
  } 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)}`);
7845
+ const name = basename(dirname4(src)) || "custom-skill";
7846
+ mkdirSync17(join29(dest, name), { recursive: true });
7847
+ cpSync(src, join29(dest, name, "SKILL.md"));
7848
+ ok(`added skill ${accent(name)} \u2192 ${join29(dest, name)}`);
6884
7849
  } else {
6885
7850
  die("expected a folder containing SKILL.md, or a SKILL.md file");
6886
7851
  }
6887
7852
  info("It loads on next launch \u2014 and is available in chat via /skill.");
6888
7853
  });
6889
- skills.command("remove <name>").description("remove a skill you added").action((name) => {
6890
- const target = join27(userSkillsDir(), name);
6891
- if (!existsSync17(target)) {
7854
+ skills.command("remove <name>").description("remove a skill you added").option("-f, --force", "skip the confirmation prompt").action(async (name, opts) => {
7855
+ const target = join29(userSkillsDir(), name);
7856
+ if (!existsSync20(target)) {
6892
7857
  info(`'${name}' is not a user-added skill \u2014 nothing to remove`);
6893
7858
  return;
6894
7859
  }
7860
+ if (!await confirmDestructive(`remove skill '${name}'`, opts)) {
7861
+ info("cancelled");
7862
+ return;
7863
+ }
6895
7864
  rmSync4(target, { recursive: true, force: true });
6896
7865
  ok(`removed ${accent(name)}`);
6897
7866
  });
6898
7867
  }
6899
7868
 
6900
7869
  // src/commands/language.ts
6901
- import { stdin as stdin8 } from "process";
7870
+ import { stdin as stdin9 } from "process";
6902
7871
  function resolveLanguage(input) {
6903
7872
  return languageByCode(input) ?? LANGUAGES.find((l) => l.name.toLowerCase() === input.trim().toLowerCase());
6904
7873
  }
@@ -6920,7 +7889,7 @@ function registerLanguageCommand(program2) {
6920
7889
  ok(`${accent(lang.name)} is now your terminal language.`);
6921
7890
  return;
6922
7891
  }
6923
- if (stdin8.isTTY) {
7892
+ if (stdin9.isTTY) {
6924
7893
  const lang = await selectLanguageInteractive();
6925
7894
  setTerminalLanguage(lang);
6926
7895
  ok(`${accent(lang.name)} is now your terminal language.`);
@@ -6933,7 +7902,7 @@ function registerLanguageCommand(program2) {
6933
7902
  }
6934
7903
 
6935
7904
  // src/commands/avatar.ts
6936
- import { stdin as stdin9 } from "process";
7905
+ import { stdin as stdin10 } from "process";
6937
7906
  function registerAvatarCommand(program2) {
6938
7907
  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
7908
  if (opts.list) {
@@ -6951,7 +7920,7 @@ function registerAvatarCommand(program2) {
6951
7920
  ok(`${accent(avatar.slug)} is now your terminal face.`);
6952
7921
  return;
6953
7922
  }
6954
- if (stdin9.isTTY) {
7923
+ if (stdin10.isTTY) {
6955
7924
  const chosen = await selectAvatarInteractive();
6956
7925
  if (!chosen) {
6957
7926
  info("no change.");
@@ -7029,12 +7998,12 @@ function registerHeadCommand(program2) {
7029
7998
  }
7030
7999
 
7031
8000
  // src/commands/voice.ts
7032
- import { stdin as stdin10, stdout as stdout9 } from "process";
8001
+ import { stdin as stdin11, stdout as stdout10 } from "process";
7033
8002
  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;
8003
+ 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) => {
8004
+ const interactive = !!stdin11.isTTY && !!stdout10.isTTY;
7036
8005
  heading("ORIRO voice \u{1F399}");
7037
- let audio = file5;
8006
+ let audio = file6;
7038
8007
  if (!audio) {
7039
8008
  if (!interactive) {
7040
8009
  info("On-device speech-to-text (experimental \u2014 needs ffmpeg + the transformers voice peer).");
@@ -7067,7 +8036,7 @@ function registerVoiceCommand(program2) {
7067
8036
  }
7068
8037
 
7069
8038
  // src/agents/catalog.ts
7070
- import { readFileSync as readFileSync23 } from "fs";
8039
+ import { readFileSync as readFileSync25 } from "fs";
7071
8040
  function parseAgentDef(raw, now) {
7072
8041
  if (!raw || typeof raw !== "object") return { ok: false, error: "not a JSON object" };
7073
8042
  const o = raw;
@@ -7094,7 +8063,7 @@ async function fetchAgentSource(pathOrUrl) {
7094
8063
  if (!res.ok) throw new Error(`fetch failed: HTTP ${res.status}`);
7095
8064
  return await res.json();
7096
8065
  }
7097
- return JSON.parse(readFileSync23(pathOrUrl, "utf8"));
8066
+ return JSON.parse(readFileSync25(pathOrUrl, "utf8"));
7098
8067
  }
7099
8068
  async function addAgentFromSource(pathOrUrl, now) {
7100
8069
  let raw;
@@ -7110,6 +8079,67 @@ async function addAgentFromSource(pathOrUrl, now) {
7110
8079
  return { ok: true, name: parsed.def.name, overwrote };
7111
8080
  }
7112
8081
 
8082
+ // src/commands/schedule.ts
8083
+ import { spawnSync } from "child_process";
8084
+ import { platform } from "process";
8085
+ var TASK_NAME = "ORIRO_Agents_Tick";
8086
+ function intervalMinutes(spec) {
8087
+ const m = /^(\d+)(m|h)$/.exec(spec.trim());
8088
+ if (!m) return null;
8089
+ const n = parseInt(m[1], 10);
8090
+ if (n <= 0) return null;
8091
+ return m[2] === "h" ? n * 60 : n;
8092
+ }
8093
+ function tickInvocation() {
8094
+ return { node: process.execPath, bin: process.argv[1] ?? "oriro" };
8095
+ }
8096
+ function buildCron(mins, remove) {
8097
+ const { node, bin } = tickInvocation();
8098
+ if (platform === "win32") {
8099
+ if (remove) return { cmd: `schtasks /Delete /TN ${TASK_NAME} /F`, note: "Windows Task Scheduler" };
8100
+ const sc = mins % 60 === 0 ? `/SC HOURLY /MO ${mins / 60}` : `/SC MINUTE /MO ${mins}`;
8101
+ return {
8102
+ cmd: `schtasks /Create /TN ${TASK_NAME} /TR "\\"${node}\\" \\"${bin}\\" agents tick" ${sc} /F`,
8103
+ note: "Windows Task Scheduler"
8104
+ };
8105
+ }
8106
+ const line = `*/${mins} * * * * "${node}" "${bin}" agents tick # ${TASK_NAME}`;
8107
+ if (remove) {
8108
+ return { cmd: `crontab -l 2>/dev/null | grep -v '# ${TASK_NAME}' | crontab -`, note: "crontab" };
8109
+ }
8110
+ return {
8111
+ cmd: `( crontab -l 2>/dev/null | grep -v '# ${TASK_NAME}'; echo '${line}' ) | crontab -`,
8112
+ note: "crontab"
8113
+ };
8114
+ }
8115
+ function runShell(cmd) {
8116
+ const r = platform === "win32" ? spawnSync("cmd", ["/c", cmd], { encoding: "utf8" }) : spawnSync("sh", ["-c", cmd], { encoding: "utf8" });
8117
+ if (r.status !== 0) {
8118
+ info(dim((r.stderr || r.stdout || "").trim().slice(0, 300)));
8119
+ return false;
8120
+ }
8121
+ return true;
8122
+ }
8123
+ function registerAgentsCron(agents) {
8124
+ 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) => {
8125
+ const mins = intervalMinutes(opts.every);
8126
+ if (!opts.remove && mins === null) die(`invalid --every '${opts.every}' \u2014 use Nm or Nh (e.g. 5m, 2h)`);
8127
+ const { cmd, note } = buildCron(mins ?? 5, Boolean(opts.remove));
8128
+ heading(opts.remove ? "Remove scheduled agents" : "Schedule agents");
8129
+ info(`${note}: runs ${accent("oriro agents tick")} ${opts.remove ? "" : `every ${accent(opts.every)}`}`);
8130
+ if (!opts.apply) {
8131
+ process.stdout.write(`
8132
+ ${cmd}
8133
+
8134
+ `);
8135
+ info(dim("printed only \u2014 re-run with --apply to make this change, or run the command yourself"));
8136
+ return;
8137
+ }
8138
+ if (runShell(cmd)) ok(opts.remove ? "scheduler entry removed" : `scheduled \u2014 agents tick will run every ${opts.every}`);
8139
+ else die("could not apply the schedule (see the message above) \u2014 you can run the printed command manually");
8140
+ });
8141
+ }
8142
+
7113
8143
  // src/commands/agents.ts
7114
8144
  function nowIso() {
7115
8145
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -7143,14 +8173,32 @@ function registerAgentsCommand(program2) {
7143
8173
  info(`make one: ${accent('oriro agents make <name> --task "\u2026" [--router <id>] [--schedule 1h]')}`);
7144
8174
  info(`then: ${accent("oriro agents run <name>")} ${dim("\xB7 or")} ${accent("oriro agents tick")} ${dim("for scheduled ones")}`);
7145
8175
  });
7146
- agents.command("list").description("list your saved agents").action(() => {
8176
+ 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) => {
8177
+ const oerr = outputError(opts);
8178
+ if (oerr) die(oerr);
7147
8179
  const all = listAgents();
8180
+ const state = loadState();
8181
+ if (isMachineOutput(opts) || opts.query) {
8182
+ const rows = all.map((a) => ({
8183
+ name: a.name,
8184
+ brain: a.router ?? "pool",
8185
+ schedule: a.schedule ?? "manual",
8186
+ description: a.description ?? "",
8187
+ lastRun: state[a.name]?.lastRunAt ? new Date(state[a.name].lastRunAt).toISOString() : "",
8188
+ lastOk: state[a.name]?.lastOk ?? null
8189
+ }));
8190
+ process.stdout.write(renderList2(rows, {
8191
+ output: opts.output,
8192
+ query: opts.query,
8193
+ columns: ["name", "brain", "schedule", "lastRun", "lastOk"]
8194
+ }) + "\n");
8195
+ return;
8196
+ }
7148
8197
  heading("Agents");
7149
8198
  if (!all.length) {
7150
8199
  info(`no agents yet \u2014 make one: ${accent('oriro agents make my-agent --task "\u2026"')}`);
7151
8200
  return;
7152
8201
  }
7153
- const state = loadState();
7154
8202
  for (const a of all) {
7155
8203
  printAgent(a);
7156
8204
  const last = state[a.name]?.lastRunAt;
@@ -7200,13 +8248,22 @@ function registerAgentsCommand(program2) {
7200
8248
  ok(`${res.overwrote ? "updated" : "added"} agent ${accent(res.name ?? "")} ${dim("\u2192 ~/.oriro/agents")}`);
7201
8249
  info(`run it: ${accent(`oriro agents run ${res.name}`)}`);
7202
8250
  });
7203
- agents.command("remove <name>").description("delete an agent").action((name) => {
8251
+ agents.command("remove <name>").description("delete an agent").option("-f, --force", "skip the confirmation prompt").action(async (name, opts) => {
8252
+ if (!loadAgent(name)) {
8253
+ info(`'${name}' is not a saved agent \u2014 nothing to remove`);
8254
+ return;
8255
+ }
8256
+ if (!await confirmDestructive(`remove agent '${name}'`, opts)) {
8257
+ info("cancelled");
8258
+ return;
8259
+ }
7204
8260
  if (!removeAgent(name)) {
7205
8261
  info(`'${name}' is not a saved agent \u2014 nothing to remove`);
7206
8262
  return;
7207
8263
  }
7208
8264
  ok(`removed ${accent(name)}`);
7209
8265
  });
8266
+ registerAgentsCron(agents);
7210
8267
  agents.command("tick").description("run every DUE scheduled agent once, then exit (wire to OS cron / Task Scheduler)").action(async () => {
7211
8268
  const state = loadState();
7212
8269
  const now = Date.now();
@@ -7241,19 +8298,327 @@ function registerAgentsCommand(program2) {
7241
8298
  });
7242
8299
  }
7243
8300
 
8301
+ // src/commands/completion.ts
8302
+ function extractTree(program2) {
8303
+ const nodes = [];
8304
+ for (const c of program2.commands) {
8305
+ const name = c.name();
8306
+ if (name === "completion") continue;
8307
+ nodes.push({
8308
+ name,
8309
+ subs: c.commands.map((s) => s.name()),
8310
+ opts: c.options.map((o) => o.long).filter((l) => Boolean(l))
8311
+ });
8312
+ }
8313
+ return nodes;
8314
+ }
8315
+ var SHELLS = ["bash", "zsh", "fish", "pwsh"];
8316
+ function topNames(tree) {
8317
+ return [...tree.map((n) => n.name), "completion", "help"].join(" ");
8318
+ }
8319
+ function genBash(tree) {
8320
+ const cases = tree.map((n) => ` ${n.name}) COMPREPLY=( $(compgen -W "${n.subs.join(" ")} ${n.opts.join(" ")}" -- "$cur") );;`).join("\n");
8321
+ return `# ORIRO bash completion. Install: oriro completion bash > /etc/bash_completion.d/oriro
8322
+ # or (per-user): oriro completion bash >> ~/.bashrc
8323
+ _oriro_complete() {
8324
+ local cur prev cword
8325
+ cur="\${COMP_WORDS[COMP_CWORD]}"
8326
+ cword=$COMP_CWORD
8327
+ if [ "$cword" -eq 1 ]; then
8328
+ COMPREPLY=( $(compgen -W "${topNames(tree)}" -- "$cur") )
8329
+ return
8330
+ fi
8331
+ case "\${COMP_WORDS[1]}" in
8332
+ ${cases}
8333
+ *) COMPREPLY=();;
8334
+ esac
8335
+ }
8336
+ complete -F _oriro_complete oriro
8337
+ `;
8338
+ }
8339
+ function genZsh(tree) {
8340
+ const cases = tree.map((n) => ` ${n.name}) compadd ${n.subs.join(" ")} ${n.opts.join(" ")} ;;`).join("\n");
8341
+ return `#compdef oriro
8342
+ # ORIRO zsh completion. Install: oriro completion zsh > "\${fpath[1]}/_oriro" (then restart the shell)
8343
+ _oriro() {
8344
+ local -a words; words=("\${(@)words}")
8345
+ if (( CURRENT == 2 )); then
8346
+ compadd ${topNames(tree)}
8347
+ return
8348
+ fi
8349
+ case "\${words[2]}" in
8350
+ ${cases}
8351
+ esac
8352
+ }
8353
+ _oriro "$@"
8354
+ `;
8355
+ }
8356
+ function genFish(tree) {
8357
+ const lines = [
8358
+ "# ORIRO fish completion. Install: oriro completion fish > ~/.config/fish/completions/oriro.fish",
8359
+ `complete -c oriro -f -n __fish_use_subcommand -a "${topNames(tree)}"`
8360
+ ];
8361
+ for (const n of tree) {
8362
+ if (n.subs.length) {
8363
+ lines.push(`complete -c oriro -f -n "__fish_seen_subcommand_from ${n.name}" -a "${n.subs.join(" ")}"`);
8364
+ }
8365
+ }
8366
+ return lines.join("\n") + "\n";
8367
+ }
8368
+ function genPwsh(tree) {
8369
+ const cases = tree.map((n) => ` '${n.name}' { @(${[...n.subs, ...n.opts].map((s) => `'${s}'`).join(", ")}) }`).join("\n");
8370
+ const top = [...tree.map((n) => n.name), "completion", "help"].map((s) => `'${s}'`).join(", ");
8371
+ return `# ORIRO PowerShell completion. Install: oriro completion pwsh >> $PROFILE (then restart pwsh)
8372
+ Register-ArgumentCompleter -Native -CommandName oriro -ScriptBlock {
8373
+ param($wordToComplete, $commandAst, $cursorPosition)
8374
+ $tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
8375
+ $candidates = if ($tokens.Count -le 2) {
8376
+ @(${top})
8377
+ } else {
8378
+ switch ($tokens[1]) {
8379
+ ${cases}
8380
+ default { @() }
8381
+ }
8382
+ }
8383
+ $candidates | Where-Object { $_ -like "$wordToComplete*" } |
8384
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
8385
+ }
8386
+ `;
8387
+ }
8388
+ var GENERATORS = {
8389
+ bash: genBash,
8390
+ zsh: genZsh,
8391
+ fish: genFish,
8392
+ pwsh: genPwsh
8393
+ };
8394
+ function registerCompletionCommand(program2) {
8395
+ program2.command("completion <shell>").description("print a shell tab-completion script (bash | zsh | fish | pwsh)").action((shell) => {
8396
+ const s = shell.toLowerCase();
8397
+ if (!SHELLS.includes(s)) {
8398
+ die(`unsupported shell '${shell}'. Use one of: ${SHELLS.join(", ")}`);
8399
+ return;
8400
+ }
8401
+ process.stdout.write(GENERATORS[s](extractTree(program2)));
8402
+ });
8403
+ }
8404
+
8405
+ // src/commands/config.ts
8406
+ function registerConfigCommand(program2) {
8407
+ const config = program2.command("config").description("your durable CLI settings (defaults in ~/.oriro/config.json)");
8408
+ config.command("list").description("show every setting, its value, and what it does").action(() => {
8409
+ const all = configAll();
8410
+ heading("Config");
8411
+ for (const { key, desc } of configKeys()) {
8412
+ const val = all[key];
8413
+ process.stdout.write(` ${accent(key.padEnd(10))} ${val !== void 0 ? accent(val) : dim("(default)")} ${dim(desc)}
8414
+ `);
8415
+ }
8416
+ info(`set: ${accent("oriro config set <key> <value>")} \xB7 clear: ${accent("oriro config unset <key>")}`);
8417
+ });
8418
+ config.command("get <key>").description("print one setting's value").action((key) => {
8419
+ if (!isConfigKey(key)) die(`unknown key '${key}' \u2014 run \`oriro config list\``);
8420
+ const val = configGet(key);
8421
+ if (val === void 0) {
8422
+ info(`${key} is unset (using the built-in default)`);
8423
+ return;
8424
+ }
8425
+ process.stdout.write(`${val}
8426
+ `);
8427
+ });
8428
+ config.command("set <key> <value>").description("set a setting (validated)").action((key, value) => {
8429
+ if (!isConfigKey(key)) die(`unknown key '${key}' \u2014 run \`oriro config list\``);
8430
+ const err = validateConfig(key, value);
8431
+ if (err) die(`invalid value for '${key}': ${err}`);
8432
+ configSet(key, value);
8433
+ ok(`${accent(key)} = ${accent(value)}`);
8434
+ });
8435
+ config.command("unset <key>").description("clear a setting back to its built-in default").action((key) => {
8436
+ if (!isConfigKey(key)) die(`unknown key '${key}' \u2014 run \`oriro config list\``);
8437
+ if (configUnset(key)) ok(`cleared ${accent(key)}`);
8438
+ else info(`${key} was already at its default`);
8439
+ });
8440
+ }
8441
+
8442
+ // src/commands/setup.ts
8443
+ import { rmSync as rmSync5 } from "fs";
8444
+ import { join as join30 } from "path";
8445
+ import { stdin as stdin12, stdout as stdout11 } from "process";
8446
+ var MARKERS = [
8447
+ "language.json",
8448
+ "avatar.json",
8449
+ "skills-onboarded.json",
8450
+ "connectors-onboarded.json",
8451
+ "models-onboarded.json",
8452
+ join30("routers", "onboarded.json")
8453
+ ];
8454
+ function registerSetupCommand(program2) {
8455
+ 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) => {
8456
+ if (opts.reset) {
8457
+ for (const m of MARKERS) {
8458
+ try {
8459
+ rmSync5(join30(oriroDir(), m), { force: true });
8460
+ } catch {
8461
+ }
8462
+ }
8463
+ ok("reset \u2014 every step will be asked again");
8464
+ }
8465
+ if (!stdin12.isTTY || !stdout11.isTTY) {
8466
+ heading("ORIRO setup");
8467
+ info(`ORIRO is ${accent("keyless")} \u2014 no login, no API keys. Run ${accent("oriro setup")} in a real terminal for the guided wizard.`);
8468
+ info(dim("or configure directly: oriro language <code> \xB7 oriro routers add <id> \xB7 oriro connectors add <slug> \xB7 oriro config set <k> <v>"));
8469
+ return;
8470
+ }
8471
+ await runOnboarding();
8472
+ });
8473
+ }
8474
+
8475
+ // src/commands/import.ts
8476
+ import { existsSync as existsSync21, readFileSync as readFileSync26, readdirSync as readdirSync3, statSync as statSync5, cpSync as cpSync2, mkdirSync as mkdirSync18 } from "fs";
8477
+ import { join as join31, basename as basename2 } from "path";
8478
+ function registerImportCommand(program2) {
8479
+ const imp = program2.command("import").description("migrate from another CLI (MCP servers, skills)");
8480
+ imp.command("mcp <file>").description("import MCP servers from a Claude-compatible mcp.json (Guardian-vetted)").action((file6) => {
8481
+ if (!existsSync21(file6)) die(`no such file: ${file6}`);
8482
+ let servers;
8483
+ try {
8484
+ const j = JSON.parse(readFileSync26(file6, "utf8"));
8485
+ servers = j.mcpServers ?? j.servers ?? {};
8486
+ } catch (e) {
8487
+ die(`could not parse ${file6}: ${e instanceof Error ? e.message : String(e)}`);
8488
+ return;
8489
+ }
8490
+ const names = Object.keys(servers);
8491
+ if (!names.length) die(`no "mcpServers" found in ${file6}`);
8492
+ heading(`Import MCP \xB7 ${names.length} server${names.length === 1 ? "" : "s"}`);
8493
+ let imported = 0, blocked2 = 0;
8494
+ for (const name of names) {
8495
+ const s = servers[name];
8496
+ const input = {
8497
+ name,
8498
+ ...s.command ? { command: s.command } : {},
8499
+ ...s.args ? { args: s.args } : {},
8500
+ ...s.env ? { env: s.env } : {},
8501
+ ...s.url ? { url: s.url } : {},
8502
+ ...s.headers ? { headers: s.headers } : {}
8503
+ };
8504
+ if (s.url) {
8505
+ try {
8506
+ assertSafeUrl(s.url);
8507
+ } catch (e) {
8508
+ process.stdout.write(` ${fgHex(PALETTE.error, "\u2717")} ${name} ${dim(`blocked: ${e instanceof Error ? e.message : String(e)}`)}
8509
+ `);
8510
+ blocked2++;
8511
+ continue;
8512
+ }
8513
+ }
8514
+ const outcome = vetServer(input);
8515
+ if (outcome.decision === "block") {
8516
+ process.stdout.write(` ${fgHex(PALETTE.error, "\u2717")} ${name} ${dim(`blocked: ${outcome.reason}`)}
8517
+ `);
8518
+ blocked2++;
8519
+ continue;
8520
+ }
8521
+ saveCustomServer({ name, config: buildServerConfig(input), trusted: outcome.decision === "allow" });
8522
+ const mark = outcome.decision === "allow" ? fgHex(PALETTE.success, "\u2713 trusted") : dim("\u25CB needs trust");
8523
+ process.stdout.write(` ${mark} ${accent(name)}
8524
+ `);
8525
+ imported++;
8526
+ }
8527
+ info(`${imported} imported \xB7 ${blocked2} blocked${imported ? ` \u2014 they connect in-session; see \`oriro connectors custom\`` : ""}`);
8528
+ });
8529
+ imp.command("skills <dir>").description("import SKILL.md skill folders from another CLI's skills directory").action((dir) => {
8530
+ if (!existsSync21(dir) || !statSync5(dir).isDirectory()) die(`no such directory: ${dir}`);
8531
+ const dest = userSkillsDir();
8532
+ mkdirSync18(dest, { recursive: true });
8533
+ heading("Import skills");
8534
+ 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")));
8535
+ let n = 0;
8536
+ for (const src of sources) {
8537
+ cpSync2(src, join31(dest, basename2(src)), { recursive: true });
8538
+ process.stdout.write(` ${fgHex(PALETTE.success, "\u2713")} ${accent(basename2(src))}
8539
+ `);
8540
+ n++;
8541
+ }
8542
+ if (n === 0) info(dim(`no SKILL.md skill folder found at or inside ${dir}`));
8543
+ else ok(`imported ${n} skill${n === 1 ? "" : "s"} \u2192 ${dim(dest)}`);
8544
+ });
8545
+ }
8546
+
8547
+ // src/commands/help-on-error.ts
8548
+ function lev(a, b) {
8549
+ const m = a.length, n = b.length;
8550
+ if (!m) return n;
8551
+ if (!n) return m;
8552
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
8553
+ let curr = new Array(n + 1);
8554
+ for (let i = 1; i <= m; i++) {
8555
+ curr[0] = i;
8556
+ for (let j = 1; j <= n; j++) {
8557
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
8558
+ curr[j] = Math.min((curr[j - 1] ?? 0) + 1, (prev[j] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
8559
+ }
8560
+ [prev, curr] = [curr, prev];
8561
+ }
8562
+ return prev[n] ?? n;
8563
+ }
8564
+ function didYouMean(input, candidates) {
8565
+ let best;
8566
+ let bestD = Infinity;
8567
+ for (const c of candidates) {
8568
+ const d = lev(input.toLowerCase(), c.toLowerCase());
8569
+ if (d < bestD) {
8570
+ bestD = d;
8571
+ best = c;
8572
+ }
8573
+ }
8574
+ return best !== void 0 && bestD <= Math.max(2, Math.floor(input.length * 0.4)) ? best : void 0;
8575
+ }
8576
+ function fullPath(cmd) {
8577
+ const parts = [];
8578
+ let c = cmd;
8579
+ while (c && c.name() !== "oriro") {
8580
+ parts.unshift(c.name());
8581
+ c = c.parent;
8582
+ }
8583
+ return parts.length ? `oriro ${parts.join(" ")}` : "oriro <command>";
8584
+ }
8585
+ function enableHelpOnError(program2) {
8586
+ const apply = (cmd) => {
8587
+ cmd.showHelpAfterError(`
8588
+ (run: ${fullPath(cmd)} --help for usage)`);
8589
+ cmd.showSuggestionAfterError(true);
8590
+ for (const sub of cmd.commands) apply(sub);
8591
+ };
8592
+ apply(program2);
8593
+ }
8594
+
7244
8595
  // src/cli.ts
7245
8596
  var version = createRequire(import.meta.url)("../package.json").version;
7246
8597
  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) => {
8598
+ 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) => {
8599
+ if (options.print !== void 0) {
8600
+ const fmt = options.outputFormat ?? "text";
8601
+ if (!isOutputFormatMode(fmt)) {
8602
+ process.stderr.write(`error: --output-format must be text | json | stream-json
8603
+ `);
8604
+ process.exitCode = 1;
8605
+ return;
8606
+ }
8607
+ await runHeadless(options.print, fmt);
8608
+ return;
8609
+ }
7248
8610
  if (command.args.length > 0) {
7249
- const arg = command.args[0];
8611
+ const arg = command.args[0] ?? "";
7250
8612
  if (arg === "help") {
7251
8613
  command.outputHelp();
7252
8614
  return;
7253
8615
  }
7254
- process.stderr.write(`error: unknown command '${arg}'
7255
- Run 'oriro --help' to see available commands.
8616
+ const names = command.commands.map((c) => c.name());
8617
+ const guess = didYouMean(arg, names);
8618
+ process.stderr.write(`error: unknown command '${arg}'${guess ? ` \u2014 did you mean '${guess}'?` : ""}
8619
+
7256
8620
  `);
8621
+ command.outputHelp();
7257
8622
  process.exitCode = 1;
7258
8623
  return;
7259
8624
  }
@@ -7269,6 +8634,11 @@ registerAvatarCommand(program);
7269
8634
  registerHeadCommand(program);
7270
8635
  registerVoiceCommand(program);
7271
8636
  registerAgentsCommand(program);
8637
+ registerConfigCommand(program);
8638
+ registerSetupCommand(program);
8639
+ registerImportCommand(program);
8640
+ registerCompletionCommand(program);
8641
+ enableHelpOnError(program);
7272
8642
  program.parseAsync().catch((e) => {
7273
8643
  if (e instanceof DieError) return;
7274
8644
  process.stderr.write(`