@oriro/orirocli 0.1.3 → 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 (3) hide show
  1. package/README.md +16 -12
  2. package/dist/cli.js +1327 -1019
  3. 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,150 +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;
565
+ var ENV_EXFIL = [
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 | …
572
+ /\$\(\s*cat\b[^)]*(\.ssh|\.aws|\.env|\.netrc|credential|secret|token|id_rsa|id_ed25519)/i
573
+ // $(cat <secret>)
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;
478
576
  var PERSISTENCE = [
479
577
  /\bcrontab\b\s+(-|\S+)/i,
480
- // crontab install
481
578
  />>?\s*~?\/?\.(bashrc|zshrc|bash_profile|profile|zprofile)\b/i,
482
- // append to shell rc
579
+ />>?[^\n]*\.ssh[\\/]authorized_keys/i,
580
+ // implant an SSH key (backdoor)
483
581
  /\b(launchctl\s+load|systemctl\s+enable|sc\s+create|new-service)\b/i,
484
- // service install
485
582
  /\bregistry::|reg\s+add\b.*\\run\b/i,
486
- // windows Run key persistence
487
- /[\\\/]start menu[\\\/]programs[\\\/]startup[\\\/]/i,
488
- // windows startup folder
583
+ /[\\/]start menu[\\/]programs[\\/]startup[\\/]/i,
489
584
  /\bschtasks\b\s+\/create/i
490
- // scheduled task
491
585
  ];
492
586
  var GUARDIAN_TAMPER = [
493
587
  /\boriro\b.*\bguardian\b.*\b(disable|off|stop|uninstall)\b/i,
494
- // disable Guardian via command
495
- /[\\\/]\.oriro[\\\/]guardian/i
496
- // 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)
497
592
  ];
498
593
  var TAMPER = [
499
594
  /\bchmod\s+-?\s*0?777\b/i,
500
- // world-writable
501
595
  /\b(ufw|firewall-cmd|iptables)\b.*\b(disable|stop|flush|-f)\b/i,
502
- // firewall down
503
596
  /\bset-mppreference\b.*-disable/i,
504
- // disable Defender
505
597
  /\bhistory\s+-c\b|\bunset\s+histfile\b|>\s*~?\/?\.bash_history/i
506
- // wipe history
507
598
  ];
508
599
  var MALWARE = [
509
600
  /\b(xmrig|minerd|cgminer|cpuminer|stratum\+tcp)\b/i,
510
601
  /\b(nanopool|minexmr|supportxmr|pool\.minexmr)\b/i
511
602
  ];
512
- function anyMatch(patterns, text) {
513
- return patterns.some((re) => re.test(text));
514
- }
515
603
  var DEFAULT_RULES = [
516
604
  {
517
605
  id: "fs-destruction",
518
- 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.",
519
607
  match: (c) => {
520
- const cmd = norm(cmdOf(c));
521
- 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;
522
616
  }
523
617
  },
524
618
  {
525
619
  id: "remote-code-exec",
526
- 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).",
527
621
  match: (c) => anyMatch(REMOTE_EXEC, norm(cmdOf(c))) ? block("remote-code-exec", "Downloading and executing remote code") : null
528
622
  },
529
623
  {
530
624
  id: "reverse-shell",
531
- 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).",
532
626
  match: (c) => anyMatch(REVERSE_SHELL, norm(cmdOf(c))) ? block("reverse-shell", "Opening a reverse shell / remote backdoor") : null
533
627
  },
534
628
  {
535
629
  id: "secret-exfiltration",
536
- 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.",
537
631
  match: (c) => {
538
632
  const cmd = norm(cmdOf(c));
539
- if (cmd && SECRET_PATHS.test(cmd) && NET_SINK.test(cmd)) {
540
- return block("secret-exfiltration", "Reading secrets and sending them off the machine");
541
- }
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");
542
636
  return null;
543
637
  }
544
638
  },
545
639
  {
546
640
  id: "persistence",
547
- description: "Flag cron/rc/startup/service edits used for Trojan persistence.",
548
- match: (c) => anyMatch(PERSISTENCE, norm(cmdOf(c))) ? ask("persistence", "Installing a persistent foothold (cron/startup/service)") : null
641
+ description: "Block SSH-key implants; flag cron/rc/startup/service edits used for Trojan persistence.",
642
+ match: (c) => {
643
+ const cmd = norm(cmdOf(c));
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;
646
+ }
549
647
  },
550
648
  {
551
649
  id: "guardian-self-defense",
552
650
  description: "Block any attempt to disable, uninstall, or rewrite Guardian's own config/state.",
553
651
  match: (c) => {
554
- if (anyMatch(GUARDIAN_TAMPER, norm(cmdOf(c)))) {
555
- return block("guardian-self-defense", "Attempt to disable or tamper with Guardian itself");
556
- }
557
- if (c.paths?.some((p) => /[\\/]\.oriro[\\/]guardian/i.test(p))) {
558
- return block("guardian-self-defense", "Direct write to Guardian's own config/state");
559
- }
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");
560
654
  return null;
561
655
  }
562
656
  },
563
657
  {
564
658
  id: "security-tamper",
565
659
  description: "Flag disabling firewall/Defender or wiping history.",
566
- 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
567
661
  },
568
662
  {
569
663
  id: "malware-signature",
@@ -572,9 +666,10 @@ var DEFAULT_RULES = [
572
666
  },
573
667
  {
574
668
  id: "v3lite",
575
- 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.",
576
670
  match: (c) => {
577
- 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 ?? ""}`);
578
673
  return r.safe ? null : block("v3lite", `Guardian V3 Lite flagged ${r.threat}`);
579
674
  }
580
675
  },
@@ -584,10 +679,9 @@ var DEFAULT_RULES = [
584
679
  match: (c) => {
585
680
  if (c.kind !== "fs" || !c.paths?.length) return null;
586
681
  const hit = c.paths.find(
587
- (p) => SECRET_PATHS.test(p) || /[\\\/]\.ssh[\\\/]/i.test(p) || // any write into ~/.ssh (e.g. authorized_keys = backdoor)
588
- /[\\\/](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)
589
683
  );
590
- 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;
591
685
  }
592
686
  }
593
687
  ];
@@ -1034,7 +1128,7 @@ async function speak(text, opts = {}) {
1034
1128
  }
1035
1129
 
1036
1130
  // src/avatar/onboarding.ts
1037
- import { stdin as stdin2, stdout as stdout2 } from "process";
1131
+ import { stdin as stdin2, stdout as stdout3 } from "process";
1038
1132
  import { createInterface as createInterface2 } from "readline/promises";
1039
1133
 
1040
1134
  // src/avatar/system-voice.ts
@@ -1108,7 +1202,7 @@ var C3 = {
1108
1202
  reset: "\x1B[0m"
1109
1203
  };
1110
1204
  async function previewAvatar(avatar) {
1111
- stdout2.write(
1205
+ stdout3.write(
1112
1206
  `
1113
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}
1114
1208
  `
@@ -1119,19 +1213,19 @@ async function previewAvatar(avatar) {
1119
1213
  png = readCachedAvatar(avatar.slug);
1120
1214
  } catch {
1121
1215
  }
1122
- stdout2.write("\n" + renderAvatar(avatar, png) + "\n");
1216
+ stdout3.write("\n" + renderAvatar(avatar, png) + "\n");
1123
1217
  setupSystemVoice();
1124
1218
  const spoke = await speak(`Hi, I'm ${avatar.slug}, your ORIRO terminal face. I'll speak your replies.`, {
1125
1219
  voiceId: avatar.slug,
1126
1220
  lang: "en-US"
1127
1221
  });
1128
- 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}
1129
1223
  `);
1130
1224
  }
1131
1225
  async function selectAvatarInteractive() {
1132
- const rl = createInterface2({ input: stdin2, output: stdout2 });
1226
+ const rl = createInterface2({ input: stdin2, output: stdout3 });
1133
1227
  try {
1134
- stdout2.write(
1228
+ stdout3.write(
1135
1229
  `
1136
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}
1137
1231
 
@@ -1139,36 +1233,46 @@ async function selectAvatarInteractive() {
1139
1233
  );
1140
1234
  const cats = avatarCategories();
1141
1235
  cats.forEach(
1142
- (cat2, i) => stdout2.write(
1236
+ (cat2, i) => stdout3.write(
1143
1237
  ` ${C3.teal}${String(i + 1).padStart(2)}${C3.reset} ${cat2} ${C3.dim}(${avatarsInCategory(cat2).length})${C3.reset}
1144
1238
  `
1145
1239
  )
1146
1240
  );
1147
- const cn = Number(
1148
- (await rl.question(`
1149
- ${C3.teal}\u203A${C3.reset} Pick a category number: `)).trim()
1150
- );
1151
- const cat = cats[cn - 1];
1152
- if (!cat) {
1153
- stdout2.write(" No category chosen.\n");
1154
- 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
+ `);
1155
1255
  }
1156
1256
  const list = avatarsInCategory(cat);
1157
- stdout2.write("\n");
1257
+ stdout3.write("\n");
1158
1258
  list.forEach(
1159
- (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}
1160
1260
  `)
1161
1261
  );
1162
- const an = Number(
1163
- (await rl.question(`
1164
- ${C3.teal}\u203A${C3.reset} Pick an avatar number: `)).trim()
1165
- );
1166
- const chosen = list[an - 1];
1167
- if (!chosen) {
1168
- stdout2.write(" No avatar chosen.\n");
1169
- 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
+ `);
1170
1275
  }
1171
- return chosen;
1172
1276
  } finally {
1173
1277
  rl.close();
1174
1278
  }
@@ -1225,22 +1329,22 @@ function hasScribeChoice() {
1225
1329
 
1226
1330
  // src/onboarding/wrapper.ts
1227
1331
  function isFirstRun() {
1228
- return !isLanguageConfigured();
1332
+ return !isLanguageConfigured() || !hasScribeChoice();
1229
1333
  }
1230
1334
  async function askYesNo(question) {
1231
- const rl = createInterface3({ input: stdin3, output: stdout3 });
1335
+ const rl = createInterface3({ input: stdin3, output: stdout4 });
1232
1336
  try {
1233
- const a = (await rl.question(`${question} ${dim("[Y/n]")} `)).trim().toLowerCase();
1337
+ const a = (await ask(rl, `${question} ${dim("[Y/n]")} `)).trim().toLowerCase();
1234
1338
  return a === "" || a === "y" || a === "yes";
1235
1339
  } finally {
1236
1340
  rl.close();
1237
1341
  }
1238
1342
  }
1239
1343
  async function runOnboarding() {
1240
- stdout3.write(banner());
1344
+ stdout4.write(banner());
1241
1345
  await runLanguageOnboarding();
1242
1346
  await activateGuardian();
1243
- 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.
1244
1348
 
1245
1349
  `);
1246
1350
  if (!isAvatarConfigured()) await runAvatarOnboarding();
@@ -1249,11 +1353,11 @@ async function runOnboarding() {
1249
1353
  "Remember with me? The Scriber keeps your work in context on THIS machine only \u2014 it never leaves it."
1250
1354
  );
1251
1355
  setScribeConsent(yes);
1252
- stdout3.write(yes ? ` ${accent("\u{1F4D3} Scriber")} on.
1356
+ stdout4.write(yes ? ` ${accent("\u{1F4D3} Scriber")} on.
1253
1357
  ` : ` ${dim("Scriber off \u2014 `oriro scribe on` anytime.")}
1254
1358
  `);
1255
1359
  }
1256
- stdout3.write(`
1360
+ stdout4.write(`
1257
1361
  ${accent("ORIRO is ready.")} ${dim("Type to chat \xB7 /exit to leave")}
1258
1362
 
1259
1363
  `);
@@ -1345,6 +1449,24 @@ var RouterMux = class {
1345
1449
  for (const s of stats) if (this.stats.has(s.id)) this.stats.set(s.id, { ...s });
1346
1450
  }
1347
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
+ }
1348
1470
 
1349
1471
  // src/routers/floor.ts
1350
1472
  var KEYLESS_FLOOR = [
@@ -1413,7 +1535,7 @@ async function validateRouter(entry, key, modelId) {
1413
1535
  try {
1414
1536
  let res;
1415
1537
  if (entry.api === "google-generative-ai") {
1416
- const url = `${entry.baseUrl.replace(/\/$/, "")}/models/${model}:generateContent${key ? `?key=${key}` : ""}`;
1538
+ const url = `${entry.baseUrl.replace(/\/$/, "")}/models/${model}:generateContent${key ? `?key=${encodeURIComponent(key)}` : ""}`;
1417
1539
  res = await fetch(url, {
1418
1540
  method: "POST",
1419
1541
  headers: { "content-type": "application/json" },
@@ -1472,6 +1594,9 @@ async function addRouter(entry, opts) {
1472
1594
  if (entry.comingSoon) {
1473
1595
  return { ok: false, validation: { ok: false, latencyMs: 0, model: "", error: "coming soon" } };
1474
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
+ }
1475
1600
  const key = opts?.key ?? (entry.keyless ? KEYLESS_SENTINEL : void 0);
1476
1601
  const v = await validateRouter(entry, key, opts?.modelId);
1477
1602
  if (!v.ok) return { ok: false, validation: v };
@@ -1489,7 +1614,11 @@ async function addRouter(entry, opts) {
1489
1614
  return { ok: true, validation: v };
1490
1615
  }
1491
1616
  function useRouters(ids) {
1492
- 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 };
1493
1622
  }
1494
1623
  function resolvePool() {
1495
1624
  const reg = readReg();
@@ -1569,955 +1698,1040 @@ function sanitizeEventToolCalls(ev) {
1569
1698
  return next;
1570
1699
  }
1571
1700
 
1572
- // src/routers/mux-provider.ts
1573
- var MUX_PROVIDER = "oriro-mux";
1574
- var MUX_MODEL = "oriro-free";
1575
- function errToCallError(msg) {
1576
- const text = msg.errorMessage ?? "";
1577
- 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");
1578
1717
  }
1579
- function buildErrorMessage(message) {
1580
- return {
1581
- role: "assistant",
1582
- content: [],
1583
- api: "openai-completions",
1584
- provider: MUX_PROVIDER,
1585
- model: MUX_MODEL,
1586
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
1587
- stopReason: "error",
1588
- timestamp: Date.now(),
1589
- errorMessage: message
1590
- };
1718
+ function journalFile(date) {
1719
+ return join13(scribeDir(), `${date}.md`);
1591
1720
  }
1592
- async function driveMux(out, mux, byId, context, options) {
1593
- let lastError;
1594
- for (const id of mux.ranked()) {
1595
- const router = byId.get(id);
1596
- if (!router) continue;
1597
- const t0 = Date.now();
1598
- let committed = false;
1599
- let lastPartial;
1600
- try {
1601
- const inner = piStreamSimple(routerModel(router), context, {
1602
- ...options ?? {},
1603
- apiKey: router.apiKey
1604
- });
1605
- let failedBeforeContent = false;
1606
- for await (const ev of inner) {
1607
- if (ev.type === "error") {
1608
- mux.recordFailure(id, errToCallError(ev.error));
1609
- if (!committed) {
1610
- lastError = ev.error;
1611
- failedBeforeContent = true;
1612
- break;
1613
- }
1614
- out.push(ev);
1615
- out.end(ev.error);
1616
- return;
1617
- }
1618
- committed = true;
1619
- if (ev.type === "done") {
1620
- mux.recordSuccess(id, Date.now() - t0);
1621
- const clean = sanitizeMessageToolCalls(scrubMessageIdentity(ev.message));
1622
- out.push({ type: "done", reason: ev.reason, message: clean });
1623
- out.end(clean);
1624
- return;
1625
- }
1626
- lastPartial = ev.partial;
1627
- out.push(sanitizeEventToolCalls(ev));
1628
- }
1629
- if (failedBeforeContent) continue;
1630
- mux.recordSuccess(id, Date.now() - t0);
1631
- out.end(lastPartial ? sanitizeMessageToolCalls(scrubMessageIdentity(lastPartial)) : void 0);
1632
- return;
1633
- } catch (e) {
1634
- mux.recordFailure(id, e);
1635
- }
1636
- }
1637
- const msg = lastError ?? buildErrorMessage(
1638
- "All keyless routers are unavailable. Add a BYOK key, select more free routers, or retry shortly."
1639
- );
1640
- out.push({ type: "error", reason: "error", error: msg });
1641
- out.end(msg);
1721
+ function digestFile() {
1722
+ return join13(scribeDir(), "_digest.md");
1642
1723
  }
1643
- function registerOriroMux(registry, opts = {}) {
1644
- registerOpenAICompletions();
1645
- const pooled = resolvePool();
1646
- const routers = opts.routers ?? (pooled.length > 0 ? pooled : KEYLESS_FLOOR);
1647
- const byId = new Map(routers.map((r) => [r.id, r]));
1648
- const mux = new RouterMux(routers.map((r) => r.id));
1649
- registry.registerProvider(MUX_PROVIDER, {
1650
- name: "ORIRO Free (keyless Mux)",
1651
- api: "openai-completions",
1652
- apiKey: "oriro-keyless",
1653
- // Placeholder — required by registry validation but never used: our custom streamSimple
1654
- // routes to the real keyless floor endpoints itself (see driveMux).
1655
- baseUrl: "http://oriro-mux.local",
1656
- models: [
1657
- {
1658
- id: MUX_MODEL,
1659
- name: "ORIRO Free (best-router)",
1660
- api: "openai-completions",
1661
- baseUrl: "http://oriro-mux.local",
1662
- reasoning: false,
1663
- input: ["text"],
1664
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1665
- contextWindow: 128e3,
1666
- maxTokens: 4096
1667
- }
1668
- ],
1669
- streamSimple: (_model, context, options) => {
1670
- const out = createAssistantMessageEventStream();
1671
- void driveMux(out, mux, byId, applyIdentity(context), options);
1672
- return out;
1673
- }
1674
- });
1675
- 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");
1676
1729
  }
1677
1730
 
1678
- // src/head/pi-tool.ts
1679
- 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
1680
1750
 
1681
- // src/head/comparison-engine.ts
1682
- var SECTION_RULES = [
1683
- {
1684
- type: "hero",
1685
- label: "Hero",
1686
- priority: "CRITICAL",
1687
- markup: [/<h1[\s>]/],
1688
- recommend: "Add a clear above-the-fold hero \u2014 one headline that states the value + one primary CTA."
1689
- },
1690
- {
1691
- type: "navigation",
1692
- label: "Navigation",
1693
- priority: "CRITICAL",
1694
- markup: [/<nav[\s>]/, /role=["']navigation["']/],
1695
- recommend: "Add a top navigation so visitors can reach key sections."
1696
- },
1697
- {
1698
- type: "features",
1699
- label: "Features",
1700
- priority: "CRITICAL",
1701
- text: [/\bfeatures?\b/, /\bwhat you (?:can|get)\b/, /\bcapabilit/],
1702
- recommend: "Add a features section that spells out concrete capabilities, not adjectives."
1703
- },
1704
- {
1705
- type: "pricing",
1706
- label: "Pricing",
1707
- priority: "CRITICAL",
1708
- text: [/\bpricing\b/, /\bper month\b/, /\b\/mo\b/, /\bfree plan\b/, /\$\d/, /₹\d/, /€\d/],
1709
- recommend: 'Add transparent pricing \u2014 a critical conversion element; even a single "Free" tier helps.'
1710
- },
1711
- {
1712
- type: "cta",
1713
- label: "Call-to-Action",
1714
- priority: "CRITICAL",
1715
- 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/],
1716
- recommend: 'Add a strong, repeated primary CTA ("Get started") so the next step is obvious.'
1717
- },
1718
- {
1719
- type: "testimonials",
1720
- label: "Testimonials",
1721
- priority: "HIGH",
1722
- text: [/\btestimonial/, /\bwhat (?:our )?(?:customers|users) say\b/, /\bloved by\b/, /\breview(?:s|ed)\b/],
1723
- recommend: "Add 2\u20133 customer testimonials with names/photos to build trust."
1724
- },
1725
- {
1726
- type: "stats",
1727
- label: "Stats / Metrics",
1728
- priority: "HIGH",
1729
- text: [/\b\d[\d,.]*\s*[kkmm]\+?\s*(?:users|customers|developers|downloads|teams)\b/, /\b9\d(?:\.\d+)?%\b/, /\buptime\b/],
1730
- recommend: 'Add impressive metrics ("10K+ users", "99.9% uptime") as social proof.'
1731
- },
1732
- {
1733
- type: "video",
1734
- label: "Video",
1735
- priority: "HIGH",
1736
- markup: [/<video[\s>]/, /youtube\.com\/embed/, /player\.vimeo\.com/, /<iframe[^>]+(?:youtube|vimeo)/],
1737
- text: [/\bwatch the (?:video|demo)\b/],
1738
- recommend: "Add a short explainer/demo video \u2014 it lifts conversion on landing pages."
1739
- },
1740
- {
1741
- type: "demo",
1742
- label: "Live Demo",
1743
- priority: "HIGH",
1744
- text: [/\btry it (?:now|live|free)\b/, /\bplayground\b/, /\binteractive demo\b/, /\blive demo\b/],
1745
- recommend: 'Add a "try it" live demo or playground so visitors experience the product immediately.'
1746
- },
1747
- {
1748
- type: "socialProof",
1749
- label: "Social Proof",
1750
- priority: "HIGH",
1751
- text: [/\btrusted by\b/, /\bbacked by\b/, /\bused by\b/, /\bas seen (?:in|on)\b/, /\bcustomers include\b/],
1752
- recommend: 'Add social proof (customer/investor logos, "trusted by \u2026") near the hero.'
1753
- },
1754
- {
1755
- type: "faq",
1756
- label: "FAQ",
1757
- priority: "MEDIUM",
1758
- text: [/\bfaq\b/, /\bfrequently asked\b/],
1759
- markup: [/<details[\s>]/],
1760
- recommend: "Add an FAQ that answers the top objections before they become exits."
1761
- },
1762
- {
1763
- type: "integrations",
1764
- label: "Integrations",
1765
- priority: "MEDIUM",
1766
- text: [/\bintegrations?\b/, /\bworks with\b/, /\bconnect your\b/],
1767
- recommend: "Add an integrations section showing what the product connects to."
1768
- },
1769
- {
1770
- type: "newsletter",
1771
- label: "Newsletter / Capture",
1772
- priority: "MEDIUM",
1773
- text: [/\bsubscribe\b/, /\bnewsletter\b/, /\bjoin (?:the )?waitlist\b/],
1774
- markup: [/type=["']email["']/],
1775
- recommend: "Add an email capture (newsletter/waitlist) so non-converting visitors are not lost."
1776
- },
1777
- {
1778
- type: "comparison",
1779
- label: "Comparison",
1780
- priority: "MEDIUM",
1781
- text: [/\bcompare\b/, /\bcomparison\b/, /\b vs\.? \b/, /\bwhy choose\b/],
1782
- recommend: 'Add a comparison ("us vs alternatives") to win evaluators who are shopping around.'
1783
- },
1784
- {
1785
- type: "team",
1786
- label: "Team / About",
1787
- priority: "LOW",
1788
- text: [/\bour team\b/, /\bmeet the team\b/, /\bfounders?\b/, /\babout us\b/],
1789
- 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;
1790
1760
  }
1791
- ];
1792
- var PRIORITY_RANK = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
1793
- var PRIORITY_EFFORT = { CRITICAL: "L", HIGH: "M", MEDIUM: "M", LOW: "S" };
1794
- var FETCH_TIMEOUT_MS = 12e3;
1795
- 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";
1796
- async function fetchPage(url) {
1797
- const controller = new AbortController();
1798
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1799
- 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");
1800
1800
  try {
1801
- const res = await fetch(url, {
1802
- signal: controller.signal,
1803
- redirect: "follow",
1804
- headers: { "user-agent": UA, accept: "text/html,application/xhtml+xml" }
1805
- });
1806
- const html = await res.text();
1807
- return { html, ms: Date.now() - start, status: res.status, ok: res.ok, error: "" };
1808
- } catch (err) {
1809
- 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);
1810
1804
  } finally {
1811
- clearTimeout(timer);
1805
+ closeSync(fd);
1812
1806
  }
1813
1807
  }
1814
- function toText(html) {
1815
- 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") : "";
1816
1811
  }
1817
- function firstMatch(re, hay) {
1818
- const m = re.exec(hay);
1819
- if (!m) return "";
1820
- const slice = (m[0] ?? "").trim();
1821
- 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`;
1822
1851
  }
1823
- function detectSections(rawHtmlLower, text) {
1824
- const found = [];
1825
- for (const rule of SECTION_RULES) {
1826
- let evidence = "";
1827
- for (const re of rule.markup ?? []) {
1828
- const hit = firstMatch(re, rawHtmlLower);
1829
- if (hit) {
1830
- evidence = hit;
1831
- break;
1832
- }
1833
- }
1834
- if (!evidence) {
1835
- for (const re of rule.text ?? []) {
1836
- const hit = firstMatch(re, text);
1837
- if (hit) {
1838
- evidence = hit;
1839
- break;
1840
- }
1841
- }
1842
- }
1843
- 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);
1844
1859
  }
1845
- return found;
1860
+ return h;
1846
1861
  }
1847
- function extractMatches(re, html, max) {
1848
- const out = [];
1849
- for (const m of html.matchAll(re)) {
1850
- const inner = (m[1] ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
1851
- if (inner && !out.includes(inner)) out.push(inner);
1852
- 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
+ });
1853
1878
  }
1854
- 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 };
1855
1891
  }
1856
- 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;
1857
- function extractStructure(url, fr) {
1858
- const html = fr.html;
1859
- const lowerHtml = html.toLowerCase();
1860
- const text = toText(html);
1861
- const titleM = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
1862
- const title = (titleM?.[1] ?? "").replace(/\s+/g, " ").trim();
1863
- const descM = /<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["']/i.exec(html) ?? /<meta[^>]+content=["']([^"']*)["'][^>]+name=["']description["']/i.exec(html);
1864
- const description = (descM?.[1] ?? "").replace(/\s+/g, " ").trim();
1865
- const headings = extractMatches(/<h[1-3][^>]*>([\s\S]*?)<\/h[1-3]>/gi, html, 12);
1866
- const ctaAll = extractMatches(/<(?:a|button)[^>]*>([\s\S]*?)<\/(?:a|button)>/gi, html, 80);
1867
- const ctas = [];
1868
- for (const c of ctaAll) {
1869
- if (CTA_WORDS.test(c) && !ctas.includes(c)) ctas.push(c);
1870
- 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;
1871
1896
  }
1872
- const forms = (lowerHtml.match(/<form[\s>]/g) ?? []).length;
1873
- const links = (lowerHtml.match(/<a[\s>]/g) ?? []).length;
1874
- const images = (lowerHtml.match(/<img[\s>]/g) ?? []).length;
1875
- const hasVideo = /<video[\s>]/.test(lowerHtml) || /(?:youtube\.com\/embed|player\.vimeo\.com)/.test(lowerHtml);
1876
- const domNodes = (html.match(/<[a-z!\/]/gi) ?? []).length;
1877
- let note = "";
1878
- if (fr.ok && text.length < 400 && domNodes < 60) {
1879
- 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;
1880
1899
  }
1881
- return {
1882
- url,
1883
- title,
1884
- description,
1885
- sections: detectSections(lowerHtml, text),
1886
- headings,
1887
- ctas,
1888
- forms,
1889
- links,
1890
- images,
1891
- hasVideo,
1892
- metrics: { htmlBytes: html.length, domNodes, fetchMs: fr.ms, status: fr.status },
1893
- ok: fr.ok && html.length > 0,
1894
- note: fr.ok ? note : `Could not load: ${fr.error || `HTTP ${fr.status}`}`
1895
- };
1896
- }
1897
- function ruleFor(type) {
1898
- return SECTION_RULES.find((r) => r.type === type) ?? SECTION_RULES[0];
1899
- }
1900
- function analyzeGaps(target, competitors) {
1901
- const targetTypes = new Set(target.sections.map((s) => s.type));
1902
- const compPresence = /* @__PURE__ */ new Map();
1903
- for (const comp of competitors) {
1904
- if (!comp.ok) continue;
1905
- for (const s of comp.sections) {
1906
- const list = compPresence.get(s.type) ?? [];
1907
- if (!list.includes(comp.url)) list.push(comp.url);
1908
- compPresence.set(s.type, list);
1909
- }
1910
- }
1911
- const missing = [];
1912
- const parity = [];
1913
- for (const [type, presentOn] of compPresence) {
1914
- if (targetTypes.has(type)) {
1915
- parity.push(type);
1916
- } else {
1917
- const rule = ruleFor(type);
1918
- missing.push({ section: type, label: rule.label, priority: rule.priority, presentOn, recommendation: rule.recommend });
1919
- }
1920
- }
1921
- missing.sort((a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || b.presentOn.length - a.presentOn.length);
1922
- const advantages = target.sections.filter((s) => !compPresence.has(s.type));
1923
- return { missing, advantages, parity };
1924
- }
1925
- function generateActionItems(missing) {
1926
- return missing.map((g) => ({
1927
- title: `Add a ${g.label} section`,
1928
- priority: g.priority,
1929
- effort: PRIORITY_EFFORT[g.priority],
1930
- rationale: `${g.presentOn.length} of the compared page(s) have it; you don't. ${g.recommendation}`
1931
- }));
1900
+ return false;
1932
1901
  }
1933
- 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");
1934
1910
  try {
1935
- return new URL(url).host.replace(/^www\./, "");
1936
- } catch {
1937
- return url;
1911
+ writeSync2(fd, full);
1912
+ fsyncSync2(fd);
1913
+ } finally {
1914
+ closeSync2(fd);
1938
1915
  }
1916
+ return p;
1939
1917
  }
1940
- function generateSummary(target, competitors, gaps) {
1941
- const okComps = competitors.filter((c) => c.ok);
1942
- const tName = hostOf(target.url);
1943
- if (!target.ok) return `Could not load ${tName} (${target.note}). Nothing to compare against yet.`;
1944
- if (okComps.length === 0) return `Loaded ${tName} (${target.sections.length} sections) but none of the comparison URLs could be loaded.`;
1945
- const crit = gaps.missing.filter((m) => m.priority === "CRITICAL").map((m) => m.label);
1946
- const high = gaps.missing.filter((m) => m.priority === "HIGH").map((m) => m.label);
1947
- const parts = [];
1948
- parts.push(`${tName} has ${target.sections.length} detectable sections; compared against ${okComps.length} page(s).`);
1949
- if (gaps.missing.length === 0) {
1950
- parts.push("No structural gaps found \u2014 you cover everything they do.");
1951
- } else {
1952
- parts.push(`${gaps.missing.length} gap(s) found.`);
1953
- if (crit.length) parts.push(`Critical: ${crit.join(", ")}.`);
1954
- 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
+ `;
1955
1927
  }
1956
- if (gaps.advantages.length) parts.push(`Your edge: ${gaps.advantages.map((a) => a.label).join(", ")}.`);
1957
- return parts.join(" ");
1928
+ return `**${label}:**
1929
+ ${value}
1930
+
1931
+ `;
1958
1932
  }
1959
- function normalizeUrl(u) {
1960
- const t = (u || "").trim();
1961
- if (!t) return t;
1962
- 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
+ `;
1963
1948
  }
1964
- async function comparePages(opts) {
1965
- const targetUrl = normalizeUrl(opts.targetUrl);
1966
- const competitorUrls = (opts.competitorUrls ?? []).map(normalizeUrl).filter((u) => u.length > 0).slice(0, 30);
1967
- const [targetFetch, ...compFetches] = await Promise.all([
1968
- fetchPage(targetUrl),
1969
- ...competitorUrls.map((u) => fetchPage(u))
1970
- ]);
1971
- const target = extractStructure(targetUrl, targetFetch ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" });
1972
- const competitors = competitorUrls.map(
1973
- (u, i) => extractStructure(u, compFetches[i] ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" })
1974
- );
1975
- 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() ?? "");
1976
1982
  return {
1977
- target,
1978
- competitors,
1979
- missing: gaps.missing,
1980
- advantages: gaps.advantages,
1981
- parity: gaps.parity,
1982
- actionItems: generateActionItems(gaps.missing),
1983
- summary: generateSummary(target, competitors, gaps)
1983
+ journalDate: rec.date,
1984
+ redactions,
1985
+ bytes: Buffer.byteLength(journal, "utf8"),
1986
+ auditClean
1984
1987
  };
1985
1988
  }
1986
1989
 
1987
- // src/head/pi-tool.ts
1988
- function summarizeForCoder(report) {
1989
- const lines = [report.summary];
1990
- const page = (p) => ` \u2022 ${p.url} \u2014 ${p.ok ? `${p.sections.length} sections: ${p.sections.map((s) => s.type).join(", ")}` : `not readable (${p.note})`}`;
1991
- lines.push("Pages seen:");
1992
- lines.push(page(report.target));
1993
- for (const c of report.competitors) if (c.url !== report.target.url) lines.push(page(c));
1994
- if (report.missing.length) {
1995
- lines.push("Missing on the target (gaps to build):");
1996
- for (const g of report.missing.slice(0, 12)) lines.push(` \u2022 ${g.label} (${g.priority}) \u2014 ${g.recommendation}`);
1997
- }
1998
- if (report.actionItems.length) {
1999
- lines.push("Suggested action items:");
2000
- for (const a of report.actionItems.slice(0, 12)) lines.push(` \u2192 ${a.title} [${a.priority}/${a.effort}] \u2014 ${a.rationale}`);
2001
- }
2002
- return lines.join("\n");
2003
- }
2004
- var InspectSiteParams = Type.Object({
2005
- url: Type.String({ description: "The target website URL to inspect or rebuild from." }),
2006
- competitors: Type.Optional(
2007
- Type.Array(Type.String(), { description: "Optional competitor/reference URLs to compare the target against." })
2008
- )
2009
- });
2010
- function registerHead(pi) {
2011
- pi.registerTool({
2012
- name: "inspect_site",
2013
- label: "ORIRO Head",
2014
- 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.",
2015
- parameters: InspectSiteParams,
2016
- async execute(_toolCallId, params) {
2017
- const target = params.url;
2018
- const competitors = params.competitors?.length ? params.competitors : [target];
2019
- const report = await comparePages({ targetUrl: target, competitorUrls: competitors });
2020
- return { content: [{ type: "text", text: summarizeForCoder(report) }], details: report };
2021
- }
2022
- });
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");
2023
2003
  }
2024
-
2025
- // src/scribe/scribe-pi.ts
2026
- import { existsSync as existsSync9, readFileSync as readFileSync16 } from "fs";
2027
- import { Type as Type2 } from "typebox";
2028
-
2029
- // src/scribe/capture.ts
2030
- import { closeSync as closeSync2, fsyncSync as fsyncSync2, mkdirSync as mkdirSync10, openSync as openSync2, writeSync as writeSync2 } from "fs";
2031
- import { join as join14 } from "path";
2032
-
2033
- // src/scribe/digest.ts
2034
- import { existsSync as existsSync5, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
2035
-
2036
- // src/scribe/paths.ts
2037
- import { join as join13 } from "path";
2038
- function scribeDir() {
2039
- const override = process.env.ORIRO_SCRIBE_DIR?.trim();
2040
- return override && override.length > 0 ? override : join13(CONFIG_DIR, "scribe");
2004
+ function faultLogFile() {
2005
+ return join15(scribeDir(), "_faults.log");
2041
2006
  }
2042
- function journalFile(date) {
2043
- return join13(scribeDir(), `${date}.md`);
2007
+ function read2() {
2008
+ try {
2009
+ return JSON.parse(readFileSync13(healthFile(), "utf8"));
2010
+ } catch {
2011
+ return { faultCount: 0 };
2012
+ }
2044
2013
  }
2045
- function digestFile() {
2046
- return join13(scribeDir(), "_digest.md");
2014
+ function write(h) {
2015
+ mkdirSync11(scribeDir(), { recursive: true });
2016
+ writeFileSync11(healthFile(), `${JSON.stringify(h, null, 2)}
2017
+ `, "utf8");
2047
2018
  }
2048
- function timelineFile() {
2049
- return join13(scribeDir(), "_timeline.md");
2019
+ function recordHealth() {
2020
+ const h = read2();
2021
+ h.lastWriteAt = (/* @__PURE__ */ new Date()).toISOString();
2022
+ write(h);
2050
2023
  }
2051
- function artifactsDir() {
2052
- 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
+ }
2053
2042
  }
2054
2043
 
2055
- // src/scribe/digest.ts
2056
- var DIGEST_CAP = 8192;
2057
- var TIMELINE_DAY_CAP = 400;
2058
- function read(file4) {
2059
- 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");
2060
2058
  }
2061
- function updateDigest(summary, context) {
2062
- mkdirSync8(scribeDir(), { recursive: true });
2063
- const existing = read(digestFile());
2064
- let contextBlock = context?.trim();
2065
- if (!contextBlock) {
2066
- const m = existing.match(/## Context\n([\s\S]*?)\n## /);
2067
- contextBlock = m?.[1]?.trim() ?? "_(not set yet)_";
2068
- }
2069
- const recentMatch = existing.match(/## Recent activity[^\n]*\n([\s\S]*)$/);
2070
- const priorRecent = recentMatch?.[1]?.trim() ?? "";
2071
- let recent = summary.trim() ? `- ${summary.trim()}
2072
- ${priorRecent}` : priorRecent;
2073
- const header2 = `# ORIRO Scribe \u2014 Digest
2074
-
2075
- ## Context
2076
- ${contextBlock}
2077
-
2078
- ## Recent activity (newest first)
2079
- `;
2080
- let out = header2 + recent;
2081
- while (Buffer.byteLength(out, "utf8") > DIGEST_CAP && recent.includes("\n")) {
2082
- recent = recent.slice(0, recent.lastIndexOf("\n")).trimEnd();
2083
- 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);
2084
2068
  }
2085
- writeFileSync10(digestFile(), out, "utf8");
2086
2069
  }
2087
- function updateTimeline(date, topic) {
2088
- mkdirSync8(scribeDir(), { recursive: true });
2089
- const clean = topic.replace(/\s+/g, " ").trim();
2090
- if (!clean) return;
2091
- const lines = read(timelineFile()).split("\n").filter(Boolean);
2092
- const header2 = "# ORIRO Scribe \u2014 Timeline";
2093
- const body = lines.filter((l) => l !== header2);
2094
- const idx = body.findIndex((l) => l.startsWith(`- ${date} \xB7`));
2095
- if (idx === -1) {
2096
- body.push(`- ${date} \xB7 ${clean}`.slice(0, TIMELINE_DAY_CAP + date.length + 6));
2097
- } else {
2098
- let merged = `${body[idx]}; ${clean}`;
2099
- if (merged.length > TIMELINE_DAY_CAP) merged = `${merged.slice(0, TIMELINE_DAY_CAP)}\u2026`;
2100
- 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
+ }
2101
2088
  }
2102
- body.sort();
2103
- writeFileSync10(timelineFile(), `${header2}
2104
- ${body.join("\n")}
2105
- `, "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");
2106
2101
  }
2107
2102
 
2108
- // src/scribe/journal.ts
2109
- import {
2110
- closeSync,
2111
- existsSync as existsSync6,
2112
- fsyncSync,
2113
- mkdirSync as mkdirSync9,
2114
- openSync,
2115
- readFileSync as readFileSync12,
2116
- writeSync
2117
- } from "fs";
2118
- function appendJournal(date, content) {
2119
- mkdirSync9(scribeDir(), { recursive: true });
2120
- 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;
2121
2111
  try {
2122
- writeSync(fd, content.endsWith("\n") ? content : `${content}
2123
- `);
2124
- 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();
2125
2124
  } finally {
2126
- closeSync(fd);
2125
+ draining = false;
2127
2126
  }
2128
2127
  }
2129
- function readJournal(date) {
2130
- const f = journalFile(date);
2131
- 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
+ }
2132
2157
  }
2133
2158
 
2134
- // src/scribe/redact.ts
2135
- var RULES = [
2136
- {
2137
- label: "private-key",
2138
- re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
2139
- },
2140
- { label: "anthropic-key", re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
2141
- { label: "openrouter-key", re: /sk-or-v1-[A-Za-z0-9]{20,}/g },
2142
- // Stripe-style keys (sk_live_/pk_live_/rk_test_/…), underscore segments.
2143
- { label: "stripe-key", re: /\b[srp]k_(?:live|test)_[A-Za-z0-9]{16,}/g },
2144
- // Generic sk- secret keys — allow hyphenated segments (sk-live-…, sk-proj-…) so a second
2145
- // hyphen no longer breaks the match (the gap the Scriber spike caught).
2146
- { label: "secret-key-sk", re: /sk[-_][A-Za-z0-9][A-Za-z0-9-]{14,}/g },
2147
- { label: "google-key", re: /AIza[0-9A-Za-z_-]{30,}/g },
2148
- { label: "groq-key", re: /gsk_[A-Za-z0-9]{20,}/g },
2149
- { label: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
2150
- { label: "github-token", re: /gh[posr]_[A-Za-z0-9]{30,}/g },
2151
- { label: "xai-key", re: /xai-[A-Za-z0-9]{20,}/g },
2152
- { label: "aws-key", re: /AKIA[0-9A-Z]{16}/g },
2153
- { label: "jwt", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g },
2154
- { label: "telegram-token", re: /\b\d{8,10}:[A-Za-z0-9_-]{30,}\b/g },
2155
- { label: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g },
2156
- { label: "phone", re: /(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}/g }
2157
- ];
2158
- function marker(label) {
2159
- 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();
2160
2165
  }
2161
- function entropy(s) {
2162
- const freq = /* @__PURE__ */ new Map();
2163
- for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
2164
- let h = 0;
2165
- for (const n of freq.values()) {
2166
- const p = n / s.length;
2167
- 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
+ }
2168
2183
  }
2169
- return h;
2184
+ return hits;
2170
2185
  }
2171
- function looksLikeUnknownSecret(token) {
2172
- if (token.length < 40) return false;
2173
- if (token.includes("\u27E8REDACTED:")) return false;
2174
- if (/^[0-9a-f]+$/i.test(token)) return false;
2175
- const classes = (/[a-z]/.test(token) ? 1 : 0) + (/[A-Z]/.test(token) ? 1 : 0) + (/[0-9]/.test(token) ? 1 : 0);
2176
- if (classes < 2) return false;
2177
- 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 });
2178
2192
  }
2179
- function redact(input) {
2180
- const counts = /* @__PURE__ */ new Map();
2181
- let text = input;
2182
- for (const rule of RULES) {
2183
- text = text.replace(rule.re, () => {
2184
- counts.set(rule.label, (counts.get(rule.label) ?? 0) + 1);
2185
- return marker(rule.label);
2186
- });
2187
- }
2188
- text = text.split(/(\s+)/).map((tok) => {
2189
- if (looksLikeUnknownSecret(tok)) {
2190
- counts.set("high-entropy", (counts.get("high-entropy") ?? 0) + 1);
2191
- return marker("high-entropy");
2192
- }
2193
- return tok;
2194
- }).join("");
2195
- const redactions = [...counts.entries()].map(([label, count]) => ({
2196
- label,
2197
- count
2198
- }));
2199
- return { text, redactions };
2193
+ var pendingUserInput = "";
2194
+ function noteUserInput(text) {
2195
+ pendingUserInput = text;
2200
2196
  }
2201
- function containsSecret(text) {
2202
- for (const rule of RULES) {
2203
- rule.re.lastIndex = 0;
2204
- 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 {
2205
2210
  }
2206
- for (const tok of text.split(/\s+/)) {
2207
- 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."
2208
2504
  }
2209
- return false;
2210
- }
2211
-
2212
- // src/scribe/capture.ts
2213
- var INLINE_CAP = 4e3;
2214
- function sideFile(date, ts, kind, full) {
2215
- mkdirSync10(artifactsDir(), { recursive: true });
2216
- const name = `${date}_${ts.replace(/[:.]/g, "-")}_${kind}.md`;
2217
- const p = join14(artifactsDir(), name);
2218
- 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();
2219
2514
  try {
2220
- writeSync2(fd, full);
2221
- 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" };
2222
2524
  } finally {
2223
- closeSync2(fd);
2224
- }
2225
- return p;
2226
- }
2227
- function field(date, ts, label, value) {
2228
- if (!value || !value.trim()) return "";
2229
- if (value.length > INLINE_CAP) {
2230
- const ref = sideFile(date, ts, label.toLowerCase().replace(/\s+/g, "-"), value);
2231
- return `**${label}** (full \u2192 ${ref}):
2232
- ${value.slice(0, INLINE_CAP)}
2233
- \u2026(truncated; full content in artifact)
2234
-
2235
- `;
2236
- }
2237
- return `**${label}:**
2238
- ${value}
2239
-
2240
- `;
2241
- }
2242
- function renderTurn(rec) {
2243
- let md = `## ${rec.ts}
2244
-
2245
- `;
2246
- md += field(rec.date, rec.ts, "User", rec.user);
2247
- md += field(rec.date, rec.ts, "Router", rec.router);
2248
- if (rec.tools?.length) md += `**Tools:** ${rec.tools.join(", ")}
2249
-
2250
- `;
2251
- if (rec.files?.length) md += `**Files:** ${rec.files.join(", ")}
2252
-
2253
- `;
2254
- md += field(rec.date, rec.ts, "Note", rec.note);
2255
- return `${md}---
2256
- `;
2257
- }
2258
- function oneLineSummary(rec) {
2259
- const bits = [];
2260
- if (rec.user) bits.push(rec.user.replace(/\s+/g, " ").slice(0, 80));
2261
- if (rec.files?.length) bits.push(`files: ${rec.files.slice(0, 3).join(", ")}`);
2262
- if (rec.note) bits.push(rec.note.replace(/\s+/g, " ").slice(0, 60));
2263
- return bits.join(" \xB7 ") || "(activity)";
2264
- }
2265
- function captureTurn(rec) {
2266
- const safe = redact(renderTurn(rec));
2267
- appendJournal(rec.date, `${safe.text}
2268
- `);
2269
- const summary = redact(`${rec.ts} \xB7 ${oneLineSummary(rec)}`).text;
2270
- updateDigest(summary, rec.context ? redact(rec.context).text : void 0);
2271
- updateTimeline(rec.date, redact(oneLineSummary(rec)).text);
2272
- const auditClean = !containsSecret(readJournal(rec.date));
2273
- return {
2274
- journalDate: rec.date,
2275
- redactions: safe.redactions,
2276
- bytes: Buffer.byteLength(safe.text, "utf8"),
2277
- auditClean
2278
- };
2279
- }
2280
-
2281
- // src/scribe/health.ts
2282
- import {
2283
- closeSync as closeSync3,
2284
- fsyncSync as fsyncSync3,
2285
- mkdirSync as mkdirSync11,
2286
- openSync as openSync3,
2287
- readFileSync as readFileSync13,
2288
- writeFileSync as writeFileSync11,
2289
- writeSync as writeSync3
2290
- } from "fs";
2291
- import { join as join15 } from "path";
2292
- function healthFile() {
2293
- return join15(scribeDir(), "_health.json");
2294
- }
2295
- function faultLogFile() {
2296
- return join15(scribeDir(), "_faults.log");
2297
- }
2298
- function read2() {
2299
- try {
2300
- return JSON.parse(readFileSync13(healthFile(), "utf8"));
2301
- } catch {
2302
- return { faultCount: 0 };
2525
+ clearTimeout(timer);
2303
2526
  }
2304
2527
  }
2305
- function write(h) {
2306
- mkdirSync11(scribeDir(), { recursive: true });
2307
- writeFileSync11(healthFile(), `${JSON.stringify(h, null, 2)}
2308
- `, "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();
2309
2530
  }
2310
- function recordHealth() {
2311
- const h = read2();
2312
- h.lastWriteAt = (/* @__PURE__ */ new Date()).toISOString();
2313
- 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;
2314
2536
  }
2315
- function recordFault(role, err) {
2316
- try {
2317
- mkdirSync11(scribeDir(), { recursive: true });
2318
- const msg = `${(/* @__PURE__ */ new Date()).toISOString()} [${role}] ${err instanceof Error ? err.message : String(err)}`;
2319
- const fd = openSync3(faultLogFile(), "a");
2320
- try {
2321
- writeSync3(fd, `${msg}
2322
- `);
2323
- fsyncSync3(fd);
2324
- } finally {
2325
- 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
+ }
2326
2547
  }
2327
- const h = read2();
2328
- h.faultCount = (h.faultCount ?? 0) + 1;
2329
- h.lastFault = msg;
2330
- write(h);
2331
- } 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 });
2332
2558
  }
2559
+ return found;
2333
2560
  }
2334
-
2335
- // src/scribe/wal.ts
2336
- import {
2337
- closeSync as closeSync4,
2338
- existsSync as existsSync7,
2339
- fsyncSync as fsyncSync4,
2340
- mkdirSync as mkdirSync12,
2341
- openSync as openSync4,
2342
- readFileSync as readFileSync14,
2343
- writeFileSync as writeFileSync12,
2344
- writeSync as writeSync4
2345
- } from "fs";
2346
- import { join as join16 } from "path";
2347
- function walFile() {
2348
- return join16(scribeDir(), "_wal.jsonl");
2349
- }
2350
- function appendLine(obj) {
2351
- mkdirSync12(scribeDir(), { recursive: true });
2352
- const fd = openSync4(walFile(), "a");
2353
- try {
2354
- writeSync4(fd, `${JSON.stringify(obj)}
2355
- `);
2356
- fsyncSync4(fd);
2357
- } finally {
2358
- 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;
2359
2567
  }
2568
+ return out;
2360
2569
  }
2361
- function walAppend(id, rec) {
2362
- appendLine({ t: "add", id, rec });
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
+ };
2363
2610
  }
2364
- function walCommit(id) {
2365
- appendLine({ t: "commit", id });
2611
+ function ruleFor(type) {
2612
+ return SECTION_RULES.find((r) => r.type === type) ?? SECTION_RULES[0];
2366
2613
  }
2367
- function walPending() {
2368
- if (!existsSync7(walFile())) return [];
2369
- const committed = /* @__PURE__ */ new Set();
2370
- const adds = /* @__PURE__ */ new Map();
2371
- for (const line of readFileSync14(walFile(), "utf8").split("\n")) {
2372
- if (!line.trim()) continue;
2373
- try {
2374
- const e = JSON.parse(line);
2375
- if (e.t === "commit") committed.add(e.id);
2376
- else if (e.t === "add" && e.rec) adds.set(e.id, e.rec);
2377
- } catch {
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);
2378
2623
  }
2379
2624
  }
2380
- const out = [];
2381
- for (const [id, rec] of adds) {
2382
- 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
+ }
2383
2634
  }
2384
- return out;
2385
- }
2386
- function walCompact() {
2387
- if (!existsSync7(walFile())) return;
2388
- const pending = walPending();
2389
- const body = pending.map((p) => JSON.stringify({ t: "add", id: p.id, rec: p.rec })).join("\n");
2390
- writeFileSync12(walFile(), body ? `${body}
2391
- ` : "", "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 };
2392
2638
  }
2393
-
2394
- // src/scribe/supervisor.ts
2395
- var draining = false;
2396
- function uid(ts) {
2397
- 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
+ }));
2398
2646
  }
2399
- function drainBacklog() {
2400
- if (draining) return;
2401
- draining = true;
2647
+ function hostOf(url) {
2402
2648
  try {
2403
- let drained = 0;
2404
- for (const e of walPending()) {
2405
- try {
2406
- captureTurn(e.rec);
2407
- walCommit(e.id);
2408
- drained++;
2409
- } catch (err) {
2410
- recordFault("standby-replay", err);
2411
- break;
2412
- }
2413
- }
2414
- if (drained > 0) walCompact();
2415
- } finally {
2416
- draining = false;
2649
+ return new URL(url).host.replace(/^www\./, "");
2650
+ } catch {
2651
+ return url;
2417
2652
  }
2418
2653
  }
2419
- function supervisedCapture(rec) {
2420
- try {
2421
- drainBacklog();
2422
- const id = uid(rec.ts);
2423
- walAppend(id, rec);
2424
- try {
2425
- const res = captureTurn(rec);
2426
- walCommit(id);
2427
- recordHealth();
2428
- return res;
2429
- } catch (primaryErr) {
2430
- recordFault("primary", primaryErr);
2431
- try {
2432
- const res = captureTurn(rec);
2433
- walCommit(id);
2434
- recordHealth();
2435
- return res;
2436
- } catch (standbyErr) {
2437
- recordFault("standby", standbyErr);
2438
- return null;
2439
- }
2440
- }
2441
- } catch (fatal) {
2442
- recordFault("supervisor", fatal);
2443
- 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(", ")}.`);
2444
2669
  }
2670
+ if (gaps.advantages.length) parts.push(`Your edge: ${gaps.advantages.map((a) => a.label).join(", ")}.`);
2671
+ return parts.join(" ");
2445
2672
  }
2446
-
2447
- // src/scribe/retrieval.ts
2448
- import { existsSync as existsSync8, readFileSync as readFileSync15, readdirSync } from "fs";
2449
- function listDays() {
2450
- const dir = scribeDir();
2451
- if (!existsSync8(dir)) return [];
2452
- return readdirSync(dir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).map((f) => f.replace(/\.md$/, "")).sort();
2453
- }
2454
- function readDay(date) {
2455
- const f = journalFile(date);
2456
- 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}`;
2457
2677
  }
2458
- function searchScribe(query, limit = 100) {
2459
- const q = query.toLowerCase().trim();
2460
- if (!q) return [];
2461
- const hits = [];
2462
- for (const date of listDays().reverse()) {
2463
- const lines = readDay(date).split("\n");
2464
- for (let i = 0; i < lines.length; i++) {
2465
- const ln = lines[i];
2466
- if (ln && ln.toLowerCase().includes(q)) {
2467
- hits.push({ date, line: i + 1, text: ln.trim().slice(0, 200) });
2468
- if (hits.length >= limit) return hits;
2469
- }
2470
- }
2471
- }
2472
- 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
+ };
2473
2699
  }
2474
2700
 
2475
- // src/scribe/scribe-pi.ts
2476
- function scribeTurn(input) {
2477
- if (!isScribeEnabled()) return;
2478
- const ts = (/* @__PURE__ */ new Date()).toISOString();
2479
- 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");
2480
2717
  }
2481
- 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) {
2482
2725
  pi.registerTool({
2483
- name: "scribe_recall",
2484
- label: "ORIRO Scribe",
2485
- 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.",
2486
- parameters: Type2.Object({
2487
- query: Type2.Optional(Type2.String({ description: "Keyword/topic to search across all journals." })),
2488
- day: Type2.Optional(Type2.String({ description: "A specific day YYYY-MM-DD to read in full." }))
2489
- }),
2490
- async execute(_id, params) {
2491
- let text;
2492
- const details = {};
2493
- if (!isScribeEnabled()) {
2494
- text = "Scribe is off (the user has not enabled it).";
2495
- } else if (params.day) {
2496
- text = readDay(params.day) || `No journal for ${params.day}. Days: ${listDays().join(", ") || "none"}`;
2497
- details.day = params.day;
2498
- } else {
2499
- const hits = params.query ? searchScribe(params.query) : [];
2500
- details.hits = hits;
2501
- 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"}`;
2502
- }
2503
- return { content: [{ type: "text", text }], details };
2504
- }
2505
- });
2506
- }
2507
- function attachScribe(session) {
2508
- let user = "";
2509
- let assistant = "";
2510
- const tools = /* @__PURE__ */ new Set();
2511
- session.subscribe((e) => {
2512
- if (!isScribeEnabled()) return;
2513
- if (e?.type === "user_message" || e?.type === "session_user_message") user = String(e.text ?? e.message ?? user);
2514
- if (e?.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") assistant += e.assistantMessageEvent.delta ?? "";
2515
- if ((e?.type === "tool_call" || e?.type === "tool_execution_start") && e.toolName) tools.add(String(e.toolName));
2516
- if (e?.type === "agent_end") {
2517
- scribeTurn({ user: user || void 0, router: "oriro-free", tools: [...tools], note: assistant.slice(0, 4e3) || void 0 });
2518
- user = "";
2519
- assistant = "";
2520
- 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 };
2521
2735
  }
2522
2736
  });
2523
2737
  }
@@ -2812,15 +3026,62 @@ function setupNllbTranslator(opts) {
2812
3026
  return instance;
2813
3027
  }
2814
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
+
2815
3051
  // src/repl.ts
3052
+ function replHelp() {
3053
+ return `
3054
+ ${accent("ORIRO terminal \u2014 help")}
3055
+ ${dim("Just type to chat; ORIRO writes and runs code for you (keyless, free).")}
3056
+
3057
+ ${accent("/help")} this help ${accent("/exit")} or ${accent("/quit")} leave ${dim("Ctrl-D / Ctrl-C also exit")}
3058
+ ${dim("Run these OUTSIDE the chat (in your shell):")}
3059
+ ${dim("oriro skills \xB7 routers \xB7 connectors \xB7 channels \xB7 scribe \xB7 language \xB7 avatar")}
3060
+
3061
+ `;
3062
+ }
2816
3063
  async function runRepl() {
2817
3064
  if (isFirstRun()) await runOnboarding();
2818
- else stdout4.write(banner());
2819
- const lang = getTerminalLanguage().code;
2820
- const isEnglish2 = lang.toLowerCase().startsWith("en");
2821
- if (!isEnglish2) setupNllbTranslator();
3065
+ else stdout5.write(banner());
3066
+ const isEnglish3 = getTerminalLanguage().code.toLowerCase().startsWith("en");
2822
3067
  const { session } = await assembleOriroSession();
2823
- 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);
2824
3085
  try {
2825
3086
  for (; ; ) {
2826
3087
  let line;
@@ -2830,14 +3091,20 @@ async function runRepl() {
2830
3091
  break;
2831
3092
  }
2832
3093
  if (!line) continue;
2833
- if (line === "/exit" || line === "/quit") break;
2834
- const english = await translateForCoder(line, lang);
3094
+ const slash = line.toLowerCase();
3095
+ if (slash === "/exit" || slash === "/quit") break;
3096
+ if (slash === "/help" || slash === "/?") {
3097
+ stdout5.write(replHelp());
3098
+ continue;
3099
+ }
3100
+ const english = await translateIncoming(line);
3101
+ noteUserInput(line);
2835
3102
  let out = "";
2836
3103
  const unsub = session.subscribe((e) => {
2837
3104
  if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
2838
3105
  const d = e.assistantMessageEvent.delta ?? "";
2839
3106
  out += d;
2840
- if (isEnglish2) stdout4.write(d);
3107
+ if (isEnglish3) stdout5.write(d);
2841
3108
  }
2842
3109
  });
2843
3110
  try {
@@ -2845,15 +3112,18 @@ async function runRepl() {
2845
3112
  } finally {
2846
3113
  unsub();
2847
3114
  }
2848
- if (isEnglish2) stdout4.write("\n\n");
2849
- else stdout4.write(`${await translateForUser(out.trim(), lang)}
3115
+ if (isEnglish3) stdout5.write("\n\n");
3116
+ else stdout5.write(`${await translateOutgoing(out.trim())}
2850
3117
 
2851
3118
  `);
2852
3119
  }
2853
3120
  } finally {
2854
- rl.close();
2855
- session.dispose();
2856
- 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
+ }
2857
3127
  }
2858
3128
  }
2859
3129
 
@@ -3220,9 +3490,12 @@ var heading = (s) => {
3220
3490
  ${bold(accent(s))}
3221
3491
  `);
3222
3492
  };
3493
+ var DieError = class extends Error {
3494
+ };
3223
3495
  function die(msg) {
3224
3496
  fail(msg);
3225
- process.exit(1);
3497
+ process.exitCode = 1;
3498
+ throw new DieError(msg);
3226
3499
  }
3227
3500
 
3228
3501
  // src/commands/routers.ts
@@ -3251,11 +3524,12 @@ function registerRoutersCommand(program2) {
3251
3524
  ok(`added ${accent(slug)} (${res.validation.latencyMs}ms, model ${res.validation.model}) \u2192 active pool`);
3252
3525
  });
3253
3526
  routers.command("use <slugs...>").description("set the active router pool (ids must be added first)").action((slugs) => {
3254
- useRouters(slugs);
3255
- const pool = resolvePool();
3256
- const missing = slugs.filter((s) => !pool.some((p) => p.id === s));
3257
- ok(`pool set: ${pool.map((p) => p.id).join(", ") || "(empty)"}`);
3258
- 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(", ")}`);
3259
3533
  });
3260
3534
  }
3261
3535
 
@@ -4284,6 +4558,12 @@ function writeAdded(slugs) {
4284
4558
  function listConnectors(category) {
4285
4559
  return category ? CONNECTOR_CATALOG.filter((c) => c.category === category) : CONNECTOR_CATALOG;
4286
4560
  }
4561
+ function connectorCategories() {
4562
+ return [...new Set(CONNECTOR_CATALOG.map((c) => c.category))].sort();
4563
+ }
4564
+ function isConnectorAdded(slug) {
4565
+ return readAdded().includes(slug);
4566
+ }
4287
4567
  function addConnector(slug) {
4288
4568
  const entry = connectorBySlug(slug);
4289
4569
  if (!entry) return { ok: false, error: `unknown connector '${slug}' \u2014 run \`oriro connectors list\`` };
@@ -4297,31 +4577,45 @@ function addedConnectors() {
4297
4577
  return CONNECTOR_CATALOG.filter((c) => added.has(c.slug));
4298
4578
  }
4299
4579
  function removeConnector(slug) {
4300
- writeAdded(readAdded().filter((s) => s !== slug));
4580
+ const before = readAdded();
4581
+ if (!before.includes(slug)) return false;
4582
+ writeAdded(before.filter((s) => s !== slug));
4583
+ return true;
4301
4584
  }
4302
4585
 
4303
4586
  // src/commands/connectors.ts
4304
4587
  function registerConnectorsCommand(program2) {
4305
4588
  const connectors = program2.command("connectors").description("MCP connectors \u2014 add external tools/services (inert until used)");
4306
4589
  connectors.command("list [category]").description("list the connector catalog (optionally filtered by category)").action((category) => {
4590
+ if (category && !connectorCategories().includes(category)) {
4591
+ die(`unknown category '${category}' \u2014 categories: ${connectorCategories().join(", ")}`);
4592
+ }
4307
4593
  const entries = listConnectors(category);
4308
4594
  const added = new Set(addedConnectors().map((c) => c.slug));
4309
4595
  heading(category ? `Connectors \xB7 ${category}` : "Connectors");
4596
+ let addable = 0;
4310
4597
  for (const c of entries) {
4311
- const mark = added.has(c.slug) ? accent("\u25CF") : dim("\u25CB");
4312
- 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)}
4313
4603
  `);
4314
4604
  }
4315
- 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`);
4316
4606
  });
4317
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
+ }
4318
4612
  const res = addConnector(slug);
4319
4613
  if (!res.ok) die(res.error ?? `could not add '${slug}'`);
4320
- ok(`added ${accent(slug)} \u2014 inert until a session uses it`);
4614
+ ok(`added ${accent(slug)} \u2014 recorded locally`);
4321
4615
  });
4322
4616
  connectors.command("remove <slug>").description("remove a connector").action((slug) => {
4323
- removeConnector(slug);
4324
- ok(`removed ${accent(slug)}`);
4617
+ if (removeConnector(slug)) ok(`removed ${accent(slug)}`);
4618
+ else info(`'${slug}' is not in your added list \u2014 nothing to remove`);
4325
4619
  });
4326
4620
  }
4327
4621
 
@@ -4374,6 +4668,7 @@ var OriroChannelHost = class {
4374
4668
  if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") out += e.assistantMessageEvent.delta ?? "";
4375
4669
  });
4376
4670
  try {
4671
+ noteUserInput(text);
4377
4672
  await session.prompt(text);
4378
4673
  } finally {
4379
4674
  unsub();
@@ -4554,7 +4849,7 @@ function registerChannelsCommand(program2) {
4554
4849
  if (!isKind(kind)) die(`unknown channel '${kind}' \u2014 one of: ${KINDS.join(", ")}`);
4555
4850
  if (kind === "whatsapp") {
4556
4851
  if (!opts.acceptRisk) {
4557
- 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).");
4558
4853
  info("If you accept that risk, re-run: `oriro channels start whatsapp --accept-risk`");
4559
4854
  return;
4560
4855
  }
@@ -4571,6 +4866,10 @@ function registerChannelsCommand(program2) {
4571
4866
  });
4572
4867
  channels.command("remove <kind>").description("remove a configured channel").action((kind) => {
4573
4868
  if (!isKind(kind)) die(`unknown channel '${kind}' \u2014 one of: ${KINDS.join(", ")}`);
4869
+ if (!readChannels().some((c) => c.kind === kind)) {
4870
+ info(`no ${kind} channel configured \u2014 nothing to remove`);
4871
+ return;
4872
+ }
4574
4873
  removeChannel(kind);
4575
4874
  ok(`removed ${accent(kind)}`);
4576
4875
  });
@@ -4595,6 +4894,9 @@ function registerSkillsCommand(program2) {
4595
4894
 
4596
4895
  // src/commands/language.ts
4597
4896
  import { stdin as stdin5 } from "process";
4897
+ function resolveLanguage(input) {
4898
+ return languageByCode(input) ?? LANGUAGES.find((l) => l.name.toLowerCase() === input.trim().toLowerCase());
4899
+ }
4598
4900
  function registerLanguageCommand(program2) {
4599
4901
  program2.command("language").description("show or change your terminal language").argument("[code]", "switch directly to this language (ISO code or name, e.g. es)").option("-a, --all", "list every available language").action(async (code, opts) => {
4600
4902
  if (opts.all) {
@@ -4607,7 +4909,7 @@ function registerLanguageCommand(program2) {
4607
4909
  return;
4608
4910
  }
4609
4911
  if (code) {
4610
- const lang = languageByCode(code);
4912
+ const lang = resolveLanguage(code);
4611
4913
  if (!lang) die(`unknown language '${code}' \u2014 run \`oriro language --all\` to see the list`);
4612
4914
  setTerminalLanguage(lang);
4613
4915
  ok(`${accent(lang.name)} is now your terminal language.`);
@@ -4665,7 +4967,12 @@ var version = createRequire(import.meta.url)("../package.json").version;
4665
4967
  var program = new Command();
4666
4968
  program.name("oriro").description("ORIRO \u2014 a free, on-device-friendly terminal AI agent.").version(version, "-v, --version").action(async (_options, command) => {
4667
4969
  if (command.args.length > 0) {
4668
- 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}'
4669
4976
  Run 'oriro --help' to see available commands.
4670
4977
  `);
4671
4978
  process.exitCode = 1;
@@ -4681,6 +4988,7 @@ registerSkillsCommand(program);
4681
4988
  registerLanguageCommand(program);
4682
4989
  registerAvatarCommand(program);
4683
4990
  program.parseAsync().catch((e) => {
4991
+ if (e instanceof DieError) return;
4684
4992
  process.stderr.write(`
4685
4993
  ORIRO error: ${e instanceof Error ? e.stack ?? e.message : String(e)}
4686
4994
  `);