@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.
- package/README.md +16 -12
- package/dist/cli.js +1327 -1019
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
300
|
+
stdout2.write(` ${C.dim}You type and read in your language; the AI works in English for you.${C.reset}
|
|
284
301
|
`);
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
494
|
+
new RegExp(`\\bmkfs\\.?\\w*\\s+/dev/${DISK}`, "i"),
|
|
436
495
|
// reformat a disk
|
|
437
|
-
|
|
496
|
+
new RegExp(`\\bdd\\b[^\\n]*\\bof=/dev/${DISK}`, "i"),
|
|
438
497
|
// overwrite raw disk
|
|
439
|
-
|
|
440
|
-
//
|
|
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.*[
|
|
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
|
-
|
|
448
|
-
//
|
|
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
|
-
|
|
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
|
-
/\
|
|
458
|
-
//
|
|
459
|
-
/\
|
|
460
|
-
//
|
|
461
|
-
/\
|
|
462
|
-
//
|
|
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
|
-
/\
|
|
466
|
-
// nc -e
|
|
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
|
-
/\
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
/
|
|
496
|
-
|
|
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
|
|
521
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
540
|
-
|
|
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: "
|
|
548
|
-
match: (c) =>
|
|
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
|
-
|
|
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))) ?
|
|
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
|
|
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) || /[
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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:
|
|
1226
|
+
const rl = createInterface2({ input: stdin2, output: stdout3 });
|
|
1133
1227
|
try {
|
|
1134
|
-
|
|
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) =>
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
-
|
|
1257
|
+
stdout3.write("\n");
|
|
1158
1258
|
list.forEach(
|
|
1159
|
-
(a, i) =>
|
|
1259
|
+
(a, i) => stdout3.write(` ${C3.teal}${String(i + 1).padStart(2)}${C3.reset} ${a.slug}
|
|
1160
1260
|
`)
|
|
1161
1261
|
);
|
|
1162
|
-
|
|
1163
|
-
(await rl
|
|
1164
|
-
${C3.teal}\u203A${C3.reset} Pick an avatar number: `)).trim()
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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:
|
|
1335
|
+
const rl = createInterface3({ input: stdin3, output: stdout4 });
|
|
1232
1336
|
try {
|
|
1233
|
-
const a = (await rl
|
|
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
|
-
|
|
1344
|
+
stdout4.write(banner());
|
|
1241
1345
|
await runLanguageOnboarding();
|
|
1242
1346
|
await activateGuardian();
|
|
1243
|
-
|
|
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
|
-
|
|
1356
|
+
stdout4.write(yes ? ` ${accent("\u{1F4D3} Scriber")} on.
|
|
1253
1357
|
` : ` ${dim("Scriber off \u2014 `oriro scribe on` anytime.")}
|
|
1254
1358
|
`);
|
|
1255
1359
|
}
|
|
1256
|
-
|
|
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
|
-
|
|
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/
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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
|
|
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
|
-
|
|
1593
|
-
|
|
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
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
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/
|
|
1679
|
-
|
|
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
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
const
|
|
1798
|
-
const
|
|
1799
|
-
const
|
|
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
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
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
|
-
|
|
1805
|
+
closeSync(fd);
|
|
1812
1806
|
}
|
|
1813
1807
|
}
|
|
1814
|
-
function
|
|
1815
|
-
|
|
1808
|
+
function readJournal(date) {
|
|
1809
|
+
const f = journalFile(date);
|
|
1810
|
+
return existsSync6(f) ? readFileSync12(f, "utf8") : "";
|
|
1816
1811
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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
|
|
1824
|
-
const
|
|
1825
|
-
for (const
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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
|
|
1860
|
+
return h;
|
|
1846
1861
|
}
|
|
1847
|
-
function
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
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
|
|
1873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1911
|
+
writeSync2(fd, full);
|
|
1912
|
+
fsyncSync2(fd);
|
|
1913
|
+
} finally {
|
|
1914
|
+
closeSync2(fd);
|
|
1938
1915
|
}
|
|
1916
|
+
return p;
|
|
1939
1917
|
}
|
|
1940
|
-
function
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
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
|
-
|
|
1957
|
-
|
|
1928
|
+
return `**${label}:**
|
|
1929
|
+
${value}
|
|
1930
|
+
|
|
1931
|
+
`;
|
|
1958
1932
|
}
|
|
1959
|
-
function
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
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
|
-
|
|
1965
|
-
const
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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/
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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
|
-
|
|
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
|
|
2043
|
-
|
|
2007
|
+
function read2() {
|
|
2008
|
+
try {
|
|
2009
|
+
return JSON.parse(readFileSync13(healthFile(), "utf8"));
|
|
2010
|
+
} catch {
|
|
2011
|
+
return { faultCount: 0 };
|
|
2012
|
+
}
|
|
2044
2013
|
}
|
|
2045
|
-
function
|
|
2046
|
-
|
|
2014
|
+
function write(h) {
|
|
2015
|
+
mkdirSync11(scribeDir(), { recursive: true });
|
|
2016
|
+
writeFileSync11(healthFile(), `${JSON.stringify(h, null, 2)}
|
|
2017
|
+
`, "utf8");
|
|
2047
2018
|
}
|
|
2048
|
-
function
|
|
2049
|
-
|
|
2019
|
+
function recordHealth() {
|
|
2020
|
+
const h = read2();
|
|
2021
|
+
h.lastWriteAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2022
|
+
write(h);
|
|
2050
2023
|
}
|
|
2051
|
-
function
|
|
2052
|
-
|
|
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/
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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
|
|
2062
|
-
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
}
|
|
2069
|
-
|
|
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
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
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
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
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/
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
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
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
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
|
-
|
|
2125
|
+
draining = false;
|
|
2127
2126
|
}
|
|
2128
2127
|
}
|
|
2129
|
-
function
|
|
2130
|
-
|
|
2131
|
-
|
|
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/
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
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
|
|
2162
|
-
const
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
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
|
|
2184
|
+
return hits;
|
|
2170
2185
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
if (
|
|
2175
|
-
const
|
|
2176
|
-
|
|
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
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
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
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
-
|
|
2207
|
-
|
|
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
|
-
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
var
|
|
2214
|
-
function
|
|
2215
|
-
|
|
2216
|
-
const
|
|
2217
|
-
const
|
|
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
|
-
|
|
2221
|
-
|
|
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
|
-
|
|
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
|
|
2306
|
-
|
|
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(/ /gi, " ").replace(/\s+/g, " ").toLowerCase().trim();
|
|
2309
2530
|
}
|
|
2310
|
-
function
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
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
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
const
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
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
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
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
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
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
|
-
|
|
2362
|
-
|
|
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
|
|
2365
|
-
|
|
2611
|
+
function ruleFor(type) {
|
|
2612
|
+
return SECTION_RULES.find((r) => r.type === type) ?? SECTION_RULES[0];
|
|
2366
2613
|
}
|
|
2367
|
-
function
|
|
2368
|
-
|
|
2369
|
-
const
|
|
2370
|
-
const
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
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
|
|
2381
|
-
|
|
2382
|
-
|
|
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
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
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
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
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
|
|
2400
|
-
if (draining) return;
|
|
2401
|
-
draining = true;
|
|
2647
|
+
function hostOf(url) {
|
|
2402
2648
|
try {
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
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
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
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
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
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
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
const
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
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/
|
|
2476
|
-
function
|
|
2477
|
-
|
|
2478
|
-
const
|
|
2479
|
-
|
|
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
|
-
|
|
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: "
|
|
2484
|
-
label: "ORIRO
|
|
2485
|
-
description: "
|
|
2486
|
-
parameters:
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
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
|
|
2819
|
-
const
|
|
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:
|
|
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
|
-
|
|
2834
|
-
|
|
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 (
|
|
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 (
|
|
2849
|
-
else
|
|
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
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
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.
|
|
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
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
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
|
-
|
|
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
|
|
4312
|
-
|
|
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(`${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
`);
|