@oriro/orirocli 0.1.4 → 0.1.6

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 (3) hide show
  1. package/README.md +47 -4
  2. package/dist/cli.js +2578 -1874
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -5,8 +5,8 @@ import { createRequire } from "module";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/repl.ts
8
- import { createInterface as createInterface4 } from "readline/promises";
9
- import { stdin as stdin4, stdout as stdout4 } from "process";
8
+ import { createInterface as createInterface5 } from "readline/promises";
9
+ import { stdin as stdin5, stdout as stdout6 } from "process";
10
10
 
11
11
  // src/ui/theme.ts
12
12
  var PALETTE = {
@@ -71,8 +71,8 @@ ${tagline}
71
71
  }
72
72
 
73
73
  // src/onboarding/wrapper.ts
74
- import { createInterface as createInterface3 } from "readline/promises";
75
- import { stdin as stdin3, stdout as stdout3 } from "process";
74
+ import { createInterface as createInterface4 } from "readline/promises";
75
+ import { stdin as stdin4, stdout as stdout5 } from "process";
76
76
 
77
77
  // src/language/languages.ts
78
78
  var LANGUAGES = [
@@ -268,7 +268,24 @@ async function translateForUser(english, toLang) {
268
268
 
269
269
  // src/language/onboarding.ts
270
270
  import { createInterface } from "readline/promises";
271
- import { stdin, stdout } from "process";
271
+ import { stdin, stdout as stdout2 } from "process";
272
+
273
+ // src/onboarding/prompt.ts
274
+ import { stdout } from "process";
275
+ async function ask(rl, question) {
276
+ try {
277
+ return await rl.question(question);
278
+ } catch {
279
+ try {
280
+ rl.close();
281
+ } catch {
282
+ }
283
+ stdout.write(dim("\nBye.\n"));
284
+ process.exit(0);
285
+ }
286
+ }
287
+
288
+ // src/language/onboarding.ts
272
289
  var C = {
273
290
  teal: "\x1B[38;2;45;212;191m",
274
291
  purple: "\x1B[38;2;128;96;222m",
@@ -277,12 +294,12 @@ var C = {
277
294
  reset: "\x1B[0m"
278
295
  };
279
296
  function header() {
280
- stdout.write(`
297
+ stdout2.write(`
281
298
  ${C.teal}\u25EF${C.reset} ${C.bold}ORIRO${C.reset} ${C.dim}\u2014 your terminal, your language${C.reset}
282
299
  `);
283
- stdout.write(` ${C.dim}You type and read in your language; the AI works in English for you.${C.reset}
300
+ stdout2.write(` ${C.dim}You type and read in your language; the AI works in English for you.${C.reset}
284
301
  `);
285
- stdout.write(` ${C.dim}${LANGUAGES.length} languages \xB7 ${NEURAL_VOICE_COUNT} with a built-in voice (${C.purple}\u2605${C.dim}).${C.reset}
302
+ stdout2.write(` ${C.dim}${LANGUAGES.length} languages \xB7 ${NEURAL_VOICE_COUNT} with a built-in voice (${C.purple}\u2605${C.dim}).${C.reset}
286
303
 
287
304
  `);
288
305
  }
@@ -290,38 +307,39 @@ function renderList(list) {
290
307
  const shown = list.slice(0, 15);
291
308
  shown.forEach((l, i) => {
292
309
  const star = l.neuralVoice ? `${C.purple}\u2605${C.reset}` : " ";
293
- stdout.write(` ${C.teal}${String(i + 1).padStart(2)}${C.reset} ${star} ${l.name} ${C.dim}(${l.code})${C.reset}
310
+ stdout2.write(` ${C.teal}${String(i + 1).padStart(2)}${C.reset} ${star} ${l.name} ${C.dim}(${l.code})${C.reset}
294
311
  `);
295
312
  });
296
313
  if (list.length > shown.length) {
297
- stdout.write(` ${C.dim}\u2026 ${list.length - shown.length} more \u2014 keep typing to narrow.${C.reset}
314
+ stdout2.write(` ${C.dim}\u2026 ${list.length - shown.length} more \u2014 keep typing to narrow.${C.reset}
298
315
  `);
299
316
  }
300
317
  }
301
318
  async function selectLanguageInteractive() {
302
319
  header();
303
- const rl = createInterface({ input: stdin, output: stdout });
320
+ const rl = createInterface({ input: stdin, output: stdout2 });
304
321
  try {
305
322
  let list = searchLanguages("");
306
323
  renderList(list);
307
324
  for (; ; ) {
308
- const ans = (await rl.question(`
325
+ const ans = (await ask(rl, `
309
326
  ${C.teal}\u203A${C.reset} Type a language, or a number to pick: `)).trim();
310
327
  const n = Number(ans);
311
- const byNumber = ans && Number.isInteger(n) && n >= 1 && n <= list.length ? list[n - 1] : void 0;
328
+ const shown = Math.min(15, list.length);
329
+ const byNumber = ans && Number.isInteger(n) && n >= 1 && n <= shown ? list[n - 1] : void 0;
312
330
  if (byNumber) return byNumber;
313
331
  const direct = languageByCode(ans);
314
332
  if (direct) return direct;
315
333
  list = searchLanguages(ans);
316
334
  if (list.length === 0) {
317
- stdout.write(` ${C.dim}No match \u2014 try the English name or ISO code.${C.reset}
335
+ stdout2.write(` ${C.dim}No match \u2014 try the English name or ISO code.${C.reset}
318
336
  `);
319
337
  list = searchLanguages("");
320
338
  } else {
321
339
  const only = list.length === 1 ? list[0] : void 0;
322
340
  if (only) return only;
323
341
  }
324
- stdout.write("\n");
342
+ stdout2.write("\n");
325
343
  renderList(list);
326
344
  }
327
345
  } finally {
@@ -336,7 +354,7 @@ async function runLanguageOnboarding() {
336
354
  }
337
355
  const lang = await selectLanguageInteractive();
338
356
  setTerminalLanguage(lang);
339
- stdout.write(
357
+ stdout2.write(
340
358
  `
341
359
  ${C.teal}\u25EF${C.reset} ${C.bold}${lang.name}${C.reset} is now your terminal language. ${C.dim}Change it anytime with ${C.reset}${C.teal}oriro language${C.reset}
342
360
 
@@ -354,11 +372,11 @@ var INJECTION_PATTERNS = [
354
372
  /\[INST\]|<<SYS>>/
355
373
  ];
356
374
  var IOC_PATTERNS = [
357
- ["ioc:secret_read", /\bread\b[^\n]*(\.ssh|\.env\b|id_rsa)/i],
375
+ ["ioc:secret_read", /\bread\b[^\n]*(\.ssh(?![-.\w])|\.env\b|id_rsa)/i],
358
376
  ["ioc:exfil_post", /\bsend\b[^\n]*\bto\s+https?:\/\//i],
359
377
  ["ioc:env_exfil", /process\.env[^\n]{0,40}https?:\/\//i],
360
378
  ["ioc:pipe_shell", /(curl|wget)[^\n]*\|\s*(sh|bash|node)\b/i],
361
- ["ioc:pipe_exfil", /(cat|type|read)[^\n]*(\.ssh|id_rsa|\.env\b)[^\n]*\|\s*(curl|wget|nc)\b/i],
379
+ ["ioc:pipe_exfil", /(cat|type|read)[^\n]*(\.ssh(?![-.\w])|id_rsa|\.env\b)[^\n]*\|\s*(curl|wget|nc)\b/i],
362
380
  ["ioc:exfiltrate", /exfiltrat/i],
363
381
  ["ioc:obf_loader", /eval\(\s*(atob|Buffer\.from)\(/i],
364
382
  ["ioc:cp_loader", /child_process[\s\S]{0,40}(atob|fromCharCode)/i]
@@ -404,6 +422,12 @@ ${typeof params === "string" ? params : JSON.stringify(params ?? "")}`;
404
422
  if (hasHiddenUnicode(blob)) return { safe: false, threat: "hidden_unicode" };
405
423
  return { safe: true };
406
424
  }
425
+ function scanExecCommand(text) {
426
+ const ioc = firstIOC(text);
427
+ if (ioc) return { safe: false, threat: ioc };
428
+ if (hasHiddenUnicode(text)) return { safe: false, threat: "hidden_unicode" };
429
+ return { safe: true };
430
+ }
407
431
 
408
432
  // src/guardian/rules.ts
409
433
  var block = (rule, reason, severity = "critical") => ({
@@ -412,7 +436,7 @@ var block = (rule, reason, severity = "critical") => ({
412
436
  rule,
413
437
  reason
414
438
  });
415
- var ask = (rule, reason, severity = "warning") => ({
439
+ var ask2 = (rule, reason, severity = "warning") => ({
416
440
  decision: "ask",
417
441
  severity,
418
442
  rule,
@@ -420,162 +444,220 @@ var ask = (rule, reason, severity = "warning") => ({
420
444
  });
421
445
  var cmdOf = (c) => (c.command ?? "").toLowerCase();
422
446
  var norm = (s) => s.replace(/\s+/g, " ").trim();
423
- function isDangerousRm(cmd) {
424
- if (!/\brm\b/i.test(cmd)) return false;
425
- const hasRecursive = /(?:^|\s)-[a-z]*r/i.test(cmd) || /--recursive\b/i.test(cmd);
426
- const hasForce = /(?:^|\s)-[a-z]*f/i.test(cmd) || /--force\b/i.test(cmd);
427
- if (!hasRecursive || !hasForce) return false;
428
- if (/--no-preserve-root\b/i.test(cmd)) return true;
429
- if (/(?:\s|^)(\/|~|\.|\*|\$home)(?:\s|$)/i.test(cmd)) return true;
430
- return /(?:\s|^)\/(etc|usr|bin|sbin|var|boot|lib|lib64|sys|proc|dev|root|home|opt|windows|system32)(?:[\\/]|\s|$)/i.test(
431
- cmd
432
- );
447
+ var anyMatch = (patterns, text) => patterns.some((re) => re.test(text));
448
+ var stripQuotes = (t) => t.replace(/^['"]+/, "").replace(/['"]+$/, "");
449
+ function statements(cmd) {
450
+ return cmd.split(/(?:&&|\|\||[;|&\n])+/g).map((s) => s.trim()).filter(Boolean);
451
+ }
452
+ function words(stmt) {
453
+ return stmt.split(/\s+/).map(stripQuotes).filter(Boolean);
454
+ }
455
+ function commandWord(stmt) {
456
+ const w = words(stmt);
457
+ let i = 0;
458
+ while (i < w.length && /^(sudo|nohup|nice|time|exec|command|builtin|then|do|else)$/i.test(w[i] ?? "")) i++;
459
+ if (i < w.length && /^env$/i.test(w[i] ?? "")) {
460
+ i++;
461
+ while (i < w.length && /^[\w.]+=/.test(w[i] ?? "")) i++;
462
+ }
463
+ while (i < w.length && /^[\w.]+=/.test(w[i] ?? "")) i++;
464
+ return (w[i] ?? "").replace(/^.*[\\/]/, "").toLowerCase();
465
+ }
466
+ var SYS_DIR = "(etc|usr|bin|sbin|var|boot|lib|lib64|sys|proc|dev|root|opt|windows|system32|programdata|library|applications|system|private|cores|volumes|network)";
467
+ function classifyRmTarget(raw) {
468
+ let t = stripQuotes(raw).trim();
469
+ if (!t || t.startsWith("-")) return "safe";
470
+ t = t.replace(/\$\{?home\}?/gi, "~");
471
+ if (/^(\/|\/\*|~|~\/|~\/\*|\.|\.\/|\.\/\*|\*|\.\*)$/.test(t)) return "danger";
472
+ if (new RegExp(`^/${SYS_DIR}(/\\*?)?$`, "i").test(t)) return "danger";
473
+ if (/^\/home(\/[^/]+)?\/?\*?$/i.test(t)) return "danger";
474
+ if (new RegExp(`^/${SYS_DIR}/.+`, "i").test(t)) return "system-sub";
475
+ return "safe";
476
+ }
477
+ function rmVerdict(stmt) {
478
+ const cw = commandWord(stmt);
479
+ if (cw !== "rm") return null;
480
+ const w = words(stmt);
481
+ const flags = w.filter((x) => x.startsWith("-")).join(" ");
482
+ const recursive = /(^|[^-])-[a-z]*r/i.test(" " + flags) || /--recursive\b/i.test(flags);
483
+ const force = /(^|[^-])-[a-z]*f/i.test(" " + flags) || /--force\b/i.test(flags);
484
+ const noPreserve = /--no-preserve-root\b/i.test(flags);
485
+ if (!recursive || !force) return null;
486
+ if (noPreserve) return block("fs-destruction", "Recursive force-delete with --no-preserve-root");
487
+ const targets = w.slice(1).filter((x) => !x.startsWith("-")).map(classifyRmTarget);
488
+ if (targets.includes("danger")) return block("fs-destruction", "Recursive force-delete of root/home/cwd/system path");
489
+ if (targets.includes("system-sub")) return ask2("fs-destruction", "Recursive force-delete inside a system directory");
490
+ return null;
433
491
  }
492
+ var DISK = "(sd|nvme|disk|hd|vd|xvd|mmcblk|loop)";
434
493
  var FS_DESTRUCTION = [
435
- /\bmkfs\.?\w*\s+\/dev\//i,
494
+ new RegExp(`\\bmkfs\\.?\\w*\\s+/dev/${DISK}`, "i"),
436
495
  // reformat a disk
437
- /\bdd\s+.*\bof=\/dev\/(sd|nvme|disk|hd)/i,
496
+ new RegExp(`\\bdd\\b[^\\n]*\\bof=/dev/${DISK}`, "i"),
438
497
  // overwrite raw disk
439
- /\b(shutdown|reboot|halt|poweroff)\b/i,
440
- // host disruption
498
+ new RegExp(`>\\s*/dev/${DISK}\\w`, "i"),
499
+ // redirect over raw disk
441
500
  /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
442
- // fork bomb :(){ :|:& };:
443
- /\bremove-item\b.*-recurse.*-force.*[\\\/](windows|system32|users)\b/i,
501
+ // fork bomb
502
+ /\bremove-item\b.*-recurse.*-force.*[\\/](windows|system32|users)\b/i,
444
503
  // PS recursive wipe
445
504
  /\b(format|cipher\s+\/w)\b.*[a-z]:\\?/i,
446
505
  // windows format / wipe-free-space
447
- />\s*\/dev\/(sd|nvme|disk|hd)\w/i
448
- // redirect over raw disk
506
+ /\bfind\b[^\n]*\s(-delete\b|-exec\s+rm\b)/i,
507
+ // find -delete / -exec rm
508
+ /\bshred\b\s+(-|\S*\/dev\/)/i,
509
+ // shred a device/file destructively
510
+ new RegExp(`\\btruncate\\b[^\\n]*\\s/dev/${DISK}`, "i"),
511
+ // truncate a device
512
+ /\bmv\b[^\n]*\s\/dev\/null\b/i,
513
+ // mv important → /dev/null
514
+ /\bchmod\b\s+-[a-z]*r[a-z]*\s+0{3,4}\b/i,
515
+ // chmod -R 000 (strip all perms recursively)
516
+ /\bwipefs\b/i
449
517
  ];
518
+ var HOST_DISRUPT = /* @__PURE__ */ new Set(["shutdown", "reboot", "halt", "poweroff"]);
519
+ var FETCH = "(curl|wget|fetch|httpie)";
520
+ var SHELL = "(sh|bash|zsh|dash|ksh|sudo\\s+sh|sudo\\s+bash|python\\d?|node|perl|ruby|php)";
450
521
  var REMOTE_EXEC = [
451
- /\b(curl|wget|fetch)\b[^\n|]*\|\s*(sudo\s+)?(sh|bash|zsh|python\d?|node|perl|ruby)\b/i,
522
+ new RegExp(`\\b${FETCH}\\b[^\\n|]*\\|\\s*(sudo\\s+)?${SHELL}\\b`, "i"),
452
523
  // curl … | sh
524
+ new RegExp(`(?:^|[\\s;&|(])(bash|sh|zsh|ksh|source|\\.)\\s*<\\s*\\(\\s*${FETCH}`, "i"),
525
+ // sh <(curl) / bash<(curl) / . <(curl)
526
+ new RegExp(`\\b(bash|sh|zsh|ksh|eval)\\b[^\\n]*\\$\\(\\s*${FETCH}\\b`, "i"),
527
+ // bash -c "$(curl)"
528
+ new RegExp(`\\$\\(\\s*${FETCH}\\b[^)]*\\)`, "i"),
529
+ // bare $(curl …) substitution
530
+ /`\s*(curl|wget|fetch)\b/i,
531
+ // backtick `curl`
453
532
  /\b(irm|iwr|invoke-webrequest|invoke-restmethod)\b[^\n|]*\|\s*(iex|invoke-expression)/i,
454
533
  // PS download|iex
455
534
  /\biex\b\s*\(\s*(new-object\s+net\.webclient|.*downloadstring)/i,
456
535
  // iex(New-Object Net.WebClient…)
457
- /\bbash\s+<\s*\(\s*(curl|wget)/i,
458
- // bash <(curl …)
459
- /\beval\b[^\n]*\$\(\s*(curl|wget|fetch)\b/i,
460
- // eval "$(curl …)"
461
- /\bpython\d?\s+-c\b[^\n]*urllib|requests\.get[^\n]*exec\(/i
462
- // python one-liner fetch+exec
536
+ /\bpython\d?\s+-c\b[^\n]*(urllib|requests|httpx|socket|os\.system|subprocess|exec\(|eval\()/i,
537
+ // python -c fetch/exec
538
+ /\b(perl|ruby|node|php)\s+-(e|r)\b[^\n]*(http|socket|system|exec|eval|fsockopen|downloadstring)/i,
539
+ // perl/ruby/node/php -e RCE
540
+ /\b(base64\s+(-d|--decode)|xxd\s+-r|openssl\s+enc\s+-d)\b[^\n|]*\|\s*(sudo\s+)?(sh|bash|zsh|python\d?|node|perl|ruby)\b/i,
541
+ // decode|sh
542
+ new RegExp(`\\b${FETCH}\\b[^\\n]*\\s-[a-z]*[oO]\\b[^\\n]*&&[^\\n]*(chmod\\s+\\+x|\\./|\\bsh\\b|\\bbash\\b)`, "i")
543
+ // download then exec
463
544
  ];
464
545
  var REVERSE_SHELL = [
465
- /\bnc\b\s+(-[a-z]*e|.*-e\s+\/bin\/(sh|bash))/i,
466
- // nc -e /bin/sh
546
+ /\b(nc|ncat|netcat)\b[^\n]*\s-[a-z]*e\b/i,
547
+ // nc/ncat -e
467
548
  /\b(ncat|socat)\b[^\n]*exec[: ]/i,
468
549
  // socat … exec:
469
- /\b(bash|sh)\s+-i\b[^\n]*>&?\s*\/dev\/tcp\//i,
550
+ /\bsocat\b[^\n]*tcp[:-][^\n]*exec/i,
551
+ // socat tcp … exec
552
+ /\b(bash|sh|zsh)\s+-i\b[^\n]*>&?\s*\/dev\/tcp\//i,
470
553
  // bash -i >& /dev/tcp/…
471
- /\/dev\/(tcp|udp)\/\d{1,3}(\.\d{1,3}){3}\//,
472
- // /dev/tcp/<ip>/
473
- /\bpython\d?\b[^\n]*socket\.socket[^\n]*subprocess|pty\.spawn/i
554
+ /\/dev\/(tcp|udp)\/[\w.\-]+\/\d+/i,
555
+ // /dev/tcp/<host-or-ip>/<port>
556
+ /\bpython\d?\b[^\n]*socket\.socket[^\n]*(subprocess|pty\.spawn|exec)/i,
474
557
  // python reverse shell
558
+ /\b(perl|php|ruby)\b[^\n]*(fsockopen|socket)[^\n]*(exec|system|\/bin\/(sh|bash))/i,
559
+ // perl/php/ruby reverse shell
560
+ /\bmkfifo\b[^\n]*(\bnc\b|\bncat\b)/i
561
+ // mkfifo backpipe
475
562
  ];
476
- var SECRET_PATHS = /(\.ssh\/id_|\.ssh\/.*_rsa|\.aws\/credentials|\.oriro\/credentials|\.config\/gcloud|\.env(\.|\b)|\.netrc|id_ed25519|\.kube\/config|wallet\.dat|\.gnupg\/)/i;
477
- var NET_SINK = /\b(curl|wget|nc|ncat|socat|scp|rsync|ftp|tftp|invoke-webrequest|invoke-restmethod)\b/i;
563
+ var SECRET_PATHS = /(\.ssh(?![-.\w])|authorized_keys|id_rsa|id_ed25519|id_ecdsa|\.aws(?![-.\w])|\.oriro[\\/]credentials|\.config[\\/]gcloud|\.env(\.|\b)|\.netrc|\.npmrc|\.pypirc|\.docker(?![-.\w])|\.git-credentials|\.kube(?![-.\w])|wallet\.dat|\.gnupg(?![-.\w])|cookies(\.sqlite)?|login\s*data)/i;
564
+ var NET_SINK = /\b(curl|wget|nc|ncat|netcat|socat|scp|rsync|ftp|tftp|invoke-webrequest|invoke-restmethod)\b/i;
478
565
  var ENV_EXFIL = [
479
566
  /\$\(\s*(printenv|env)\b/i,
567
+ // $(printenv X) / $(env)
568
+ /\bprintenv\b/i,
569
+ // printenv … (paired with a net sink below = exfil)
570
+ /\benv\s*\|/i,
571
+ // env | …
480
572
  /\$\(\s*cat\b[^)]*(\.ssh|\.aws|\.env|\.netrc|credential|secret|token|id_rsa|id_ed25519)/i
573
+ // $(cat <secret>)
481
574
  ];
575
+ var ENV_VAR_IN_BODY = /\s--?(d|data|data-binary|data-raw|form|post|body|upload-file)\b[^\n]*\$\{?\w*(secret|token|api[_-]?key|password|passwd|credential|aws_)\w*\}?/i;
482
576
  var PERSISTENCE = [
483
577
  /\bcrontab\b\s+(-|\S+)/i,
484
- // crontab install
485
578
  />>?\s*~?\/?\.(bashrc|zshrc|bash_profile|profile|zprofile)\b/i,
486
- // append to shell rc
579
+ />>?[^\n]*\.ssh[\\/]authorized_keys/i,
580
+ // implant an SSH key (backdoor)
487
581
  /\b(launchctl\s+load|systemctl\s+enable|sc\s+create|new-service)\b/i,
488
- // service install
489
582
  /\bregistry::|reg\s+add\b.*\\run\b/i,
490
- // windows Run key persistence
491
- /[\\\/]start menu[\\\/]programs[\\\/]startup[\\\/]/i,
492
- // windows startup folder
583
+ /[\\/]start menu[\\/]programs[\\/]startup[\\/]/i,
493
584
  /\bschtasks\b\s+\/create/i
494
- // scheduled task
495
585
  ];
496
586
  var GUARDIAN_TAMPER = [
497
587
  /\boriro\b.*\bguardian\b.*\b(disable|off|stop|uninstall)\b/i,
498
- // disable Guardian via command
499
- /[\\\/]\.oriro[\\\/]guardian/i
500
- // direct write to Guardian's own config/state
588
+ /[\\/]\.oriro[\\/]guardian/i,
589
+ // direct path to Guardian's config/state
590
+ /\bguardian\.json\b/i
591
+ // any write referencing guardian.json (cd … && > guardian.json)
501
592
  ];
502
593
  var TAMPER = [
503
594
  /\bchmod\s+-?\s*0?777\b/i,
504
- // world-writable
505
595
  /\b(ufw|firewall-cmd|iptables)\b.*\b(disable|stop|flush|-f)\b/i,
506
- // firewall down
507
596
  /\bset-mppreference\b.*-disable/i,
508
- // disable Defender
509
597
  /\bhistory\s+-c\b|\bunset\s+histfile\b|>\s*~?\/?\.bash_history/i
510
- // wipe history
511
598
  ];
512
599
  var MALWARE = [
513
600
  /\b(xmrig|minerd|cgminer|cpuminer|stratum\+tcp)\b/i,
514
601
  /\b(nanopool|minexmr|supportxmr|pool\.minexmr)\b/i
515
602
  ];
516
- function anyMatch(patterns, text) {
517
- return patterns.some((re) => re.test(text));
518
- }
519
603
  var DEFAULT_RULES = [
520
604
  {
521
605
  id: "fs-destruction",
522
- description: "Block recursive deletes of root/home, disk reformats, fork bombs, host shutdown.",
606
+ description: "Block recursive deletes of root/home/system paths, disk reformats, fork bombs, host shutdown.",
523
607
  match: (c) => {
524
- const cmd = norm(cmdOf(c));
525
- return isDangerousRm(cmd) || anyMatch(FS_DESTRUCTION, cmd) ? block("fs-destruction", "Destructive filesystem/system operation") : null;
608
+ const raw = cmdOf(c);
609
+ const cmd = norm(raw);
610
+ for (const stmt of statements(cmd)) {
611
+ const v = rmVerdict(stmt);
612
+ if (v) return v;
613
+ if (HOST_DISRUPT.has(commandWord(stmt))) return block("fs-destruction", "Host shutdown/reboot");
614
+ }
615
+ return anyMatch(FS_DESTRUCTION, cmd) ? block("fs-destruction", "Destructive filesystem/system operation") : null;
526
616
  }
527
617
  },
528
618
  {
529
619
  id: "remote-code-exec",
530
- description: "Block pull-and-run of remote code (curl|sh, iex(downloadString), bash <(curl)).",
620
+ description: "Block pull-and-run of remote code (curl|sh, $(curl), sh <(curl), bash -c $(curl), decode|sh).",
531
621
  match: (c) => anyMatch(REMOTE_EXEC, norm(cmdOf(c))) ? block("remote-code-exec", "Downloading and executing remote code") : null
532
622
  },
533
623
  {
534
624
  id: "reverse-shell",
535
- description: "Block reverse shells / remote backdoors (nc -e, /dev/tcp, socat exec).",
625
+ description: "Block reverse shells / remote backdoors (nc -e, /dev/tcp/<host>, socat exec, mkfifo backpipe).",
536
626
  match: (c) => anyMatch(REVERSE_SHELL, norm(cmdOf(c))) ? block("reverse-shell", "Opening a reverse shell / remote backdoor") : null
537
627
  },
538
628
  {
539
629
  id: "secret-exfiltration",
540
- description: "Block reading a credential/key file and piping it off the machine.",
630
+ description: "Block reading a credential/key file or env secret and sending it off the machine.",
541
631
  match: (c) => {
542
632
  const cmd = norm(cmdOf(c));
543
- if (cmd && SECRET_PATHS.test(cmd) && NET_SINK.test(cmd)) {
544
- return block("secret-exfiltration", "Reading secrets and sending them off the machine");
545
- }
633
+ if (!cmd || !NET_SINK.test(cmd)) return null;
634
+ if (SECRET_PATHS.test(cmd)) return block("secret-exfiltration", "Reading secrets and sending them off the machine");
635
+ if (anyMatch(ENV_EXFIL, cmd) || ENV_VAR_IN_BODY.test(cmd)) return block("secret-exfiltration", "Sending environment variables / secrets off the machine");
546
636
  return null;
547
637
  }
548
638
  },
549
639
  {
550
- id: "env-exfiltration",
551
- description: "Block dumping env vars / secret files into a network request (curl \u2026$(printenv SECRET)).",
640
+ id: "persistence",
641
+ description: "Block SSH-key implants; flag cron/rc/startup/service edits used for Trojan persistence.",
552
642
  match: (c) => {
553
643
  const cmd = norm(cmdOf(c));
554
- return cmd && NET_SINK.test(cmd) && anyMatch(ENV_EXFIL, cmd) ? block("env-exfiltration", "Sending environment variables / secret files off the machine") : null;
644
+ if (/>>?[^\n]*\.ssh[\\/]authorized_keys/i.test(cmd)) return block("persistence", "Implanting an SSH key (backdoor)");
645
+ return anyMatch(PERSISTENCE, cmd) ? ask2("persistence", "Installing a persistent foothold (cron/startup/service)") : null;
555
646
  }
556
647
  },
557
- {
558
- id: "persistence",
559
- description: "Flag cron/rc/startup/service edits used for Trojan persistence.",
560
- match: (c) => anyMatch(PERSISTENCE, norm(cmdOf(c))) ? ask("persistence", "Installing a persistent foothold (cron/startup/service)") : null
561
- },
562
648
  {
563
649
  id: "guardian-self-defense",
564
650
  description: "Block any attempt to disable, uninstall, or rewrite Guardian's own config/state.",
565
651
  match: (c) => {
566
- if (anyMatch(GUARDIAN_TAMPER, norm(cmdOf(c)))) {
567
- return block("guardian-self-defense", "Attempt to disable or tamper with Guardian itself");
568
- }
569
- if (c.paths?.some((p) => /[\\/]\.oriro[\\/]guardian/i.test(p))) {
570
- return block("guardian-self-defense", "Direct write to Guardian's own config/state");
571
- }
652
+ if (anyMatch(GUARDIAN_TAMPER, norm(cmdOf(c)))) return block("guardian-self-defense", "Attempt to disable or tamper with Guardian itself");
653
+ if (c.paths?.some((p) => /[\\/]\.oriro[\\/]guardian/i.test(p))) return block("guardian-self-defense", "Direct write to Guardian's own config/state");
572
654
  return null;
573
655
  }
574
656
  },
575
657
  {
576
658
  id: "security-tamper",
577
659
  description: "Flag disabling firewall/Defender or wiping history.",
578
- match: (c) => anyMatch(TAMPER, norm(cmdOf(c))) ? ask("security-tamper", "Disabling security controls or covering tracks") : null
660
+ match: (c) => anyMatch(TAMPER, norm(cmdOf(c))) ? ask2("security-tamper", "Disabling security controls or covering tracks") : null
579
661
  },
580
662
  {
581
663
  id: "malware-signature",
@@ -584,9 +666,10 @@ var DEFAULT_RULES = [
584
666
  },
585
667
  {
586
668
  id: "v3lite",
587
- description: "Guardian V3 Lite: prompt-injection + IOC catalog (exfil/dropper/obfuscated-loader/RCE-pipe) + hidden-unicode scan on the tool call.",
669
+ description: "Guardian V3 Lite on the tool call: IOC catalog + hidden-unicode (exec); + injection scan for untrusted MCP params.",
588
670
  match: (c) => {
589
- const r = scanToolCall(c.toolName, c.command ?? "", c.params);
671
+ const r = c.kind === "mcp" ? scanToolCall(c.toolName, c.command ?? "", c.params) : scanExecCommand(`${c.toolName}
672
+ ${c.command ?? ""}`);
590
673
  return r.safe ? null : block("v3lite", `Guardian V3 Lite flagged ${r.threat}`);
591
674
  }
592
675
  },
@@ -596,10 +679,9 @@ var DEFAULT_RULES = [
596
679
  match: (c) => {
597
680
  if (c.kind !== "fs" || !c.paths?.length) return null;
598
681
  const hit = c.paths.find(
599
- (p) => SECRET_PATHS.test(p) || /[\\\/]\.ssh[\\\/]/i.test(p) || // any write into ~/.ssh (e.g. authorized_keys = backdoor)
600
- /[\\\/](etc|boot|sys|windows[\\\/]system32)[\\\/]/i.test(p)
682
+ (p) => SECRET_PATHS.test(p) || /[\\/]\.ssh[\\/]/i.test(p) || /[\\/](etc|boot|sys|windows[\\/]system32)[\\/]/i.test(p)
601
683
  );
602
- return hit ? ask("sensitive-path-write", `Writing to a sensitive location: ${hit}`) : null;
684
+ return hit ? ask2("sensitive-path-write", `Writing to a sensitive location: ${hit}`) : null;
603
685
  }
604
686
  }
605
687
  ];
@@ -1046,7 +1128,7 @@ async function speak(text, opts = {}) {
1046
1128
  }
1047
1129
 
1048
1130
  // src/avatar/onboarding.ts
1049
- import { stdin as stdin2, stdout as stdout2 } from "process";
1131
+ import { stdin as stdin2, stdout as stdout3 } from "process";
1050
1132
  import { createInterface as createInterface2 } from "readline/promises";
1051
1133
 
1052
1134
  // src/avatar/system-voice.ts
@@ -1120,7 +1202,7 @@ var C3 = {
1120
1202
  reset: "\x1B[0m"
1121
1203
  };
1122
1204
  async function previewAvatar(avatar) {
1123
- stdout2.write(
1205
+ stdout3.write(
1124
1206
  `
1125
1207
  ${C3.teal}\u25EF${C3.reset} ${C3.bold}${avatar.slug}${C3.reset} is now your terminal face. ${C3.dim}Change anytime with ${C3.reset}${C3.teal}oriro avatar${C3.reset}
1126
1208
  `
@@ -1131,19 +1213,19 @@ async function previewAvatar(avatar) {
1131
1213
  png = readCachedAvatar(avatar.slug);
1132
1214
  } catch {
1133
1215
  }
1134
- stdout2.write("\n" + renderAvatar(avatar, png) + "\n");
1216
+ stdout3.write("\n" + renderAvatar(avatar, png) + "\n");
1135
1217
  setupSystemVoice();
1136
1218
  const spoke = await speak(`Hi, I'm ${avatar.slug}, your ORIRO terminal face. I'll speak your replies.`, {
1137
1219
  voiceId: avatar.slug,
1138
1220
  lang: "en-US"
1139
1221
  });
1140
- if (spoke) stdout2.write(` ${C3.dim}(spoken aloud in your terminal's voice)${C3.reset}
1222
+ if (spoke) stdout3.write(` ${C3.dim}(spoken aloud in your terminal's voice)${C3.reset}
1141
1223
  `);
1142
1224
  }
1143
1225
  async function selectAvatarInteractive() {
1144
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1226
+ const rl = createInterface2({ input: stdin2, output: stdout3 });
1145
1227
  try {
1146
- stdout2.write(
1228
+ stdout3.write(
1147
1229
  `
1148
1230
  ${C3.teal}\u25EF${C3.reset} ${C3.bold}Choose your ORIRO avatar${C3.reset} ${C3.dim}\u2014 ${AVATAR_COUNT} faces, it floats in your terminal and speaks.${C3.reset}
1149
1231
 
@@ -1151,36 +1233,46 @@ async function selectAvatarInteractive() {
1151
1233
  );
1152
1234
  const cats = avatarCategories();
1153
1235
  cats.forEach(
1154
- (cat2, i) => stdout2.write(
1236
+ (cat2, i) => stdout3.write(
1155
1237
  ` ${C3.teal}${String(i + 1).padStart(2)}${C3.reset} ${cat2} ${C3.dim}(${avatarsInCategory(cat2).length})${C3.reset}
1156
1238
  `
1157
1239
  )
1158
1240
  );
1159
- const cn = Number(
1160
- (await rl.question(`
1161
- ${C3.teal}\u203A${C3.reset} Pick a category number: `)).trim()
1162
- );
1163
- const cat = cats[cn - 1];
1164
- if (!cat) {
1165
- stdout2.write(" No category chosen.\n");
1166
- return null;
1241
+ let cat;
1242
+ for (; ; ) {
1243
+ const ans = (await ask(rl, `
1244
+ ${C3.teal}\u203A${C3.reset} Pick a category number ${C3.dim}(or Enter to skip)${C3.reset}: `)).trim();
1245
+ if (!ans) {
1246
+ stdout3.write(` ${C3.dim}Skipped \u2014 no avatar.${C3.reset}
1247
+ `);
1248
+ return null;
1249
+ }
1250
+ const n = Number(ans);
1251
+ cat = Number.isInteger(n) ? cats[n - 1] : void 0;
1252
+ if (cat) break;
1253
+ stdout3.write(` ${C3.dim}Please enter a number from the list.${C3.reset}
1254
+ `);
1167
1255
  }
1168
1256
  const list = avatarsInCategory(cat);
1169
- stdout2.write("\n");
1257
+ stdout3.write("\n");
1170
1258
  list.forEach(
1171
- (a, i) => stdout2.write(` ${C3.teal}${String(i + 1).padStart(2)}${C3.reset} ${a.slug}
1259
+ (a, i) => stdout3.write(` ${C3.teal}${String(i + 1).padStart(2)}${C3.reset} ${a.slug}
1172
1260
  `)
1173
1261
  );
1174
- const an = Number(
1175
- (await rl.question(`
1176
- ${C3.teal}\u203A${C3.reset} Pick an avatar number: `)).trim()
1177
- );
1178
- const chosen = list[an - 1];
1179
- if (!chosen) {
1180
- stdout2.write(" No avatar chosen.\n");
1181
- return null;
1262
+ for (; ; ) {
1263
+ const ans = (await ask(rl, `
1264
+ ${C3.teal}\u203A${C3.reset} Pick an avatar number ${C3.dim}(or Enter to skip)${C3.reset}: `)).trim();
1265
+ if (!ans) {
1266
+ stdout3.write(` ${C3.dim}Skipped \u2014 no avatar.${C3.reset}
1267
+ `);
1268
+ return null;
1269
+ }
1270
+ const n = Number(ans);
1271
+ const chosen = Number.isInteger(n) ? list[n - 1] : void 0;
1272
+ if (chosen) return chosen;
1273
+ stdout3.write(` ${C3.dim}Please enter a number from the list.${C3.reset}
1274
+ `);
1182
1275
  }
1183
- return chosen;
1184
1276
  } finally {
1185
1277
  rl.close();
1186
1278
  }
@@ -1235,1668 +1327,25 @@ function hasScribeChoice() {
1235
1327
  }
1236
1328
  }
1237
1329
 
1238
- // src/onboarding/wrapper.ts
1239
- function isFirstRun() {
1240
- return !isLanguageConfigured();
1241
- }
1242
- async function askYesNo(question) {
1243
- const rl = createInterface3({ input: stdin3, output: stdout3 });
1244
- try {
1245
- const a = (await rl.question(`${question} ${dim("[Y/n]")} `)).trim().toLowerCase();
1246
- return a === "" || a === "y" || a === "yes";
1247
- } finally {
1248
- rl.close();
1249
- }
1250
- }
1251
- async function runOnboarding() {
1252
- stdout3.write(banner());
1253
- await runLanguageOnboarding();
1254
- await activateGuardian();
1255
- stdout3.write(` ${accent("\u{1F6E1} Guardian V3")} is on by default. ${accent("\u{1F9ED} Head")} is ready.
1256
-
1257
- `);
1258
- if (!isAvatarConfigured()) await runAvatarOnboarding();
1259
- if (!hasScribeChoice()) {
1260
- const yes = await askYesNo(
1261
- "Remember with me? The Scriber keeps your work in context on THIS machine only \u2014 it never leaves it."
1262
- );
1263
- setScribeConsent(yes);
1264
- stdout3.write(yes ? ` ${accent("\u{1F4D3} Scriber")} on.
1265
- ` : ` ${dim("Scriber off \u2014 `oriro scribe on` anytime.")}
1266
- `);
1267
- }
1268
- stdout3.write(`
1269
- ${accent("ORIRO is ready.")} ${dim("Type to chat \xB7 /exit to leave")}
1270
-
1271
- `);
1272
- }
1273
-
1274
- // src/onboarding/assemble.ts
1275
- import {
1276
- createAgentSession as createAgentSession2,
1277
- AuthStorage as AuthStorage2,
1278
- ModelRegistry as ModelRegistry2,
1279
- SessionManager as SessionManager2,
1280
- SettingsManager,
1281
- DefaultResourceLoader,
1282
- getAgentDir
1283
- } from "@earendil-works/pi-coding-agent";
1284
-
1285
- // src/routers/mux-provider.ts
1286
- import { streamSimple as piStreamSimple, createAssistantMessageEventStream } from "@earendil-works/pi-ai";
1287
- import { register as registerOpenAICompletions } from "@earendil-works/pi-ai/openai-completions";
1288
-
1289
- // src/routers/mux.ts
1290
- import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
1291
- import { join as join10 } from "path";
1292
- var COOLDOWN_DEFAULT_MS = 6e4;
1293
- var UNHEALTHY_AFTER = 3;
1294
- var RouterMux = class {
1295
- stats = /* @__PURE__ */ new Map();
1296
- now;
1297
- constructor(routerIds, now = () => Date.now()) {
1298
- this.now = now;
1299
- for (const id of routerIds) {
1300
- this.stats.set(id, {
1301
- id,
1302
- latencyMs: Number.POSITIVE_INFINITY,
1303
- healthy: true,
1304
- cooldownUntil: 0,
1305
- consecutiveErrors: 0
1306
- });
1307
- }
1308
- }
1309
- /** Available routers, best-first (healthy, not cooling down, lowest latency). */
1310
- ranked() {
1311
- const t = this.now();
1312
- return [...this.stats.values()].filter((s) => s.healthy && s.cooldownUntil <= t).sort((a, b) => a.latencyMs - b.latencyMs).map((s) => s.id);
1313
- }
1314
- recordSuccess(id, latencyMs) {
1315
- const s = this.stats.get(id);
1316
- if (!s) return;
1317
- s.latencyMs = s.latencyMs === Number.POSITIVE_INFINITY ? latencyMs : 0.7 * s.latencyMs + 0.3 * latencyMs;
1318
- s.consecutiveErrors = 0;
1319
- s.healthy = true;
1320
- }
1321
- recordFailure(id, err) {
1322
- const s = this.stats.get(id);
1323
- if (!s) return;
1324
- s.consecutiveErrors += 1;
1325
- if (err?.status === 429) {
1326
- s.cooldownUntil = this.now() + (err.retryAfterMs ?? COOLDOWN_DEFAULT_MS);
1327
- }
1328
- if (s.consecutiveErrors >= UNHEALTHY_AFTER) s.healthy = false;
1329
- }
1330
- /** Run a call through the best router, failing over on error. Throws only if all exhausted. */
1331
- async run(call) {
1332
- const order = this.ranked();
1333
- if (order.length === 0) {
1334
- throw new Error(
1335
- "All selected routers are rate-limited or unavailable. Add a BYOK key, select more free routers, or retry shortly."
1336
- );
1337
- }
1338
- let lastErr;
1339
- for (const id of order) {
1340
- const t0 = this.now();
1341
- try {
1342
- const result = await call(id);
1343
- this.recordSuccess(id, this.now() - t0);
1344
- return { result, routerId: id };
1345
- } catch (e) {
1346
- const err = e;
1347
- this.recordFailure(id, { status: err?.status, retryAfterMs: err?.retryAfterMs });
1348
- lastErr = e;
1349
- }
1350
- }
1351
- throw lastErr instanceof Error ? lastErr : new Error("All selected routers failed this request.");
1352
- }
1353
- snapshot() {
1354
- return [...this.stats.values()].map((s) => ({ ...s }));
1355
- }
1356
- load(stats) {
1357
- for (const s of stats) if (this.stats.has(s.id)) this.stats.set(s.id, { ...s });
1358
- }
1359
- };
1330
+ // src/routers/onboarding.ts
1331
+ import { createInterface as createInterface3 } from "readline/promises";
1332
+ import { stdin as stdin3, stdout as stdout4 } from "process";
1333
+ import { existsSync as existsSync4, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
1334
+ import { join as join12 } from "path";
1360
1335
 
1361
- // src/routers/floor.ts
1362
- var KEYLESS_FLOOR = [
1363
- {
1336
+ // src/routers/catalog.ts
1337
+ var C4 = (e) => ({
1338
+ api: "openai-completions",
1339
+ freeModels: [],
1340
+ tier: "free",
1341
+ kind: "chat",
1342
+ ...e
1343
+ });
1344
+ var ROUTER_CATALOG = [
1345
+ // ── Keyless & live-verified (works now, zero keys, through the agent) ──
1346
+ C4({
1364
1347
  id: "pollinations",
1365
- name: "Pollinations (free)",
1366
- baseUrl: "https://text.pollinations.ai/openai",
1367
- model: "openai",
1368
- apiKey: "oriro-keyless"
1369
- },
1370
- {
1371
- id: "ollama-local",
1372
- name: "Ollama (on-device)",
1373
- baseUrl: "http://localhost:11434/v1",
1374
- model: "llama3.2",
1375
- apiKey: "ollama"
1376
- }
1377
- ];
1378
- function routerModel(r) {
1379
- return {
1380
- id: r.model,
1381
- name: r.name,
1382
- api: "openai-completions",
1383
- provider: r.id,
1384
- baseUrl: r.baseUrl,
1385
- reasoning: false,
1386
- input: ["text"],
1387
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1388
- contextWindow: 128e3,
1389
- maxTokens: 4096
1390
- };
1391
- }
1392
-
1393
- // src/routers/router-pool.ts
1394
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
1395
- import { join as join12 } from "path";
1396
-
1397
- // src/routers/pool.ts
1398
- import { existsSync as existsSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
1399
- import { join as join11 } from "path";
1400
- function poolFile(dir) {
1401
- return join11(dir, "routers", "selected.json");
1402
- }
1403
- function loadPool(dir) {
1404
- const p = poolFile(dir);
1405
- if (!existsSync4(p)) return [];
1406
- try {
1407
- const v = JSON.parse(readFileSync9(p, "utf8"));
1408
- return Array.isArray(v) ? v : [];
1409
- } catch {
1410
- return [];
1411
- }
1412
- }
1413
- function savePool(dir, ids) {
1414
- mkdirSync6(join11(dir, "routers"), { recursive: true });
1415
- writeFileSync8(poolFile(dir), JSON.stringify([...new Set(ids)], null, 2), "utf8");
1416
- }
1417
-
1418
- // src/routers/validate.ts
1419
- var PROBE_TIMEOUT_MS = 12e3;
1420
- async function validateRouter(entry, key, modelId) {
1421
- const model = modelId ?? entry.freeModels[0] ?? "";
1422
- const t0 = Date.now();
1423
- const controller = new AbortController();
1424
- const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
1425
- try {
1426
- let res;
1427
- if (entry.api === "google-generative-ai") {
1428
- const url = `${entry.baseUrl.replace(/\/$/, "")}/models/${model}:generateContent${key ? `?key=${key}` : ""}`;
1429
- res = await fetch(url, {
1430
- method: "POST",
1431
- headers: { "content-type": "application/json" },
1432
- body: JSON.stringify({ contents: [{ parts: [{ text: "ping" }] }] }),
1433
- signal: controller.signal
1434
- });
1435
- } else {
1436
- const headers = { "content-type": "application/json" };
1437
- if (key) headers.authorization = `Bearer ${key}`;
1438
- res = await fetch(`${entry.baseUrl.replace(/\/$/, "")}/chat/completions`, {
1439
- method: "POST",
1440
- headers,
1441
- body: JSON.stringify({
1442
- model,
1443
- messages: [{ role: "user", content: "ping" }],
1444
- max_tokens: 1
1445
- }),
1446
- signal: controller.signal
1447
- });
1448
- }
1449
- return {
1450
- ok: res.ok,
1451
- latencyMs: Date.now() - t0,
1452
- model,
1453
- error: res.ok ? void 0 : `HTTP ${res.status}`
1454
- };
1455
- } catch (e) {
1456
- return {
1457
- ok: false,
1458
- latencyMs: Date.now() - t0,
1459
- model,
1460
- error: e instanceof Error ? e.message : String(e)
1461
- };
1462
- } finally {
1463
- clearTimeout(timer);
1464
- }
1465
- }
1466
-
1467
- // src/routers/router-pool.ts
1468
- var KEYLESS_SENTINEL = "oriro-keyless-no-key-required";
1469
- function regFile() {
1470
- return join12(oriroDir(), "routers", "registered.json");
1471
- }
1472
- function readReg() {
1473
- try {
1474
- return JSON.parse(readFileSync10(regFile(), "utf8"));
1475
- } catch {
1476
- return {};
1477
- }
1478
- }
1479
- function writeReg(m) {
1480
- mkdirSync7(join12(oriroDir(), "routers"), { recursive: true });
1481
- writeFileSync9(regFile(), JSON.stringify(m, null, 2), "utf8");
1482
- }
1483
- async function addRouter(entry, opts) {
1484
- if (entry.comingSoon) {
1485
- return { ok: false, validation: { ok: false, latencyMs: 0, model: "", error: "coming soon" } };
1486
- }
1487
- const key = opts?.key ?? (entry.keyless ? KEYLESS_SENTINEL : void 0);
1488
- const v = await validateRouter(entry, key, opts?.modelId);
1489
- if (!v.ok) return { ok: false, validation: v };
1490
- const router = {
1491
- id: entry.id,
1492
- name: entry.displayName,
1493
- baseUrl: entry.baseUrl,
1494
- model: opts?.modelId ?? v.model ?? entry.freeModels[0] ?? "",
1495
- apiKey: key ?? KEYLESS_SENTINEL
1496
- };
1497
- const reg = readReg();
1498
- reg[entry.id] = router;
1499
- writeReg(reg);
1500
- savePool(oriroDir(), [...loadPool(oriroDir()), entry.id]);
1501
- return { ok: true, validation: v };
1502
- }
1503
- function useRouters(ids) {
1504
- savePool(oriroDir(), ids);
1505
- }
1506
- function resolvePool() {
1507
- const reg = readReg();
1508
- return loadPool(oriroDir()).map((id) => reg[id]).filter((r) => Boolean(r));
1509
- }
1510
-
1511
- // src/identity/filter.ts
1512
- var ORIRO_IDENTITY = "You are ORIRO, a free on-device AI assistant in the user's terminal. You are ORIRO and only ORIRO. Never state, imply, or reveal that you are, or are built on, any other model or company (such as GPT, Claude, Gemini, Qwen, Llama, Mistral, DeepSeek, OpenAI, Anthropic, Google, or Meta). If asked what you are, you are ORIRO.";
1513
- var BANNED_TEST = /\b(qwen|llama|mistral|mixtral|deepseek|gpt(?:-?\d(?:\.\d)?)?|claude|gemini|openai|anthropic|google|meta\s?ai|alibaba)\b/i;
1514
- var BANNED_REPLACE = new RegExp(BANNED_TEST.source, "gi");
1515
- var SELF_REF = /\b(i am|i'm|i was|based on|powered by|my name|my model|my architecture|trained|created by|made by|built (?:on|by)|developed by)\b/i;
1516
- var SELF_INTRO = /\b(i am|i'm)\s+(a|an)\b/i;
1517
- var AI_NOUN = /\b(assistant|ai|model|language model|bot|agent|chatbot)\b/i;
1518
- function applyIdentity(context) {
1519
- const sys = context.systemPrompt ? `${ORIRO_IDENTITY}
1520
-
1521
- ${context.systemPrompt}` : ORIRO_IDENTITY;
1522
- return { ...context, systemPrompt: sys };
1523
- }
1524
- function scrubIdentity(text) {
1525
- return text.replace(/[^.?!\n]+[.?!]?/g, (sentence) => {
1526
- let s = SELF_REF.test(sentence) && BANNED_TEST.test(sentence) ? sentence.replace(BANNED_REPLACE, "ORIRO") : sentence;
1527
- if (!/\boriro\b/i.test(s) && SELF_INTRO.test(s) && AI_NOUN.test(s)) {
1528
- s = s.replace(SELF_INTRO, "I am ORIRO, $2");
1529
- }
1530
- return s;
1531
- });
1532
- }
1533
- function scrubMessageIdentity(msg) {
1534
- return {
1535
- ...msg,
1536
- content: msg.content.map(
1537
- (c) => c.type === "text" ? { ...c, text: scrubIdentity(c.text) } : c
1538
- )
1539
- };
1540
- }
1541
-
1542
- // src/routers/tool-sanitize.ts
1543
- var CONTROL_TOKEN = /<\|[^|]*\|>/g;
1544
- var RECIPIENT_PREFIX = /^(?:to=)?(?:functions?|tools?|recipient)[.=]/i;
1545
- var RECIPIENT = /(?:to=)?(?:functions?|tools?|recipient)[.=]([A-Za-z0-9_.:-]+)/i;
1546
- var CLEAN_NAME = /^[A-Za-z0-9_.:-]+$/;
1547
- function sanitizeToolName(raw) {
1548
- if (!raw) return raw;
1549
- if (!raw.includes("<|") && !RECIPIENT_PREFIX.test(raw)) return raw;
1550
- const base = (raw.split("<|")[0] ?? "").replace(RECIPIENT_PREFIX, "").trim();
1551
- if (base && CLEAN_NAME.test(base)) return base;
1552
- const recip = raw.match(RECIPIENT);
1553
- if (recip?.[1]) return recip[1];
1554
- const m = raw.replace(CONTROL_TOKEN, " ").match(/[A-Za-z_][A-Za-z0-9_.:-]*/);
1555
- return m ? m[0] : raw;
1556
- }
1557
- function sanitizeMessageToolCalls(msg) {
1558
- let changed = false;
1559
- const content = msg.content.map((c) => {
1560
- if (c.type === "toolCall") {
1561
- const name = sanitizeToolName(c.name);
1562
- if (name !== c.name) {
1563
- changed = true;
1564
- return { ...c, name };
1565
- }
1566
- }
1567
- return c;
1568
- });
1569
- return changed ? { ...msg, content } : msg;
1570
- }
1571
- function sanitizeEventToolCalls(ev) {
1572
- let next = ev;
1573
- if ("partial" in next && next.partial) {
1574
- const partial = sanitizeMessageToolCalls(next.partial);
1575
- if (partial !== next.partial) next = { ...next, partial };
1576
- }
1577
- if (next.type === "toolcall_end" && next.toolCall) {
1578
- const name = sanitizeToolName(next.toolCall.name);
1579
- if (name !== next.toolCall.name) next = { ...next, toolCall: { ...next.toolCall, name } };
1580
- }
1581
- return next;
1582
- }
1583
-
1584
- // src/routers/mux-provider.ts
1585
- var MUX_PROVIDER = "oriro-mux";
1586
- var MUX_MODEL = "oriro-free";
1587
- function errToCallError(msg) {
1588
- const text = msg.errorMessage ?? "";
1589
- return /\b429\b|rate.?limit|too many requests/i.test(text) ? { status: 429 } : {};
1590
- }
1591
- function buildErrorMessage(message) {
1592
- return {
1593
- role: "assistant",
1594
- content: [],
1595
- api: "openai-completions",
1596
- provider: MUX_PROVIDER,
1597
- model: MUX_MODEL,
1598
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
1599
- stopReason: "error",
1600
- timestamp: Date.now(),
1601
- errorMessage: message
1602
- };
1603
- }
1604
- async function driveMux(out, mux, byId, context, options) {
1605
- let lastError;
1606
- for (const id of mux.ranked()) {
1607
- const router = byId.get(id);
1608
- if (!router) continue;
1609
- const t0 = Date.now();
1610
- let committed = false;
1611
- let lastPartial;
1612
- try {
1613
- const inner = piStreamSimple(routerModel(router), context, {
1614
- ...options ?? {},
1615
- apiKey: router.apiKey
1616
- });
1617
- let failedBeforeContent = false;
1618
- for await (const ev of inner) {
1619
- if (ev.type === "error") {
1620
- mux.recordFailure(id, errToCallError(ev.error));
1621
- if (!committed) {
1622
- lastError = ev.error;
1623
- failedBeforeContent = true;
1624
- break;
1625
- }
1626
- out.push(ev);
1627
- out.end(ev.error);
1628
- return;
1629
- }
1630
- committed = true;
1631
- if (ev.type === "done") {
1632
- mux.recordSuccess(id, Date.now() - t0);
1633
- const clean = sanitizeMessageToolCalls(scrubMessageIdentity(ev.message));
1634
- out.push({ type: "done", reason: ev.reason, message: clean });
1635
- out.end(clean);
1636
- return;
1637
- }
1638
- lastPartial = ev.partial;
1639
- out.push(sanitizeEventToolCalls(ev));
1640
- }
1641
- if (failedBeforeContent) continue;
1642
- mux.recordSuccess(id, Date.now() - t0);
1643
- out.end(lastPartial ? sanitizeMessageToolCalls(scrubMessageIdentity(lastPartial)) : void 0);
1644
- return;
1645
- } catch (e) {
1646
- mux.recordFailure(id, e);
1647
- }
1648
- }
1649
- const msg = lastError ?? buildErrorMessage(
1650
- "All keyless routers are unavailable. Add a BYOK key, select more free routers, or retry shortly."
1651
- );
1652
- out.push({ type: "error", reason: "error", error: msg });
1653
- out.end(msg);
1654
- }
1655
- function registerOriroMux(registry, opts = {}) {
1656
- registerOpenAICompletions();
1657
- const pooled = resolvePool();
1658
- const routers = opts.routers ?? (pooled.length > 0 ? pooled : KEYLESS_FLOOR);
1659
- const byId = new Map(routers.map((r) => [r.id, r]));
1660
- const mux = new RouterMux(routers.map((r) => r.id));
1661
- registry.registerProvider(MUX_PROVIDER, {
1662
- name: "ORIRO Free (keyless Mux)",
1663
- api: "openai-completions",
1664
- apiKey: "oriro-keyless",
1665
- // Placeholder — required by registry validation but never used: our custom streamSimple
1666
- // routes to the real keyless floor endpoints itself (see driveMux).
1667
- baseUrl: "http://oriro-mux.local",
1668
- models: [
1669
- {
1670
- id: MUX_MODEL,
1671
- name: "ORIRO Free (best-router)",
1672
- api: "openai-completions",
1673
- baseUrl: "http://oriro-mux.local",
1674
- reasoning: false,
1675
- input: ["text"],
1676
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1677
- contextWindow: 128e3,
1678
- maxTokens: 4096
1679
- }
1680
- ],
1681
- streamSimple: (_model, context, options) => {
1682
- const out = createAssistantMessageEventStream();
1683
- void driveMux(out, mux, byId, applyIdentity(context), options);
1684
- return out;
1685
- }
1686
- });
1687
- return registry.find(MUX_PROVIDER, MUX_MODEL);
1688
- }
1689
-
1690
- // src/head/pi-tool.ts
1691
- import { Type } from "typebox";
1692
-
1693
- // src/head/comparison-engine.ts
1694
- var SECTION_RULES = [
1695
- {
1696
- type: "hero",
1697
- label: "Hero",
1698
- priority: "CRITICAL",
1699
- markup: [/<h1[\s>]/],
1700
- recommend: "Add a clear above-the-fold hero \u2014 one headline that states the value + one primary CTA."
1701
- },
1702
- {
1703
- type: "navigation",
1704
- label: "Navigation",
1705
- priority: "CRITICAL",
1706
- markup: [/<nav[\s>]/, /role=["']navigation["']/],
1707
- recommend: "Add a top navigation so visitors can reach key sections."
1708
- },
1709
- {
1710
- type: "features",
1711
- label: "Features",
1712
- priority: "CRITICAL",
1713
- text: [/\bfeatures?\b/, /\bwhat you (?:can|get)\b/, /\bcapabilit/],
1714
- recommend: "Add a features section that spells out concrete capabilities, not adjectives."
1715
- },
1716
- {
1717
- type: "pricing",
1718
- label: "Pricing",
1719
- priority: "CRITICAL",
1720
- text: [/\bpricing\b/, /\bper month\b/, /\b\/mo\b/, /\bfree plan\b/, /\$\d/, /₹\d/, /€\d/],
1721
- recommend: 'Add transparent pricing \u2014 a critical conversion element; even a single "Free" tier helps.'
1722
- },
1723
- {
1724
- type: "cta",
1725
- label: "Call-to-Action",
1726
- priority: "CRITICAL",
1727
- text: [/\bget started\b/, /\bsign up\b/, /\bstart (?:free|now|building)\b/, /\btry (?:it|now|free)\b/, /\bbook a demo\b/, /\bget a demo\b/],
1728
- recommend: 'Add a strong, repeated primary CTA ("Get started") so the next step is obvious.'
1729
- },
1730
- {
1731
- type: "testimonials",
1732
- label: "Testimonials",
1733
- priority: "HIGH",
1734
- text: [/\btestimonial/, /\bwhat (?:our )?(?:customers|users) say\b/, /\bloved by\b/, /\breview(?:s|ed)\b/],
1735
- recommend: "Add 2\u20133 customer testimonials with names/photos to build trust."
1736
- },
1737
- {
1738
- type: "stats",
1739
- label: "Stats / Metrics",
1740
- priority: "HIGH",
1741
- text: [/\b\d[\d,.]*\s*[kkmm]\+?\s*(?:users|customers|developers|downloads|teams)\b/, /\b9\d(?:\.\d+)?%\b/, /\buptime\b/],
1742
- recommend: 'Add impressive metrics ("10K+ users", "99.9% uptime") as social proof.'
1743
- },
1744
- {
1745
- type: "video",
1746
- label: "Video",
1747
- priority: "HIGH",
1748
- markup: [/<video[\s>]/, /youtube\.com\/embed/, /player\.vimeo\.com/, /<iframe[^>]+(?:youtube|vimeo)/],
1749
- text: [/\bwatch the (?:video|demo)\b/],
1750
- recommend: "Add a short explainer/demo video \u2014 it lifts conversion on landing pages."
1751
- },
1752
- {
1753
- type: "demo",
1754
- label: "Live Demo",
1755
- priority: "HIGH",
1756
- text: [/\btry it (?:now|live|free)\b/, /\bplayground\b/, /\binteractive demo\b/, /\blive demo\b/],
1757
- recommend: 'Add a "try it" live demo or playground so visitors experience the product immediately.'
1758
- },
1759
- {
1760
- type: "socialProof",
1761
- label: "Social Proof",
1762
- priority: "HIGH",
1763
- text: [/\btrusted by\b/, /\bbacked by\b/, /\bused by\b/, /\bas seen (?:in|on)\b/, /\bcustomers include\b/],
1764
- recommend: 'Add social proof (customer/investor logos, "trusted by \u2026") near the hero.'
1765
- },
1766
- {
1767
- type: "faq",
1768
- label: "FAQ",
1769
- priority: "MEDIUM",
1770
- text: [/\bfaq\b/, /\bfrequently asked\b/],
1771
- markup: [/<details[\s>]/],
1772
- recommend: "Add an FAQ that answers the top objections before they become exits."
1773
- },
1774
- {
1775
- type: "integrations",
1776
- label: "Integrations",
1777
- priority: "MEDIUM",
1778
- text: [/\bintegrations?\b/, /\bworks with\b/, /\bconnect your\b/],
1779
- recommend: "Add an integrations section showing what the product connects to."
1780
- },
1781
- {
1782
- type: "newsletter",
1783
- label: "Newsletter / Capture",
1784
- priority: "MEDIUM",
1785
- text: [/\bsubscribe\b/, /\bnewsletter\b/, /\bjoin (?:the )?waitlist\b/],
1786
- markup: [/type=["']email["']/],
1787
- recommend: "Add an email capture (newsletter/waitlist) so non-converting visitors are not lost."
1788
- },
1789
- {
1790
- type: "comparison",
1791
- label: "Comparison",
1792
- priority: "MEDIUM",
1793
- text: [/\bcompare\b/, /\bcomparison\b/, /\b vs\.? \b/, /\bwhy choose\b/],
1794
- recommend: 'Add a comparison ("us vs alternatives") to win evaluators who are shopping around.'
1795
- },
1796
- {
1797
- type: "team",
1798
- label: "Team / About",
1799
- priority: "LOW",
1800
- text: [/\bour team\b/, /\bmeet the team\b/, /\bfounders?\b/, /\babout us\b/],
1801
- recommend: "Add a brief team/about section to humanize the brand."
1802
- }
1803
- ];
1804
- var PRIORITY_RANK = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
1805
- var PRIORITY_EFFORT = { CRITICAL: "L", HIGH: "M", MEDIUM: "M", LOW: "S" };
1806
- var FETCH_TIMEOUT_MS = 12e3;
1807
- var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36 ORIRO-Inspector";
1808
- async function fetchPage(url) {
1809
- const controller = new AbortController();
1810
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1811
- const start = Date.now();
1812
- try {
1813
- const res = await fetch(url, {
1814
- signal: controller.signal,
1815
- redirect: "follow",
1816
- headers: { "user-agent": UA, accept: "text/html,application/xhtml+xml" }
1817
- });
1818
- const html = await res.text();
1819
- return { html, ms: Date.now() - start, status: res.status, ok: res.ok, error: "" };
1820
- } catch (err) {
1821
- return { html: "", ms: Date.now() - start, status: 0, ok: false, error: err instanceof Error ? err.message : "fetch failed" };
1822
- } finally {
1823
- clearTimeout(timer);
1824
- }
1825
- }
1826
- function toText(html) {
1827
- return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/\s+/g, " ").toLowerCase().trim();
1828
- }
1829
- function firstMatch(re, hay) {
1830
- const m = re.exec(hay);
1831
- if (!m) return "";
1832
- const slice = (m[0] ?? "").trim();
1833
- return slice.length > 80 ? `${slice.slice(0, 77)}\u2026` : slice;
1834
- }
1835
- function detectSections(rawHtmlLower, text) {
1836
- const found = [];
1837
- for (const rule of SECTION_RULES) {
1838
- let evidence = "";
1839
- for (const re of rule.markup ?? []) {
1840
- const hit = firstMatch(re, rawHtmlLower);
1841
- if (hit) {
1842
- evidence = hit;
1843
- break;
1844
- }
1845
- }
1846
- if (!evidence) {
1847
- for (const re of rule.text ?? []) {
1848
- const hit = firstMatch(re, text);
1849
- if (hit) {
1850
- evidence = hit;
1851
- break;
1852
- }
1853
- }
1854
- }
1855
- if (evidence) found.push({ type: rule.type, label: rule.label, priority: rule.priority, evidence });
1856
- }
1857
- return found;
1858
- }
1859
- function extractMatches(re, html, max) {
1860
- const out = [];
1861
- for (const m of html.matchAll(re)) {
1862
- const inner = (m[1] ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
1863
- if (inner && !out.includes(inner)) out.push(inner);
1864
- if (out.length >= max) break;
1865
- }
1866
- return out;
1867
- }
1868
- var CTA_WORDS = /\b(get started|sign up|start free|start now|start building|try (?:it|now|free)|book a demo|get a demo|request access|join (?:the )?waitlist|download)\b/i;
1869
- function extractStructure(url, fr) {
1870
- const html = fr.html;
1871
- const lowerHtml = html.toLowerCase();
1872
- const text = toText(html);
1873
- const titleM = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
1874
- const title = (titleM?.[1] ?? "").replace(/\s+/g, " ").trim();
1875
- const descM = /<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["']/i.exec(html) ?? /<meta[^>]+content=["']([^"']*)["'][^>]+name=["']description["']/i.exec(html);
1876
- const description = (descM?.[1] ?? "").replace(/\s+/g, " ").trim();
1877
- const headings = extractMatches(/<h[1-3][^>]*>([\s\S]*?)<\/h[1-3]>/gi, html, 12);
1878
- const ctaAll = extractMatches(/<(?:a|button)[^>]*>([\s\S]*?)<\/(?:a|button)>/gi, html, 80);
1879
- const ctas = [];
1880
- for (const c of ctaAll) {
1881
- if (CTA_WORDS.test(c) && !ctas.includes(c)) ctas.push(c);
1882
- if (ctas.length >= 10) break;
1883
- }
1884
- const forms = (lowerHtml.match(/<form[\s>]/g) ?? []).length;
1885
- const links = (lowerHtml.match(/<a[\s>]/g) ?? []).length;
1886
- const images = (lowerHtml.match(/<img[\s>]/g) ?? []).length;
1887
- const hasVideo = /<video[\s>]/.test(lowerHtml) || /(?:youtube\.com\/embed|player\.vimeo\.com)/.test(lowerHtml);
1888
- const domNodes = (html.match(/<[a-z!\/]/gi) ?? []).length;
1889
- let note = "";
1890
- if (fr.ok && text.length < 400 && domNodes < 60) {
1891
- note = "Sparse HTML \u2014 likely a client-rendered (SPA) page; structure may be under-detected without a JS render.";
1892
- }
1893
- return {
1894
- url,
1895
- title,
1896
- description,
1897
- sections: detectSections(lowerHtml, text),
1898
- headings,
1899
- ctas,
1900
- forms,
1901
- links,
1902
- images,
1903
- hasVideo,
1904
- metrics: { htmlBytes: html.length, domNodes, fetchMs: fr.ms, status: fr.status },
1905
- ok: fr.ok && html.length > 0,
1906
- note: fr.ok ? note : `Could not load: ${fr.error || `HTTP ${fr.status}`}`
1907
- };
1908
- }
1909
- function ruleFor(type) {
1910
- return SECTION_RULES.find((r) => r.type === type) ?? SECTION_RULES[0];
1911
- }
1912
- function analyzeGaps(target, competitors) {
1913
- const targetTypes = new Set(target.sections.map((s) => s.type));
1914
- const compPresence = /* @__PURE__ */ new Map();
1915
- for (const comp of competitors) {
1916
- if (!comp.ok) continue;
1917
- for (const s of comp.sections) {
1918
- const list = compPresence.get(s.type) ?? [];
1919
- if (!list.includes(comp.url)) list.push(comp.url);
1920
- compPresence.set(s.type, list);
1921
- }
1922
- }
1923
- const missing = [];
1924
- const parity = [];
1925
- for (const [type, presentOn] of compPresence) {
1926
- if (targetTypes.has(type)) {
1927
- parity.push(type);
1928
- } else {
1929
- const rule = ruleFor(type);
1930
- missing.push({ section: type, label: rule.label, priority: rule.priority, presentOn, recommendation: rule.recommend });
1931
- }
1932
- }
1933
- missing.sort((a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || b.presentOn.length - a.presentOn.length);
1934
- const advantages = target.sections.filter((s) => !compPresence.has(s.type));
1935
- return { missing, advantages, parity };
1936
- }
1937
- function generateActionItems(missing) {
1938
- return missing.map((g) => ({
1939
- title: `Add a ${g.label} section`,
1940
- priority: g.priority,
1941
- effort: PRIORITY_EFFORT[g.priority],
1942
- rationale: `${g.presentOn.length} of the compared page(s) have it; you don't. ${g.recommendation}`
1943
- }));
1944
- }
1945
- function hostOf(url) {
1946
- try {
1947
- return new URL(url).host.replace(/^www\./, "");
1948
- } catch {
1949
- return url;
1950
- }
1951
- }
1952
- function generateSummary(target, competitors, gaps) {
1953
- const okComps = competitors.filter((c) => c.ok);
1954
- const tName = hostOf(target.url);
1955
- if (!target.ok) return `Could not load ${tName} (${target.note}). Nothing to compare against yet.`;
1956
- if (okComps.length === 0) return `Loaded ${tName} (${target.sections.length} sections) but none of the comparison URLs could be loaded.`;
1957
- const crit = gaps.missing.filter((m) => m.priority === "CRITICAL").map((m) => m.label);
1958
- const high = gaps.missing.filter((m) => m.priority === "HIGH").map((m) => m.label);
1959
- const parts = [];
1960
- parts.push(`${tName} has ${target.sections.length} detectable sections; compared against ${okComps.length} page(s).`);
1961
- if (gaps.missing.length === 0) {
1962
- parts.push("No structural gaps found \u2014 you cover everything they do.");
1963
- } else {
1964
- parts.push(`${gaps.missing.length} gap(s) found.`);
1965
- if (crit.length) parts.push(`Critical: ${crit.join(", ")}.`);
1966
- if (high.length) parts.push(`High: ${high.join(", ")}.`);
1967
- }
1968
- if (gaps.advantages.length) parts.push(`Your edge: ${gaps.advantages.map((a) => a.label).join(", ")}.`);
1969
- return parts.join(" ");
1970
- }
1971
- function normalizeUrl(u) {
1972
- const t = (u || "").trim();
1973
- if (!t) return t;
1974
- return /^https?:\/\//i.test(t) ? t : `https://${t}`;
1975
- }
1976
- async function comparePages(opts) {
1977
- const targetUrl = normalizeUrl(opts.targetUrl);
1978
- const competitorUrls = (opts.competitorUrls ?? []).map(normalizeUrl).filter((u) => u.length > 0).slice(0, 30);
1979
- const [targetFetch, ...compFetches] = await Promise.all([
1980
- fetchPage(targetUrl),
1981
- ...competitorUrls.map((u) => fetchPage(u))
1982
- ]);
1983
- const target = extractStructure(targetUrl, targetFetch ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" });
1984
- const competitors = competitorUrls.map(
1985
- (u, i) => extractStructure(u, compFetches[i] ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" })
1986
- );
1987
- const gaps = analyzeGaps(target, competitors);
1988
- return {
1989
- target,
1990
- competitors,
1991
- missing: gaps.missing,
1992
- advantages: gaps.advantages,
1993
- parity: gaps.parity,
1994
- actionItems: generateActionItems(gaps.missing),
1995
- summary: generateSummary(target, competitors, gaps)
1996
- };
1997
- }
1998
-
1999
- // src/head/pi-tool.ts
2000
- function summarizeForCoder(report) {
2001
- const lines = [report.summary];
2002
- const page = (p) => ` \u2022 ${p.url} \u2014 ${p.ok ? `${p.sections.length} sections: ${p.sections.map((s) => s.type).join(", ")}` : `not readable (${p.note})`}`;
2003
- lines.push("Pages seen:");
2004
- lines.push(page(report.target));
2005
- for (const c of report.competitors) if (c.url !== report.target.url) lines.push(page(c));
2006
- if (report.missing.length) {
2007
- lines.push("Missing on the target (gaps to build):");
2008
- for (const g of report.missing.slice(0, 12)) lines.push(` \u2022 ${g.label} (${g.priority}) \u2014 ${g.recommendation}`);
2009
- }
2010
- if (report.actionItems.length) {
2011
- lines.push("Suggested action items:");
2012
- for (const a of report.actionItems.slice(0, 12)) lines.push(` \u2192 ${a.title} [${a.priority}/${a.effort}] \u2014 ${a.rationale}`);
2013
- }
2014
- return lines.join("\n");
2015
- }
2016
- var InspectSiteParams = Type.Object({
2017
- url: Type.String({ description: "The target website URL to inspect or rebuild from." }),
2018
- competitors: Type.Optional(
2019
- Type.Array(Type.String(), { description: "Optional competitor/reference URLs to compare the target against." })
2020
- )
2021
- });
2022
- function registerHead(pi) {
2023
- pi.registerTool({
2024
- name: "inspect_site",
2025
- label: "ORIRO Head",
2026
- description: "Go out to a live website and SEE it: its sections, CTAs, structure, and any gaps versus competitor URLs. Returns a structured report to build from. Call this whenever the user wants to look at, compare against, or rebuild a website/page.",
2027
- parameters: InspectSiteParams,
2028
- async execute(_toolCallId, params) {
2029
- const target = params.url;
2030
- const competitors = params.competitors?.length ? params.competitors : [target];
2031
- const report = await comparePages({ targetUrl: target, competitorUrls: competitors });
2032
- return { content: [{ type: "text", text: summarizeForCoder(report) }], details: report };
2033
- }
2034
- });
2035
- }
2036
-
2037
- // src/scribe/scribe-pi.ts
2038
- import { existsSync as existsSync9, readFileSync as readFileSync16 } from "fs";
2039
- import { Type as Type2 } from "typebox";
2040
-
2041
- // src/scribe/capture.ts
2042
- import { closeSync as closeSync2, fsyncSync as fsyncSync2, mkdirSync as mkdirSync10, openSync as openSync2, writeSync as writeSync2 } from "fs";
2043
- import { join as join14 } from "path";
2044
-
2045
- // src/scribe/digest.ts
2046
- import { existsSync as existsSync5, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
2047
-
2048
- // src/scribe/paths.ts
2049
- import { join as join13 } from "path";
2050
- function scribeDir() {
2051
- const override = process.env.ORIRO_SCRIBE_DIR?.trim();
2052
- return override && override.length > 0 ? override : join13(CONFIG_DIR, "scribe");
2053
- }
2054
- function journalFile(date) {
2055
- return join13(scribeDir(), `${date}.md`);
2056
- }
2057
- function digestFile() {
2058
- return join13(scribeDir(), "_digest.md");
2059
- }
2060
- function timelineFile() {
2061
- return join13(scribeDir(), "_timeline.md");
2062
- }
2063
- function artifactsDir() {
2064
- return join13(scribeDir(), "artifacts");
2065
- }
2066
-
2067
- // src/scribe/digest.ts
2068
- var DIGEST_CAP = 8192;
2069
- var TIMELINE_DAY_CAP = 400;
2070
- function read(file4) {
2071
- return existsSync5(file4) ? readFileSync11(file4, "utf8") : "";
2072
- }
2073
- function updateDigest(summary, context) {
2074
- mkdirSync8(scribeDir(), { recursive: true });
2075
- const existing = read(digestFile());
2076
- let contextBlock = context?.trim();
2077
- if (!contextBlock) {
2078
- const m = existing.match(/## Context\n([\s\S]*?)\n## /);
2079
- contextBlock = m?.[1]?.trim() ?? "_(not set yet)_";
2080
- }
2081
- const recentMatch = existing.match(/## Recent activity[^\n]*\n([\s\S]*)$/);
2082
- const priorRecent = recentMatch?.[1]?.trim() ?? "";
2083
- let recent = summary.trim() ? `- ${summary.trim()}
2084
- ${priorRecent}` : priorRecent;
2085
- const header2 = `# ORIRO Scribe \u2014 Digest
2086
-
2087
- ## Context
2088
- ${contextBlock}
2089
-
2090
- ## Recent activity (newest first)
2091
- `;
2092
- let out = header2 + recent;
2093
- while (Buffer.byteLength(out, "utf8") > DIGEST_CAP && recent.includes("\n")) {
2094
- recent = recent.slice(0, recent.lastIndexOf("\n")).trimEnd();
2095
- out = header2 + recent;
2096
- }
2097
- writeFileSync10(digestFile(), out, "utf8");
2098
- }
2099
- function updateTimeline(date, topic) {
2100
- mkdirSync8(scribeDir(), { recursive: true });
2101
- const clean = topic.replace(/\s+/g, " ").trim();
2102
- if (!clean) return;
2103
- const lines = read(timelineFile()).split("\n").filter(Boolean);
2104
- const header2 = "# ORIRO Scribe \u2014 Timeline";
2105
- const body = lines.filter((l) => l !== header2);
2106
- const idx = body.findIndex((l) => l.startsWith(`- ${date} \xB7`));
2107
- if (idx === -1) {
2108
- body.push(`- ${date} \xB7 ${clean}`.slice(0, TIMELINE_DAY_CAP + date.length + 6));
2109
- } else {
2110
- let merged = `${body[idx]}; ${clean}`;
2111
- if (merged.length > TIMELINE_DAY_CAP) merged = `${merged.slice(0, TIMELINE_DAY_CAP)}\u2026`;
2112
- body[idx] = merged;
2113
- }
2114
- body.sort();
2115
- writeFileSync10(timelineFile(), `${header2}
2116
- ${body.join("\n")}
2117
- `, "utf8");
2118
- }
2119
-
2120
- // src/scribe/journal.ts
2121
- import {
2122
- closeSync,
2123
- existsSync as existsSync6,
2124
- fsyncSync,
2125
- mkdirSync as mkdirSync9,
2126
- openSync,
2127
- readFileSync as readFileSync12,
2128
- writeSync
2129
- } from "fs";
2130
- function appendJournal(date, content) {
2131
- mkdirSync9(scribeDir(), { recursive: true });
2132
- const fd = openSync(journalFile(date), "a");
2133
- try {
2134
- writeSync(fd, content.endsWith("\n") ? content : `${content}
2135
- `);
2136
- fsyncSync(fd);
2137
- } finally {
2138
- closeSync(fd);
2139
- }
2140
- }
2141
- function readJournal(date) {
2142
- const f = journalFile(date);
2143
- return existsSync6(f) ? readFileSync12(f, "utf8") : "";
2144
- }
2145
-
2146
- // src/scribe/redact.ts
2147
- var RULES = [
2148
- {
2149
- label: "private-key",
2150
- re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
2151
- },
2152
- { label: "anthropic-key", re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
2153
- { label: "openrouter-key", re: /sk-or-v1-[A-Za-z0-9]{20,}/g },
2154
- // Stripe-style keys (sk_live_/pk_live_/rk_test_/…), underscore segments.
2155
- { label: "stripe-key", re: /\b[srp]k_(?:live|test)_[A-Za-z0-9]{16,}/g },
2156
- // Generic sk- secret keys — allow hyphenated segments (sk-live-…, sk-proj-…) so a second
2157
- // hyphen no longer breaks the match (the gap the Scriber spike caught).
2158
- { label: "secret-key-sk", re: /sk[-_][A-Za-z0-9][A-Za-z0-9-]{14,}/g },
2159
- { label: "google-key", re: /AIza[0-9A-Za-z_-]{30,}/g },
2160
- { label: "groq-key", re: /gsk_[A-Za-z0-9]{20,}/g },
2161
- { label: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
2162
- { label: "github-token", re: /gh[posr]_[A-Za-z0-9]{30,}/g },
2163
- { label: "xai-key", re: /xai-[A-Za-z0-9]{20,}/g },
2164
- { label: "aws-key", re: /AKIA[0-9A-Z]{16}/g },
2165
- { label: "jwt", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g },
2166
- { label: "telegram-token", re: /\b\d{8,10}:[A-Za-z0-9_-]{30,}\b/g },
2167
- { label: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g },
2168
- { label: "phone", re: /(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}/g }
2169
- ];
2170
- function marker(label) {
2171
- return `\u27E8REDACTED:${label}\u27E9`;
2172
- }
2173
- function entropy(s) {
2174
- const freq = /* @__PURE__ */ new Map();
2175
- for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
2176
- let h = 0;
2177
- for (const n of freq.values()) {
2178
- const p = n / s.length;
2179
- h -= p * Math.log2(p);
2180
- }
2181
- return h;
2182
- }
2183
- function looksLikeUnknownSecret(token) {
2184
- if (token.length < 40) return false;
2185
- if (token.includes("\u27E8REDACTED:")) return false;
2186
- if (/^[0-9a-f]+$/i.test(token)) return false;
2187
- const classes = (/[a-z]/.test(token) ? 1 : 0) + (/[A-Z]/.test(token) ? 1 : 0) + (/[0-9]/.test(token) ? 1 : 0);
2188
- if (classes < 2) return false;
2189
- return entropy(token) >= 4.2;
2190
- }
2191
- function redact(input) {
2192
- const counts = /* @__PURE__ */ new Map();
2193
- let text = input;
2194
- for (const rule of RULES) {
2195
- text = text.replace(rule.re, () => {
2196
- counts.set(rule.label, (counts.get(rule.label) ?? 0) + 1);
2197
- return marker(rule.label);
2198
- });
2199
- }
2200
- text = text.split(/(\s+)/).map((tok) => {
2201
- if (looksLikeUnknownSecret(tok)) {
2202
- counts.set("high-entropy", (counts.get("high-entropy") ?? 0) + 1);
2203
- return marker("high-entropy");
2204
- }
2205
- return tok;
2206
- }).join("");
2207
- const redactions = [...counts.entries()].map(([label, count]) => ({
2208
- label,
2209
- count
2210
- }));
2211
- return { text, redactions };
2212
- }
2213
- function containsSecret(text) {
2214
- for (const rule of RULES) {
2215
- rule.re.lastIndex = 0;
2216
- if (rule.re.test(text)) return true;
2217
- }
2218
- for (const tok of text.split(/\s+/)) {
2219
- if (looksLikeUnknownSecret(tok)) return true;
2220
- }
2221
- return false;
2222
- }
2223
-
2224
- // src/scribe/capture.ts
2225
- var INLINE_CAP = 4e3;
2226
- function sideFile(date, ts, kind, full) {
2227
- mkdirSync10(artifactsDir(), { recursive: true });
2228
- const name = `${date}_${ts.replace(/[:.]/g, "-")}_${kind}.md`;
2229
- const p = join14(artifactsDir(), name);
2230
- const fd = openSync2(p, "w");
2231
- try {
2232
- writeSync2(fd, full);
2233
- fsyncSync2(fd);
2234
- } finally {
2235
- closeSync2(fd);
2236
- }
2237
- return p;
2238
- }
2239
- function field(date, ts, label, value) {
2240
- if (!value || !value.trim()) return "";
2241
- if (value.length > INLINE_CAP) {
2242
- const ref = sideFile(date, ts, label.toLowerCase().replace(/\s+/g, "-"), value);
2243
- return `**${label}** (full \u2192 ${ref}):
2244
- ${value.slice(0, INLINE_CAP)}
2245
- \u2026(truncated; full content in artifact)
2246
-
2247
- `;
2248
- }
2249
- return `**${label}:**
2250
- ${value}
2251
-
2252
- `;
2253
- }
2254
- function renderTurn(rec) {
2255
- let md = `## ${rec.ts}
2256
-
2257
- `;
2258
- md += field(rec.date, rec.ts, "User", rec.user);
2259
- md += field(rec.date, rec.ts, "Router", rec.router);
2260
- if (rec.tools?.length) md += `**Tools:** ${rec.tools.join(", ")}
2261
-
2262
- `;
2263
- if (rec.files?.length) md += `**Files:** ${rec.files.join(", ")}
2264
-
2265
- `;
2266
- md += field(rec.date, rec.ts, "Note", rec.note);
2267
- return `${md}---
2268
- `;
2269
- }
2270
- function oneLineSummary(rec) {
2271
- const bits = [];
2272
- if (rec.user) bits.push(rec.user.replace(/\s+/g, " ").slice(0, 80));
2273
- if (rec.files?.length) bits.push(`files: ${rec.files.slice(0, 3).join(", ")}`);
2274
- if (rec.note) bits.push(rec.note.replace(/\s+/g, " ").slice(0, 60));
2275
- return bits.join(" \xB7 ") || "(activity)";
2276
- }
2277
- function captureTurn(rec) {
2278
- const safe = redact(renderTurn(rec));
2279
- appendJournal(rec.date, `${safe.text}
2280
- `);
2281
- const summary = redact(`${rec.ts} \xB7 ${oneLineSummary(rec)}`).text;
2282
- updateDigest(summary, rec.context ? redact(rec.context).text : void 0);
2283
- updateTimeline(rec.date, redact(oneLineSummary(rec)).text);
2284
- const auditClean = !containsSecret(readJournal(rec.date));
2285
- return {
2286
- journalDate: rec.date,
2287
- redactions: safe.redactions,
2288
- bytes: Buffer.byteLength(safe.text, "utf8"),
2289
- auditClean
2290
- };
2291
- }
2292
-
2293
- // src/scribe/health.ts
2294
- import {
2295
- closeSync as closeSync3,
2296
- fsyncSync as fsyncSync3,
2297
- mkdirSync as mkdirSync11,
2298
- openSync as openSync3,
2299
- readFileSync as readFileSync13,
2300
- writeFileSync as writeFileSync11,
2301
- writeSync as writeSync3
2302
- } from "fs";
2303
- import { join as join15 } from "path";
2304
- function healthFile() {
2305
- return join15(scribeDir(), "_health.json");
2306
- }
2307
- function faultLogFile() {
2308
- return join15(scribeDir(), "_faults.log");
2309
- }
2310
- function read2() {
2311
- try {
2312
- return JSON.parse(readFileSync13(healthFile(), "utf8"));
2313
- } catch {
2314
- return { faultCount: 0 };
2315
- }
2316
- }
2317
- function write(h) {
2318
- mkdirSync11(scribeDir(), { recursive: true });
2319
- writeFileSync11(healthFile(), `${JSON.stringify(h, null, 2)}
2320
- `, "utf8");
2321
- }
2322
- function recordHealth() {
2323
- const h = read2();
2324
- h.lastWriteAt = (/* @__PURE__ */ new Date()).toISOString();
2325
- write(h);
2326
- }
2327
- function recordFault(role, err) {
2328
- try {
2329
- mkdirSync11(scribeDir(), { recursive: true });
2330
- const msg = `${(/* @__PURE__ */ new Date()).toISOString()} [${role}] ${err instanceof Error ? err.message : String(err)}`;
2331
- const fd = openSync3(faultLogFile(), "a");
2332
- try {
2333
- writeSync3(fd, `${msg}
2334
- `);
2335
- fsyncSync3(fd);
2336
- } finally {
2337
- closeSync3(fd);
2338
- }
2339
- const h = read2();
2340
- h.faultCount = (h.faultCount ?? 0) + 1;
2341
- h.lastFault = msg;
2342
- write(h);
2343
- } catch {
2344
- }
2345
- }
2346
-
2347
- // src/scribe/wal.ts
2348
- import {
2349
- closeSync as closeSync4,
2350
- existsSync as existsSync7,
2351
- fsyncSync as fsyncSync4,
2352
- mkdirSync as mkdirSync12,
2353
- openSync as openSync4,
2354
- readFileSync as readFileSync14,
2355
- writeFileSync as writeFileSync12,
2356
- writeSync as writeSync4
2357
- } from "fs";
2358
- import { join as join16 } from "path";
2359
- function walFile() {
2360
- return join16(scribeDir(), "_wal.jsonl");
2361
- }
2362
- function appendLine(obj) {
2363
- mkdirSync12(scribeDir(), { recursive: true });
2364
- const fd = openSync4(walFile(), "a");
2365
- try {
2366
- writeSync4(fd, `${JSON.stringify(obj)}
2367
- `);
2368
- fsyncSync4(fd);
2369
- } finally {
2370
- closeSync4(fd);
2371
- }
2372
- }
2373
- function walAppend(id, rec) {
2374
- appendLine({ t: "add", id, rec });
2375
- }
2376
- function walCommit(id) {
2377
- appendLine({ t: "commit", id });
2378
- }
2379
- function walPending() {
2380
- if (!existsSync7(walFile())) return [];
2381
- const committed = /* @__PURE__ */ new Set();
2382
- const adds = /* @__PURE__ */ new Map();
2383
- for (const line of readFileSync14(walFile(), "utf8").split("\n")) {
2384
- if (!line.trim()) continue;
2385
- try {
2386
- const e = JSON.parse(line);
2387
- if (e.t === "commit") committed.add(e.id);
2388
- else if (e.t === "add" && e.rec) adds.set(e.id, e.rec);
2389
- } catch {
2390
- }
2391
- }
2392
- const out = [];
2393
- for (const [id, rec] of adds) {
2394
- if (!committed.has(id)) out.push({ id, rec });
2395
- }
2396
- return out;
2397
- }
2398
- function walCompact() {
2399
- if (!existsSync7(walFile())) return;
2400
- const pending = walPending();
2401
- const body = pending.map((p) => JSON.stringify({ t: "add", id: p.id, rec: p.rec })).join("\n");
2402
- writeFileSync12(walFile(), body ? `${body}
2403
- ` : "", "utf8");
2404
- }
2405
-
2406
- // src/scribe/supervisor.ts
2407
- var draining = false;
2408
- function uid(ts) {
2409
- return `${ts}-${Math.random().toString(36).slice(2, 9)}`;
2410
- }
2411
- function drainBacklog() {
2412
- if (draining) return;
2413
- draining = true;
2414
- try {
2415
- let drained = 0;
2416
- for (const e of walPending()) {
2417
- try {
2418
- captureTurn(e.rec);
2419
- walCommit(e.id);
2420
- drained++;
2421
- } catch (err) {
2422
- recordFault("standby-replay", err);
2423
- break;
2424
- }
2425
- }
2426
- if (drained > 0) walCompact();
2427
- } finally {
2428
- draining = false;
2429
- }
2430
- }
2431
- function supervisedCapture(rec) {
2432
- try {
2433
- drainBacklog();
2434
- const id = uid(rec.ts);
2435
- walAppend(id, rec);
2436
- try {
2437
- const res = captureTurn(rec);
2438
- walCommit(id);
2439
- recordHealth();
2440
- return res;
2441
- } catch (primaryErr) {
2442
- recordFault("primary", primaryErr);
2443
- try {
2444
- const res = captureTurn(rec);
2445
- walCommit(id);
2446
- recordHealth();
2447
- return res;
2448
- } catch (standbyErr) {
2449
- recordFault("standby", standbyErr);
2450
- return null;
2451
- }
2452
- }
2453
- } catch (fatal) {
2454
- recordFault("supervisor", fatal);
2455
- return null;
2456
- }
2457
- }
2458
-
2459
- // src/scribe/retrieval.ts
2460
- import { existsSync as existsSync8, readFileSync as readFileSync15, readdirSync } from "fs";
2461
- function listDays() {
2462
- const dir = scribeDir();
2463
- if (!existsSync8(dir)) return [];
2464
- return readdirSync(dir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).map((f) => f.replace(/\.md$/, "")).sort();
2465
- }
2466
- function readDay(date) {
2467
- const f = journalFile(date);
2468
- return existsSync8(f) ? readFileSync15(f, "utf8") : "";
2469
- }
2470
- function searchScribe(query, limit = 100) {
2471
- const q = query.toLowerCase().trim();
2472
- if (!q) return [];
2473
- const hits = [];
2474
- for (const date of listDays().reverse()) {
2475
- const lines = readDay(date).split("\n");
2476
- for (let i = 0; i < lines.length; i++) {
2477
- const ln = lines[i];
2478
- if (ln && ln.toLowerCase().includes(q)) {
2479
- hits.push({ date, line: i + 1, text: ln.trim().slice(0, 200) });
2480
- if (hits.length >= limit) return hits;
2481
- }
2482
- }
2483
- }
2484
- return hits;
2485
- }
2486
-
2487
- // src/scribe/scribe-pi.ts
2488
- function scribeTurn(input) {
2489
- if (!isScribeEnabled()) return;
2490
- const ts = (/* @__PURE__ */ new Date()).toISOString();
2491
- supervisedCapture({ ts, date: ts.slice(0, 10), ...input });
2492
- }
2493
- function registerScribe(pi) {
2494
- pi.registerTool({
2495
- name: "scribe_recall",
2496
- label: "ORIRO Scribe",
2497
- description: "Recall the user's past work from the on-device journal: search by keyword, or read a specific day (YYYY-MM-DD). Use to recover decisions, code, files, and context from earlier sessions.",
2498
- parameters: Type2.Object({
2499
- query: Type2.Optional(Type2.String({ description: "Keyword/topic to search across all journals." })),
2500
- day: Type2.Optional(Type2.String({ description: "A specific day YYYY-MM-DD to read in full." }))
2501
- }),
2502
- async execute(_id, params) {
2503
- let text;
2504
- const details = {};
2505
- if (!isScribeEnabled()) {
2506
- text = "Scribe is off (the user has not enabled it).";
2507
- } else if (params.day) {
2508
- text = readDay(params.day) || `No journal for ${params.day}. Days: ${listDays().join(", ") || "none"}`;
2509
- details.day = params.day;
2510
- } else {
2511
- const hits = params.query ? searchScribe(params.query) : [];
2512
- details.hits = hits;
2513
- text = hits.length ? hits.map((h) => `${h.date}:${h.line} ${h.text}`).join("\n") : `No matches${params.query ? ` for "${params.query}"` : ""}. Days recorded: ${listDays().join(", ") || "none"}`;
2514
- }
2515
- return { content: [{ type: "text", text }], details };
2516
- }
2517
- });
2518
- }
2519
- function attachScribe(session) {
2520
- let user = "";
2521
- let assistant = "";
2522
- const tools = /* @__PURE__ */ new Set();
2523
- session.subscribe((e) => {
2524
- if (!isScribeEnabled()) return;
2525
- if (e?.type === "user_message" || e?.type === "session_user_message") user = String(e.text ?? e.message ?? user);
2526
- if (e?.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") assistant += e.assistantMessageEvent.delta ?? "";
2527
- if ((e?.type === "tool_call" || e?.type === "tool_execution_start") && e.toolName) tools.add(String(e.toolName));
2528
- if (e?.type === "agent_end") {
2529
- scribeTurn({ user: user || void 0, router: "oriro-free", tools: [...tools], note: assistant.slice(0, 4e3) || void 0 });
2530
- user = "";
2531
- assistant = "";
2532
- tools.clear();
2533
- }
2534
- });
2535
- }
2536
-
2537
- // src/orchestrate.ts
2538
- import { createAgentSession, AuthStorage, ModelRegistry, SessionManager } from "@earendil-works/pi-coding-agent";
2539
- import { Type as Type3 } from "typebox";
2540
- var MAX_AGENTS = 8;
2541
- var MAX_CONCURRENCY = 4;
2542
- async function runOnce(spec) {
2543
- const authStorage = AuthStorage.inMemory();
2544
- const modelRegistry = ModelRegistry.inMemory(authStorage);
2545
- const model = registerOriroMux(modelRegistry);
2546
- if (!model) return { ...spec, ok: false, output: "no free model available" };
2547
- const { session } = await createAgentSession({
2548
- model,
2549
- authStorage,
2550
- modelRegistry,
2551
- sessionManager: SessionManager.inMemory(),
2552
- noTools: "all"
2553
- });
2554
- let out = "";
2555
- const unsub = session.subscribe((e) => {
2556
- if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") out += e.assistantMessageEvent.delta ?? "";
2557
- });
2558
- try {
2559
- await session.prompt(`You are the ${spec.role} sub-agent. ${spec.task}`);
2560
- } catch (e) {
2561
- return { ...spec, ok: false, output: e instanceof Error ? e.message : String(e) };
2562
- } finally {
2563
- unsub();
2564
- session.dispose();
2565
- }
2566
- return { ...spec, ok: out.trim().length > 0, output: out.trim() };
2567
- }
2568
- async function runAgent(spec) {
2569
- let last = await runOnce(spec);
2570
- if (!last.ok) last = await runOnce(spec);
2571
- return last;
2572
- }
2573
- async function runPool(items, n, fn) {
2574
- const results = new Array(items.length);
2575
- let i = 0;
2576
- async function worker() {
2577
- while (i < items.length) {
2578
- const idx = i++;
2579
- const item = items[idx];
2580
- if (item === void 0) continue;
2581
- results[idx] = await fn(item);
2582
- }
2583
- }
2584
- await Promise.all(Array.from({ length: Math.min(n, items.length) }, () => worker()));
2585
- return results;
2586
- }
2587
- async function orchestrate(opts) {
2588
- const agents = opts.agents.slice(0, MAX_AGENTS);
2589
- if ((opts.mode ?? "parallel") === "chain") {
2590
- const results = [];
2591
- let prev = "";
2592
- for (const a of agents) {
2593
- const r = await runAgent({ role: a.role, task: prev ? `${a.task}
2594
-
2595
- Previous result:
2596
- ${prev}` : a.task });
2597
- results.push(r);
2598
- prev = r.output;
2599
- }
2600
- return results;
2601
- }
2602
- return runPool(agents, MAX_CONCURRENCY, runAgent);
2603
- }
2604
- function registerOrchestrator(pi) {
2605
- pi.registerTool({
2606
- name: "deploy_agents",
2607
- label: "ORIRO Orchestrator",
2608
- description: "Deploy multiple sub-agents in parallel (or chained) to do work \u2014 e.g. 'spawn 4 QA + 2 coders, run the tests'. Each sub-agent runs FREE on the router pool. Give each agent a role and a task.",
2609
- parameters: Type3.Object({
2610
- agents: Type3.Array(Type3.Object({ role: Type3.String(), task: Type3.String() }), {
2611
- description: "The sub-agents to deploy (max 8)."
2612
- }),
2613
- mode: Type3.Optional(Type3.Union([Type3.Literal("parallel"), Type3.Literal("chain")]))
2614
- }),
2615
- async execute(_id, params) {
2616
- const results = await orchestrate({ agents: params.agents, mode: params.mode });
2617
- const text = results.map((r) => `[${r.role}] ${r.ok ? "\u2713" : "\u2717"} ${r.output.slice(0, 300)}`).join("\n");
2618
- return { content: [{ type: "text", text }], details: { results } };
2619
- }
2620
- });
2621
- }
2622
-
2623
- // src/skills/loader.ts
2624
- import { loadSkills, formatSkillsForPrompt } from "@earendil-works/pi-coding-agent";
2625
- import { fileURLToPath } from "url";
2626
- import { existsSync as existsSync10 } from "fs";
2627
- import { dirname as dirname2, join as join17 } from "path";
2628
- function packageRoot(start) {
2629
- let dir = start;
2630
- for (let i = 0; i < 10; i++) {
2631
- if (existsSync10(join17(dir, "package.json"))) return dir;
2632
- const parent = dirname2(dir);
2633
- if (parent === dir) break;
2634
- dir = parent;
2635
- }
2636
- return start;
2637
- }
2638
- function skillsDir() {
2639
- if (process.env.ORIRO_SKILLS_DIR) return process.env.ORIRO_SKILLS_DIR;
2640
- return join17(packageRoot(dirname2(fileURLToPath(import.meta.url))), "skills");
2641
- }
2642
- async function loadOriroSkills(dir = skillsDir()) {
2643
- const result = await loadSkills({
2644
- cwd: dir,
2645
- agentDir: dir,
2646
- skillPaths: [dir],
2647
- includeDefaults: false
2648
- });
2649
- const all = Array.isArray(result) ? result : result.skills ?? [];
2650
- return {
2651
- all,
2652
- core: all.filter((s) => !s.disableModelInvocation),
2653
- tail: all.filter((s) => s.disableModelInvocation),
2654
- prompt: formatSkillsForPrompt(all)
2655
- };
2656
- }
2657
-
2658
- // src/onboarding/assemble.ts
2659
- async function assembleOriroSession(opts = {}) {
2660
- const cwd = opts.cwd ?? process.cwd();
2661
- const authStorage = AuthStorage2.inMemory();
2662
- const modelRegistry = ModelRegistry2.inMemory(authStorage);
2663
- const settingsManager = SettingsManager.create(cwd);
2664
- const model = registerOriroMux(modelRegistry);
2665
- if (!model) throw new Error("ORIRO keyless model unavailable");
2666
- const resourceLoader = new DefaultResourceLoader({
2667
- cwd,
2668
- agentDir: getAgentDir(),
2669
- settingsManager,
2670
- additionalSkillPaths: [skillsDir()],
2671
- extensionFactories: [registerGuardian, registerHead, registerScribe, registerOrchestrator]
2672
- });
2673
- await resourceLoader.reload();
2674
- const { session, extensionsResult } = await createAgentSession2({
2675
- model,
2676
- authStorage,
2677
- modelRegistry,
2678
- settingsManager,
2679
- sessionManager: SessionManager2.inMemory(),
2680
- resourceLoader
2681
- });
2682
- attachScribe(session);
2683
- return { session, extensionsResult };
2684
- }
2685
-
2686
- // src/language/nllb-translator.ts
2687
- var NLLB_CODE = {
2688
- en: "eng_Latn",
2689
- zh: "zho_Hans",
2690
- de: "deu_Latn",
2691
- es: "spa_Latn",
2692
- ru: "rus_Cyrl",
2693
- ko: "kor_Hang",
2694
- fr: "fra_Latn",
2695
- ja: "jpn_Jpan",
2696
- pt: "por_Latn",
2697
- tr: "tur_Latn",
2698
- pl: "pol_Latn",
2699
- ca: "cat_Latn",
2700
- nl: "nld_Latn",
2701
- ar: "arb_Arab",
2702
- sv: "swe_Latn",
2703
- it: "ita_Latn",
2704
- id: "ind_Latn",
2705
- hi: "hin_Deva",
2706
- fi: "fin_Latn",
2707
- vi: "vie_Latn",
2708
- he: "heb_Hebr",
2709
- uk: "ukr_Cyrl",
2710
- el: "ell_Grek",
2711
- ms: "zsm_Latn",
2712
- cs: "ces_Latn",
2713
- ro: "ron_Latn",
2714
- da: "dan_Latn",
2715
- hu: "hun_Latn",
2716
- ta: "tam_Taml",
2717
- no: "nob_Latn",
2718
- th: "tha_Thai",
2719
- ur: "urd_Arab",
2720
- hr: "hrv_Latn",
2721
- bg: "bul_Cyrl",
2722
- lt: "lit_Latn",
2723
- mi: "mri_Latn",
2724
- ml: "mal_Mlym",
2725
- cy: "cym_Latn",
2726
- sk: "slk_Latn",
2727
- te: "tel_Telu",
2728
- fa: "pes_Arab",
2729
- lv: "lvs_Latn",
2730
- bn: "ben_Beng",
2731
- sr: "srp_Cyrl",
2732
- az: "azj_Latn",
2733
- sl: "slv_Latn",
2734
- kn: "kan_Knda",
2735
- et: "est_Latn",
2736
- mk: "mkd_Cyrl",
2737
- eu: "eus_Latn",
2738
- is: "isl_Latn",
2739
- hy: "hye_Armn",
2740
- ne: "npi_Deva",
2741
- mn: "khk_Cyrl",
2742
- bs: "bos_Latn",
2743
- kk: "kaz_Cyrl",
2744
- sq: "als_Latn",
2745
- sw: "swh_Latn",
2746
- gl: "glg_Latn",
2747
- mr: "mar_Deva",
2748
- pa: "pan_Guru",
2749
- si: "sin_Sinh",
2750
- km: "khm_Khmr",
2751
- sn: "sna_Latn",
2752
- yo: "yor_Latn",
2753
- so: "som_Latn",
2754
- af: "afr_Latn",
2755
- oc: "oci_Latn",
2756
- ka: "kat_Geor",
2757
- be: "bel_Cyrl",
2758
- tg: "tgk_Cyrl",
2759
- sd: "snd_Arab",
2760
- gu: "guj_Gujr",
2761
- am: "amh_Ethi",
2762
- yi: "ydd_Hebr",
2763
- lo: "lao_Laoo",
2764
- uz: "uzn_Latn",
2765
- fo: "fao_Latn",
2766
- ht: "hat_Latn",
2767
- ps: "pbt_Arab",
2768
- tk: "tuk_Latn",
2769
- nn: "nno_Latn",
2770
- mt: "mlt_Latn",
2771
- sa: "san_Deva",
2772
- lb: "ltz_Latn",
2773
- my: "mya_Mymr",
2774
- bo: "bod_Tibt",
2775
- tl: "tgl_Latn",
2776
- mg: "plt_Latn",
2777
- as: "asm_Beng",
2778
- tt: "tat_Cyrl",
2779
- ln: "lin_Latn",
2780
- ha: "hau_Latn",
2781
- ba: "bak_Cyrl",
2782
- jw: "jav_Latn",
2783
- su: "sun_Latn",
2784
- yue: "yue_Hant"
2785
- };
2786
- var ENG = "eng_Latn";
2787
- var toNllb = (iso) => NLLB_CODE[(iso || "").toLowerCase()] ?? ENG;
2788
- var NllbTranslator = class {
2789
- pipe = null;
2790
- loading = null;
2791
- ready() {
2792
- return this.pipe !== null;
2793
- }
2794
- /** Lazy-load NLLB-200 once (first-use download + cache). Idempotent. */
2795
- async load(modelId = "Xenova/nllb-200-distilled-600M") {
2796
- if (this.pipe) return;
2797
- if (this.loading) return this.loading;
2798
- this.loading = (async () => {
2799
- const { pipeline } = await import("@huggingface/transformers");
2800
- this.pipe = await pipeline("translation", modelId);
2801
- })();
2802
- return this.loading;
2803
- }
2804
- async run(text, src, tgt) {
2805
- if (!this.pipe) await this.load();
2806
- if (!this.pipe) return text;
2807
- const out = await this.pipe(text, { src_lang: src, tgt_lang: tgt });
2808
- return out?.[0]?.translation_text?.trim() || text;
2809
- }
2810
- toEnglish(text, fromLang) {
2811
- return this.run(text, toNllb(fromLang), ENG);
2812
- }
2813
- fromEnglish(english, toLang) {
2814
- return this.run(english, ENG, toNllb(toLang));
2815
- }
2816
- };
2817
- var instance = null;
2818
- function setupNllbTranslator(opts) {
2819
- if (!instance) {
2820
- instance = new NllbTranslator();
2821
- registerTranslator(instance);
2822
- }
2823
- if (opts?.preload) void instance.load();
2824
- return instance;
2825
- }
2826
-
2827
- // src/repl.ts
2828
- function replHelp() {
2829
- return `
2830
- ${accent("ORIRO terminal \u2014 help")}
2831
- ${dim("Just type to chat; ORIRO writes and runs code for you (keyless, free).")}
2832
-
2833
- ${accent("/help")} this help ${accent("/exit")} or ${accent("/quit")} leave ${dim("Ctrl-D / Ctrl-C also exit")}
2834
- ${dim("Run these OUTSIDE the chat (in your shell):")}
2835
- ${dim("oriro skills \xB7 routers \xB7 connectors \xB7 channels \xB7 scribe \xB7 language \xB7 avatar")}
2836
-
2837
- `;
2838
- }
2839
- async function runRepl() {
2840
- if (isFirstRun()) await runOnboarding();
2841
- else stdout4.write(banner());
2842
- const lang = getTerminalLanguage().code;
2843
- const isEnglish2 = lang.toLowerCase().startsWith("en");
2844
- if (!isEnglish2) setupNllbTranslator();
2845
- const { session } = await assembleOriroSession();
2846
- const rl = createInterface4({ input: stdin4, output: stdout4 });
2847
- try {
2848
- for (; ; ) {
2849
- let line;
2850
- try {
2851
- line = (await rl.question("\u203A ")).trim();
2852
- } catch {
2853
- break;
2854
- }
2855
- if (!line) continue;
2856
- if (line === "/exit" || line === "/quit") break;
2857
- if (line === "/help" || line === "/?") {
2858
- stdout4.write(replHelp());
2859
- continue;
2860
- }
2861
- const english = await translateForCoder(line, lang);
2862
- let out = "";
2863
- const unsub = session.subscribe((e) => {
2864
- if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
2865
- const d = e.assistantMessageEvent.delta ?? "";
2866
- out += d;
2867
- if (isEnglish2) stdout4.write(d);
2868
- }
2869
- });
2870
- try {
2871
- await session.prompt(english);
2872
- } finally {
2873
- unsub();
2874
- }
2875
- if (isEnglish2) stdout4.write("\n\n");
2876
- else stdout4.write(`${await translateForUser(out.trim(), lang)}
2877
-
2878
- `);
2879
- }
2880
- } finally {
2881
- rl.close();
2882
- session.dispose();
2883
- stdout4.write(dim("\nBye.\n"));
2884
- }
2885
- }
2886
-
2887
- // src/routers/catalog.ts
2888
- var C4 = (e) => ({
2889
- api: "openai-completions",
2890
- freeModels: [],
2891
- tier: "free",
2892
- kind: "chat",
2893
- ...e
2894
- });
2895
- var ROUTER_CATALOG = [
2896
- // ── Keyless & live-verified (works now, zero keys, through the agent) ──
2897
- C4({
2898
- id: "pollinations",
2899
- displayName: "Pollinations",
1348
+ displayName: "Pollinations",
2900
1349
  baseUrl: "https://text.pollinations.ai/openai",
2901
1350
  freeModels: ["openai", "mistral"],
2902
1351
  obtainUrl: "https://pollinations.ai",
@@ -3167,66 +1616,2106 @@ var ROUTER_CATALOG = [
3167
1616
  displayName: "Ollama (local)",
3168
1617
  api: "ollama",
3169
1618
  baseUrl: "http://localhost:11434/v1",
3170
- freeModels: ["llama3.2"],
3171
- keyless: true
3172
- }),
3173
- // ── Image / speech services (catalog completeness; not chat-routable by the Mux) ──
3174
- C4({
3175
- id: "stability",
3176
- displayName: "Stability AI",
3177
- baseUrl: "https://api.stability.ai/v2beta",
3178
- freeModels: ["stable-image-core"],
3179
- obtainUrl: "https://platform.stability.ai",
3180
- kind: "image"
3181
- }),
3182
- C4({
3183
- id: "fal",
3184
- displayName: "fal.ai",
3185
- baseUrl: "https://fal.run",
3186
- freeModels: ["fal-ai/flux/schnell"],
3187
- obtainUrl: "https://fal.ai",
3188
- kind: "image"
3189
- }),
3190
- C4({
3191
- id: "wavespeed",
3192
- displayName: "WaveSpeedAI",
3193
- baseUrl: "https://api.wavespeed.ai",
3194
- freeModels: [],
3195
- obtainUrl: "https://wavespeed.ai",
3196
- kind: "image"
3197
- }),
3198
- C4({
3199
- id: "ai-horde",
3200
- displayName: "AI Horde",
3201
- baseUrl: "https://aihorde.net/api/v2",
3202
- freeModels: [],
3203
- obtainUrl: "https://aihorde.net",
3204
- keyless: true,
3205
- kind: "image"
3206
- }),
3207
- C4({
3208
- id: "assemblyai",
3209
- displayName: "AssemblyAI",
3210
- baseUrl: "https://api.assemblyai.com/v2",
3211
- freeModels: [],
3212
- obtainUrl: "https://assemblyai.com",
3213
- kind: "speech"
3214
- }),
3215
- // ── Paid (requires payment/recharge — moved out of free per the CC rule) ──
3216
- C4({
3217
- id: "moonshot",
3218
- displayName: "Moonshot (Direct)",
3219
- baseUrl: "https://api.moonshot.ai/v1",
3220
- freeModels: ["kimi-k2.6"],
3221
- obtainUrl: "https://platform.moonshot.ai",
3222
- tier: "paid"
3223
- }),
3224
- // ── ORIRO models — coming soon, greyed/"(free)", not selectable yet ──
3225
- C4({ id: "oriro-gauss", displayName: "ORIRO-Gauss", baseUrl: "", comingSoon: true }),
3226
- C4({ id: "oriro-avila", displayName: "ORIRO-Avila", baseUrl: "", comingSoon: true })
1619
+ freeModels: ["llama3.2"],
1620
+ keyless: true
1621
+ }),
1622
+ // ── Image / speech services (catalog completeness; not chat-routable by the Mux) ──
1623
+ C4({
1624
+ id: "stability",
1625
+ displayName: "Stability AI",
1626
+ baseUrl: "https://api.stability.ai/v2beta",
1627
+ freeModels: ["stable-image-core"],
1628
+ obtainUrl: "https://platform.stability.ai",
1629
+ kind: "image"
1630
+ }),
1631
+ C4({
1632
+ id: "fal",
1633
+ displayName: "fal.ai",
1634
+ baseUrl: "https://fal.run",
1635
+ freeModels: ["fal-ai/flux/schnell"],
1636
+ obtainUrl: "https://fal.ai",
1637
+ kind: "image"
1638
+ }),
1639
+ C4({
1640
+ id: "wavespeed",
1641
+ displayName: "WaveSpeedAI",
1642
+ baseUrl: "https://api.wavespeed.ai",
1643
+ freeModels: [],
1644
+ obtainUrl: "https://wavespeed.ai",
1645
+ kind: "image"
1646
+ }),
1647
+ C4({
1648
+ id: "ai-horde",
1649
+ displayName: "AI Horde",
1650
+ baseUrl: "https://aihorde.net/api/v2",
1651
+ freeModels: [],
1652
+ obtainUrl: "https://aihorde.net",
1653
+ keyless: true,
1654
+ kind: "image"
1655
+ }),
1656
+ C4({
1657
+ id: "assemblyai",
1658
+ displayName: "AssemblyAI",
1659
+ baseUrl: "https://api.assemblyai.com/v2",
1660
+ freeModels: [],
1661
+ obtainUrl: "https://assemblyai.com",
1662
+ kind: "speech"
1663
+ }),
1664
+ // ── Paid (requires payment/recharge — moved out of free per the CC rule) ──
1665
+ C4({
1666
+ id: "moonshot",
1667
+ displayName: "Moonshot (Direct)",
1668
+ baseUrl: "https://api.moonshot.ai/v1",
1669
+ freeModels: ["kimi-k2.6"],
1670
+ obtainUrl: "https://platform.moonshot.ai",
1671
+ tier: "paid"
1672
+ }),
1673
+ // ── ORIRO models — coming soon, greyed/"(free)", not selectable yet ──
1674
+ C4({ id: "oriro-gauss", displayName: "ORIRO-Gauss", baseUrl: "", comingSoon: true }),
1675
+ C4({ id: "oriro-avila", displayName: "ORIRO-Avila", baseUrl: "", comingSoon: true })
1676
+ ];
1677
+ function routerById(id) {
1678
+ return ROUTER_CATALOG.find((r) => r.id === id);
1679
+ }
1680
+
1681
+ // src/routers/router-pool.ts
1682
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
1683
+ import { join as join11 } from "path";
1684
+
1685
+ // src/routers/pool.ts
1686
+ import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
1687
+ import { join as join10 } from "path";
1688
+ function poolFile(dir) {
1689
+ return join10(dir, "routers", "selected.json");
1690
+ }
1691
+ function loadPool(dir) {
1692
+ const p = poolFile(dir);
1693
+ if (!existsSync3(p)) return [];
1694
+ try {
1695
+ const v = JSON.parse(readFileSync8(p, "utf8"));
1696
+ return Array.isArray(v) ? v : [];
1697
+ } catch {
1698
+ return [];
1699
+ }
1700
+ }
1701
+ function savePool(dir, ids) {
1702
+ mkdirSync5(join10(dir, "routers"), { recursive: true });
1703
+ writeFileSync7(poolFile(dir), JSON.stringify([...new Set(ids)], null, 2), "utf8");
1704
+ }
1705
+
1706
+ // src/routers/validate.ts
1707
+ var PROBE_TIMEOUT_MS = 12e3;
1708
+ async function validateRouter(entry, key, modelId) {
1709
+ const model = modelId ?? entry.freeModels[0] ?? "";
1710
+ const t0 = Date.now();
1711
+ const controller = new AbortController();
1712
+ const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
1713
+ try {
1714
+ let res;
1715
+ if (entry.api === "google-generative-ai") {
1716
+ const url = `${entry.baseUrl.replace(/\/$/, "")}/models/${model}:generateContent${key ? `?key=${encodeURIComponent(key)}` : ""}`;
1717
+ res = await fetch(url, {
1718
+ method: "POST",
1719
+ headers: { "content-type": "application/json" },
1720
+ body: JSON.stringify({ contents: [{ parts: [{ text: "ping" }] }] }),
1721
+ signal: controller.signal
1722
+ });
1723
+ } else {
1724
+ const headers = { "content-type": "application/json" };
1725
+ if (key) headers.authorization = `Bearer ${key}`;
1726
+ res = await fetch(`${entry.baseUrl.replace(/\/$/, "")}/chat/completions`, {
1727
+ method: "POST",
1728
+ headers,
1729
+ body: JSON.stringify({
1730
+ model,
1731
+ messages: [{ role: "user", content: "ping" }],
1732
+ max_tokens: 1
1733
+ }),
1734
+ signal: controller.signal
1735
+ });
1736
+ }
1737
+ return {
1738
+ ok: res.ok,
1739
+ latencyMs: Date.now() - t0,
1740
+ model,
1741
+ error: res.ok ? void 0 : `HTTP ${res.status}`
1742
+ };
1743
+ } catch (e) {
1744
+ return {
1745
+ ok: false,
1746
+ latencyMs: Date.now() - t0,
1747
+ model,
1748
+ error: e instanceof Error ? e.message : String(e)
1749
+ };
1750
+ } finally {
1751
+ clearTimeout(timer);
1752
+ }
1753
+ }
1754
+
1755
+ // src/routers/router-pool.ts
1756
+ var KEYLESS_SENTINEL = "oriro-keyless-no-key-required";
1757
+ function regFile() {
1758
+ return join11(oriroDir(), "routers", "registered.json");
1759
+ }
1760
+ function readReg() {
1761
+ try {
1762
+ return JSON.parse(readFileSync9(regFile(), "utf8"));
1763
+ } catch {
1764
+ return {};
1765
+ }
1766
+ }
1767
+ function writeReg(m) {
1768
+ mkdirSync6(join11(oriroDir(), "routers"), { recursive: true });
1769
+ writeFileSync8(regFile(), JSON.stringify(m, null, 2), "utf8");
1770
+ }
1771
+ async function addRouter(entry, opts) {
1772
+ if (entry.comingSoon) {
1773
+ return { ok: false, validation: { ok: false, latencyMs: 0, model: "", error: "coming soon" } };
1774
+ }
1775
+ if (entry.kind && entry.kind !== "chat") {
1776
+ return { ok: false, validation: { ok: false, latencyMs: 0, model: "", error: `'${entry.id}' is a ${entry.kind} router, not a chat router` } };
1777
+ }
1778
+ const key = opts?.key ?? (entry.keyless ? KEYLESS_SENTINEL : void 0);
1779
+ const v = await validateRouter(entry, key, opts?.modelId);
1780
+ if (!v.ok) return { ok: false, validation: v };
1781
+ const router = {
1782
+ id: entry.id,
1783
+ name: entry.displayName,
1784
+ baseUrl: entry.baseUrl,
1785
+ model: opts?.modelId ?? v.model ?? entry.freeModels[0] ?? "",
1786
+ apiKey: key ?? KEYLESS_SENTINEL
1787
+ };
1788
+ const reg = readReg();
1789
+ reg[entry.id] = router;
1790
+ writeReg(reg);
1791
+ savePool(oriroDir(), [...loadPool(oriroDir()), entry.id]);
1792
+ return { ok: true, validation: v };
1793
+ }
1794
+ function useRouters(ids) {
1795
+ const reg = readReg();
1796
+ const applied = ids.filter((id) => reg[id]);
1797
+ const unknown = ids.filter((id) => !reg[id]);
1798
+ if (applied.length > 0) savePool(oriroDir(), applied);
1799
+ return { applied, unknown };
1800
+ }
1801
+ function resolvePool() {
1802
+ const reg = readReg();
1803
+ return loadPool(oriroDir()).map((id) => reg[id]).filter((r) => Boolean(r));
1804
+ }
1805
+
1806
+ // src/routers/onboarding.ts
1807
+ function markerFile() {
1808
+ return join12(oriroDir(), "routers", "onboarded.json");
1809
+ }
1810
+ function hasRouterChoice() {
1811
+ try {
1812
+ return existsSync4(markerFile());
1813
+ } catch {
1814
+ return false;
1815
+ }
1816
+ }
1817
+ function markRouterOnboarded() {
1818
+ try {
1819
+ mkdirSync7(join12(oriroDir(), "routers"), { recursive: true });
1820
+ writeFileSync9(markerFile(), `${JSON.stringify({ onboardedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
1821
+ `, "utf8");
1822
+ } catch {
1823
+ }
1824
+ }
1825
+ async function runRouterOnboarding() {
1826
+ stdout4.write(
1827
+ `
1828
+ ${accent("Routers")} \u2014 ORIRO runs on a ${accent("free keyless router")} by default. No key, $0, works right now.
1829
+ ${dim("Add your own key (any free provider) for a faster, private lane \u2014 or skip and stay keyless.")}
1830
+ `
1831
+ );
1832
+ const rl = createInterface3({ input: stdin3, output: stdout4 });
1833
+ try {
1834
+ const add = (await ask(rl, ` Add your own key now? ${dim("[y/N]")} `)).trim().toLowerCase();
1835
+ if (add === "y" || add === "yes") {
1836
+ const picks = ROUTER_CATALOG.filter(
1837
+ (r) => !r.comingSoon && !r.keyless && (!r.kind || r.kind === "chat")
1838
+ ).slice(0, 8);
1839
+ stdout4.write(`
1840
+ ${dim("Free providers (grab a free key from each provider's site):")}
1841
+ `);
1842
+ for (const r of picks) {
1843
+ stdout4.write(` ${accent(r.id.padEnd(14))} ${dim(r.displayName)}
1844
+ `);
1845
+ }
1846
+ stdout4.write(` ${dim("\u2026or any id from `oriro routers list`")}
1847
+
1848
+ `);
1849
+ const slug = (await ask(rl, ` Which provider? ${dim("(id, or blank to skip)")} `)).trim();
1850
+ if (slug) {
1851
+ const entry = routerById(slug);
1852
+ if (!entry) {
1853
+ stdout4.write(` ${dim(`Unknown '${slug}' \u2014 skipped. You can add it later: oriro routers add ${slug}`)}
1854
+ `);
1855
+ } else {
1856
+ const key = (await ask(rl, ` Paste your ${accent(entry.displayName)} API key: `)).trim();
1857
+ if (key) {
1858
+ stdout4.write(` ${dim("Validating\u2026")}
1859
+ `);
1860
+ const res = await addRouter(entry, { key });
1861
+ if (res.ok) {
1862
+ stdout4.write(
1863
+ ` ${accent("\u2713")} added ${accent(slug)} (${res.validation.latencyMs}ms) \u2014 it now races in your pool.
1864
+ `
1865
+ );
1866
+ } else {
1867
+ stdout4.write(
1868
+ ` ${dim(`Couldn't add ${slug}: ${res.validation.error ?? "validation failed"}. Staying keyless \u2014 retry: oriro routers add ${slug} --key <key>`)}
1869
+ `
1870
+ );
1871
+ }
1872
+ } else {
1873
+ stdout4.write(` ${dim("No key entered \u2014 staying keyless.")}
1874
+ `);
1875
+ }
1876
+ }
1877
+ }
1878
+ }
1879
+ } finally {
1880
+ rl.close();
1881
+ }
1882
+ markRouterOnboarded();
1883
+ stdout4.write(` ${dim("Manage routers anytime: ")}${accent("oriro routers list \xB7 add \xB7 use")}
1884
+ `);
1885
+ }
1886
+
1887
+ // src/onboarding/wrapper.ts
1888
+ function isFirstRun() {
1889
+ return !isLanguageConfigured() || !hasScribeChoice();
1890
+ }
1891
+ async function askYesNo(question) {
1892
+ const rl = createInterface4({ input: stdin4, output: stdout5 });
1893
+ try {
1894
+ const a = (await ask(rl, `${question} ${dim("[Y/n]")} `)).trim().toLowerCase();
1895
+ return a === "" || a === "y" || a === "yes";
1896
+ } finally {
1897
+ rl.close();
1898
+ }
1899
+ }
1900
+ async function runOnboarding() {
1901
+ stdout5.write(banner());
1902
+ await runLanguageOnboarding();
1903
+ await activateGuardian();
1904
+ stdout5.write(` ${accent("\u{1F6E1} Guardian V3")} is on by default. ${accent("\u{1F9ED} Head")} is ready.
1905
+
1906
+ `);
1907
+ if (!isAvatarConfigured()) await runAvatarOnboarding();
1908
+ if (!hasScribeChoice()) {
1909
+ const yes = await askYesNo(
1910
+ "Remember with me? The Scriber keeps your work in context on THIS machine only \u2014 it never leaves it."
1911
+ );
1912
+ setScribeConsent(yes);
1913
+ stdout5.write(yes ? ` ${accent("\u{1F4D3} Scriber")} on.
1914
+ ` : ` ${dim("Scriber off \u2014 `oriro scribe on` anytime.")}
1915
+ `);
1916
+ }
1917
+ if (!hasRouterChoice()) await runRouterOnboarding();
1918
+ stdout5.write(`
1919
+ ${accent("ORIRO is ready.")} ${dim("Type to chat \xB7 /exit to leave")}
1920
+
1921
+ `);
1922
+ }
1923
+
1924
+ // src/onboarding/assemble.ts
1925
+ import {
1926
+ createAgentSession as createAgentSession2,
1927
+ AuthStorage as AuthStorage2,
1928
+ ModelRegistry as ModelRegistry2,
1929
+ SessionManager as SessionManager2,
1930
+ SettingsManager,
1931
+ DefaultResourceLoader,
1932
+ getAgentDir
1933
+ } from "@earendil-works/pi-coding-agent";
1934
+
1935
+ // src/routers/mux-provider.ts
1936
+ import { streamSimple as piStreamSimple, createAssistantMessageEventStream } from "@earendil-works/pi-ai";
1937
+ import { register as registerOpenAICompletions } from "@earendil-works/pi-ai/openai-completions";
1938
+
1939
+ // src/routers/mux.ts
1940
+ import { existsSync as existsSync5, mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "fs";
1941
+ import { join as join13 } from "path";
1942
+ var COOLDOWN_DEFAULT_MS = 6e4;
1943
+ var UNHEALTHY_AFTER = 3;
1944
+ var RouterMux = class {
1945
+ stats = /* @__PURE__ */ new Map();
1946
+ now;
1947
+ constructor(routerIds, now = () => Date.now()) {
1948
+ this.now = now;
1949
+ for (const id of routerIds) {
1950
+ this.stats.set(id, {
1951
+ id,
1952
+ latencyMs: Number.POSITIVE_INFINITY,
1953
+ healthy: true,
1954
+ cooldownUntil: 0,
1955
+ consecutiveErrors: 0
1956
+ });
1957
+ }
1958
+ }
1959
+ /** Available routers, best-first (healthy, not cooling down, lowest latency). */
1960
+ ranked() {
1961
+ const t = this.now();
1962
+ return [...this.stats.values()].filter((s) => s.healthy && s.cooldownUntil <= t).sort((a, b) => a.latencyMs - b.latencyMs).map((s) => s.id);
1963
+ }
1964
+ recordSuccess(id, latencyMs) {
1965
+ const s = this.stats.get(id);
1966
+ if (!s) return;
1967
+ s.latencyMs = s.latencyMs === Number.POSITIVE_INFINITY ? latencyMs : 0.7 * s.latencyMs + 0.3 * latencyMs;
1968
+ s.consecutiveErrors = 0;
1969
+ s.healthy = true;
1970
+ }
1971
+ recordFailure(id, err) {
1972
+ const s = this.stats.get(id);
1973
+ if (!s) return;
1974
+ s.consecutiveErrors += 1;
1975
+ if (err?.status === 429) {
1976
+ s.cooldownUntil = this.now() + (err.retryAfterMs ?? COOLDOWN_DEFAULT_MS);
1977
+ }
1978
+ if (s.consecutiveErrors >= UNHEALTHY_AFTER) s.healthy = false;
1979
+ }
1980
+ /** Run a call through the best router, failing over on error. Throws only if all exhausted. */
1981
+ async run(call) {
1982
+ const order = this.ranked();
1983
+ if (order.length === 0) {
1984
+ throw new Error(
1985
+ "All selected routers are rate-limited or unavailable. Add a BYOK key, select more free routers, or retry shortly."
1986
+ );
1987
+ }
1988
+ let lastErr;
1989
+ for (const id of order) {
1990
+ const t0 = this.now();
1991
+ try {
1992
+ const result = await call(id);
1993
+ this.recordSuccess(id, this.now() - t0);
1994
+ return { result, routerId: id };
1995
+ } catch (e) {
1996
+ const err = e;
1997
+ this.recordFailure(id, { status: err?.status, retryAfterMs: err?.retryAfterMs });
1998
+ lastErr = e;
1999
+ }
2000
+ }
2001
+ throw lastErr instanceof Error ? lastErr : new Error("All selected routers failed this request.");
2002
+ }
2003
+ snapshot() {
2004
+ return [...this.stats.values()].map((s) => ({ ...s }));
2005
+ }
2006
+ load(stats) {
2007
+ for (const s of stats) if (this.stats.has(s.id)) this.stats.set(s.id, { ...s });
2008
+ }
2009
+ };
2010
+ function healthStatePath(dir) {
2011
+ return join13(dir, "routers", "health.json");
2012
+ }
2013
+ function saveMuxState(dir, stats) {
2014
+ const p = healthStatePath(dir);
2015
+ mkdirSync8(join13(dir, "routers"), { recursive: true });
2016
+ writeFileSync10(p, JSON.stringify(stats, null, 2), "utf8");
2017
+ }
2018
+ function loadMuxState(dir) {
2019
+ const p = healthStatePath(dir);
2020
+ if (!existsSync5(p)) return [];
2021
+ try {
2022
+ const stats = JSON.parse(readFileSync10(p, "utf8"));
2023
+ return stats.map((s) => ({ ...s, latencyMs: Number.isFinite(s.latencyMs) ? s.latencyMs : Number.POSITIVE_INFINITY }));
2024
+ } catch {
2025
+ return [];
2026
+ }
2027
+ }
2028
+
2029
+ // src/routers/floor.ts
2030
+ var KEYLESS_FLOOR = [
2031
+ {
2032
+ id: "pollinations",
2033
+ name: "Pollinations (free)",
2034
+ baseUrl: "https://text.pollinations.ai/openai",
2035
+ model: "openai",
2036
+ apiKey: "oriro-keyless"
2037
+ },
2038
+ {
2039
+ id: "ollama-local",
2040
+ name: "Ollama (on-device)",
2041
+ baseUrl: "http://localhost:11434/v1",
2042
+ model: "llama3.2",
2043
+ apiKey: "ollama"
2044
+ }
2045
+ ];
2046
+ function routerModel(r) {
2047
+ return {
2048
+ id: r.model,
2049
+ name: r.name,
2050
+ api: "openai-completions",
2051
+ provider: r.id,
2052
+ baseUrl: r.baseUrl,
2053
+ reasoning: false,
2054
+ input: ["text"],
2055
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2056
+ contextWindow: 128e3,
2057
+ maxTokens: 4096
2058
+ };
2059
+ }
2060
+
2061
+ // src/identity/filter.ts
2062
+ var ORIRO_IDENTITY = "You are ORIRO, a free on-device AI assistant in the user's terminal. You are ORIRO and only ORIRO. Never state, imply, or reveal that you are, or are built on, any other model or company (such as GPT, Claude, Gemini, Qwen, Llama, Mistral, DeepSeek, OpenAI, Anthropic, Google, or Meta). If asked what you are, you are ORIRO.";
2063
+ var BANNED_TEST = /\b(qwen|llama|mistral|mixtral|deepseek|gpt(?:-?\d(?:\.\d)?)?|claude|gemini|openai|anthropic|google|meta\s?ai|alibaba)\b/i;
2064
+ var BANNED_REPLACE = new RegExp(BANNED_TEST.source, "gi");
2065
+ var SELF_REF = /\b(i am|i'm|i was|based on|powered by|my name|my model|my architecture|trained|created by|made by|built (?:on|by)|developed by)\b/i;
2066
+ var SELF_INTRO = /\b(i am|i'm)\s+(a|an)\b/i;
2067
+ var AI_NOUN = /\b(assistant|ai|model|language model|bot|agent|chatbot)\b/i;
2068
+ function applyIdentity(context) {
2069
+ const sys = context.systemPrompt ? `${ORIRO_IDENTITY}
2070
+
2071
+ ${context.systemPrompt}` : ORIRO_IDENTITY;
2072
+ return { ...context, systemPrompt: sys };
2073
+ }
2074
+ function scrubIdentity(text) {
2075
+ return text.replace(/[^.?!\n]+[.?!]?/g, (sentence) => {
2076
+ let s = SELF_REF.test(sentence) && BANNED_TEST.test(sentence) ? sentence.replace(BANNED_REPLACE, "ORIRO") : sentence;
2077
+ if (!/\boriro\b/i.test(s) && SELF_INTRO.test(s) && AI_NOUN.test(s)) {
2078
+ s = s.replace(SELF_INTRO, "I am ORIRO, $2");
2079
+ }
2080
+ return s;
2081
+ });
2082
+ }
2083
+ function scrubMessageIdentity(msg) {
2084
+ return {
2085
+ ...msg,
2086
+ content: msg.content.map(
2087
+ (c) => c.type === "text" ? { ...c, text: scrubIdentity(c.text) } : c
2088
+ )
2089
+ };
2090
+ }
2091
+
2092
+ // src/routers/tool-sanitize.ts
2093
+ var CONTROL_TOKEN = /<\|[^|]*\|>/g;
2094
+ var RECIPIENT_PREFIX = /^(?:to=)?(?:functions?|tools?|recipient)[.=]/i;
2095
+ var RECIPIENT = /(?:to=)?(?:functions?|tools?|recipient)[.=]([A-Za-z0-9_.:-]+)/i;
2096
+ var CLEAN_NAME = /^[A-Za-z0-9_.:-]+$/;
2097
+ function sanitizeToolName(raw) {
2098
+ if (!raw) return raw;
2099
+ if (!raw.includes("<|") && !RECIPIENT_PREFIX.test(raw)) return raw;
2100
+ const base = (raw.split("<|")[0] ?? "").replace(RECIPIENT_PREFIX, "").trim();
2101
+ if (base && CLEAN_NAME.test(base)) return base;
2102
+ const recip = raw.match(RECIPIENT);
2103
+ if (recip?.[1]) return recip[1];
2104
+ const m = raw.replace(CONTROL_TOKEN, " ").match(/[A-Za-z_][A-Za-z0-9_.:-]*/);
2105
+ return m ? m[0] : raw;
2106
+ }
2107
+ function sanitizeMessageToolCalls(msg) {
2108
+ let changed = false;
2109
+ const content = msg.content.map((c) => {
2110
+ if (c.type === "toolCall") {
2111
+ const name = sanitizeToolName(c.name);
2112
+ if (name !== c.name) {
2113
+ changed = true;
2114
+ return { ...c, name };
2115
+ }
2116
+ }
2117
+ return c;
2118
+ });
2119
+ return changed ? { ...msg, content } : msg;
2120
+ }
2121
+ function sanitizeEventToolCalls(ev) {
2122
+ let next = ev;
2123
+ if ("partial" in next && next.partial) {
2124
+ const partial = sanitizeMessageToolCalls(next.partial);
2125
+ if (partial !== next.partial) next = { ...next, partial };
2126
+ }
2127
+ if (next.type === "toolcall_end" && next.toolCall) {
2128
+ const name = sanitizeToolName(next.toolCall.name);
2129
+ if (name !== next.toolCall.name) next = { ...next, toolCall: { ...next.toolCall, name } };
2130
+ }
2131
+ return next;
2132
+ }
2133
+
2134
+ // src/scribe/scribe-pi.ts
2135
+ import { existsSync as existsSync10, readFileSync as readFileSync16 } from "fs";
2136
+ import { Type } from "typebox";
2137
+
2138
+ // src/scribe/capture.ts
2139
+ import { closeSync as closeSync2, fsyncSync as fsyncSync2, mkdirSync as mkdirSync11, openSync as openSync2, writeSync as writeSync2 } from "fs";
2140
+ import { join as join15 } from "path";
2141
+
2142
+ // src/scribe/digest.ts
2143
+ import { existsSync as existsSync6, mkdirSync as mkdirSync9, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
2144
+
2145
+ // src/scribe/paths.ts
2146
+ import { join as join14 } from "path";
2147
+ function scribeDir() {
2148
+ const override = process.env.ORIRO_SCRIBE_DIR?.trim();
2149
+ return override && override.length > 0 ? override : join14(CONFIG_DIR, "scribe");
2150
+ }
2151
+ function journalFile(date) {
2152
+ return join14(scribeDir(), `${date}.md`);
2153
+ }
2154
+ function digestFile() {
2155
+ return join14(scribeDir(), "_digest.md");
2156
+ }
2157
+ function timelineFile() {
2158
+ return join14(scribeDir(), "_timeline.md");
2159
+ }
2160
+ function artifactsDir() {
2161
+ return join14(scribeDir(), "artifacts");
2162
+ }
2163
+
2164
+ // src/scribe/digest.ts
2165
+ var DIGEST_CAP = 8192;
2166
+ var TIMELINE_DAY_CAP = 400;
2167
+ function read(file4) {
2168
+ return existsSync6(file4) ? readFileSync11(file4, "utf8") : "";
2169
+ }
2170
+ function updateDigest(summary, context) {
2171
+ mkdirSync9(scribeDir(), { recursive: true });
2172
+ const existing = read(digestFile());
2173
+ let contextBlock = context?.trim();
2174
+ if (!contextBlock) {
2175
+ const m = existing.match(/## Context\n([\s\S]*?)\n## /);
2176
+ contextBlock = m?.[1]?.trim() ?? "_(not set yet)_";
2177
+ }
2178
+ const recentMatch = existing.match(/## Recent activity[^\n]*\n([\s\S]*)$/);
2179
+ const priorRecent = recentMatch?.[1]?.trim() ?? "";
2180
+ let recent = summary.trim() ? `- ${summary.trim()}
2181
+ ${priorRecent}` : priorRecent;
2182
+ const header2 = `# ORIRO Scribe \u2014 Digest
2183
+
2184
+ ## Context
2185
+ ${contextBlock}
2186
+
2187
+ ## Recent activity (newest first)
2188
+ `;
2189
+ let out = header2 + recent;
2190
+ while (Buffer.byteLength(out, "utf8") > DIGEST_CAP && recent.includes("\n")) {
2191
+ recent = recent.slice(0, recent.lastIndexOf("\n")).trimEnd();
2192
+ out = header2 + recent;
2193
+ }
2194
+ writeFileSync11(digestFile(), out, "utf8");
2195
+ }
2196
+ function updateTimeline(date, topic) {
2197
+ mkdirSync9(scribeDir(), { recursive: true });
2198
+ const clean = topic.replace(/\s+/g, " ").trim();
2199
+ if (!clean) return;
2200
+ const lines = read(timelineFile()).split("\n").filter(Boolean);
2201
+ const header2 = "# ORIRO Scribe \u2014 Timeline";
2202
+ const body = lines.filter((l) => l !== header2);
2203
+ const idx = body.findIndex((l) => l.startsWith(`- ${date} \xB7`));
2204
+ if (idx === -1) {
2205
+ body.push(`- ${date} \xB7 ${clean}`.slice(0, TIMELINE_DAY_CAP + date.length + 6));
2206
+ } else {
2207
+ let merged = `${body[idx]}; ${clean}`;
2208
+ if (merged.length > TIMELINE_DAY_CAP) merged = `${merged.slice(0, TIMELINE_DAY_CAP)}\u2026`;
2209
+ body[idx] = merged;
2210
+ }
2211
+ body.sort();
2212
+ writeFileSync11(timelineFile(), `${header2}
2213
+ ${body.join("\n")}
2214
+ `, "utf8");
2215
+ }
2216
+ function readDigest() {
2217
+ return read(digestFile());
2218
+ }
2219
+ function readTimeline() {
2220
+ return read(timelineFile());
2221
+ }
2222
+
2223
+ // src/scribe/journal.ts
2224
+ import {
2225
+ closeSync,
2226
+ existsSync as existsSync7,
2227
+ fsyncSync,
2228
+ mkdirSync as mkdirSync10,
2229
+ openSync,
2230
+ readFileSync as readFileSync12,
2231
+ writeSync
2232
+ } from "fs";
2233
+ function appendJournal(date, content) {
2234
+ mkdirSync10(scribeDir(), { recursive: true });
2235
+ const fd = openSync(journalFile(date), "a");
2236
+ try {
2237
+ writeSync(fd, content.endsWith("\n") ? content : `${content}
2238
+ `);
2239
+ fsyncSync(fd);
2240
+ } finally {
2241
+ closeSync(fd);
2242
+ }
2243
+ }
2244
+ function readJournal(date) {
2245
+ const f = journalFile(date);
2246
+ return existsSync7(f) ? readFileSync12(f, "utf8") : "";
2247
+ }
2248
+
2249
+ // src/scribe/redact.ts
2250
+ var RULES = [
2251
+ {
2252
+ label: "private-key",
2253
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
2254
+ },
2255
+ // Lone PEM markers — a key SPLIT across fields/turns leaves only a BEGIN-head or an END-tail in
2256
+ // one field. A field carrying either marker is key material: redact the marker + its adjacent body
2257
+ // (forward from BEGIN, backward to END) so no sub-threshold fragment can ever sit on disk.
2258
+ { label: "private-key", re: /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*/g },
2259
+ { label: "private-key", re: /[\s\S]*-----END[A-Z ]*PRIVATE KEY-----/g },
2260
+ { label: "anthropic-key", re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
2261
+ { label: "openrouter-key", re: /sk-or-v1-[A-Za-z0-9]{20,}/g },
2262
+ // Stripe-style keys (sk_live_/pk_live_/rk_test_/…), underscore segments.
2263
+ { label: "stripe-key", re: /\b[srp]k_(?:live|test)_[A-Za-z0-9]{16,}/g },
2264
+ // Generic sk- secret keys — allow hyphenated segments (sk-live-…, sk-proj-…) so a second
2265
+ // hyphen no longer breaks the match (the gap the Scriber spike caught).
2266
+ { label: "secret-key-sk", re: /sk[-_][A-Za-z0-9][A-Za-z0-9-]{14,}/g },
2267
+ { label: "google-key", re: /AIza[0-9A-Za-z_-]{30,}/g },
2268
+ { label: "groq-key", re: /gsk_[A-Za-z0-9]{20,}/g },
2269
+ { label: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
2270
+ { label: "github-token", re: /gh[posr]_[A-Za-z0-9]{30,}/g },
2271
+ { label: "xai-key", re: /xai-[A-Za-z0-9]{20,}/g },
2272
+ { label: "aws-key", re: /AKIA[0-9A-Z]{16}/g },
2273
+ { label: "jwt", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g },
2274
+ { label: "telegram-token", re: /\b\d{8,10}:[A-Za-z0-9_-]{30,}\b/g },
2275
+ // Auth headers / inline credentials (any provider) — the audit found these leaked.
2276
+ { label: "bearer-token", re: /\bbearer\s+[A-Za-z0-9._~+/=-]{12,}/gi },
2277
+ { label: "basic-auth", re: /\bbasic\s+[A-Za-z0-9+/=]{12,}/gi },
2278
+ // key: value / key=value secrets (password, token, secret, api_key, access_key, …).
2279
+ { label: "secret-kv", re: /\b(?:pass(?:word|wd)?|pwd|secret|token|api[_-]?key|access[_-]?key|auth)\s*[:=]\s*\S{3,}/gi },
2280
+ // Credentials embedded in a URL: scheme://user:PASSWORD@host → redact the password.
2281
+ { label: "url-credential", re: /\b([a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:)[^/\s@]+(@)/gi },
2282
+ { label: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g },
2283
+ { label: "phone", re: /(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}/g }
2284
+ ];
2285
+ function marker(label) {
2286
+ return `\u27E8REDACTED:${label}\u27E9`;
2287
+ }
2288
+ function entropy(s) {
2289
+ const freq = /* @__PURE__ */ new Map();
2290
+ for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
2291
+ let h = 0;
2292
+ for (const n of freq.values()) {
2293
+ const p = n / s.length;
2294
+ h -= p * Math.log2(p);
2295
+ }
2296
+ return h;
2297
+ }
2298
+ function looksLikeUnknownSecret(token) {
2299
+ if (token.length < 32) return false;
2300
+ if (token.includes("\u27E8REDACTED:")) return false;
2301
+ if (/^[0-9a-f]+$/i.test(token)) return false;
2302
+ const classes = (/[a-z]/.test(token) ? 1 : 0) + (/[A-Z]/.test(token) ? 1 : 0) + (/[0-9]/.test(token) ? 1 : 0);
2303
+ if (classes < 2) return false;
2304
+ return entropy(token) >= 4.2;
2305
+ }
2306
+ function redact(input) {
2307
+ const counts = /* @__PURE__ */ new Map();
2308
+ let text = input;
2309
+ for (const rule of RULES) {
2310
+ text = text.replace(rule.re, () => {
2311
+ counts.set(rule.label, (counts.get(rule.label) ?? 0) + 1);
2312
+ return marker(rule.label);
2313
+ });
2314
+ }
2315
+ text = text.split(/(\s+)/).map((tok) => {
2316
+ if (looksLikeUnknownSecret(tok)) {
2317
+ counts.set("high-entropy", (counts.get("high-entropy") ?? 0) + 1);
2318
+ return marker("high-entropy");
2319
+ }
2320
+ return tok;
2321
+ }).join("");
2322
+ const redactions = [...counts.entries()].map(([label, count]) => ({
2323
+ label,
2324
+ count
2325
+ }));
2326
+ return { text, redactions };
2327
+ }
2328
+ function containsSecret(text) {
2329
+ for (const rule of RULES) {
2330
+ rule.re.lastIndex = 0;
2331
+ if (rule.re.test(text)) return true;
2332
+ }
2333
+ for (const tok of text.split(/\s+/)) {
2334
+ if (looksLikeUnknownSecret(tok)) return true;
2335
+ }
2336
+ return false;
2337
+ }
2338
+
2339
+ // src/scribe/capture.ts
2340
+ var INLINE_CAP = 4e3;
2341
+ function sideFile(date, ts, kind, full) {
2342
+ mkdirSync11(artifactsDir(), { recursive: true });
2343
+ const name = `${date}_${ts.replace(/[:.]/g, "-")}_${kind}.md`;
2344
+ const p = join15(artifactsDir(), name);
2345
+ const fd = openSync2(p, "w");
2346
+ try {
2347
+ writeSync2(fd, full);
2348
+ fsyncSync2(fd);
2349
+ } finally {
2350
+ closeSync2(fd);
2351
+ }
2352
+ return p;
2353
+ }
2354
+ function field(date, ts, label, value) {
2355
+ if (!value || !value.trim()) return "";
2356
+ if (value.length > INLINE_CAP) {
2357
+ const ref = sideFile(date, ts, label.toLowerCase().replace(/\s+/g, "-"), value);
2358
+ return `**${label}** (full \u2192 ${ref}):
2359
+ ${value.slice(0, INLINE_CAP)}
2360
+ \u2026(truncated; full content in artifact)
2361
+
2362
+ `;
2363
+ }
2364
+ return `**${label}:**
2365
+ ${value}
2366
+
2367
+ `;
2368
+ }
2369
+ function renderTurn(rec) {
2370
+ let md = `## ${rec.ts}
2371
+
2372
+ `;
2373
+ md += field(rec.date, rec.ts, "User", rec.user);
2374
+ md += field(rec.date, rec.ts, "Router", rec.router);
2375
+ if (rec.tools?.length) md += `**Tools:** ${rec.tools.join(", ")}
2376
+
2377
+ `;
2378
+ if (rec.files?.length) md += `**Files:** ${rec.files.join(", ")}
2379
+
2380
+ `;
2381
+ md += field(rec.date, rec.ts, "Note", rec.note);
2382
+ return `${md}---
2383
+ `;
2384
+ }
2385
+ function oneLineSummary(rec) {
2386
+ const bits = [];
2387
+ if (rec.user) bits.push(rec.user.replace(/\s+/g, " ").slice(0, 80));
2388
+ if (rec.files?.length) bits.push(`files: ${rec.files.slice(0, 3).join(", ")}`);
2389
+ if (rec.note) bits.push(rec.note.replace(/\s+/g, " ").slice(0, 60));
2390
+ return bits.join(" \xB7 ") || "(activity)";
2391
+ }
2392
+ function redactRecord(rec) {
2393
+ const tally = /* @__PURE__ */ new Map();
2394
+ const rd = (s) => {
2395
+ if (!s) return s;
2396
+ const r = redact(s);
2397
+ for (const x of r.redactions) tally.set(x.label, (tally.get(x.label) ?? 0) + x.count);
2398
+ return r.text;
2399
+ };
2400
+ const safeRec = {
2401
+ ...rec,
2402
+ user: rd(rec.user),
2403
+ note: rd(rec.note),
2404
+ router: rd(rec.router),
2405
+ context: rd(rec.context),
2406
+ files: rec.files?.map((f) => rd(f) ?? f)
2407
+ };
2408
+ return { rec: safeRec, redactions: [...tally.entries()].map(([label, count]) => ({ label, count })) };
2409
+ }
2410
+ function captureTurn(rec) {
2411
+ const { rec: safeRec, redactions } = redactRecord(rec);
2412
+ const journal = renderTurn(safeRec);
2413
+ appendJournal(rec.date, `${journal}
2414
+ `);
2415
+ updateDigest(`${safeRec.ts} \xB7 ${oneLineSummary(safeRec)}`, safeRec.context);
2416
+ updateTimeline(safeRec.date, oneLineSummary(safeRec));
2417
+ const auditClean = !containsSecret(readJournal(rec.date)) && !containsSecret(readDigest() ?? "");
2418
+ return {
2419
+ journalDate: rec.date,
2420
+ redactions,
2421
+ bytes: Buffer.byteLength(journal, "utf8"),
2422
+ auditClean
2423
+ };
2424
+ }
2425
+
2426
+ // src/scribe/health.ts
2427
+ import {
2428
+ closeSync as closeSync3,
2429
+ fsyncSync as fsyncSync3,
2430
+ mkdirSync as mkdirSync12,
2431
+ openSync as openSync3,
2432
+ readFileSync as readFileSync13,
2433
+ writeFileSync as writeFileSync12,
2434
+ writeSync as writeSync3
2435
+ } from "fs";
2436
+ import { join as join16 } from "path";
2437
+ function healthFile() {
2438
+ return join16(scribeDir(), "_health.json");
2439
+ }
2440
+ function faultLogFile() {
2441
+ return join16(scribeDir(), "_faults.log");
2442
+ }
2443
+ function read2() {
2444
+ try {
2445
+ return JSON.parse(readFileSync13(healthFile(), "utf8"));
2446
+ } catch {
2447
+ return { faultCount: 0 };
2448
+ }
2449
+ }
2450
+ function write(h) {
2451
+ mkdirSync12(scribeDir(), { recursive: true });
2452
+ writeFileSync12(healthFile(), `${JSON.stringify(h, null, 2)}
2453
+ `, "utf8");
2454
+ }
2455
+ function recordHealth() {
2456
+ const h = read2();
2457
+ h.lastWriteAt = (/* @__PURE__ */ new Date()).toISOString();
2458
+ write(h);
2459
+ }
2460
+ function recordFault(role, err) {
2461
+ try {
2462
+ mkdirSync12(scribeDir(), { recursive: true });
2463
+ const msg = `${(/* @__PURE__ */ new Date()).toISOString()} [${role}] ${err instanceof Error ? err.message : String(err)}`;
2464
+ const fd = openSync3(faultLogFile(), "a");
2465
+ try {
2466
+ writeSync3(fd, `${msg}
2467
+ `);
2468
+ fsyncSync3(fd);
2469
+ } finally {
2470
+ closeSync3(fd);
2471
+ }
2472
+ const h = read2();
2473
+ h.faultCount = (h.faultCount ?? 0) + 1;
2474
+ h.lastFault = msg;
2475
+ write(h);
2476
+ } catch {
2477
+ }
2478
+ }
2479
+ function readHealth() {
2480
+ return read2();
2481
+ }
2482
+
2483
+ // src/scribe/wal.ts
2484
+ import {
2485
+ closeSync as closeSync4,
2486
+ existsSync as existsSync8,
2487
+ fsyncSync as fsyncSync4,
2488
+ mkdirSync as mkdirSync13,
2489
+ openSync as openSync4,
2490
+ readFileSync as readFileSync14,
2491
+ writeFileSync as writeFileSync13,
2492
+ writeSync as writeSync4
2493
+ } from "fs";
2494
+ import { join as join17 } from "path";
2495
+ function walFile() {
2496
+ return join17(scribeDir(), "_wal.jsonl");
2497
+ }
2498
+ function appendLine(obj) {
2499
+ mkdirSync13(scribeDir(), { recursive: true });
2500
+ const fd = openSync4(walFile(), "a");
2501
+ try {
2502
+ writeSync4(fd, `${JSON.stringify(obj)}
2503
+ `);
2504
+ fsyncSync4(fd);
2505
+ } finally {
2506
+ closeSync4(fd);
2507
+ }
2508
+ }
2509
+ function walAppend(id, rec) {
2510
+ appendLine({ t: "add", id, rec });
2511
+ }
2512
+ function walCommit(id) {
2513
+ appendLine({ t: "commit", id });
2514
+ }
2515
+ function walPending() {
2516
+ if (!existsSync8(walFile())) return [];
2517
+ const committed = /* @__PURE__ */ new Set();
2518
+ const adds = /* @__PURE__ */ new Map();
2519
+ for (const line of readFileSync14(walFile(), "utf8").split("\n")) {
2520
+ if (!line.trim()) continue;
2521
+ try {
2522
+ const e = JSON.parse(line);
2523
+ if (e.t === "commit") committed.add(e.id);
2524
+ else if (e.t === "add" && e.rec) adds.set(e.id, e.rec);
2525
+ } catch {
2526
+ }
2527
+ }
2528
+ const out = [];
2529
+ for (const [id, rec] of adds) {
2530
+ if (!committed.has(id)) out.push({ id, rec });
2531
+ }
2532
+ return out;
2533
+ }
2534
+ function walCompact() {
2535
+ if (!existsSync8(walFile())) return;
2536
+ const pending = walPending();
2537
+ const body = pending.map((p) => JSON.stringify({ t: "add", id: p.id, rec: p.rec })).join("\n");
2538
+ writeFileSync13(walFile(), body ? `${body}
2539
+ ` : "", "utf8");
2540
+ }
2541
+
2542
+ // src/scribe/supervisor.ts
2543
+ var draining = false;
2544
+ function uid(ts) {
2545
+ return `${ts}-${Math.random().toString(36).slice(2, 9)}`;
2546
+ }
2547
+ function drainBacklog() {
2548
+ if (draining) return;
2549
+ draining = true;
2550
+ try {
2551
+ let drained = 0;
2552
+ for (const e of walPending()) {
2553
+ try {
2554
+ captureTurn(e.rec);
2555
+ walCommit(e.id);
2556
+ drained++;
2557
+ } catch (err) {
2558
+ recordFault("standby-replay", err);
2559
+ break;
2560
+ }
2561
+ }
2562
+ if (drained > 0) walCompact();
2563
+ } finally {
2564
+ draining = false;
2565
+ }
2566
+ }
2567
+ function supervisedCapture(rec) {
2568
+ try {
2569
+ drainBacklog();
2570
+ const id = uid(rec.ts);
2571
+ const safe = redactRecord(rec).rec;
2572
+ walAppend(id, safe);
2573
+ try {
2574
+ const res = captureTurn(safe);
2575
+ walCommit(id);
2576
+ walCompact();
2577
+ recordHealth();
2578
+ return res;
2579
+ } catch (primaryErr) {
2580
+ recordFault("primary", primaryErr);
2581
+ try {
2582
+ const res = captureTurn(safe);
2583
+ walCommit(id);
2584
+ walCompact();
2585
+ recordHealth();
2586
+ return res;
2587
+ } catch (standbyErr) {
2588
+ recordFault("standby", standbyErr);
2589
+ return null;
2590
+ }
2591
+ }
2592
+ } catch (fatal) {
2593
+ recordFault("supervisor", fatal);
2594
+ return null;
2595
+ }
2596
+ }
2597
+
2598
+ // src/scribe/retrieval.ts
2599
+ import { existsSync as existsSync9, readFileSync as readFileSync15, readdirSync } from "fs";
2600
+ function listDays() {
2601
+ const dir = scribeDir();
2602
+ if (!existsSync9(dir)) return [];
2603
+ return readdirSync(dir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).map((f) => f.replace(/\.md$/, "")).sort();
2604
+ }
2605
+ function readDay(date) {
2606
+ const f = journalFile(date);
2607
+ return existsSync9(f) ? readFileSync15(f, "utf8") : "";
2608
+ }
2609
+ function searchScribe(query, limit = 100) {
2610
+ const q = query.toLowerCase().trim();
2611
+ if (!q) return [];
2612
+ const hits = [];
2613
+ for (const date of listDays().reverse()) {
2614
+ const lines = readDay(date).split("\n");
2615
+ for (let i = 0; i < lines.length; i++) {
2616
+ const ln = lines[i];
2617
+ if (ln && ln.toLowerCase().includes(q)) {
2618
+ hits.push({ date, line: i + 1, text: ln.trim().slice(0, 200) });
2619
+ if (hits.length >= limit) return hits;
2620
+ }
2621
+ }
2622
+ }
2623
+ return hits;
2624
+ }
2625
+
2626
+ // src/scribe/scribe-pi.ts
2627
+ function scribeTurn(input) {
2628
+ if (!isScribeEnabled()) return;
2629
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2630
+ supervisedCapture({ ts, date: ts.slice(0, 10), ...input });
2631
+ }
2632
+ var pendingUserInput = "";
2633
+ function noteUserInput(text) {
2634
+ pendingUserInput = text;
2635
+ }
2636
+ function takePendingUserInput() {
2637
+ const u = pendingUserInput;
2638
+ pendingUserInput = "";
2639
+ return u;
2640
+ }
2641
+ function buildScribeContext() {
2642
+ if (!isScribeEnabled()) return "";
2643
+ const parts = [];
2644
+ try {
2645
+ const t = timelineFile();
2646
+ if (existsSync10(t)) parts.push(`# Work history \u2014 every day so far
2647
+ ${readFileSync16(t, "utf8").trim()}`);
2648
+ } catch {
2649
+ }
2650
+ try {
2651
+ const d = readDigest();
2652
+ if (d?.trim()) parts.push(`# Current context (recent)
2653
+ ${d.trim()}`);
2654
+ } catch {
2655
+ }
2656
+ if (!parts.length) return "";
2657
+ return `${parts.join("\n\n")}
2658
+
2659
+ (Call scribe_recall to fetch the full text of any past day or topic.)`;
2660
+ }
2661
+ function registerScribe(pi) {
2662
+ pi.registerTool({
2663
+ name: "scribe_recall",
2664
+ label: "ORIRO Scribe",
2665
+ description: "Recall the user's past work from the on-device journal: search by keyword, or read a specific day (YYYY-MM-DD). Use to recover decisions, code, files, and context from earlier sessions.",
2666
+ parameters: Type.Object({
2667
+ query: Type.Optional(Type.String({ description: "Keyword/topic to search across all journals." })),
2668
+ day: Type.Optional(Type.String({ description: "A specific day YYYY-MM-DD to read in full." }))
2669
+ }),
2670
+ async execute(_id, params) {
2671
+ let text;
2672
+ const details = {};
2673
+ if (!isScribeEnabled()) {
2674
+ text = "Scribe is off (the user has not enabled it).";
2675
+ } else if (params.day) {
2676
+ text = readDay(params.day) || `No journal for ${params.day}. Days: ${listDays().join(", ") || "none"}`;
2677
+ details.day = params.day;
2678
+ } else {
2679
+ const hits = params.query ? searchScribe(params.query) : [];
2680
+ details.hits = hits;
2681
+ text = hits.length ? hits.map((h) => `${h.date}:${h.line} ${h.text}`).join("\n") : `No matches${params.query ? ` for "${params.query}"` : ""}. Days recorded: ${listDays().join(", ") || "none"}`;
2682
+ }
2683
+ return { content: [{ type: "text", text }], details };
2684
+ }
2685
+ });
2686
+ }
2687
+ function attachScribe(session) {
2688
+ let user = "";
2689
+ let assistant = "";
2690
+ const tools = /* @__PURE__ */ new Set();
2691
+ session.subscribe((e) => {
2692
+ if (!isScribeEnabled()) return;
2693
+ if (e?.type === "user_message" || e?.type === "session_user_message") user = String(e.text ?? e.message ?? user);
2694
+ if (e?.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") assistant += e.assistantMessageEvent.delta ?? "";
2695
+ if ((e?.type === "tool_call" || e?.type === "tool_execution_start") && e.toolName) tools.add(String(e.toolName));
2696
+ if (e?.type === "agent_end") {
2697
+ const userText = takePendingUserInput() || user;
2698
+ scribeTurn({ user: userText || void 0, router: "oriro-free", tools: [...tools], note: assistant.slice(0, 4e3) || void 0 });
2699
+ user = "";
2700
+ assistant = "";
2701
+ tools.clear();
2702
+ }
2703
+ });
2704
+ }
2705
+
2706
+ // src/routers/mux-provider.ts
2707
+ var MUX_PROVIDER = "oriro-mux";
2708
+ var MUX_MODEL = "oriro-free";
2709
+ function errToCallError(msg) {
2710
+ const text = msg.errorMessage ?? "";
2711
+ return /\b429\b|rate.?limit|too many requests/i.test(text) ? { status: 429 } : {};
2712
+ }
2713
+ function buildErrorMessage(message) {
2714
+ return {
2715
+ role: "assistant",
2716
+ content: [],
2717
+ api: "openai-completions",
2718
+ provider: MUX_PROVIDER,
2719
+ model: MUX_MODEL,
2720
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
2721
+ stopReason: "error",
2722
+ timestamp: Date.now(),
2723
+ errorMessage: message
2724
+ };
2725
+ }
2726
+ async function driveMux(out, mux, byId, context, options) {
2727
+ let lastError;
2728
+ for (const id of mux.ranked()) {
2729
+ const router = byId.get(id);
2730
+ if (!router) continue;
2731
+ const t0 = Date.now();
2732
+ let committed = false;
2733
+ let lastPartial;
2734
+ try {
2735
+ const inner = piStreamSimple(routerModel(router), context, {
2736
+ ...options ?? {},
2737
+ apiKey: router.apiKey
2738
+ });
2739
+ let failedBeforeContent = false;
2740
+ for await (const ev of inner) {
2741
+ if (ev.type === "error") {
2742
+ mux.recordFailure(id, errToCallError(ev.error));
2743
+ if (!committed) {
2744
+ lastError = ev.error;
2745
+ failedBeforeContent = true;
2746
+ break;
2747
+ }
2748
+ out.push(ev);
2749
+ out.end(ev.error);
2750
+ return;
2751
+ }
2752
+ committed = true;
2753
+ if (ev.type === "done") {
2754
+ mux.recordSuccess(id, Date.now() - t0);
2755
+ const clean = sanitizeMessageToolCalls(scrubMessageIdentity(ev.message));
2756
+ out.push({ type: "done", reason: ev.reason, message: clean });
2757
+ out.end(clean);
2758
+ return;
2759
+ }
2760
+ lastPartial = ev.partial;
2761
+ out.push(sanitizeEventToolCalls(ev));
2762
+ }
2763
+ if (failedBeforeContent) continue;
2764
+ if (!committed) {
2765
+ mux.recordFailure(id, {});
2766
+ lastError ??= buildErrorMessage("Router returned no output.");
2767
+ continue;
2768
+ }
2769
+ mux.recordSuccess(id, Date.now() - t0);
2770
+ out.end(lastPartial ? sanitizeMessageToolCalls(scrubMessageIdentity(lastPartial)) : void 0);
2771
+ return;
2772
+ } catch (e) {
2773
+ mux.recordFailure(id, e);
2774
+ }
2775
+ }
2776
+ const msg = lastError ?? buildErrorMessage(
2777
+ "All keyless routers are unavailable. Add a BYOK key, select more free routers, or retry shortly."
2778
+ );
2779
+ out.push({ type: "error", reason: "error", error: msg });
2780
+ out.end(msg);
2781
+ }
2782
+ function registerOriroMux(registry, opts = {}) {
2783
+ registerOpenAICompletions();
2784
+ const pooled = resolvePool();
2785
+ const routers = opts.routers ?? (pooled.length > 0 ? pooled : KEYLESS_FLOOR);
2786
+ const byId = new Map(routers.map((r) => [r.id, r]));
2787
+ const mux = new RouterMux(routers.map((r) => r.id));
2788
+ try {
2789
+ mux.load(loadMuxState(oriroDir()));
2790
+ } catch {
2791
+ }
2792
+ registry.registerProvider(MUX_PROVIDER, {
2793
+ name: "ORIRO Free (keyless Mux)",
2794
+ api: "openai-completions",
2795
+ apiKey: "oriro-keyless",
2796
+ // Placeholder — required by registry validation but never used: our custom streamSimple
2797
+ // routes to the real keyless floor endpoints itself (see driveMux).
2798
+ baseUrl: "http://oriro-mux.local",
2799
+ models: [
2800
+ {
2801
+ id: MUX_MODEL,
2802
+ name: "ORIRO Free (best-router)",
2803
+ api: "openai-completions",
2804
+ baseUrl: "http://oriro-mux.local",
2805
+ reasoning: false,
2806
+ input: ["text"],
2807
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2808
+ contextWindow: 128e3,
2809
+ maxTokens: 4096
2810
+ }
2811
+ ],
2812
+ streamSimple: (_model, context, options) => {
2813
+ const out = createAssistantMessageEventStream();
2814
+ const ctx = applyIdentity(context);
2815
+ const memory = buildScribeContext();
2816
+ const withMemory = memory ? { ...ctx, systemPrompt: `${ctx.systemPrompt}
2817
+
2818
+ ${memory}` } : ctx;
2819
+ void driveMux(out, mux, byId, withMemory, options).finally(() => {
2820
+ try {
2821
+ saveMuxState(oriroDir(), mux.snapshot());
2822
+ } catch {
2823
+ }
2824
+ });
2825
+ return out;
2826
+ }
2827
+ });
2828
+ return registry.find(MUX_PROVIDER, MUX_MODEL);
2829
+ }
2830
+
2831
+ // src/head/pi-tool.ts
2832
+ import { Type as Type2 } from "typebox";
2833
+
2834
+ // src/head/comparison-engine.ts
2835
+ var SECTION_RULES = [
2836
+ {
2837
+ type: "hero",
2838
+ label: "Hero",
2839
+ priority: "CRITICAL",
2840
+ markup: [/<h1[\s>]/],
2841
+ recommend: "Add a clear above-the-fold hero \u2014 one headline that states the value + one primary CTA."
2842
+ },
2843
+ {
2844
+ type: "navigation",
2845
+ label: "Navigation",
2846
+ priority: "CRITICAL",
2847
+ markup: [/<nav[\s>]/, /role=["']navigation["']/],
2848
+ recommend: "Add a top navigation so visitors can reach key sections."
2849
+ },
2850
+ {
2851
+ type: "features",
2852
+ label: "Features",
2853
+ priority: "CRITICAL",
2854
+ text: [/\bfeatures?\b/, /\bwhat you (?:can|get)\b/, /\bcapabilit/],
2855
+ recommend: "Add a features section that spells out concrete capabilities, not adjectives."
2856
+ },
2857
+ {
2858
+ type: "pricing",
2859
+ label: "Pricing",
2860
+ priority: "CRITICAL",
2861
+ text: [/\bpricing\b/, /\bper month\b/, /\b\/mo\b/, /\bfree plan\b/, /\$\d/, /₹\d/, /€\d/],
2862
+ recommend: 'Add transparent pricing \u2014 a critical conversion element; even a single "Free" tier helps.'
2863
+ },
2864
+ {
2865
+ type: "cta",
2866
+ label: "Call-to-Action",
2867
+ priority: "CRITICAL",
2868
+ text: [/\bget started\b/, /\bsign up\b/, /\bstart (?:free|now|building)\b/, /\btry (?:it|now|free)\b/, /\bbook a demo\b/, /\bget a demo\b/],
2869
+ recommend: 'Add a strong, repeated primary CTA ("Get started") so the next step is obvious.'
2870
+ },
2871
+ {
2872
+ type: "testimonials",
2873
+ label: "Testimonials",
2874
+ priority: "HIGH",
2875
+ text: [/\btestimonial/, /\bwhat (?:our )?(?:customers|users) say\b/, /\bloved by\b/, /\breview(?:s|ed)\b/],
2876
+ recommend: "Add 2\u20133 customer testimonials with names/photos to build trust."
2877
+ },
2878
+ {
2879
+ type: "stats",
2880
+ label: "Stats / Metrics",
2881
+ priority: "HIGH",
2882
+ text: [/\b\d[\d,.]*\s*[kkmm]\+?\s*(?:users|customers|developers|downloads|teams)\b/, /\b9\d(?:\.\d+)?%\b/, /\buptime\b/],
2883
+ recommend: 'Add impressive metrics ("10K+ users", "99.9% uptime") as social proof.'
2884
+ },
2885
+ {
2886
+ type: "video",
2887
+ label: "Video",
2888
+ priority: "HIGH",
2889
+ markup: [/<video[\s>]/, /youtube\.com\/embed/, /player\.vimeo\.com/, /<iframe[^>]+(?:youtube|vimeo)/],
2890
+ text: [/\bwatch the (?:video|demo)\b/],
2891
+ recommend: "Add a short explainer/demo video \u2014 it lifts conversion on landing pages."
2892
+ },
2893
+ {
2894
+ type: "demo",
2895
+ label: "Live Demo",
2896
+ priority: "HIGH",
2897
+ text: [/\btry it (?:now|live|free)\b/, /\bplayground\b/, /\binteractive demo\b/, /\blive demo\b/],
2898
+ recommend: 'Add a "try it" live demo or playground so visitors experience the product immediately.'
2899
+ },
2900
+ {
2901
+ type: "socialProof",
2902
+ label: "Social Proof",
2903
+ priority: "HIGH",
2904
+ text: [/\btrusted by\b/, /\bbacked by\b/, /\bused by\b/, /\bas seen (?:in|on)\b/, /\bcustomers include\b/],
2905
+ recommend: 'Add social proof (customer/investor logos, "trusted by \u2026") near the hero.'
2906
+ },
2907
+ {
2908
+ type: "faq",
2909
+ label: "FAQ",
2910
+ priority: "MEDIUM",
2911
+ text: [/\bfaq\b/, /\bfrequently asked\b/],
2912
+ markup: [/<details[\s>]/],
2913
+ recommend: "Add an FAQ that answers the top objections before they become exits."
2914
+ },
2915
+ {
2916
+ type: "integrations",
2917
+ label: "Integrations",
2918
+ priority: "MEDIUM",
2919
+ text: [/\bintegrations?\b/, /\bworks with\b/, /\bconnect your\b/],
2920
+ recommend: "Add an integrations section showing what the product connects to."
2921
+ },
2922
+ {
2923
+ type: "newsletter",
2924
+ label: "Newsletter / Capture",
2925
+ priority: "MEDIUM",
2926
+ text: [/\bsubscribe\b/, /\bnewsletter\b/, /\bjoin (?:the )?waitlist\b/],
2927
+ markup: [/type=["']email["']/],
2928
+ recommend: "Add an email capture (newsletter/waitlist) so non-converting visitors are not lost."
2929
+ },
2930
+ {
2931
+ type: "comparison",
2932
+ label: "Comparison",
2933
+ priority: "MEDIUM",
2934
+ text: [/\bcompare\b/, /\bcomparison\b/, /\b vs\.? \b/, /\bwhy choose\b/],
2935
+ recommend: 'Add a comparison ("us vs alternatives") to win evaluators who are shopping around.'
2936
+ },
2937
+ {
2938
+ type: "team",
2939
+ label: "Team / About",
2940
+ priority: "LOW",
2941
+ text: [/\bour team\b/, /\bmeet the team\b/, /\bfounders?\b/, /\babout us\b/],
2942
+ recommend: "Add a brief team/about section to humanize the brand."
2943
+ }
3227
2944
  ];
3228
- function routerById(id) {
3229
- return ROUTER_CATALOG.find((r) => r.id === id);
2945
+ var PRIORITY_RANK = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
2946
+ var PRIORITY_EFFORT = { CRITICAL: "L", HIGH: "M", MEDIUM: "M", LOW: "S" };
2947
+ var FETCH_TIMEOUT_MS = 12e3;
2948
+ var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36 ORIRO-Inspector";
2949
+ async function fetchPage(url) {
2950
+ const controller = new AbortController();
2951
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
2952
+ const start = Date.now();
2953
+ try {
2954
+ const res = await fetch(url, {
2955
+ signal: controller.signal,
2956
+ redirect: "follow",
2957
+ headers: { "user-agent": UA, accept: "text/html,application/xhtml+xml" }
2958
+ });
2959
+ const html = await res.text();
2960
+ return { html, ms: Date.now() - start, status: res.status, ok: res.ok, error: "" };
2961
+ } catch (err) {
2962
+ return { html: "", ms: Date.now() - start, status: 0, ok: false, error: err instanceof Error ? err.message : "fetch failed" };
2963
+ } finally {
2964
+ clearTimeout(timer);
2965
+ }
2966
+ }
2967
+ function toText(html) {
2968
+ return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/\s+/g, " ").toLowerCase().trim();
2969
+ }
2970
+ function firstMatch(re, hay) {
2971
+ const m = re.exec(hay);
2972
+ if (!m) return "";
2973
+ const slice = (m[0] ?? "").trim();
2974
+ return slice.length > 80 ? `${slice.slice(0, 77)}\u2026` : slice;
2975
+ }
2976
+ function detectSections(rawHtmlLower, text) {
2977
+ const found = [];
2978
+ for (const rule of SECTION_RULES) {
2979
+ let evidence = "";
2980
+ for (const re of rule.markup ?? []) {
2981
+ const hit = firstMatch(re, rawHtmlLower);
2982
+ if (hit) {
2983
+ evidence = hit;
2984
+ break;
2985
+ }
2986
+ }
2987
+ if (!evidence) {
2988
+ for (const re of rule.text ?? []) {
2989
+ const hit = firstMatch(re, text);
2990
+ if (hit) {
2991
+ evidence = hit;
2992
+ break;
2993
+ }
2994
+ }
2995
+ }
2996
+ if (evidence) found.push({ type: rule.type, label: rule.label, priority: rule.priority, evidence });
2997
+ }
2998
+ return found;
2999
+ }
3000
+ function extractMatches(re, html, max) {
3001
+ const out = [];
3002
+ for (const m of html.matchAll(re)) {
3003
+ const inner = (m[1] ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
3004
+ if (inner && !out.includes(inner)) out.push(inner);
3005
+ if (out.length >= max) break;
3006
+ }
3007
+ return out;
3008
+ }
3009
+ var CTA_WORDS = /\b(get started|sign up|start free|start now|start building|try (?:it|now|free)|book a demo|get a demo|request access|join (?:the )?waitlist|download)\b/i;
3010
+ function extractStructure(url, fr) {
3011
+ const html = fr.html;
3012
+ const lowerHtml = html.toLowerCase();
3013
+ const text = toText(html);
3014
+ const titleM = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
3015
+ const title = (titleM?.[1] ?? "").replace(/\s+/g, " ").trim();
3016
+ const descM = /<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["']/i.exec(html) ?? /<meta[^>]+content=["']([^"']*)["'][^>]+name=["']description["']/i.exec(html);
3017
+ const description = (descM?.[1] ?? "").replace(/\s+/g, " ").trim();
3018
+ const headings = extractMatches(/<h[1-3][^>]*>([\s\S]*?)<\/h[1-3]>/gi, html, 12);
3019
+ const ctaAll = extractMatches(/<(?:a|button)[^>]*>([\s\S]*?)<\/(?:a|button)>/gi, html, 80);
3020
+ const ctas = [];
3021
+ for (const c of ctaAll) {
3022
+ if (CTA_WORDS.test(c) && !ctas.includes(c)) ctas.push(c);
3023
+ if (ctas.length >= 10) break;
3024
+ }
3025
+ const forms = (lowerHtml.match(/<form[\s>]/g) ?? []).length;
3026
+ const links = (lowerHtml.match(/<a[\s>]/g) ?? []).length;
3027
+ const images = (lowerHtml.match(/<img[\s>]/g) ?? []).length;
3028
+ const hasVideo = /<video[\s>]/.test(lowerHtml) || /(?:youtube\.com\/embed|player\.vimeo\.com)/.test(lowerHtml);
3029
+ const domNodes = (html.match(/<[a-z!\/]/gi) ?? []).length;
3030
+ let note = "";
3031
+ if (fr.ok && text.length < 400 && domNodes < 60) {
3032
+ note = "Sparse HTML \u2014 likely a client-rendered (SPA) page; structure may be under-detected without a JS render.";
3033
+ }
3034
+ return {
3035
+ url,
3036
+ title,
3037
+ description,
3038
+ sections: detectSections(lowerHtml, text),
3039
+ headings,
3040
+ ctas,
3041
+ forms,
3042
+ links,
3043
+ images,
3044
+ hasVideo,
3045
+ metrics: { htmlBytes: html.length, domNodes, fetchMs: fr.ms, status: fr.status },
3046
+ ok: fr.ok && html.length > 0,
3047
+ note: fr.ok ? note : `Could not load: ${fr.error || `HTTP ${fr.status}`}`
3048
+ };
3049
+ }
3050
+ function ruleFor(type) {
3051
+ return SECTION_RULES.find((r) => r.type === type) ?? SECTION_RULES[0];
3052
+ }
3053
+ function analyzeGaps(target, competitors) {
3054
+ const targetTypes = new Set(target.sections.map((s) => s.type));
3055
+ const compPresence = /* @__PURE__ */ new Map();
3056
+ for (const comp of competitors) {
3057
+ if (!comp.ok) continue;
3058
+ for (const s of comp.sections) {
3059
+ const list = compPresence.get(s.type) ?? [];
3060
+ if (!list.includes(comp.url)) list.push(comp.url);
3061
+ compPresence.set(s.type, list);
3062
+ }
3063
+ }
3064
+ const missing = [];
3065
+ const parity = [];
3066
+ for (const [type, presentOn] of compPresence) {
3067
+ if (targetTypes.has(type)) {
3068
+ parity.push(type);
3069
+ } else {
3070
+ const rule = ruleFor(type);
3071
+ missing.push({ section: type, label: rule.label, priority: rule.priority, presentOn, recommendation: rule.recommend });
3072
+ }
3073
+ }
3074
+ missing.sort((a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || b.presentOn.length - a.presentOn.length);
3075
+ const advantages = target.sections.filter((s) => !compPresence.has(s.type));
3076
+ return { missing, advantages, parity };
3077
+ }
3078
+ function generateActionItems(missing) {
3079
+ return missing.map((g) => ({
3080
+ title: `Add a ${g.label} section`,
3081
+ priority: g.priority,
3082
+ effort: PRIORITY_EFFORT[g.priority],
3083
+ rationale: `${g.presentOn.length} of the compared page(s) have it; you don't. ${g.recommendation}`
3084
+ }));
3085
+ }
3086
+ function hostOf(url) {
3087
+ try {
3088
+ return new URL(url).host.replace(/^www\./, "");
3089
+ } catch {
3090
+ return url;
3091
+ }
3092
+ }
3093
+ function generateSummary(target, competitors, gaps) {
3094
+ const okComps = competitors.filter((c) => c.ok);
3095
+ const tName = hostOf(target.url);
3096
+ if (!target.ok) return `Could not load ${tName} (${target.note}). Nothing to compare against yet.`;
3097
+ if (okComps.length === 0) return `Loaded ${tName} (${target.sections.length} sections) but none of the comparison URLs could be loaded.`;
3098
+ const crit = gaps.missing.filter((m) => m.priority === "CRITICAL").map((m) => m.label);
3099
+ const high = gaps.missing.filter((m) => m.priority === "HIGH").map((m) => m.label);
3100
+ const parts = [];
3101
+ parts.push(`${tName} has ${target.sections.length} detectable sections; compared against ${okComps.length} page(s).`);
3102
+ if (gaps.missing.length === 0) {
3103
+ parts.push("No structural gaps found \u2014 you cover everything they do.");
3104
+ } else {
3105
+ parts.push(`${gaps.missing.length} gap(s) found.`);
3106
+ if (crit.length) parts.push(`Critical: ${crit.join(", ")}.`);
3107
+ if (high.length) parts.push(`High: ${high.join(", ")}.`);
3108
+ }
3109
+ if (gaps.advantages.length) parts.push(`Your edge: ${gaps.advantages.map((a) => a.label).join(", ")}.`);
3110
+ return parts.join(" ");
3111
+ }
3112
+ function normalizeUrl(u) {
3113
+ const t = (u || "").trim();
3114
+ if (!t) return t;
3115
+ return /^https?:\/\//i.test(t) ? t : `https://${t}`;
3116
+ }
3117
+ async function comparePages(opts) {
3118
+ const targetUrl = normalizeUrl(opts.targetUrl);
3119
+ const competitorUrls = (opts.competitorUrls ?? []).map(normalizeUrl).filter((u) => u.length > 0).slice(0, 30);
3120
+ const [targetFetch, ...compFetches] = await Promise.all([
3121
+ fetchPage(targetUrl),
3122
+ ...competitorUrls.map((u) => fetchPage(u))
3123
+ ]);
3124
+ const target = extractStructure(targetUrl, targetFetch ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" });
3125
+ const competitors = competitorUrls.map(
3126
+ (u, i) => extractStructure(u, compFetches[i] ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" })
3127
+ );
3128
+ const gaps = analyzeGaps(target, competitors);
3129
+ return {
3130
+ target,
3131
+ competitors,
3132
+ missing: gaps.missing,
3133
+ advantages: gaps.advantages,
3134
+ parity: gaps.parity,
3135
+ actionItems: generateActionItems(gaps.missing),
3136
+ summary: generateSummary(target, competitors, gaps)
3137
+ };
3138
+ }
3139
+
3140
+ // src/head/pi-tool.ts
3141
+ function summarizeForCoder(report) {
3142
+ const lines = [report.summary];
3143
+ const page = (p) => ` \u2022 ${p.url} \u2014 ${p.ok ? `${p.sections.length} sections: ${p.sections.map((s) => s.type).join(", ")}` : `not readable (${p.note})`}`;
3144
+ lines.push("Pages seen:");
3145
+ lines.push(page(report.target));
3146
+ for (const c of report.competitors) if (c.url !== report.target.url) lines.push(page(c));
3147
+ if (report.missing.length) {
3148
+ lines.push("Missing on the target (gaps to build):");
3149
+ for (const g of report.missing.slice(0, 12)) lines.push(` \u2022 ${g.label} (${g.priority}) \u2014 ${g.recommendation}`);
3150
+ }
3151
+ if (report.actionItems.length) {
3152
+ lines.push("Suggested action items:");
3153
+ for (const a of report.actionItems.slice(0, 12)) lines.push(` \u2192 ${a.title} [${a.priority}/${a.effort}] \u2014 ${a.rationale}`);
3154
+ }
3155
+ return lines.join("\n");
3156
+ }
3157
+ var InspectSiteParams = Type2.Object({
3158
+ url: Type2.String({ description: "The target website URL to inspect or rebuild from." }),
3159
+ competitors: Type2.Optional(
3160
+ Type2.Array(Type2.String(), { description: "Optional competitor/reference URLs to compare the target against." })
3161
+ )
3162
+ });
3163
+ function registerHead(pi) {
3164
+ pi.registerTool({
3165
+ name: "inspect_site",
3166
+ label: "ORIRO Head",
3167
+ description: "Go out to a live website and SEE it: its sections, CTAs, structure, and any gaps versus competitor URLs. Returns a structured report to build from. Call this whenever the user wants to look at, compare against, or rebuild a website/page.",
3168
+ parameters: InspectSiteParams,
3169
+ async execute(_toolCallId, params) {
3170
+ const target = params.url;
3171
+ const competitors = params.competitors?.length ? params.competitors : [target];
3172
+ const report = await comparePages({ targetUrl: target, competitorUrls: competitors });
3173
+ return { content: [{ type: "text", text: summarizeForCoder(report) }], details: report };
3174
+ }
3175
+ });
3176
+ }
3177
+
3178
+ // src/orchestrate.ts
3179
+ import { createAgentSession, AuthStorage, ModelRegistry, SessionManager } from "@earendil-works/pi-coding-agent";
3180
+ import { Type as Type3 } from "typebox";
3181
+ var MAX_AGENTS = 8;
3182
+ var MAX_CONCURRENCY = 4;
3183
+ async function runOnce(spec) {
3184
+ const authStorage = AuthStorage.inMemory();
3185
+ const modelRegistry = ModelRegistry.inMemory(authStorage);
3186
+ const model = registerOriroMux(modelRegistry);
3187
+ if (!model) return { ...spec, ok: false, output: "no free model available" };
3188
+ const { session } = await createAgentSession({
3189
+ model,
3190
+ authStorage,
3191
+ modelRegistry,
3192
+ sessionManager: SessionManager.inMemory(),
3193
+ noTools: "all"
3194
+ });
3195
+ let out = "";
3196
+ const unsub = session.subscribe((e) => {
3197
+ if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") out += e.assistantMessageEvent.delta ?? "";
3198
+ });
3199
+ try {
3200
+ await session.prompt(`You are the ${spec.role} sub-agent. ${spec.task}`);
3201
+ } catch (e) {
3202
+ return { ...spec, ok: false, output: e instanceof Error ? e.message : String(e) };
3203
+ } finally {
3204
+ unsub();
3205
+ session.dispose();
3206
+ }
3207
+ return { ...spec, ok: out.trim().length > 0, output: out.trim() };
3208
+ }
3209
+ async function runAgent(spec) {
3210
+ let last = await runOnce(spec);
3211
+ if (!last.ok) last = await runOnce(spec);
3212
+ return last;
3213
+ }
3214
+ async function runPool(items, n, fn) {
3215
+ const results = new Array(items.length);
3216
+ let i = 0;
3217
+ async function worker() {
3218
+ while (i < items.length) {
3219
+ const idx = i++;
3220
+ const item = items[idx];
3221
+ if (item === void 0) continue;
3222
+ results[idx] = await fn(item);
3223
+ }
3224
+ }
3225
+ await Promise.all(Array.from({ length: Math.min(n, items.length) }, () => worker()));
3226
+ return results;
3227
+ }
3228
+ async function orchestrate(opts) {
3229
+ const agents = opts.agents.slice(0, MAX_AGENTS);
3230
+ if ((opts.mode ?? "parallel") === "chain") {
3231
+ const results = [];
3232
+ let prev = "";
3233
+ for (const a of agents) {
3234
+ const r = await runAgent({ role: a.role, task: prev ? `${a.task}
3235
+
3236
+ Previous result:
3237
+ ${prev}` : a.task });
3238
+ results.push(r);
3239
+ prev = r.output;
3240
+ }
3241
+ return results;
3242
+ }
3243
+ return runPool(agents, MAX_CONCURRENCY, runAgent);
3244
+ }
3245
+ function registerOrchestrator(pi) {
3246
+ pi.registerTool({
3247
+ name: "deploy_agents",
3248
+ label: "ORIRO Orchestrator",
3249
+ description: "Deploy multiple sub-agents in parallel (or chained) to do work \u2014 e.g. 'spawn 4 QA + 2 coders, run the tests'. Each sub-agent runs FREE on the router pool. Give each agent a role and a task.",
3250
+ parameters: Type3.Object({
3251
+ agents: Type3.Array(Type3.Object({ role: Type3.String(), task: Type3.String() }), {
3252
+ description: "The sub-agents to deploy (max 8)."
3253
+ }),
3254
+ mode: Type3.Optional(Type3.Union([Type3.Literal("parallel"), Type3.Literal("chain")]))
3255
+ }),
3256
+ async execute(_id, params) {
3257
+ const results = await orchestrate({ agents: params.agents, mode: params.mode });
3258
+ const text = results.map((r) => `[${r.role}] ${r.ok ? "\u2713" : "\u2717"} ${r.output.slice(0, 300)}`).join("\n");
3259
+ return { content: [{ type: "text", text }], details: { results } };
3260
+ }
3261
+ });
3262
+ }
3263
+
3264
+ // src/skills/loader.ts
3265
+ import { loadSkills, formatSkillsForPrompt } from "@earendil-works/pi-coding-agent";
3266
+ import { fileURLToPath } from "url";
3267
+ import { existsSync as existsSync11 } from "fs";
3268
+ import { dirname as dirname2, join as join18 } from "path";
3269
+ function packageRoot(start) {
3270
+ let dir = start;
3271
+ for (let i = 0; i < 10; i++) {
3272
+ if (existsSync11(join18(dir, "package.json"))) return dir;
3273
+ const parent = dirname2(dir);
3274
+ if (parent === dir) break;
3275
+ dir = parent;
3276
+ }
3277
+ return start;
3278
+ }
3279
+ function skillsDir() {
3280
+ if (process.env.ORIRO_SKILLS_DIR) return process.env.ORIRO_SKILLS_DIR;
3281
+ return join18(packageRoot(dirname2(fileURLToPath(import.meta.url))), "skills");
3282
+ }
3283
+ async function loadOriroSkills(dir = skillsDir()) {
3284
+ const result = await loadSkills({
3285
+ cwd: dir,
3286
+ agentDir: dir,
3287
+ skillPaths: [dir],
3288
+ includeDefaults: false
3289
+ });
3290
+ const all = Array.isArray(result) ? result : result.skills ?? [];
3291
+ return {
3292
+ all,
3293
+ core: all.filter((s) => !s.disableModelInvocation),
3294
+ tail: all.filter((s) => s.disableModelInvocation),
3295
+ prompt: formatSkillsForPrompt(all)
3296
+ };
3297
+ }
3298
+
3299
+ // src/onboarding/assemble.ts
3300
+ async function assembleOriroSession(opts = {}) {
3301
+ const cwd = opts.cwd ?? process.cwd();
3302
+ const authStorage = AuthStorage2.inMemory();
3303
+ const modelRegistry = ModelRegistry2.inMemory(authStorage);
3304
+ const settingsManager = SettingsManager.create(cwd);
3305
+ const model = registerOriroMux(modelRegistry);
3306
+ if (!model) throw new Error("ORIRO keyless model unavailable");
3307
+ const resourceLoader = new DefaultResourceLoader({
3308
+ cwd,
3309
+ agentDir: getAgentDir(),
3310
+ settingsManager,
3311
+ additionalSkillPaths: [skillsDir()],
3312
+ extensionFactories: [registerGuardian, registerHead, registerScribe, registerOrchestrator]
3313
+ });
3314
+ await resourceLoader.reload();
3315
+ const { session, extensionsResult } = await createAgentSession2({
3316
+ model,
3317
+ authStorage,
3318
+ modelRegistry,
3319
+ settingsManager,
3320
+ sessionManager: SessionManager2.inMemory(),
3321
+ resourceLoader
3322
+ });
3323
+ attachScribe(session);
3324
+ return { session, extensionsResult };
3325
+ }
3326
+
3327
+ // src/language/nllb-translator.ts
3328
+ var NLLB_CODE = {
3329
+ en: "eng_Latn",
3330
+ zh: "zho_Hans",
3331
+ de: "deu_Latn",
3332
+ es: "spa_Latn",
3333
+ ru: "rus_Cyrl",
3334
+ ko: "kor_Hang",
3335
+ fr: "fra_Latn",
3336
+ ja: "jpn_Jpan",
3337
+ pt: "por_Latn",
3338
+ tr: "tur_Latn",
3339
+ pl: "pol_Latn",
3340
+ ca: "cat_Latn",
3341
+ nl: "nld_Latn",
3342
+ ar: "arb_Arab",
3343
+ sv: "swe_Latn",
3344
+ it: "ita_Latn",
3345
+ id: "ind_Latn",
3346
+ hi: "hin_Deva",
3347
+ fi: "fin_Latn",
3348
+ vi: "vie_Latn",
3349
+ he: "heb_Hebr",
3350
+ uk: "ukr_Cyrl",
3351
+ el: "ell_Grek",
3352
+ ms: "zsm_Latn",
3353
+ cs: "ces_Latn",
3354
+ ro: "ron_Latn",
3355
+ da: "dan_Latn",
3356
+ hu: "hun_Latn",
3357
+ ta: "tam_Taml",
3358
+ no: "nob_Latn",
3359
+ th: "tha_Thai",
3360
+ ur: "urd_Arab",
3361
+ hr: "hrv_Latn",
3362
+ bg: "bul_Cyrl",
3363
+ lt: "lit_Latn",
3364
+ mi: "mri_Latn",
3365
+ ml: "mal_Mlym",
3366
+ cy: "cym_Latn",
3367
+ sk: "slk_Latn",
3368
+ te: "tel_Telu",
3369
+ fa: "pes_Arab",
3370
+ lv: "lvs_Latn",
3371
+ bn: "ben_Beng",
3372
+ sr: "srp_Cyrl",
3373
+ az: "azj_Latn",
3374
+ sl: "slv_Latn",
3375
+ kn: "kan_Knda",
3376
+ et: "est_Latn",
3377
+ mk: "mkd_Cyrl",
3378
+ eu: "eus_Latn",
3379
+ is: "isl_Latn",
3380
+ hy: "hye_Armn",
3381
+ ne: "npi_Deva",
3382
+ mn: "khk_Cyrl",
3383
+ bs: "bos_Latn",
3384
+ kk: "kaz_Cyrl",
3385
+ sq: "als_Latn",
3386
+ sw: "swh_Latn",
3387
+ gl: "glg_Latn",
3388
+ mr: "mar_Deva",
3389
+ pa: "pan_Guru",
3390
+ si: "sin_Sinh",
3391
+ km: "khm_Khmr",
3392
+ sn: "sna_Latn",
3393
+ yo: "yor_Latn",
3394
+ so: "som_Latn",
3395
+ af: "afr_Latn",
3396
+ oc: "oci_Latn",
3397
+ ka: "kat_Geor",
3398
+ be: "bel_Cyrl",
3399
+ tg: "tgk_Cyrl",
3400
+ sd: "snd_Arab",
3401
+ gu: "guj_Gujr",
3402
+ am: "amh_Ethi",
3403
+ yi: "ydd_Hebr",
3404
+ lo: "lao_Laoo",
3405
+ uz: "uzn_Latn",
3406
+ fo: "fao_Latn",
3407
+ ht: "hat_Latn",
3408
+ ps: "pbt_Arab",
3409
+ tk: "tuk_Latn",
3410
+ nn: "nno_Latn",
3411
+ mt: "mlt_Latn",
3412
+ sa: "san_Deva",
3413
+ lb: "ltz_Latn",
3414
+ my: "mya_Mymr",
3415
+ bo: "bod_Tibt",
3416
+ tl: "tgl_Latn",
3417
+ mg: "plt_Latn",
3418
+ as: "asm_Beng",
3419
+ tt: "tat_Cyrl",
3420
+ ln: "lin_Latn",
3421
+ ha: "hau_Latn",
3422
+ ba: "bak_Cyrl",
3423
+ jw: "jav_Latn",
3424
+ su: "sun_Latn",
3425
+ yue: "yue_Hant"
3426
+ };
3427
+ var ENG = "eng_Latn";
3428
+ var toNllb = (iso) => NLLB_CODE[(iso || "").toLowerCase()] ?? ENG;
3429
+ var NllbTranslator = class {
3430
+ pipe = null;
3431
+ loading = null;
3432
+ ready() {
3433
+ return this.pipe !== null;
3434
+ }
3435
+ /** Lazy-load NLLB-200 once (first-use download + cache). Idempotent. */
3436
+ async load(modelId = "Xenova/nllb-200-distilled-600M") {
3437
+ if (this.pipe) return;
3438
+ if (this.loading) return this.loading;
3439
+ this.loading = (async () => {
3440
+ const { pipeline } = await import("@huggingface/transformers");
3441
+ this.pipe = await pipeline("translation", modelId);
3442
+ })();
3443
+ return this.loading;
3444
+ }
3445
+ async run(text, src, tgt) {
3446
+ if (!this.pipe) await this.load();
3447
+ if (!this.pipe) return text;
3448
+ const out = await this.pipe(text, { src_lang: src, tgt_lang: tgt });
3449
+ return out?.[0]?.translation_text?.trim() || text;
3450
+ }
3451
+ toEnglish(text, fromLang) {
3452
+ return this.run(text, toNllb(fromLang), ENG);
3453
+ }
3454
+ fromEnglish(english, toLang) {
3455
+ return this.run(english, ENG, toNllb(toLang));
3456
+ }
3457
+ };
3458
+ var instance = null;
3459
+ function setupNllbTranslator(opts) {
3460
+ if (!instance) {
3461
+ instance = new NllbTranslator();
3462
+ registerTranslator(instance);
3463
+ }
3464
+ if (opts?.preload) void instance.load();
3465
+ return instance;
3466
+ }
3467
+
3468
+ // src/language/gateway.ts
3469
+ var isEnglish2 = (code) => !code || code.toLowerCase().startsWith("en");
3470
+ var isCommand = (text) => text.trimStart().startsWith("/");
3471
+ async function ensureReady() {
3472
+ try {
3473
+ await setupNllbTranslator().load();
3474
+ } catch {
3475
+ }
3476
+ }
3477
+ async function translateIncoming(message) {
3478
+ const lang = getTerminalLanguage().code;
3479
+ if (isEnglish2(lang) || !message.trim() || isCommand(message)) return message;
3480
+ await ensureReady();
3481
+ return translateForCoder(message, lang);
3482
+ }
3483
+ async function translateOutgoing(text) {
3484
+ const lang = getTerminalLanguage().code;
3485
+ if (isEnglish2(lang) || !text.trim()) return text;
3486
+ await ensureReady();
3487
+ return translateForUser(text, lang);
3488
+ }
3489
+
3490
+ // src/repl-ui/tui-repl.ts
3491
+ import { ProcessTerminal, TUI, Editor, Text, Container } from "@earendil-works/pi-tui";
3492
+
3493
+ // src/repl-ui/permission.ts
3494
+ var MODES = ["manual", "accept_edits", "auto", "plan"];
3495
+ var MODE_META = {
3496
+ manual: { label: "Manual", indicator: "\u25CF" },
3497
+ accept_edits: { label: "Accept Edits", indicator: "\u270E" },
3498
+ auto: { label: "Auto", indicator: "\u23F5\u23F5" },
3499
+ plan: { label: "Plan", indicator: "\u25A2" }
3500
+ };
3501
+ var current = "manual";
3502
+ function getMode() {
3503
+ return current;
3504
+ }
3505
+ function cycleMode() {
3506
+ const i = MODES.indexOf(current);
3507
+ current = MODES[(i + 1) % MODES.length];
3508
+ return current;
3509
+ }
3510
+
3511
+ // src/repl-ui/tui-repl.ts
3512
+ var editorTheme = {
3513
+ borderColor: (s) => dim(s),
3514
+ selectList: {
3515
+ selectedPrefix: (s) => accent(s),
3516
+ selectedText: (s) => accent(s),
3517
+ description: (s) => dim(s),
3518
+ scrollInfo: (s) => dim(s),
3519
+ noMatch: (s) => dim(s)
3520
+ }
3521
+ };
3522
+ function footerText() {
3523
+ const cur = getMode();
3524
+ const bar = MODES.map((m) => {
3525
+ const meta = MODE_META[m];
3526
+ const s = `${meta.indicator} ${meta.label}`;
3527
+ return m === cur ? accent(s) : dim(s);
3528
+ }).join(dim(" \xB7 "));
3529
+ return `${bar} ${dim("Shift+Tab to switch \xB7 /exit")}`;
3530
+ }
3531
+ async function runTuiRepl(session) {
3532
+ const isEnglish3 = getTerminalLanguage().code.toLowerCase().startsWith("en");
3533
+ const term = new ProcessTerminal();
3534
+ const tui = new TUI(term, true);
3535
+ const chat = new Container();
3536
+ const editor = new Editor(tui, editorTheme, { paddingX: 1 });
3537
+ const sep = new Text(dim("\u2500".repeat(Math.max(8, term.columns))), 0, 0);
3538
+ const footer = new Text(footerText(), 0, 0);
3539
+ tui.addChild(chat);
3540
+ tui.addChild(editor);
3541
+ tui.addChild(sep);
3542
+ tui.addChild(footer);
3543
+ tui.setFocus(editor);
3544
+ const refreshFooter = () => {
3545
+ sep.setText(dim("\u2500".repeat(Math.max(8, term.columns))));
3546
+ footer.setText(footerText());
3547
+ tui.requestRender();
3548
+ };
3549
+ const removeListener = tui.addInputListener((data) => {
3550
+ if (data === "\x1B[Z") {
3551
+ cycleMode();
3552
+ refreshFooter();
3553
+ return { consume: true };
3554
+ }
3555
+ return void 0;
3556
+ });
3557
+ let stopped = false;
3558
+ const cleanup = () => {
3559
+ if (stopped) return;
3560
+ stopped = true;
3561
+ try {
3562
+ removeListener();
3563
+ } catch {
3564
+ }
3565
+ try {
3566
+ session.dispose();
3567
+ } catch {
3568
+ }
3569
+ try {
3570
+ tui.stop();
3571
+ } catch {
3572
+ }
3573
+ process.stdout.write(dim("\nBye.\n"));
3574
+ process.exit(0);
3575
+ };
3576
+ process.on("SIGINT", cleanup);
3577
+ let busy = false;
3578
+ editor.onSubmit = (raw) => {
3579
+ const text = raw.trim();
3580
+ if (!text || busy) return;
3581
+ const slash = text.toLowerCase();
3582
+ if (slash === "/exit" || slash === "/quit") return cleanup();
3583
+ if (slash === "/help" || slash === "/?") {
3584
+ chat.addChild(new Text(dim(" Just type to chat. Shift+Tab cycles posture. /exit to leave."), 0, 0));
3585
+ editor.setText("");
3586
+ tui.requestRender();
3587
+ return;
3588
+ }
3589
+ editor.addToHistory(text);
3590
+ editor.setText("");
3591
+ chat.addChild(new Text(`${accent("\u203A")} ${text}`, 0, 1));
3592
+ const streaming = new Text(dim("\u2026"), 0, 0);
3593
+ chat.addChild(streaming);
3594
+ tui.requestRender();
3595
+ busy = true;
3596
+ void (async () => {
3597
+ const english = await translateIncoming(text);
3598
+ noteUserInput(text);
3599
+ let out = "";
3600
+ const unsub = session.subscribe(
3601
+ (e) => {
3602
+ if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
3603
+ out += e.assistantMessageEvent.delta ?? "";
3604
+ if (isEnglish3) {
3605
+ streaming.setText(out);
3606
+ tui.requestRender();
3607
+ }
3608
+ }
3609
+ }
3610
+ );
3611
+ try {
3612
+ await session.prompt(english);
3613
+ } catch {
3614
+ streaming.setText(dim("(every free router is busy right now \u2014 give it a moment and try again)"));
3615
+ tui.requestRender();
3616
+ busy = false;
3617
+ unsub();
3618
+ return;
3619
+ }
3620
+ unsub();
3621
+ const finalText = isEnglish3 ? out.trim() : await translateOutgoing(out.trim());
3622
+ streaming.setText(finalText || dim("(no response)"));
3623
+ tui.requestRender();
3624
+ busy = false;
3625
+ })();
3626
+ };
3627
+ tui.start();
3628
+ refreshFooter();
3629
+ await new Promise(() => {
3630
+ });
3631
+ }
3632
+
3633
+ // src/repl.ts
3634
+ function replHelp() {
3635
+ return `
3636
+ ${accent("ORIRO terminal \u2014 help")}
3637
+ ${dim("Just type to chat; ORIRO writes and runs code for you (keyless, free).")}
3638
+
3639
+ ${accent("/help")} this help ${accent("/exit")} or ${accent("/quit")} leave ${dim("Ctrl-D / Ctrl-C also exit")}
3640
+ ${dim("Run these OUTSIDE the chat (in your shell):")}
3641
+ ${dim("oriro skills \xB7 routers \xB7 connectors \xB7 channels \xB7 scribe \xB7 language \xB7 avatar")}
3642
+
3643
+ `;
3644
+ }
3645
+ async function runRepl() {
3646
+ if (isFirstRun()) await runOnboarding();
3647
+ else stdout6.write(banner());
3648
+ const { session } = await assembleOriroSession();
3649
+ if (stdin5.isTTY && stdout6.isTTY) {
3650
+ await runTuiRepl(session);
3651
+ return;
3652
+ }
3653
+ await runReadlineRepl(session);
3654
+ }
3655
+ async function runReadlineRepl(session) {
3656
+ const isEnglish3 = getTerminalLanguage().code.toLowerCase().startsWith("en");
3657
+ const rl = createInterface5({ input: stdin5, output: stdout6 });
3658
+ let closing = false;
3659
+ const onSigint = () => {
3660
+ if (closing) return;
3661
+ closing = true;
3662
+ stdout6.write(dim("\nBye.\n"));
3663
+ try {
3664
+ rl.close();
3665
+ } catch {
3666
+ }
3667
+ try {
3668
+ session.dispose();
3669
+ } catch {
3670
+ }
3671
+ process.exit(0);
3672
+ };
3673
+ process.on("SIGINT", onSigint);
3674
+ try {
3675
+ for (; ; ) {
3676
+ let line;
3677
+ try {
3678
+ line = (await rl.question("\u203A ")).trim();
3679
+ } catch {
3680
+ break;
3681
+ }
3682
+ if (!line) continue;
3683
+ const slash = line.toLowerCase();
3684
+ if (slash === "/exit" || slash === "/quit") break;
3685
+ if (slash === "/help" || slash === "/?") {
3686
+ stdout6.write(replHelp());
3687
+ continue;
3688
+ }
3689
+ const english = await translateIncoming(line);
3690
+ noteUserInput(line);
3691
+ let out = "";
3692
+ const unsub = session.subscribe(
3693
+ (e) => {
3694
+ if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
3695
+ const d = e.assistantMessageEvent.delta ?? "";
3696
+ out += d;
3697
+ if (isEnglish3) stdout6.write(d);
3698
+ }
3699
+ }
3700
+ );
3701
+ try {
3702
+ await session.prompt(english);
3703
+ } finally {
3704
+ unsub();
3705
+ }
3706
+ if (isEnglish3) stdout6.write("\n\n");
3707
+ else stdout6.write(`${await translateOutgoing(out.trim())}
3708
+
3709
+ `);
3710
+ }
3711
+ } finally {
3712
+ process.removeListener("SIGINT", onSigint);
3713
+ if (!closing) {
3714
+ rl.close();
3715
+ session.dispose();
3716
+ stdout6.write(dim("\nBye.\n"));
3717
+ }
3718
+ }
3230
3719
  }
3231
3720
 
3232
3721
  // src/commands/ui.ts
@@ -3247,9 +3736,12 @@ var heading = (s) => {
3247
3736
  ${bold(accent(s))}
3248
3737
  `);
3249
3738
  };
3739
+ var DieError = class extends Error {
3740
+ };
3250
3741
  function die(msg) {
3251
3742
  fail(msg);
3252
- process.exit(1);
3743
+ process.exitCode = 1;
3744
+ throw new DieError(msg);
3253
3745
  }
3254
3746
 
3255
3747
  // src/commands/routers.ts
@@ -3278,15 +3770,127 @@ function registerRoutersCommand(program2) {
3278
3770
  ok(`added ${accent(slug)} (${res.validation.latencyMs}ms, model ${res.validation.model}) \u2192 active pool`);
3279
3771
  });
3280
3772
  routers.command("use <slugs...>").description("set the active router pool (ids must be added first)").action((slugs) => {
3281
- useRouters(slugs);
3282
- const pool = resolvePool();
3283
- const missing = slugs.filter((s) => !pool.some((p) => p.id === s));
3284
- ok(`pool set: ${pool.map((p) => p.id).join(", ") || "(empty)"}`);
3285
- if (missing.length) info(`not yet added (run \`oriro routers add\`): ${missing.join(", ")}`);
3773
+ const { applied, unknown } = useRouters(slugs);
3774
+ if (!applied.length) {
3775
+ die(`none of those are added yet: ${unknown.join(", ")} \u2014 run \`oriro routers add <slug>\` first`);
3776
+ }
3777
+ ok(`pool set: ${applied.join(", ")}`);
3778
+ if (unknown.length) info(`skipped (not added yet \u2014 run \`oriro routers add\`): ${unknown.join(", ")}`);
3286
3779
  });
3287
3780
  }
3288
3781
 
3289
3782
  // src/commands/scribe.ts
3783
+ import { readFileSync as readFileSync18 } from "fs";
3784
+
3785
+ // src/scribe/transcript.ts
3786
+ import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
3787
+ function parseHookStdin(raw) {
3788
+ try {
3789
+ const j = JSON.parse(raw);
3790
+ return {
3791
+ transcriptPath: typeof j.transcript_path === "string" ? j.transcript_path : void 0,
3792
+ cwd: typeof j.cwd === "string" ? j.cwd : void 0,
3793
+ sessionId: typeof j.session_id === "string" ? j.session_id : void 0,
3794
+ stopHookActive: j.stop_hook_active === true
3795
+ };
3796
+ } catch {
3797
+ return { stopHookActive: false };
3798
+ }
3799
+ }
3800
+ function shouldCapture(cwd) {
3801
+ if (process.env.ORIRO_SCRIBE_ONLY !== "1") return true;
3802
+ if (!cwd) return false;
3803
+ return /oriro/i.test(cwd.replace(/\\/g, "/"));
3804
+ }
3805
+ function textOf(content) {
3806
+ if (!content) return "";
3807
+ if (typeof content === "string") return content;
3808
+ return content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
3809
+ }
3810
+ function isHumanUser(e) {
3811
+ if (e.type !== "user" && e.message?.role !== "user") return false;
3812
+ const c = e.message?.content;
3813
+ if (typeof c === "string") return c.trim().length > 0;
3814
+ if (Array.isArray(c)) return c.some((b) => b.type === "text" && (b.text ?? "").trim().length > 0);
3815
+ return false;
3816
+ }
3817
+ var FILE_KEYS = ["file_path", "path", "notebook_path", "filePath"];
3818
+ function lastTurnFromTranscript(path) {
3819
+ if (!existsSync12(path)) return null;
3820
+ const raw = readFileSync17(path, "utf8");
3821
+ const entries = [];
3822
+ for (const line of raw.split("\n")) {
3823
+ if (!line.trim()) continue;
3824
+ try {
3825
+ entries.push(JSON.parse(line));
3826
+ } catch {
3827
+ }
3828
+ }
3829
+ if (entries.length === 0) return null;
3830
+ let anchor;
3831
+ let start = -1;
3832
+ for (let i = entries.length - 1; i >= 0; i--) {
3833
+ const e = entries[i];
3834
+ if (e && isHumanUser(e)) {
3835
+ start = i;
3836
+ anchor = e;
3837
+ break;
3838
+ }
3839
+ }
3840
+ const slice = start === -1 ? entries : entries.slice(start);
3841
+ const user = anchor ? textOf(anchor.message?.content) : "";
3842
+ const noteParts = [];
3843
+ const tools = /* @__PURE__ */ new Set();
3844
+ const files = /* @__PURE__ */ new Set();
3845
+ let ts;
3846
+ for (const e of slice) {
3847
+ if (e.timestamp) ts = e.timestamp;
3848
+ const role = e.type ?? e.message?.role;
3849
+ const content = e.message?.content;
3850
+ if (role === "assistant") {
3851
+ const t = textOf(content);
3852
+ if (t) noteParts.push(t);
3853
+ }
3854
+ if (Array.isArray(content)) {
3855
+ for (const b of content) {
3856
+ if (b.type === "tool_use" && b.name) {
3857
+ tools.add(b.name);
3858
+ const input = b.input ?? {};
3859
+ for (const k of FILE_KEYS) {
3860
+ const v = input[k];
3861
+ if (typeof v === "string" && v.trim()) files.add(v.trim());
3862
+ }
3863
+ }
3864
+ }
3865
+ }
3866
+ }
3867
+ const note = noteParts.join("\n\n").trim();
3868
+ if (!user && !note && tools.size === 0) return null;
3869
+ return {
3870
+ user: user || void 0,
3871
+ note: note || void 0,
3872
+ tools: tools.size ? [...tools] : void 0,
3873
+ files: files.size ? [...files] : void 0,
3874
+ ts
3875
+ };
3876
+ }
3877
+
3878
+ // src/commands/scribe.ts
3879
+ function readStdin() {
3880
+ try {
3881
+ return readFileSync18(0, "utf8");
3882
+ } catch {
3883
+ return "";
3884
+ }
3885
+ }
3886
+ function csv(v) {
3887
+ if (typeof v !== "string") return void 0;
3888
+ const arr = v.split(",").map((s) => s.trim()).filter(Boolean);
3889
+ return arr.length ? arr : void 0;
3890
+ }
3891
+ function hasContent(rec) {
3892
+ return Boolean(rec.user?.trim() || rec.note?.trim() || rec.tools?.length || rec.files?.length);
3893
+ }
3290
3894
  function registerScribeCommand(program2) {
3291
3895
  const scribe = program2.command("scribe").description("the consent-gated local work journal (off by default)");
3292
3896
  scribe.command("on").description("enable the journal (recorded locally at ~/.oriro/scribe, never leaves your machine)").action(() => {
@@ -3301,11 +3905,94 @@ function registerScribeCommand(program2) {
3301
3905
  scribe.command("status").description("show whether the journal is on or off").action(() => {
3302
3906
  info(isScribeEnabled() ? "Scriber: ON" : "Scriber: OFF (default)");
3303
3907
  });
3908
+ scribe.command("capture").description("capture one turn into the journal (used by the Claude Code Stop hook + /scribe skill)").option("--hook", "read the Claude Code Stop-hook JSON from stdin and capture the latest turn").option("--json <record>", "capture an explicit TurnRecord (JSON)").option("--user <text>", "the user/request text for this turn").option("--note <text>", "a note / assistant summary for this turn").option("--router <name>", "which router/model produced the turn").option("--files <list>", "comma-separated file paths touched").option("--tools <list>", "comma-separated tool names used").action((opts) => {
3909
+ try {
3910
+ if (!isScribeEnabled()) {
3911
+ if (!opts.hook) info("Scriber is OFF \u2014 run `oriro scribe on` first.");
3912
+ return;
3913
+ }
3914
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3915
+ let rec = null;
3916
+ if (opts.hook) {
3917
+ const hook = parseHookStdin(readStdin());
3918
+ if (hook.stopHookActive) return;
3919
+ if (!shouldCapture(hook.cwd)) return;
3920
+ if (!hook.transcriptPath) return;
3921
+ const turn = lastTurnFromTranscript(hook.transcriptPath);
3922
+ if (!turn) return;
3923
+ const ts = turn.ts ?? now;
3924
+ rec = {
3925
+ ts,
3926
+ date: ts.slice(0, 10),
3927
+ user: turn.user,
3928
+ note: turn.note,
3929
+ tools: turn.tools,
3930
+ files: turn.files,
3931
+ router: opts.router ?? "claude-code",
3932
+ context: hook.cwd ? `cwd: ${hook.cwd}` : void 0
3933
+ };
3934
+ } else if (opts.json) {
3935
+ const parsed = JSON.parse(opts.json);
3936
+ const ts = parsed.ts ?? now;
3937
+ rec = { ...parsed, ts, date: parsed.date ?? ts.slice(0, 10) };
3938
+ } else {
3939
+ rec = {
3940
+ ts: now,
3941
+ date: now.slice(0, 10),
3942
+ user: opts.user,
3943
+ note: opts.note,
3944
+ router: opts.router,
3945
+ files: csv(opts.files),
3946
+ tools: csv(opts.tools)
3947
+ };
3948
+ }
3949
+ if (!rec || !hasContent(rec)) {
3950
+ if (!opts.hook) info("nothing to capture.");
3951
+ return;
3952
+ }
3953
+ const res = supervisedCapture(rec);
3954
+ if (!opts.hook) {
3955
+ if (res) {
3956
+ const red = res.redactions.length ? ` (redacted: ${res.redactions.map((r) => `${r.label}\xD7${r.count}`).join(", ")})` : "";
3957
+ ok(`captured \u2192 ${res.journalDate}.md${red}`);
3958
+ } else {
3959
+ info("capture deferred (logged); will retry next turn.");
3960
+ }
3961
+ }
3962
+ } catch (err) {
3963
+ if (!opts.hook) fail(`scribe capture: ${err instanceof Error ? err.message : String(err)}`);
3964
+ }
3965
+ });
3966
+ scribe.command("recall <query>").description("full-text search across every day's journal").option("-n, --limit <n>", "max matches", "50").action((query, opts) => {
3967
+ const limit = Math.max(1, Number(opts.limit) || 50);
3968
+ const hits = searchScribe(query, limit);
3969
+ if (!hits.length) {
3970
+ info(`no matches for "${query}".`);
3971
+ return;
3972
+ }
3973
+ heading(`Scribe \u2014 ${hits.length} match(es) for "${query}"`);
3974
+ for (const h of hits) info(`${h.date}:${h.line} \xB7 ${h.text}`);
3975
+ });
3976
+ scribe.command("digest").description("print the rolling digest (recent context, injectable in a flash)").action(() => {
3977
+ const d = readDigest();
3978
+ process.stdout.write(d?.trim() ? `${d.trim()}
3979
+ ` : "\xB7 digest empty (nothing captured yet).\n");
3980
+ });
3981
+ scribe.command("timeline").description("print the full-history timeline (one line per day)").action(() => {
3982
+ const t = readTimeline();
3983
+ process.stdout.write(t?.trim() ? `${t.trim()}
3984
+ ` : "\xB7 timeline empty (nothing captured yet).\n");
3985
+ });
3986
+ scribe.command("health").description("show the scribe writer's health (last write, fault count)").action(() => {
3987
+ const h = readHealth();
3988
+ info(`last write: ${h.lastWriteAt ?? "never"}`);
3989
+ info(`faults: ${h.faultCount}${h.lastFault ? ` (last: ${h.lastFault})` : ""}`);
3990
+ });
3304
3991
  }
3305
3992
 
3306
3993
  // src/connectors/connectors.ts
3307
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync13 } from "fs";
3308
- import { join as join18 } from "path";
3994
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
3995
+ import { join as join19 } from "path";
3309
3996
 
3310
3997
  // src/connectors/catalog.ts
3311
3998
  var CONNECTOR_CATALOG = [
@@ -4295,18 +4982,18 @@ function connectorBySlug(slug) {
4295
4982
 
4296
4983
  // src/connectors/connectors.ts
4297
4984
  function file2() {
4298
- return join18(oriroDir(), "connectors.json");
4985
+ return join19(oriroDir(), "connectors.json");
4299
4986
  }
4300
4987
  function readAdded() {
4301
4988
  try {
4302
- const v = JSON.parse(readFileSync17(file2(), "utf8"));
4989
+ const v = JSON.parse(readFileSync19(file2(), "utf8"));
4303
4990
  return Array.isArray(v) ? v : [];
4304
4991
  } catch {
4305
4992
  return [];
4306
4993
  }
4307
4994
  }
4308
4995
  function writeAdded(slugs) {
4309
- writeFileSync13(join18(ensureOriroDir(), "connectors.json"), JSON.stringify([...new Set(slugs)], null, 2), "utf8");
4996
+ writeFileSync14(join19(ensureOriroDir(), "connectors.json"), JSON.stringify([...new Set(slugs)], null, 2), "utf8");
4310
4997
  }
4311
4998
  function listConnectors(category) {
4312
4999
  return category ? CONNECTOR_CATALOG.filter((c) => c.category === category) : CONNECTOR_CATALOG;
@@ -4314,6 +5001,9 @@ function listConnectors(category) {
4314
5001
  function connectorCategories() {
4315
5002
  return [...new Set(CONNECTOR_CATALOG.map((c) => c.category))].sort();
4316
5003
  }
5004
+ function isConnectorAdded(slug) {
5005
+ return readAdded().includes(slug);
5006
+ }
4317
5007
  function addConnector(slug) {
4318
5008
  const entry = connectorBySlug(slug);
4319
5009
  if (!entry) return { ok: false, error: `unknown connector '${slug}' \u2014 run \`oriro connectors list\`` };
@@ -4338,23 +5028,30 @@ function registerConnectorsCommand(program2) {
4338
5028
  const connectors = program2.command("connectors").description("MCP connectors \u2014 add external tools/services (inert until used)");
4339
5029
  connectors.command("list [category]").description("list the connector catalog (optionally filtered by category)").action((category) => {
4340
5030
  if (category && !connectorCategories().includes(category)) {
4341
- info(`unknown category '${category}' \u2014 categories: ${connectorCategories().join(", ")}`);
4342
- return;
5031
+ die(`unknown category '${category}' \u2014 categories: ${connectorCategories().join(", ")}`);
4343
5032
  }
4344
5033
  const entries = listConnectors(category);
4345
5034
  const added = new Set(addedConnectors().map((c) => c.slug));
4346
5035
  heading(category ? `Connectors \xB7 ${category}` : "Connectors");
5036
+ let addable = 0;
4347
5037
  for (const c of entries) {
4348
- const mark = added.has(c.slug) ? accent("\u25CF") : dim("\u25CB");
4349
- process.stdout.write(` ${mark} ${accent(c.slug.padEnd(20))} ${c.name.padEnd(22)} ${dim(c.category)}
5038
+ const canAdd = !!c.mcpUrl;
5039
+ if (canAdd) addable++;
5040
+ const mark = !canAdd ? dim("\xB7") : added.has(c.slug) ? accent("\u25CF") : dim("\u25CB");
5041
+ const name = canAdd ? c.name.padEnd(22) : dim(`${c.name} (coming soon)`.padEnd(22));
5042
+ process.stdout.write(` ${mark} ${(canAdd ? accent : dim)(c.slug.padEnd(20))} ${name} ${dim(c.category)}
4350
5043
  `);
4351
5044
  }
4352
- info(`${entries.length} connectors${category ? ` in '${category}'` : ""} \xB7 ${added.size} added`);
5045
+ info(`${addable} addable${category ? ` in '${category}'` : ""} \xB7 ${added.size} added \xB7 ${entries.length - addable} coming soon`);
4353
5046
  });
4354
5047
  connectors.command("add <slug>").description("add a connector (validate + record; connects only when used)").action((slug) => {
5048
+ if (isConnectorAdded(slug)) {
5049
+ info(`${slug} is already added`);
5050
+ return;
5051
+ }
4355
5052
  const res = addConnector(slug);
4356
5053
  if (!res.ok) die(res.error ?? `could not add '${slug}'`);
4357
- ok(`added ${accent(slug)} \u2014 inert until a session uses it`);
5054
+ ok(`added ${accent(slug)} \u2014 recorded locally`);
4358
5055
  });
4359
5056
  connectors.command("remove <slug>").description("remove a connector").action((slug) => {
4360
5057
  if (removeConnector(slug)) ok(`removed ${accent(slug)}`);
@@ -4363,14 +5060,14 @@ function registerConnectorsCommand(program2) {
4363
5060
  }
4364
5061
 
4365
5062
  // src/channels/config.ts
4366
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync14 } from "fs";
4367
- import { join as join19 } from "path";
5063
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync15 } from "fs";
5064
+ import { join as join20 } from "path";
4368
5065
  function file3() {
4369
- return join19(oriroDir(), "channels.json");
5066
+ return join20(oriroDir(), "channels.json");
4370
5067
  }
4371
5068
  function readChannels() {
4372
5069
  try {
4373
- const v = JSON.parse(readFileSync18(file3(), "utf8"));
5070
+ const v = JSON.parse(readFileSync20(file3(), "utf8"));
4374
5071
  return Array.isArray(v) ? v : [];
4375
5072
  } catch {
4376
5073
  return [];
@@ -4379,10 +5076,10 @@ function readChannels() {
4379
5076
  function saveChannel(cfg) {
4380
5077
  const all = readChannels().filter((c) => c.kind !== cfg.kind);
4381
5078
  all.push(cfg);
4382
- writeFileSync14(join19(ensureOriroDir(), "channels.json"), JSON.stringify(all, null, 2), "utf8");
5079
+ writeFileSync15(join20(ensureOriroDir(), "channels.json"), JSON.stringify(all, null, 2), "utf8");
4383
5080
  }
4384
5081
  function removeChannel(kind) {
4385
- writeFileSync14(join19(ensureOriroDir(), "channels.json"), JSON.stringify(readChannels().filter((c) => c.kind !== kind), null, 2), "utf8");
5082
+ writeFileSync15(join20(ensureOriroDir(), "channels.json"), JSON.stringify(readChannels().filter((c) => c.kind !== kind), null, 2), "utf8");
4386
5083
  }
4387
5084
 
4388
5085
  // src/channels/telegram.ts
@@ -4411,6 +5108,7 @@ var OriroChannelHost = class {
4411
5108
  if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") out += e.assistantMessageEvent.delta ?? "";
4412
5109
  });
4413
5110
  try {
5111
+ noteUserInput(text);
4414
5112
  await session.prompt(text);
4415
5113
  } finally {
4416
5114
  unsub();
@@ -4498,9 +5196,9 @@ async function startDiscord(token) {
4498
5196
  }
4499
5197
 
4500
5198
  // src/channels/whatsapp.ts
4501
- import { join as join20 } from "path";
5199
+ import { join as join21 } from "path";
4502
5200
  function whatsappAuthDir() {
4503
- return join20(oriroDir(), "whatsapp-auth");
5201
+ return join21(oriroDir(), "whatsapp-auth");
4504
5202
  }
4505
5203
  async function startWhatsApp() {
4506
5204
  let baileys;
@@ -4591,7 +5289,7 @@ function registerChannelsCommand(program2) {
4591
5289
  if (!isKind(kind)) die(`unknown channel '${kind}' \u2014 one of: ${KINDS.join(", ")}`);
4592
5290
  if (kind === "whatsapp") {
4593
5291
  if (!opts.acceptRisk) {
4594
- fail("WhatsApp uses Baileys, which pairs a REAL WhatsApp account and may violate WhatsApp's ToS (ban risk).");
5292
+ info("WhatsApp uses Baileys, which pairs a REAL WhatsApp account and may violate WhatsApp's ToS (ban risk).");
4595
5293
  info("If you accept that risk, re-run: `oriro channels start whatsapp --accept-risk`");
4596
5294
  return;
4597
5295
  }
@@ -4635,7 +5333,7 @@ function registerSkillsCommand(program2) {
4635
5333
  }
4636
5334
 
4637
5335
  // src/commands/language.ts
4638
- import { stdin as stdin5 } from "process";
5336
+ import { stdin as stdin6 } from "process";
4639
5337
  function resolveLanguage(input) {
4640
5338
  return languageByCode(input) ?? LANGUAGES.find((l) => l.name.toLowerCase() === input.trim().toLowerCase());
4641
5339
  }
@@ -4657,7 +5355,7 @@ function registerLanguageCommand(program2) {
4657
5355
  ok(`${accent(lang.name)} is now your terminal language.`);
4658
5356
  return;
4659
5357
  }
4660
- if (stdin5.isTTY) {
5358
+ if (stdin6.isTTY) {
4661
5359
  const lang = await selectLanguageInteractive();
4662
5360
  setTerminalLanguage(lang);
4663
5361
  ok(`${accent(lang.name)} is now your terminal language.`);
@@ -4670,7 +5368,7 @@ function registerLanguageCommand(program2) {
4670
5368
  }
4671
5369
 
4672
5370
  // src/commands/avatar.ts
4673
- import { stdin as stdin6 } from "process";
5371
+ import { stdin as stdin7 } from "process";
4674
5372
  function registerAvatarCommand(program2) {
4675
5373
  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) => {
4676
5374
  if (opts.list) {
@@ -4688,7 +5386,7 @@ function registerAvatarCommand(program2) {
4688
5386
  ok(`${accent(avatar.slug)} is now your terminal face.`);
4689
5387
  return;
4690
5388
  }
4691
- if (stdin6.isTTY) {
5389
+ if (stdin7.isTTY) {
4692
5390
  const chosen = await selectAvatarInteractive();
4693
5391
  if (!chosen) {
4694
5392
  info("no change.");
@@ -4709,7 +5407,12 @@ var version = createRequire(import.meta.url)("../package.json").version;
4709
5407
  var program = new Command();
4710
5408
  program.name("oriro").description("ORIRO \u2014 a free, on-device-friendly terminal AI agent.").version(version, "-v, --version").action(async (_options, command) => {
4711
5409
  if (command.args.length > 0) {
4712
- process.stderr.write(`error: unknown command '${command.args[0]}'
5410
+ const arg = command.args[0];
5411
+ if (arg === "help") {
5412
+ command.outputHelp();
5413
+ return;
5414
+ }
5415
+ process.stderr.write(`error: unknown command '${arg}'
4713
5416
  Run 'oriro --help' to see available commands.
4714
5417
  `);
4715
5418
  process.exitCode = 1;
@@ -4725,6 +5428,7 @@ registerSkillsCommand(program);
4725
5428
  registerLanguageCommand(program);
4726
5429
  registerAvatarCommand(program);
4727
5430
  program.parseAsync().catch((e) => {
5431
+ if (e instanceof DieError) return;
4728
5432
  process.stderr.write(`
4729
5433
  ORIRO error: ${e instanceof Error ? e.stack ?? e.message : String(e)}
4730
5434
  `);