@oriro/orirocli 0.1.4 → 0.1.5

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 (2) hide show
  1. package/dist/cli.js +1290 -1026
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
 
7
7
  // src/repl.ts
8
8
  import { createInterface as createInterface4 } from "readline/promises";
9
- import { stdin as stdin4, stdout as stdout4 } from "process";
9
+ import { stdin as stdin4, stdout as stdout5 } from "process";
10
10
 
11
11
  // src/ui/theme.ts
12
12
  var PALETTE = {
@@ -72,7 +72,7 @@ ${tagline}
72
72
 
73
73
  // src/onboarding/wrapper.ts
74
74
  import { createInterface as createInterface3 } from "readline/promises";
75
- import { stdin as stdin3, stdout as stdout3 } from "process";
75
+ import { stdin as stdin3, stdout as stdout4 } 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);
433
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;
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
  }
@@ -1237,22 +1329,22 @@ function hasScribeChoice() {
1237
1329
 
1238
1330
  // src/onboarding/wrapper.ts
1239
1331
  function isFirstRun() {
1240
- return !isLanguageConfigured();
1332
+ return !isLanguageConfigured() || !hasScribeChoice();
1241
1333
  }
1242
1334
  async function askYesNo(question) {
1243
- const rl = createInterface3({ input: stdin3, output: stdout3 });
1335
+ const rl = createInterface3({ input: stdin3, output: stdout4 });
1244
1336
  try {
1245
- const a = (await rl.question(`${question} ${dim("[Y/n]")} `)).trim().toLowerCase();
1337
+ const a = (await ask(rl, `${question} ${dim("[Y/n]")} `)).trim().toLowerCase();
1246
1338
  return a === "" || a === "y" || a === "yes";
1247
1339
  } finally {
1248
1340
  rl.close();
1249
1341
  }
1250
1342
  }
1251
1343
  async function runOnboarding() {
1252
- stdout3.write(banner());
1344
+ stdout4.write(banner());
1253
1345
  await runLanguageOnboarding();
1254
1346
  await activateGuardian();
1255
- stdout3.write(` ${accent("\u{1F6E1} Guardian V3")} is on by default. ${accent("\u{1F9ED} Head")} is ready.
1347
+ stdout4.write(` ${accent("\u{1F6E1} Guardian V3")} is on by default. ${accent("\u{1F9ED} Head")} is ready.
1256
1348
 
1257
1349
  `);
1258
1350
  if (!isAvatarConfigured()) await runAvatarOnboarding();
@@ -1261,11 +1353,11 @@ async function runOnboarding() {
1261
1353
  "Remember with me? The Scriber keeps your work in context on THIS machine only \u2014 it never leaves it."
1262
1354
  );
1263
1355
  setScribeConsent(yes);
1264
- stdout3.write(yes ? ` ${accent("\u{1F4D3} Scriber")} on.
1356
+ stdout4.write(yes ? ` ${accent("\u{1F4D3} Scriber")} on.
1265
1357
  ` : ` ${dim("Scriber off \u2014 `oriro scribe on` anytime.")}
1266
1358
  `);
1267
1359
  }
1268
- stdout3.write(`
1360
+ stdout4.write(`
1269
1361
  ${accent("ORIRO is ready.")} ${dim("Type to chat \xB7 /exit to leave")}
1270
1362
 
1271
1363
  `);
@@ -1357,6 +1449,24 @@ var RouterMux = class {
1357
1449
  for (const s of stats) if (this.stats.has(s.id)) this.stats.set(s.id, { ...s });
1358
1450
  }
1359
1451
  };
1452
+ function healthStatePath(dir) {
1453
+ return join10(dir, "routers", "health.json");
1454
+ }
1455
+ function saveMuxState(dir, stats) {
1456
+ const p = healthStatePath(dir);
1457
+ mkdirSync5(join10(dir, "routers"), { recursive: true });
1458
+ writeFileSync7(p, JSON.stringify(stats, null, 2), "utf8");
1459
+ }
1460
+ function loadMuxState(dir) {
1461
+ const p = healthStatePath(dir);
1462
+ if (!existsSync3(p)) return [];
1463
+ try {
1464
+ const stats = JSON.parse(readFileSync8(p, "utf8"));
1465
+ return stats.map((s) => ({ ...s, latencyMs: Number.isFinite(s.latencyMs) ? s.latencyMs : Number.POSITIVE_INFINITY }));
1466
+ } catch {
1467
+ return [];
1468
+ }
1469
+ }
1360
1470
 
1361
1471
  // src/routers/floor.ts
1362
1472
  var KEYLESS_FLOOR = [
@@ -1425,7 +1535,7 @@ async function validateRouter(entry, key, modelId) {
1425
1535
  try {
1426
1536
  let res;
1427
1537
  if (entry.api === "google-generative-ai") {
1428
- const url = `${entry.baseUrl.replace(/\/$/, "")}/models/${model}:generateContent${key ? `?key=${key}` : ""}`;
1538
+ const url = `${entry.baseUrl.replace(/\/$/, "")}/models/${model}:generateContent${key ? `?key=${encodeURIComponent(key)}` : ""}`;
1429
1539
  res = await fetch(url, {
1430
1540
  method: "POST",
1431
1541
  headers: { "content-type": "application/json" },
@@ -1484,6 +1594,9 @@ async function addRouter(entry, opts) {
1484
1594
  if (entry.comingSoon) {
1485
1595
  return { ok: false, validation: { ok: false, latencyMs: 0, model: "", error: "coming soon" } };
1486
1596
  }
1597
+ if (entry.kind && entry.kind !== "chat") {
1598
+ return { ok: false, validation: { ok: false, latencyMs: 0, model: "", error: `'${entry.id}' is a ${entry.kind} router, not a chat router` } };
1599
+ }
1487
1600
  const key = opts?.key ?? (entry.keyless ? KEYLESS_SENTINEL : void 0);
1488
1601
  const v = await validateRouter(entry, key, opts?.modelId);
1489
1602
  if (!v.ok) return { ok: false, validation: v };
@@ -1501,7 +1614,11 @@ async function addRouter(entry, opts) {
1501
1614
  return { ok: true, validation: v };
1502
1615
  }
1503
1616
  function useRouters(ids) {
1504
- savePool(oriroDir(), ids);
1617
+ const reg = readReg();
1618
+ const applied = ids.filter((id) => reg[id]);
1619
+ const unknown = ids.filter((id) => !reg[id]);
1620
+ if (applied.length > 0) savePool(oriroDir(), applied);
1621
+ return { applied, unknown };
1505
1622
  }
1506
1623
  function resolvePool() {
1507
1624
  const reg = readReg();
@@ -1581,955 +1698,1040 @@ function sanitizeEventToolCalls(ev) {
1581
1698
  return next;
1582
1699
  }
1583
1700
 
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 } : {};
1701
+ // src/scribe/scribe-pi.ts
1702
+ import { existsSync as existsSync9, readFileSync as readFileSync16 } from "fs";
1703
+ import { Type } from "typebox";
1704
+
1705
+ // src/scribe/capture.ts
1706
+ import { closeSync as closeSync2, fsyncSync as fsyncSync2, mkdirSync as mkdirSync10, openSync as openSync2, writeSync as writeSync2 } from "fs";
1707
+ import { join as join14 } from "path";
1708
+
1709
+ // src/scribe/digest.ts
1710
+ import { existsSync as existsSync5, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
1711
+
1712
+ // src/scribe/paths.ts
1713
+ import { join as join13 } from "path";
1714
+ function scribeDir() {
1715
+ const override = process.env.ORIRO_SCRIBE_DIR?.trim();
1716
+ return override && override.length > 0 ? override : join13(CONFIG_DIR, "scribe");
1590
1717
  }
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
- };
1718
+ function journalFile(date) {
1719
+ return join13(scribeDir(), `${date}.md`);
1603
1720
  }
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);
1721
+ function digestFile() {
1722
+ return join13(scribeDir(), "_digest.md");
1654
1723
  }
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);
1724
+ function timelineFile() {
1725
+ return join13(scribeDir(), "_timeline.md");
1726
+ }
1727
+ function artifactsDir() {
1728
+ return join13(scribeDir(), "artifacts");
1688
1729
  }
1689
1730
 
1690
- // src/head/pi-tool.ts
1691
- import { Type } from "typebox";
1731
+ // src/scribe/digest.ts
1732
+ var DIGEST_CAP = 8192;
1733
+ var TIMELINE_DAY_CAP = 400;
1734
+ function read(file4) {
1735
+ return existsSync5(file4) ? readFileSync11(file4, "utf8") : "";
1736
+ }
1737
+ function updateDigest(summary, context) {
1738
+ mkdirSync8(scribeDir(), { recursive: true });
1739
+ const existing = read(digestFile());
1740
+ let contextBlock = context?.trim();
1741
+ if (!contextBlock) {
1742
+ const m = existing.match(/## Context\n([\s\S]*?)\n## /);
1743
+ contextBlock = m?.[1]?.trim() ?? "_(not set yet)_";
1744
+ }
1745
+ const recentMatch = existing.match(/## Recent activity[^\n]*\n([\s\S]*)$/);
1746
+ const priorRecent = recentMatch?.[1]?.trim() ?? "";
1747
+ let recent = summary.trim() ? `- ${summary.trim()}
1748
+ ${priorRecent}` : priorRecent;
1749
+ const header2 = `# ORIRO Scribe \u2014 Digest
1692
1750
 
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."
1751
+ ## Context
1752
+ ${contextBlock}
1753
+
1754
+ ## Recent activity (newest first)
1755
+ `;
1756
+ let out = header2 + recent;
1757
+ while (Buffer.byteLength(out, "utf8") > DIGEST_CAP && recent.includes("\n")) {
1758
+ recent = recent.slice(0, recent.lastIndexOf("\n")).trimEnd();
1759
+ out = header2 + recent;
1802
1760
  }
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();
1761
+ writeFileSync10(digestFile(), out, "utf8");
1762
+ }
1763
+ function updateTimeline(date, topic) {
1764
+ mkdirSync8(scribeDir(), { recursive: true });
1765
+ const clean = topic.replace(/\s+/g, " ").trim();
1766
+ if (!clean) return;
1767
+ const lines = read(timelineFile()).split("\n").filter(Boolean);
1768
+ const header2 = "# ORIRO Scribe \u2014 Timeline";
1769
+ const body = lines.filter((l) => l !== header2);
1770
+ const idx = body.findIndex((l) => l.startsWith(`- ${date} \xB7`));
1771
+ if (idx === -1) {
1772
+ body.push(`- ${date} \xB7 ${clean}`.slice(0, TIMELINE_DAY_CAP + date.length + 6));
1773
+ } else {
1774
+ let merged = `${body[idx]}; ${clean}`;
1775
+ if (merged.length > TIMELINE_DAY_CAP) merged = `${merged.slice(0, TIMELINE_DAY_CAP)}\u2026`;
1776
+ body[idx] = merged;
1777
+ }
1778
+ body.sort();
1779
+ writeFileSync10(timelineFile(), `${header2}
1780
+ ${body.join("\n")}
1781
+ `, "utf8");
1782
+ }
1783
+ function readDigest() {
1784
+ return read(digestFile());
1785
+ }
1786
+
1787
+ // src/scribe/journal.ts
1788
+ import {
1789
+ closeSync,
1790
+ existsSync as existsSync6,
1791
+ fsyncSync,
1792
+ mkdirSync as mkdirSync9,
1793
+ openSync,
1794
+ readFileSync as readFileSync12,
1795
+ writeSync
1796
+ } from "fs";
1797
+ function appendJournal(date, content) {
1798
+ mkdirSync9(scribeDir(), { recursive: true });
1799
+ const fd = openSync(journalFile(date), "a");
1812
1800
  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" };
1801
+ writeSync(fd, content.endsWith("\n") ? content : `${content}
1802
+ `);
1803
+ fsyncSync(fd);
1822
1804
  } finally {
1823
- clearTimeout(timer);
1805
+ closeSync(fd);
1824
1806
  }
1825
1807
  }
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();
1808
+ function readJournal(date) {
1809
+ const f = journalFile(date);
1810
+ return existsSync6(f) ? readFileSync12(f, "utf8") : "";
1828
1811
  }
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;
1812
+
1813
+ // src/scribe/redact.ts
1814
+ var RULES = [
1815
+ {
1816
+ label: "private-key",
1817
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
1818
+ },
1819
+ // Lone PEM markers — a key SPLIT across fields/turns leaves only a BEGIN-head or an END-tail in
1820
+ // one field. A field carrying either marker is key material: redact the marker + its adjacent body
1821
+ // (forward from BEGIN, backward to END) so no sub-threshold fragment can ever sit on disk.
1822
+ { label: "private-key", re: /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*/g },
1823
+ { label: "private-key", re: /[\s\S]*-----END[A-Z ]*PRIVATE KEY-----/g },
1824
+ { label: "anthropic-key", re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
1825
+ { label: "openrouter-key", re: /sk-or-v1-[A-Za-z0-9]{20,}/g },
1826
+ // Stripe-style keys (sk_live_/pk_live_/rk_test_/…), underscore segments.
1827
+ { label: "stripe-key", re: /\b[srp]k_(?:live|test)_[A-Za-z0-9]{16,}/g },
1828
+ // Generic sk- secret keys — allow hyphenated segments (sk-live-…, sk-proj-…) so a second
1829
+ // hyphen no longer breaks the match (the gap the Scriber spike caught).
1830
+ { label: "secret-key-sk", re: /sk[-_][A-Za-z0-9][A-Za-z0-9-]{14,}/g },
1831
+ { label: "google-key", re: /AIza[0-9A-Za-z_-]{30,}/g },
1832
+ { label: "groq-key", re: /gsk_[A-Za-z0-9]{20,}/g },
1833
+ { label: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
1834
+ { label: "github-token", re: /gh[posr]_[A-Za-z0-9]{30,}/g },
1835
+ { label: "xai-key", re: /xai-[A-Za-z0-9]{20,}/g },
1836
+ { label: "aws-key", re: /AKIA[0-9A-Z]{16}/g },
1837
+ { label: "jwt", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g },
1838
+ { label: "telegram-token", re: /\b\d{8,10}:[A-Za-z0-9_-]{30,}\b/g },
1839
+ // Auth headers / inline credentials (any provider) — the audit found these leaked.
1840
+ { label: "bearer-token", re: /\bbearer\s+[A-Za-z0-9._~+/=-]{12,}/gi },
1841
+ { label: "basic-auth", re: /\bbasic\s+[A-Za-z0-9+/=]{12,}/gi },
1842
+ // key: value / key=value secrets (password, token, secret, api_key, access_key, …).
1843
+ { label: "secret-kv", re: /\b(?:pass(?:word|wd)?|pwd|secret|token|api[_-]?key|access[_-]?key|auth)\s*[:=]\s*\S{3,}/gi },
1844
+ // Credentials embedded in a URL: scheme://user:PASSWORD@host → redact the password.
1845
+ { label: "url-credential", re: /\b([a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:)[^/\s@]+(@)/gi },
1846
+ { label: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g },
1847
+ { label: "phone", re: /(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}/g }
1848
+ ];
1849
+ function marker(label) {
1850
+ return `\u27E8REDACTED:${label}\u27E9`;
1834
1851
  }
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 });
1852
+ function entropy(s) {
1853
+ const freq = /* @__PURE__ */ new Map();
1854
+ for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
1855
+ let h = 0;
1856
+ for (const n of freq.values()) {
1857
+ const p = n / s.length;
1858
+ h -= p * Math.log2(p);
1856
1859
  }
1857
- return found;
1860
+ return h;
1858
1861
  }
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;
1862
+ function looksLikeUnknownSecret(token) {
1863
+ if (token.length < 32) return false;
1864
+ if (token.includes("\u27E8REDACTED:")) return false;
1865
+ if (/^[0-9a-f]+$/i.test(token)) return false;
1866
+ const classes = (/[a-z]/.test(token) ? 1 : 0) + (/[A-Z]/.test(token) ? 1 : 0) + (/[0-9]/.test(token) ? 1 : 0);
1867
+ if (classes < 2) return false;
1868
+ return entropy(token) >= 4.2;
1869
+ }
1870
+ function redact(input) {
1871
+ const counts = /* @__PURE__ */ new Map();
1872
+ let text = input;
1873
+ for (const rule of RULES) {
1874
+ text = text.replace(rule.re, () => {
1875
+ counts.set(rule.label, (counts.get(rule.label) ?? 0) + 1);
1876
+ return marker(rule.label);
1877
+ });
1865
1878
  }
1866
- return out;
1879
+ text = text.split(/(\s+)/).map((tok) => {
1880
+ if (looksLikeUnknownSecret(tok)) {
1881
+ counts.set("high-entropy", (counts.get("high-entropy") ?? 0) + 1);
1882
+ return marker("high-entropy");
1883
+ }
1884
+ return tok;
1885
+ }).join("");
1886
+ const redactions = [...counts.entries()].map(([label, count]) => ({
1887
+ label,
1888
+ count
1889
+ }));
1890
+ return { text, redactions };
1867
1891
  }
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;
1892
+ function containsSecret(text) {
1893
+ for (const rule of RULES) {
1894
+ rule.re.lastIndex = 0;
1895
+ if (rule.re.test(text)) return true;
1883
1896
  }
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.";
1897
+ for (const tok of text.split(/\s+/)) {
1898
+ if (looksLikeUnknownSecret(tok)) return true;
1892
1899
  }
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
- }));
1900
+ return false;
1944
1901
  }
1945
- function hostOf(url) {
1902
+
1903
+ // src/scribe/capture.ts
1904
+ var INLINE_CAP = 4e3;
1905
+ function sideFile(date, ts, kind, full) {
1906
+ mkdirSync10(artifactsDir(), { recursive: true });
1907
+ const name = `${date}_${ts.replace(/[:.]/g, "-")}_${kind}.md`;
1908
+ const p = join14(artifactsDir(), name);
1909
+ const fd = openSync2(p, "w");
1946
1910
  try {
1947
- return new URL(url).host.replace(/^www\./, "");
1948
- } catch {
1949
- return url;
1911
+ writeSync2(fd, full);
1912
+ fsyncSync2(fd);
1913
+ } finally {
1914
+ closeSync2(fd);
1950
1915
  }
1916
+ return p;
1951
1917
  }
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(", ")}.`);
1918
+ function field(date, ts, label, value) {
1919
+ if (!value || !value.trim()) return "";
1920
+ if (value.length > INLINE_CAP) {
1921
+ const ref = sideFile(date, ts, label.toLowerCase().replace(/\s+/g, "-"), value);
1922
+ return `**${label}** (full \u2192 ${ref}):
1923
+ ${value.slice(0, INLINE_CAP)}
1924
+ \u2026(truncated; full content in artifact)
1925
+
1926
+ `;
1967
1927
  }
1968
- if (gaps.advantages.length) parts.push(`Your edge: ${gaps.advantages.map((a) => a.label).join(", ")}.`);
1969
- return parts.join(" ");
1928
+ return `**${label}:**
1929
+ ${value}
1930
+
1931
+ `;
1970
1932
  }
1971
- function normalizeUrl(u) {
1972
- const t = (u || "").trim();
1973
- if (!t) return t;
1974
- return /^https?:\/\//i.test(t) ? t : `https://${t}`;
1933
+ function renderTurn(rec) {
1934
+ let md = `## ${rec.ts}
1935
+
1936
+ `;
1937
+ md += field(rec.date, rec.ts, "User", rec.user);
1938
+ md += field(rec.date, rec.ts, "Router", rec.router);
1939
+ if (rec.tools?.length) md += `**Tools:** ${rec.tools.join(", ")}
1940
+
1941
+ `;
1942
+ if (rec.files?.length) md += `**Files:** ${rec.files.join(", ")}
1943
+
1944
+ `;
1945
+ md += field(rec.date, rec.ts, "Note", rec.note);
1946
+ return `${md}---
1947
+ `;
1975
1948
  }
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);
1949
+ function oneLineSummary(rec) {
1950
+ const bits = [];
1951
+ if (rec.user) bits.push(rec.user.replace(/\s+/g, " ").slice(0, 80));
1952
+ if (rec.files?.length) bits.push(`files: ${rec.files.slice(0, 3).join(", ")}`);
1953
+ if (rec.note) bits.push(rec.note.replace(/\s+/g, " ").slice(0, 60));
1954
+ return bits.join(" \xB7 ") || "(activity)";
1955
+ }
1956
+ function redactRecord(rec) {
1957
+ const tally = /* @__PURE__ */ new Map();
1958
+ const rd = (s) => {
1959
+ if (!s) return s;
1960
+ const r = redact(s);
1961
+ for (const x of r.redactions) tally.set(x.label, (tally.get(x.label) ?? 0) + x.count);
1962
+ return r.text;
1963
+ };
1964
+ const safeRec = {
1965
+ ...rec,
1966
+ user: rd(rec.user),
1967
+ note: rd(rec.note),
1968
+ router: rd(rec.router),
1969
+ context: rd(rec.context),
1970
+ files: rec.files?.map((f) => rd(f) ?? f)
1971
+ };
1972
+ return { rec: safeRec, redactions: [...tally.entries()].map(([label, count]) => ({ label, count })) };
1973
+ }
1974
+ function captureTurn(rec) {
1975
+ const { rec: safeRec, redactions } = redactRecord(rec);
1976
+ const journal = renderTurn(safeRec);
1977
+ appendJournal(rec.date, `${journal}
1978
+ `);
1979
+ updateDigest(`${safeRec.ts} \xB7 ${oneLineSummary(safeRec)}`, safeRec.context);
1980
+ updateTimeline(safeRec.date, oneLineSummary(safeRec));
1981
+ const auditClean = !containsSecret(readJournal(rec.date)) && !containsSecret(readDigest() ?? "");
1988
1982
  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)
1983
+ journalDate: rec.date,
1984
+ redactions,
1985
+ bytes: Buffer.byteLength(journal, "utf8"),
1986
+ auditClean
1996
1987
  };
1997
1988
  }
1998
1989
 
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
- });
1990
+ // src/scribe/health.ts
1991
+ import {
1992
+ closeSync as closeSync3,
1993
+ fsyncSync as fsyncSync3,
1994
+ mkdirSync as mkdirSync11,
1995
+ openSync as openSync3,
1996
+ readFileSync as readFileSync13,
1997
+ writeFileSync as writeFileSync11,
1998
+ writeSync as writeSync3
1999
+ } from "fs";
2000
+ import { join as join15 } from "path";
2001
+ function healthFile() {
2002
+ return join15(scribeDir(), "_health.json");
2035
2003
  }
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");
2004
+ function faultLogFile() {
2005
+ return join15(scribeDir(), "_faults.log");
2053
2006
  }
2054
- function journalFile(date) {
2055
- return join13(scribeDir(), `${date}.md`);
2007
+ function read2() {
2008
+ try {
2009
+ return JSON.parse(readFileSync13(healthFile(), "utf8"));
2010
+ } catch {
2011
+ return { faultCount: 0 };
2012
+ }
2056
2013
  }
2057
- function digestFile() {
2058
- return join13(scribeDir(), "_digest.md");
2014
+ function write(h) {
2015
+ mkdirSync11(scribeDir(), { recursive: true });
2016
+ writeFileSync11(healthFile(), `${JSON.stringify(h, null, 2)}
2017
+ `, "utf8");
2059
2018
  }
2060
- function timelineFile() {
2061
- return join13(scribeDir(), "_timeline.md");
2019
+ function recordHealth() {
2020
+ const h = read2();
2021
+ h.lastWriteAt = (/* @__PURE__ */ new Date()).toISOString();
2022
+ write(h);
2062
2023
  }
2063
- function artifactsDir() {
2064
- return join13(scribeDir(), "artifacts");
2024
+ function recordFault(role, err) {
2025
+ try {
2026
+ mkdirSync11(scribeDir(), { recursive: true });
2027
+ const msg = `${(/* @__PURE__ */ new Date()).toISOString()} [${role}] ${err instanceof Error ? err.message : String(err)}`;
2028
+ const fd = openSync3(faultLogFile(), "a");
2029
+ try {
2030
+ writeSync3(fd, `${msg}
2031
+ `);
2032
+ fsyncSync3(fd);
2033
+ } finally {
2034
+ closeSync3(fd);
2035
+ }
2036
+ const h = read2();
2037
+ h.faultCount = (h.faultCount ?? 0) + 1;
2038
+ h.lastFault = msg;
2039
+ write(h);
2040
+ } catch {
2041
+ }
2065
2042
  }
2066
2043
 
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") : "";
2044
+ // src/scribe/wal.ts
2045
+ import {
2046
+ closeSync as closeSync4,
2047
+ existsSync as existsSync7,
2048
+ fsyncSync as fsyncSync4,
2049
+ mkdirSync as mkdirSync12,
2050
+ openSync as openSync4,
2051
+ readFileSync as readFileSync14,
2052
+ writeFileSync as writeFileSync12,
2053
+ writeSync as writeSync4
2054
+ } from "fs";
2055
+ import { join as join16 } from "path";
2056
+ function walFile() {
2057
+ return join16(scribeDir(), "_wal.jsonl");
2072
2058
  }
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;
2059
+ function appendLine(obj) {
2060
+ mkdirSync12(scribeDir(), { recursive: true });
2061
+ const fd = openSync4(walFile(), "a");
2062
+ try {
2063
+ writeSync4(fd, `${JSON.stringify(obj)}
2064
+ `);
2065
+ fsyncSync4(fd);
2066
+ } finally {
2067
+ closeSync4(fd);
2096
2068
  }
2097
- writeFileSync10(digestFile(), out, "utf8");
2098
2069
  }
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;
2070
+ function walAppend(id, rec) {
2071
+ appendLine({ t: "add", id, rec });
2072
+ }
2073
+ function walCommit(id) {
2074
+ appendLine({ t: "commit", id });
2075
+ }
2076
+ function walPending() {
2077
+ if (!existsSync7(walFile())) return [];
2078
+ const committed = /* @__PURE__ */ new Set();
2079
+ const adds = /* @__PURE__ */ new Map();
2080
+ for (const line of readFileSync14(walFile(), "utf8").split("\n")) {
2081
+ if (!line.trim()) continue;
2082
+ try {
2083
+ const e = JSON.parse(line);
2084
+ if (e.t === "commit") committed.add(e.id);
2085
+ else if (e.t === "add" && e.rec) adds.set(e.id, e.rec);
2086
+ } catch {
2087
+ }
2113
2088
  }
2114
- body.sort();
2115
- writeFileSync10(timelineFile(), `${header2}
2116
- ${body.join("\n")}
2117
- `, "utf8");
2089
+ const out = [];
2090
+ for (const [id, rec] of adds) {
2091
+ if (!committed.has(id)) out.push({ id, rec });
2092
+ }
2093
+ return out;
2094
+ }
2095
+ function walCompact() {
2096
+ if (!existsSync7(walFile())) return;
2097
+ const pending = walPending();
2098
+ const body = pending.map((p) => JSON.stringify({ t: "add", id: p.id, rec: p.rec })).join("\n");
2099
+ writeFileSync12(walFile(), body ? `${body}
2100
+ ` : "", "utf8");
2118
2101
  }
2119
2102
 
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");
2103
+ // src/scribe/supervisor.ts
2104
+ var draining = false;
2105
+ function uid(ts) {
2106
+ return `${ts}-${Math.random().toString(36).slice(2, 9)}`;
2107
+ }
2108
+ function drainBacklog() {
2109
+ if (draining) return;
2110
+ draining = true;
2133
2111
  try {
2134
- writeSync(fd, content.endsWith("\n") ? content : `${content}
2135
- `);
2136
- fsyncSync(fd);
2112
+ let drained = 0;
2113
+ for (const e of walPending()) {
2114
+ try {
2115
+ captureTurn(e.rec);
2116
+ walCommit(e.id);
2117
+ drained++;
2118
+ } catch (err) {
2119
+ recordFault("standby-replay", err);
2120
+ break;
2121
+ }
2122
+ }
2123
+ if (drained > 0) walCompact();
2137
2124
  } finally {
2138
- closeSync(fd);
2125
+ draining = false;
2139
2126
  }
2140
2127
  }
2141
- function readJournal(date) {
2142
- const f = journalFile(date);
2143
- return existsSync6(f) ? readFileSync12(f, "utf8") : "";
2128
+ function supervisedCapture(rec) {
2129
+ try {
2130
+ drainBacklog();
2131
+ const id = uid(rec.ts);
2132
+ const safe = redactRecord(rec).rec;
2133
+ walAppend(id, safe);
2134
+ try {
2135
+ const res = captureTurn(safe);
2136
+ walCommit(id);
2137
+ walCompact();
2138
+ recordHealth();
2139
+ return res;
2140
+ } catch (primaryErr) {
2141
+ recordFault("primary", primaryErr);
2142
+ try {
2143
+ const res = captureTurn(safe);
2144
+ walCommit(id);
2145
+ walCompact();
2146
+ recordHealth();
2147
+ return res;
2148
+ } catch (standbyErr) {
2149
+ recordFault("standby", standbyErr);
2150
+ return null;
2151
+ }
2152
+ }
2153
+ } catch (fatal) {
2154
+ recordFault("supervisor", fatal);
2155
+ return null;
2156
+ }
2144
2157
  }
2145
2158
 
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`;
2159
+ // src/scribe/retrieval.ts
2160
+ import { existsSync as existsSync8, readFileSync as readFileSync15, readdirSync } from "fs";
2161
+ function listDays() {
2162
+ const dir = scribeDir();
2163
+ if (!existsSync8(dir)) return [];
2164
+ return readdirSync(dir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).map((f) => f.replace(/\.md$/, "")).sort();
2172
2165
  }
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);
2166
+ function readDay(date) {
2167
+ const f = journalFile(date);
2168
+ return existsSync8(f) ? readFileSync15(f, "utf8") : "";
2169
+ }
2170
+ function searchScribe(query, limit = 100) {
2171
+ const q = query.toLowerCase().trim();
2172
+ if (!q) return [];
2173
+ const hits = [];
2174
+ for (const date of listDays().reverse()) {
2175
+ const lines = readDay(date).split("\n");
2176
+ for (let i = 0; i < lines.length; i++) {
2177
+ const ln = lines[i];
2178
+ if (ln && ln.toLowerCase().includes(q)) {
2179
+ hits.push({ date, line: i + 1, text: ln.trim().slice(0, 200) });
2180
+ if (hits.length >= limit) return hits;
2181
+ }
2182
+ }
2180
2183
  }
2181
- return h;
2184
+ return hits;
2182
2185
  }
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;
2186
+
2187
+ // src/scribe/scribe-pi.ts
2188
+ function scribeTurn(input) {
2189
+ if (!isScribeEnabled()) return;
2190
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2191
+ supervisedCapture({ ts, date: ts.slice(0, 10), ...input });
2190
2192
  }
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 };
2193
+ var pendingUserInput = "";
2194
+ function noteUserInput(text) {
2195
+ pendingUserInput = text;
2212
2196
  }
2213
- function containsSecret(text) {
2214
- for (const rule of RULES) {
2215
- rule.re.lastIndex = 0;
2216
- if (rule.re.test(text)) return true;
2197
+ function takePendingUserInput() {
2198
+ const u = pendingUserInput;
2199
+ pendingUserInput = "";
2200
+ return u;
2201
+ }
2202
+ function buildScribeContext() {
2203
+ if (!isScribeEnabled()) return "";
2204
+ const parts = [];
2205
+ try {
2206
+ const t = timelineFile();
2207
+ if (existsSync9(t)) parts.push(`# Work history \u2014 every day so far
2208
+ ${readFileSync16(t, "utf8").trim()}`);
2209
+ } catch {
2217
2210
  }
2218
- for (const tok of text.split(/\s+/)) {
2219
- if (looksLikeUnknownSecret(tok)) return true;
2211
+ try {
2212
+ const d = readDigest();
2213
+ if (d?.trim()) parts.push(`# Current context (recent)
2214
+ ${d.trim()}`);
2215
+ } catch {
2216
+ }
2217
+ if (!parts.length) return "";
2218
+ return `${parts.join("\n\n")}
2219
+
2220
+ (Call scribe_recall to fetch the full text of any past day or topic.)`;
2221
+ }
2222
+ function registerScribe(pi) {
2223
+ pi.registerTool({
2224
+ name: "scribe_recall",
2225
+ label: "ORIRO Scribe",
2226
+ 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.",
2227
+ parameters: Type.Object({
2228
+ query: Type.Optional(Type.String({ description: "Keyword/topic to search across all journals." })),
2229
+ day: Type.Optional(Type.String({ description: "A specific day YYYY-MM-DD to read in full." }))
2230
+ }),
2231
+ async execute(_id, params) {
2232
+ let text;
2233
+ const details = {};
2234
+ if (!isScribeEnabled()) {
2235
+ text = "Scribe is off (the user has not enabled it).";
2236
+ } else if (params.day) {
2237
+ text = readDay(params.day) || `No journal for ${params.day}. Days: ${listDays().join(", ") || "none"}`;
2238
+ details.day = params.day;
2239
+ } else {
2240
+ const hits = params.query ? searchScribe(params.query) : [];
2241
+ details.hits = hits;
2242
+ 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"}`;
2243
+ }
2244
+ return { content: [{ type: "text", text }], details };
2245
+ }
2246
+ });
2247
+ }
2248
+ function attachScribe(session) {
2249
+ let user = "";
2250
+ let assistant = "";
2251
+ const tools = /* @__PURE__ */ new Set();
2252
+ session.subscribe((e) => {
2253
+ if (!isScribeEnabled()) return;
2254
+ if (e?.type === "user_message" || e?.type === "session_user_message") user = String(e.text ?? e.message ?? user);
2255
+ if (e?.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") assistant += e.assistantMessageEvent.delta ?? "";
2256
+ if ((e?.type === "tool_call" || e?.type === "tool_execution_start") && e.toolName) tools.add(String(e.toolName));
2257
+ if (e?.type === "agent_end") {
2258
+ const userText = takePendingUserInput() || user;
2259
+ scribeTurn({ user: userText || void 0, router: "oriro-free", tools: [...tools], note: assistant.slice(0, 4e3) || void 0 });
2260
+ user = "";
2261
+ assistant = "";
2262
+ tools.clear();
2263
+ }
2264
+ });
2265
+ }
2266
+
2267
+ // src/routers/mux-provider.ts
2268
+ var MUX_PROVIDER = "oriro-mux";
2269
+ var MUX_MODEL = "oriro-free";
2270
+ function errToCallError(msg) {
2271
+ const text = msg.errorMessage ?? "";
2272
+ return /\b429\b|rate.?limit|too many requests/i.test(text) ? { status: 429 } : {};
2273
+ }
2274
+ function buildErrorMessage(message) {
2275
+ return {
2276
+ role: "assistant",
2277
+ content: [],
2278
+ api: "openai-completions",
2279
+ provider: MUX_PROVIDER,
2280
+ model: MUX_MODEL,
2281
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
2282
+ stopReason: "error",
2283
+ timestamp: Date.now(),
2284
+ errorMessage: message
2285
+ };
2286
+ }
2287
+ async function driveMux(out, mux, byId, context, options) {
2288
+ let lastError;
2289
+ for (const id of mux.ranked()) {
2290
+ const router = byId.get(id);
2291
+ if (!router) continue;
2292
+ const t0 = Date.now();
2293
+ let committed = false;
2294
+ let lastPartial;
2295
+ try {
2296
+ const inner = piStreamSimple(routerModel(router), context, {
2297
+ ...options ?? {},
2298
+ apiKey: router.apiKey
2299
+ });
2300
+ let failedBeforeContent = false;
2301
+ for await (const ev of inner) {
2302
+ if (ev.type === "error") {
2303
+ mux.recordFailure(id, errToCallError(ev.error));
2304
+ if (!committed) {
2305
+ lastError = ev.error;
2306
+ failedBeforeContent = true;
2307
+ break;
2308
+ }
2309
+ out.push(ev);
2310
+ out.end(ev.error);
2311
+ return;
2312
+ }
2313
+ committed = true;
2314
+ if (ev.type === "done") {
2315
+ mux.recordSuccess(id, Date.now() - t0);
2316
+ const clean = sanitizeMessageToolCalls(scrubMessageIdentity(ev.message));
2317
+ out.push({ type: "done", reason: ev.reason, message: clean });
2318
+ out.end(clean);
2319
+ return;
2320
+ }
2321
+ lastPartial = ev.partial;
2322
+ out.push(sanitizeEventToolCalls(ev));
2323
+ }
2324
+ if (failedBeforeContent) continue;
2325
+ if (!committed) {
2326
+ mux.recordFailure(id, {});
2327
+ lastError ??= buildErrorMessage("Router returned no output.");
2328
+ continue;
2329
+ }
2330
+ mux.recordSuccess(id, Date.now() - t0);
2331
+ out.end(lastPartial ? sanitizeMessageToolCalls(scrubMessageIdentity(lastPartial)) : void 0);
2332
+ return;
2333
+ } catch (e) {
2334
+ mux.recordFailure(id, e);
2335
+ }
2336
+ }
2337
+ const msg = lastError ?? buildErrorMessage(
2338
+ "All keyless routers are unavailable. Add a BYOK key, select more free routers, or retry shortly."
2339
+ );
2340
+ out.push({ type: "error", reason: "error", error: msg });
2341
+ out.end(msg);
2342
+ }
2343
+ function registerOriroMux(registry, opts = {}) {
2344
+ registerOpenAICompletions();
2345
+ const pooled = resolvePool();
2346
+ const routers = opts.routers ?? (pooled.length > 0 ? pooled : KEYLESS_FLOOR);
2347
+ const byId = new Map(routers.map((r) => [r.id, r]));
2348
+ const mux = new RouterMux(routers.map((r) => r.id));
2349
+ try {
2350
+ mux.load(loadMuxState(oriroDir()));
2351
+ } catch {
2352
+ }
2353
+ registry.registerProvider(MUX_PROVIDER, {
2354
+ name: "ORIRO Free (keyless Mux)",
2355
+ api: "openai-completions",
2356
+ apiKey: "oriro-keyless",
2357
+ // Placeholder — required by registry validation but never used: our custom streamSimple
2358
+ // routes to the real keyless floor endpoints itself (see driveMux).
2359
+ baseUrl: "http://oriro-mux.local",
2360
+ models: [
2361
+ {
2362
+ id: MUX_MODEL,
2363
+ name: "ORIRO Free (best-router)",
2364
+ api: "openai-completions",
2365
+ baseUrl: "http://oriro-mux.local",
2366
+ reasoning: false,
2367
+ input: ["text"],
2368
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2369
+ contextWindow: 128e3,
2370
+ maxTokens: 4096
2371
+ }
2372
+ ],
2373
+ streamSimple: (_model, context, options) => {
2374
+ const out = createAssistantMessageEventStream();
2375
+ const ctx = applyIdentity(context);
2376
+ const memory = buildScribeContext();
2377
+ const withMemory = memory ? { ...ctx, systemPrompt: `${ctx.systemPrompt}
2378
+
2379
+ ${memory}` } : ctx;
2380
+ void driveMux(out, mux, byId, withMemory, options).finally(() => {
2381
+ try {
2382
+ saveMuxState(oriroDir(), mux.snapshot());
2383
+ } catch {
2384
+ }
2385
+ });
2386
+ return out;
2387
+ }
2388
+ });
2389
+ return registry.find(MUX_PROVIDER, MUX_MODEL);
2390
+ }
2391
+
2392
+ // src/head/pi-tool.ts
2393
+ import { Type as Type2 } from "typebox";
2394
+
2395
+ // src/head/comparison-engine.ts
2396
+ var SECTION_RULES = [
2397
+ {
2398
+ type: "hero",
2399
+ label: "Hero",
2400
+ priority: "CRITICAL",
2401
+ markup: [/<h1[\s>]/],
2402
+ recommend: "Add a clear above-the-fold hero \u2014 one headline that states the value + one primary CTA."
2403
+ },
2404
+ {
2405
+ type: "navigation",
2406
+ label: "Navigation",
2407
+ priority: "CRITICAL",
2408
+ markup: [/<nav[\s>]/, /role=["']navigation["']/],
2409
+ recommend: "Add a top navigation so visitors can reach key sections."
2410
+ },
2411
+ {
2412
+ type: "features",
2413
+ label: "Features",
2414
+ priority: "CRITICAL",
2415
+ text: [/\bfeatures?\b/, /\bwhat you (?:can|get)\b/, /\bcapabilit/],
2416
+ recommend: "Add a features section that spells out concrete capabilities, not adjectives."
2417
+ },
2418
+ {
2419
+ type: "pricing",
2420
+ label: "Pricing",
2421
+ priority: "CRITICAL",
2422
+ text: [/\bpricing\b/, /\bper month\b/, /\b\/mo\b/, /\bfree plan\b/, /\$\d/, /₹\d/, /€\d/],
2423
+ recommend: 'Add transparent pricing \u2014 a critical conversion element; even a single "Free" tier helps.'
2424
+ },
2425
+ {
2426
+ type: "cta",
2427
+ label: "Call-to-Action",
2428
+ priority: "CRITICAL",
2429
+ 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/],
2430
+ recommend: 'Add a strong, repeated primary CTA ("Get started") so the next step is obvious.'
2431
+ },
2432
+ {
2433
+ type: "testimonials",
2434
+ label: "Testimonials",
2435
+ priority: "HIGH",
2436
+ text: [/\btestimonial/, /\bwhat (?:our )?(?:customers|users) say\b/, /\bloved by\b/, /\breview(?:s|ed)\b/],
2437
+ recommend: "Add 2\u20133 customer testimonials with names/photos to build trust."
2438
+ },
2439
+ {
2440
+ type: "stats",
2441
+ label: "Stats / Metrics",
2442
+ priority: "HIGH",
2443
+ text: [/\b\d[\d,.]*\s*[kkmm]\+?\s*(?:users|customers|developers|downloads|teams)\b/, /\b9\d(?:\.\d+)?%\b/, /\buptime\b/],
2444
+ recommend: 'Add impressive metrics ("10K+ users", "99.9% uptime") as social proof.'
2445
+ },
2446
+ {
2447
+ type: "video",
2448
+ label: "Video",
2449
+ priority: "HIGH",
2450
+ markup: [/<video[\s>]/, /youtube\.com\/embed/, /player\.vimeo\.com/, /<iframe[^>]+(?:youtube|vimeo)/],
2451
+ text: [/\bwatch the (?:video|demo)\b/],
2452
+ recommend: "Add a short explainer/demo video \u2014 it lifts conversion on landing pages."
2453
+ },
2454
+ {
2455
+ type: "demo",
2456
+ label: "Live Demo",
2457
+ priority: "HIGH",
2458
+ text: [/\btry it (?:now|live|free)\b/, /\bplayground\b/, /\binteractive demo\b/, /\blive demo\b/],
2459
+ recommend: 'Add a "try it" live demo or playground so visitors experience the product immediately.'
2460
+ },
2461
+ {
2462
+ type: "socialProof",
2463
+ label: "Social Proof",
2464
+ priority: "HIGH",
2465
+ text: [/\btrusted by\b/, /\bbacked by\b/, /\bused by\b/, /\bas seen (?:in|on)\b/, /\bcustomers include\b/],
2466
+ recommend: 'Add social proof (customer/investor logos, "trusted by \u2026") near the hero.'
2467
+ },
2468
+ {
2469
+ type: "faq",
2470
+ label: "FAQ",
2471
+ priority: "MEDIUM",
2472
+ text: [/\bfaq\b/, /\bfrequently asked\b/],
2473
+ markup: [/<details[\s>]/],
2474
+ recommend: "Add an FAQ that answers the top objections before they become exits."
2475
+ },
2476
+ {
2477
+ type: "integrations",
2478
+ label: "Integrations",
2479
+ priority: "MEDIUM",
2480
+ text: [/\bintegrations?\b/, /\bworks with\b/, /\bconnect your\b/],
2481
+ recommend: "Add an integrations section showing what the product connects to."
2482
+ },
2483
+ {
2484
+ type: "newsletter",
2485
+ label: "Newsletter / Capture",
2486
+ priority: "MEDIUM",
2487
+ text: [/\bsubscribe\b/, /\bnewsletter\b/, /\bjoin (?:the )?waitlist\b/],
2488
+ markup: [/type=["']email["']/],
2489
+ recommend: "Add an email capture (newsletter/waitlist) so non-converting visitors are not lost."
2490
+ },
2491
+ {
2492
+ type: "comparison",
2493
+ label: "Comparison",
2494
+ priority: "MEDIUM",
2495
+ text: [/\bcompare\b/, /\bcomparison\b/, /\b vs\.? \b/, /\bwhy choose\b/],
2496
+ recommend: 'Add a comparison ("us vs alternatives") to win evaluators who are shopping around.'
2497
+ },
2498
+ {
2499
+ type: "team",
2500
+ label: "Team / About",
2501
+ priority: "LOW",
2502
+ text: [/\bour team\b/, /\bmeet the team\b/, /\bfounders?\b/, /\babout us\b/],
2503
+ recommend: "Add a brief team/about section to humanize the brand."
2220
2504
  }
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");
2505
+ ];
2506
+ var PRIORITY_RANK = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
2507
+ var PRIORITY_EFFORT = { CRITICAL: "L", HIGH: "M", MEDIUM: "M", LOW: "S" };
2508
+ var FETCH_TIMEOUT_MS = 12e3;
2509
+ 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";
2510
+ async function fetchPage(url) {
2511
+ const controller = new AbortController();
2512
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
2513
+ const start = Date.now();
2231
2514
  try {
2232
- writeSync2(fd, full);
2233
- fsyncSync2(fd);
2515
+ const res = await fetch(url, {
2516
+ signal: controller.signal,
2517
+ redirect: "follow",
2518
+ headers: { "user-agent": UA, accept: "text/html,application/xhtml+xml" }
2519
+ });
2520
+ const html = await res.text();
2521
+ return { html, ms: Date.now() - start, status: res.status, ok: res.ok, error: "" };
2522
+ } catch (err) {
2523
+ return { html: "", ms: Date.now() - start, status: 0, ok: false, error: err instanceof Error ? err.message : "fetch failed" };
2234
2524
  } 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 };
2525
+ clearTimeout(timer);
2315
2526
  }
2316
2527
  }
2317
- function write(h) {
2318
- mkdirSync11(scribeDir(), { recursive: true });
2319
- writeFileSync11(healthFile(), `${JSON.stringify(h, null, 2)}
2320
- `, "utf8");
2528
+ function toText(html) {
2529
+ return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/\s+/g, " ").toLowerCase().trim();
2321
2530
  }
2322
- function recordHealth() {
2323
- const h = read2();
2324
- h.lastWriteAt = (/* @__PURE__ */ new Date()).toISOString();
2325
- write(h);
2531
+ function firstMatch(re, hay) {
2532
+ const m = re.exec(hay);
2533
+ if (!m) return "";
2534
+ const slice = (m[0] ?? "").trim();
2535
+ return slice.length > 80 ? `${slice.slice(0, 77)}\u2026` : slice;
2326
2536
  }
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);
2537
+ function detectSections(rawHtmlLower, text) {
2538
+ const found = [];
2539
+ for (const rule of SECTION_RULES) {
2540
+ let evidence = "";
2541
+ for (const re of rule.markup ?? []) {
2542
+ const hit = firstMatch(re, rawHtmlLower);
2543
+ if (hit) {
2544
+ evidence = hit;
2545
+ break;
2546
+ }
2338
2547
  }
2339
- const h = read2();
2340
- h.faultCount = (h.faultCount ?? 0) + 1;
2341
- h.lastFault = msg;
2342
- write(h);
2343
- } catch {
2548
+ if (!evidence) {
2549
+ for (const re of rule.text ?? []) {
2550
+ const hit = firstMatch(re, text);
2551
+ if (hit) {
2552
+ evidence = hit;
2553
+ break;
2554
+ }
2555
+ }
2556
+ }
2557
+ if (evidence) found.push({ type: rule.type, label: rule.label, priority: rule.priority, evidence });
2344
2558
  }
2559
+ return found;
2345
2560
  }
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);
2561
+ function extractMatches(re, html, max) {
2562
+ const out = [];
2563
+ for (const m of html.matchAll(re)) {
2564
+ const inner = (m[1] ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
2565
+ if (inner && !out.includes(inner)) out.push(inner);
2566
+ if (out.length >= max) break;
2371
2567
  }
2568
+ return out;
2372
2569
  }
2373
- function walAppend(id, rec) {
2374
- appendLine({ t: "add", id, rec });
2375
- }
2376
- function walCommit(id) {
2377
- appendLine({ t: "commit", id });
2570
+ 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;
2571
+ function extractStructure(url, fr) {
2572
+ const html = fr.html;
2573
+ const lowerHtml = html.toLowerCase();
2574
+ const text = toText(html);
2575
+ const titleM = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
2576
+ const title = (titleM?.[1] ?? "").replace(/\s+/g, " ").trim();
2577
+ const descM = /<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["']/i.exec(html) ?? /<meta[^>]+content=["']([^"']*)["'][^>]+name=["']description["']/i.exec(html);
2578
+ const description = (descM?.[1] ?? "").replace(/\s+/g, " ").trim();
2579
+ const headings = extractMatches(/<h[1-3][^>]*>([\s\S]*?)<\/h[1-3]>/gi, html, 12);
2580
+ const ctaAll = extractMatches(/<(?:a|button)[^>]*>([\s\S]*?)<\/(?:a|button)>/gi, html, 80);
2581
+ const ctas = [];
2582
+ for (const c of ctaAll) {
2583
+ if (CTA_WORDS.test(c) && !ctas.includes(c)) ctas.push(c);
2584
+ if (ctas.length >= 10) break;
2585
+ }
2586
+ const forms = (lowerHtml.match(/<form[\s>]/g) ?? []).length;
2587
+ const links = (lowerHtml.match(/<a[\s>]/g) ?? []).length;
2588
+ const images = (lowerHtml.match(/<img[\s>]/g) ?? []).length;
2589
+ const hasVideo = /<video[\s>]/.test(lowerHtml) || /(?:youtube\.com\/embed|player\.vimeo\.com)/.test(lowerHtml);
2590
+ const domNodes = (html.match(/<[a-z!\/]/gi) ?? []).length;
2591
+ let note = "";
2592
+ if (fr.ok && text.length < 400 && domNodes < 60) {
2593
+ note = "Sparse HTML \u2014 likely a client-rendered (SPA) page; structure may be under-detected without a JS render.";
2594
+ }
2595
+ return {
2596
+ url,
2597
+ title,
2598
+ description,
2599
+ sections: detectSections(lowerHtml, text),
2600
+ headings,
2601
+ ctas,
2602
+ forms,
2603
+ links,
2604
+ images,
2605
+ hasVideo,
2606
+ metrics: { htmlBytes: html.length, domNodes, fetchMs: fr.ms, status: fr.status },
2607
+ ok: fr.ok && html.length > 0,
2608
+ note: fr.ok ? note : `Could not load: ${fr.error || `HTTP ${fr.status}`}`
2609
+ };
2378
2610
  }
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 {
2611
+ function ruleFor(type) {
2612
+ return SECTION_RULES.find((r) => r.type === type) ?? SECTION_RULES[0];
2613
+ }
2614
+ function analyzeGaps(target, competitors) {
2615
+ const targetTypes = new Set(target.sections.map((s) => s.type));
2616
+ const compPresence = /* @__PURE__ */ new Map();
2617
+ for (const comp of competitors) {
2618
+ if (!comp.ok) continue;
2619
+ for (const s of comp.sections) {
2620
+ const list = compPresence.get(s.type) ?? [];
2621
+ if (!list.includes(comp.url)) list.push(comp.url);
2622
+ compPresence.set(s.type, list);
2390
2623
  }
2391
2624
  }
2392
- const out = [];
2393
- for (const [id, rec] of adds) {
2394
- if (!committed.has(id)) out.push({ id, rec });
2625
+ const missing = [];
2626
+ const parity = [];
2627
+ for (const [type, presentOn] of compPresence) {
2628
+ if (targetTypes.has(type)) {
2629
+ parity.push(type);
2630
+ } else {
2631
+ const rule = ruleFor(type);
2632
+ missing.push({ section: type, label: rule.label, priority: rule.priority, presentOn, recommendation: rule.recommend });
2633
+ }
2395
2634
  }
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");
2635
+ missing.sort((a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || b.presentOn.length - a.presentOn.length);
2636
+ const advantages = target.sections.filter((s) => !compPresence.has(s.type));
2637
+ return { missing, advantages, parity };
2404
2638
  }
2405
-
2406
- // src/scribe/supervisor.ts
2407
- var draining = false;
2408
- function uid(ts) {
2409
- return `${ts}-${Math.random().toString(36).slice(2, 9)}`;
2639
+ function generateActionItems(missing) {
2640
+ return missing.map((g) => ({
2641
+ title: `Add a ${g.label} section`,
2642
+ priority: g.priority,
2643
+ effort: PRIORITY_EFFORT[g.priority],
2644
+ rationale: `${g.presentOn.length} of the compared page(s) have it; you don't. ${g.recommendation}`
2645
+ }));
2410
2646
  }
2411
- function drainBacklog() {
2412
- if (draining) return;
2413
- draining = true;
2647
+ function hostOf(url) {
2414
2648
  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;
2649
+ return new URL(url).host.replace(/^www\./, "");
2650
+ } catch {
2651
+ return url;
2429
2652
  }
2430
2653
  }
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;
2654
+ function generateSummary(target, competitors, gaps) {
2655
+ const okComps = competitors.filter((c) => c.ok);
2656
+ const tName = hostOf(target.url);
2657
+ if (!target.ok) return `Could not load ${tName} (${target.note}). Nothing to compare against yet.`;
2658
+ if (okComps.length === 0) return `Loaded ${tName} (${target.sections.length} sections) but none of the comparison URLs could be loaded.`;
2659
+ const crit = gaps.missing.filter((m) => m.priority === "CRITICAL").map((m) => m.label);
2660
+ const high = gaps.missing.filter((m) => m.priority === "HIGH").map((m) => m.label);
2661
+ const parts = [];
2662
+ parts.push(`${tName} has ${target.sections.length} detectable sections; compared against ${okComps.length} page(s).`);
2663
+ if (gaps.missing.length === 0) {
2664
+ parts.push("No structural gaps found \u2014 you cover everything they do.");
2665
+ } else {
2666
+ parts.push(`${gaps.missing.length} gap(s) found.`);
2667
+ if (crit.length) parts.push(`Critical: ${crit.join(", ")}.`);
2668
+ if (high.length) parts.push(`High: ${high.join(", ")}.`);
2456
2669
  }
2670
+ if (gaps.advantages.length) parts.push(`Your edge: ${gaps.advantages.map((a) => a.label).join(", ")}.`);
2671
+ return parts.join(" ");
2457
2672
  }
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") : "";
2673
+ function normalizeUrl(u) {
2674
+ const t = (u || "").trim();
2675
+ if (!t) return t;
2676
+ return /^https?:\/\//i.test(t) ? t : `https://${t}`;
2469
2677
  }
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;
2678
+ async function comparePages(opts) {
2679
+ const targetUrl = normalizeUrl(opts.targetUrl);
2680
+ const competitorUrls = (opts.competitorUrls ?? []).map(normalizeUrl).filter((u) => u.length > 0).slice(0, 30);
2681
+ const [targetFetch, ...compFetches] = await Promise.all([
2682
+ fetchPage(targetUrl),
2683
+ ...competitorUrls.map((u) => fetchPage(u))
2684
+ ]);
2685
+ const target = extractStructure(targetUrl, targetFetch ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" });
2686
+ const competitors = competitorUrls.map(
2687
+ (u, i) => extractStructure(u, compFetches[i] ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" })
2688
+ );
2689
+ const gaps = analyzeGaps(target, competitors);
2690
+ return {
2691
+ target,
2692
+ competitors,
2693
+ missing: gaps.missing,
2694
+ advantages: gaps.advantages,
2695
+ parity: gaps.parity,
2696
+ actionItems: generateActionItems(gaps.missing),
2697
+ summary: generateSummary(target, competitors, gaps)
2698
+ };
2485
2699
  }
2486
2700
 
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 });
2701
+ // src/head/pi-tool.ts
2702
+ function summarizeForCoder(report) {
2703
+ const lines = [report.summary];
2704
+ const page = (p) => ` \u2022 ${p.url} \u2014 ${p.ok ? `${p.sections.length} sections: ${p.sections.map((s) => s.type).join(", ")}` : `not readable (${p.note})`}`;
2705
+ lines.push("Pages seen:");
2706
+ lines.push(page(report.target));
2707
+ for (const c of report.competitors) if (c.url !== report.target.url) lines.push(page(c));
2708
+ if (report.missing.length) {
2709
+ lines.push("Missing on the target (gaps to build):");
2710
+ for (const g of report.missing.slice(0, 12)) lines.push(` \u2022 ${g.label} (${g.priority}) \u2014 ${g.recommendation}`);
2711
+ }
2712
+ if (report.actionItems.length) {
2713
+ lines.push("Suggested action items:");
2714
+ for (const a of report.actionItems.slice(0, 12)) lines.push(` \u2192 ${a.title} [${a.priority}/${a.effort}] \u2014 ${a.rationale}`);
2715
+ }
2716
+ return lines.join("\n");
2492
2717
  }
2493
- function registerScribe(pi) {
2718
+ var InspectSiteParams = Type2.Object({
2719
+ url: Type2.String({ description: "The target website URL to inspect or rebuild from." }),
2720
+ competitors: Type2.Optional(
2721
+ Type2.Array(Type2.String(), { description: "Optional competitor/reference URLs to compare the target against." })
2722
+ )
2723
+ });
2724
+ function registerHead(pi) {
2494
2725
  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();
2726
+ name: "inspect_site",
2727
+ label: "ORIRO Head",
2728
+ 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.",
2729
+ parameters: InspectSiteParams,
2730
+ async execute(_toolCallId, params) {
2731
+ const target = params.url;
2732
+ const competitors = params.competitors?.length ? params.competitors : [target];
2733
+ const report = await comparePages({ targetUrl: target, competitorUrls: competitors });
2734
+ return { content: [{ type: "text", text: summarizeForCoder(report) }], details: report };
2533
2735
  }
2534
2736
  });
2535
2737
  }
@@ -2824,6 +3026,28 @@ function setupNllbTranslator(opts) {
2824
3026
  return instance;
2825
3027
  }
2826
3028
 
3029
+ // src/language/gateway.ts
3030
+ var isEnglish2 = (code) => !code || code.toLowerCase().startsWith("en");
3031
+ var isCommand = (text) => text.trimStart().startsWith("/");
3032
+ async function ensureReady() {
3033
+ try {
3034
+ await setupNllbTranslator().load();
3035
+ } catch {
3036
+ }
3037
+ }
3038
+ async function translateIncoming(message) {
3039
+ const lang = getTerminalLanguage().code;
3040
+ if (isEnglish2(lang) || !message.trim() || isCommand(message)) return message;
3041
+ await ensureReady();
3042
+ return translateForCoder(message, lang);
3043
+ }
3044
+ async function translateOutgoing(text) {
3045
+ const lang = getTerminalLanguage().code;
3046
+ if (isEnglish2(lang) || !text.trim()) return text;
3047
+ await ensureReady();
3048
+ return translateForUser(text, lang);
3049
+ }
3050
+
2827
3051
  // src/repl.ts
2828
3052
  function replHelp() {
2829
3053
  return `
@@ -2838,12 +3062,26 @@ function replHelp() {
2838
3062
  }
2839
3063
  async function runRepl() {
2840
3064
  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();
3065
+ else stdout5.write(banner());
3066
+ const isEnglish3 = getTerminalLanguage().code.toLowerCase().startsWith("en");
2845
3067
  const { session } = await assembleOriroSession();
2846
- const rl = createInterface4({ input: stdin4, output: stdout4 });
3068
+ const rl = createInterface4({ input: stdin4, output: stdout5 });
3069
+ let closing = false;
3070
+ const onSigint = () => {
3071
+ if (closing) return;
3072
+ closing = true;
3073
+ stdout5.write(dim("\nBye.\n"));
3074
+ try {
3075
+ rl.close();
3076
+ } catch {
3077
+ }
3078
+ try {
3079
+ session.dispose();
3080
+ } catch {
3081
+ }
3082
+ process.exit(0);
3083
+ };
3084
+ process.on("SIGINT", onSigint);
2847
3085
  try {
2848
3086
  for (; ; ) {
2849
3087
  let line;
@@ -2853,18 +3091,20 @@ async function runRepl() {
2853
3091
  break;
2854
3092
  }
2855
3093
  if (!line) continue;
2856
- if (line === "/exit" || line === "/quit") break;
2857
- if (line === "/help" || line === "/?") {
2858
- stdout4.write(replHelp());
3094
+ const slash = line.toLowerCase();
3095
+ if (slash === "/exit" || slash === "/quit") break;
3096
+ if (slash === "/help" || slash === "/?") {
3097
+ stdout5.write(replHelp());
2859
3098
  continue;
2860
3099
  }
2861
- const english = await translateForCoder(line, lang);
3100
+ const english = await translateIncoming(line);
3101
+ noteUserInput(line);
2862
3102
  let out = "";
2863
3103
  const unsub = session.subscribe((e) => {
2864
3104
  if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
2865
3105
  const d = e.assistantMessageEvent.delta ?? "";
2866
3106
  out += d;
2867
- if (isEnglish2) stdout4.write(d);
3107
+ if (isEnglish3) stdout5.write(d);
2868
3108
  }
2869
3109
  });
2870
3110
  try {
@@ -2872,15 +3112,18 @@ async function runRepl() {
2872
3112
  } finally {
2873
3113
  unsub();
2874
3114
  }
2875
- if (isEnglish2) stdout4.write("\n\n");
2876
- else stdout4.write(`${await translateForUser(out.trim(), lang)}
3115
+ if (isEnglish3) stdout5.write("\n\n");
3116
+ else stdout5.write(`${await translateOutgoing(out.trim())}
2877
3117
 
2878
3118
  `);
2879
3119
  }
2880
3120
  } finally {
2881
- rl.close();
2882
- session.dispose();
2883
- stdout4.write(dim("\nBye.\n"));
3121
+ process.removeListener("SIGINT", onSigint);
3122
+ if (!closing) {
3123
+ rl.close();
3124
+ session.dispose();
3125
+ stdout5.write(dim("\nBye.\n"));
3126
+ }
2884
3127
  }
2885
3128
  }
2886
3129
 
@@ -3247,9 +3490,12 @@ var heading = (s) => {
3247
3490
  ${bold(accent(s))}
3248
3491
  `);
3249
3492
  };
3493
+ var DieError = class extends Error {
3494
+ };
3250
3495
  function die(msg) {
3251
3496
  fail(msg);
3252
- process.exit(1);
3497
+ process.exitCode = 1;
3498
+ throw new DieError(msg);
3253
3499
  }
3254
3500
 
3255
3501
  // src/commands/routers.ts
@@ -3278,11 +3524,12 @@ function registerRoutersCommand(program2) {
3278
3524
  ok(`added ${accent(slug)} (${res.validation.latencyMs}ms, model ${res.validation.model}) \u2192 active pool`);
3279
3525
  });
3280
3526
  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(", ")}`);
3527
+ const { applied, unknown } = useRouters(slugs);
3528
+ if (!applied.length) {
3529
+ die(`none of those are added yet: ${unknown.join(", ")} \u2014 run \`oriro routers add <slug>\` first`);
3530
+ }
3531
+ ok(`pool set: ${applied.join(", ")}`);
3532
+ if (unknown.length) info(`skipped (not added yet \u2014 run \`oriro routers add\`): ${unknown.join(", ")}`);
3286
3533
  });
3287
3534
  }
3288
3535
 
@@ -4314,6 +4561,9 @@ function listConnectors(category) {
4314
4561
  function connectorCategories() {
4315
4562
  return [...new Set(CONNECTOR_CATALOG.map((c) => c.category))].sort();
4316
4563
  }
4564
+ function isConnectorAdded(slug) {
4565
+ return readAdded().includes(slug);
4566
+ }
4317
4567
  function addConnector(slug) {
4318
4568
  const entry = connectorBySlug(slug);
4319
4569
  if (!entry) return { ok: false, error: `unknown connector '${slug}' \u2014 run \`oriro connectors list\`` };
@@ -4338,23 +4588,30 @@ function registerConnectorsCommand(program2) {
4338
4588
  const connectors = program2.command("connectors").description("MCP connectors \u2014 add external tools/services (inert until used)");
4339
4589
  connectors.command("list [category]").description("list the connector catalog (optionally filtered by category)").action((category) => {
4340
4590
  if (category && !connectorCategories().includes(category)) {
4341
- info(`unknown category '${category}' \u2014 categories: ${connectorCategories().join(", ")}`);
4342
- return;
4591
+ die(`unknown category '${category}' \u2014 categories: ${connectorCategories().join(", ")}`);
4343
4592
  }
4344
4593
  const entries = listConnectors(category);
4345
4594
  const added = new Set(addedConnectors().map((c) => c.slug));
4346
4595
  heading(category ? `Connectors \xB7 ${category}` : "Connectors");
4596
+ let addable = 0;
4347
4597
  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)}
4598
+ const canAdd = !!c.mcpUrl;
4599
+ if (canAdd) addable++;
4600
+ const mark = !canAdd ? dim("\xB7") : added.has(c.slug) ? accent("\u25CF") : dim("\u25CB");
4601
+ const name = canAdd ? c.name.padEnd(22) : dim(`${c.name} (coming soon)`.padEnd(22));
4602
+ process.stdout.write(` ${mark} ${(canAdd ? accent : dim)(c.slug.padEnd(20))} ${name} ${dim(c.category)}
4350
4603
  `);
4351
4604
  }
4352
- info(`${entries.length} connectors${category ? ` in '${category}'` : ""} \xB7 ${added.size} added`);
4605
+ info(`${addable} addable${category ? ` in '${category}'` : ""} \xB7 ${added.size} added \xB7 ${entries.length - addable} coming soon`);
4353
4606
  });
4354
4607
  connectors.command("add <slug>").description("add a connector (validate + record; connects only when used)").action((slug) => {
4608
+ if (isConnectorAdded(slug)) {
4609
+ info(`${slug} is already added`);
4610
+ return;
4611
+ }
4355
4612
  const res = addConnector(slug);
4356
4613
  if (!res.ok) die(res.error ?? `could not add '${slug}'`);
4357
- ok(`added ${accent(slug)} \u2014 inert until a session uses it`);
4614
+ ok(`added ${accent(slug)} \u2014 recorded locally`);
4358
4615
  });
4359
4616
  connectors.command("remove <slug>").description("remove a connector").action((slug) => {
4360
4617
  if (removeConnector(slug)) ok(`removed ${accent(slug)}`);
@@ -4411,6 +4668,7 @@ var OriroChannelHost = class {
4411
4668
  if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") out += e.assistantMessageEvent.delta ?? "";
4412
4669
  });
4413
4670
  try {
4671
+ noteUserInput(text);
4414
4672
  await session.prompt(text);
4415
4673
  } finally {
4416
4674
  unsub();
@@ -4591,7 +4849,7 @@ function registerChannelsCommand(program2) {
4591
4849
  if (!isKind(kind)) die(`unknown channel '${kind}' \u2014 one of: ${KINDS.join(", ")}`);
4592
4850
  if (kind === "whatsapp") {
4593
4851
  if (!opts.acceptRisk) {
4594
- fail("WhatsApp uses Baileys, which pairs a REAL WhatsApp account and may violate WhatsApp's ToS (ban risk).");
4852
+ info("WhatsApp uses Baileys, which pairs a REAL WhatsApp account and may violate WhatsApp's ToS (ban risk).");
4595
4853
  info("If you accept that risk, re-run: `oriro channels start whatsapp --accept-risk`");
4596
4854
  return;
4597
4855
  }
@@ -4709,7 +4967,12 @@ var version = createRequire(import.meta.url)("../package.json").version;
4709
4967
  var program = new Command();
4710
4968
  program.name("oriro").description("ORIRO \u2014 a free, on-device-friendly terminal AI agent.").version(version, "-v, --version").action(async (_options, command) => {
4711
4969
  if (command.args.length > 0) {
4712
- process.stderr.write(`error: unknown command '${command.args[0]}'
4970
+ const arg = command.args[0];
4971
+ if (arg === "help") {
4972
+ command.outputHelp();
4973
+ return;
4974
+ }
4975
+ process.stderr.write(`error: unknown command '${arg}'
4713
4976
  Run 'oriro --help' to see available commands.
4714
4977
  `);
4715
4978
  process.exitCode = 1;
@@ -4725,6 +4988,7 @@ registerSkillsCommand(program);
4725
4988
  registerLanguageCommand(program);
4726
4989
  registerAvatarCommand(program);
4727
4990
  program.parseAsync().catch((e) => {
4991
+ if (e instanceof DieError) return;
4728
4992
  process.stderr.write(`
4729
4993
  ORIRO error: ${e instanceof Error ? e.stack ?? e.message : String(e)}
4730
4994
  `);