@oriro/orirocli 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1290 -1026
- 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,162 +444,220 @@ var ask = (rule, reason, severity = "warning") => ({
|
|
|
420
444
|
});
|
|
421
445
|
var cmdOf = (c) => (c.command ?? "").toLowerCase();
|
|
422
446
|
var norm = (s) => s.replace(/\s+/g, " ").trim();
|
|
423
|
-
|
|
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;
|
|
478
565
|
var ENV_EXFIL = [
|
|
479
566
|
/\$\(\s*(printenv|env)\b/i,
|
|
567
|
+
// $(printenv X) / $(env)
|
|
568
|
+
/\bprintenv\b/i,
|
|
569
|
+
// printenv … (paired with a net sink below = exfil)
|
|
570
|
+
/\benv\s*\|/i,
|
|
571
|
+
// env | …
|
|
480
572
|
/\$\(\s*cat\b[^)]*(\.ssh|\.aws|\.env|\.netrc|credential|secret|token|id_rsa|id_ed25519)/i
|
|
573
|
+
// $(cat <secret>)
|
|
481
574
|
];
|
|
575
|
+
var ENV_VAR_IN_BODY = /\s--?(d|data|data-binary|data-raw|form|post|body|upload-file)\b[^\n]*\$\{?\w*(secret|token|api[_-]?key|password|passwd|credential|aws_)\w*\}?/i;
|
|
482
576
|
var PERSISTENCE = [
|
|
483
577
|
/\bcrontab\b\s+(-|\S+)/i,
|
|
484
|
-
// crontab install
|
|
485
578
|
/>>?\s*~?\/?\.(bashrc|zshrc|bash_profile|profile|zprofile)\b/i,
|
|
486
|
-
|
|
579
|
+
/>>?[^\n]*\.ssh[\\/]authorized_keys/i,
|
|
580
|
+
// implant an SSH key (backdoor)
|
|
487
581
|
/\b(launchctl\s+load|systemctl\s+enable|sc\s+create|new-service)\b/i,
|
|
488
|
-
// service install
|
|
489
582
|
/\bregistry::|reg\s+add\b.*\\run\b/i,
|
|
490
|
-
|
|
491
|
-
/[\\\/]start menu[\\\/]programs[\\\/]startup[\\\/]/i,
|
|
492
|
-
// windows startup folder
|
|
583
|
+
/[\\/]start menu[\\/]programs[\\/]startup[\\/]/i,
|
|
493
584
|
/\bschtasks\b\s+\/create/i
|
|
494
|
-
// scheduled task
|
|
495
585
|
];
|
|
496
586
|
var GUARDIAN_TAMPER = [
|
|
497
587
|
/\boriro\b.*\bguardian\b.*\b(disable|off|stop|uninstall)\b/i,
|
|
498
|
-
|
|
499
|
-
/
|
|
500
|
-
|
|
588
|
+
/[\\/]\.oriro[\\/]guardian/i,
|
|
589
|
+
// direct path to Guardian's config/state
|
|
590
|
+
/\bguardian\.json\b/i
|
|
591
|
+
// any write referencing guardian.json (cd … && > guardian.json)
|
|
501
592
|
];
|
|
502
593
|
var TAMPER = [
|
|
503
594
|
/\bchmod\s+-?\s*0?777\b/i,
|
|
504
|
-
// world-writable
|
|
505
595
|
/\b(ufw|firewall-cmd|iptables)\b.*\b(disable|stop|flush|-f)\b/i,
|
|
506
|
-
// firewall down
|
|
507
596
|
/\bset-mppreference\b.*-disable/i,
|
|
508
|
-
// disable Defender
|
|
509
597
|
/\bhistory\s+-c\b|\bunset\s+histfile\b|>\s*~?\/?\.bash_history/i
|
|
510
|
-
// wipe history
|
|
511
598
|
];
|
|
512
599
|
var MALWARE = [
|
|
513
600
|
/\b(xmrig|minerd|cgminer|cpuminer|stratum\+tcp)\b/i,
|
|
514
601
|
/\b(nanopool|minexmr|supportxmr|pool\.minexmr)\b/i
|
|
515
602
|
];
|
|
516
|
-
function anyMatch(patterns, text) {
|
|
517
|
-
return patterns.some((re) => re.test(text));
|
|
518
|
-
}
|
|
519
603
|
var DEFAULT_RULES = [
|
|
520
604
|
{
|
|
521
605
|
id: "fs-destruction",
|
|
522
|
-
description: "Block recursive deletes of root/home, disk reformats, fork bombs, host shutdown.",
|
|
606
|
+
description: "Block recursive deletes of root/home/system paths, disk reformats, fork bombs, host shutdown.",
|
|
523
607
|
match: (c) => {
|
|
524
|
-
const
|
|
525
|
-
|
|
608
|
+
const raw = cmdOf(c);
|
|
609
|
+
const cmd = norm(raw);
|
|
610
|
+
for (const stmt of statements(cmd)) {
|
|
611
|
+
const v = rmVerdict(stmt);
|
|
612
|
+
if (v) return v;
|
|
613
|
+
if (HOST_DISRUPT.has(commandWord(stmt))) return block("fs-destruction", "Host shutdown/reboot");
|
|
614
|
+
}
|
|
615
|
+
return anyMatch(FS_DESTRUCTION, cmd) ? block("fs-destruction", "Destructive filesystem/system operation") : null;
|
|
526
616
|
}
|
|
527
617
|
},
|
|
528
618
|
{
|
|
529
619
|
id: "remote-code-exec",
|
|
530
|
-
description: "Block pull-and-run of remote code (curl|sh,
|
|
620
|
+
description: "Block pull-and-run of remote code (curl|sh, $(curl), sh <(curl), bash -c $(curl), decode|sh).",
|
|
531
621
|
match: (c) => anyMatch(REMOTE_EXEC, norm(cmdOf(c))) ? block("remote-code-exec", "Downloading and executing remote code") : null
|
|
532
622
|
},
|
|
533
623
|
{
|
|
534
624
|
id: "reverse-shell",
|
|
535
|
-
description: "Block reverse shells / remote backdoors (nc -e, /dev/tcp
|
|
625
|
+
description: "Block reverse shells / remote backdoors (nc -e, /dev/tcp/<host>, socat exec, mkfifo backpipe).",
|
|
536
626
|
match: (c) => anyMatch(REVERSE_SHELL, norm(cmdOf(c))) ? block("reverse-shell", "Opening a reverse shell / remote backdoor") : null
|
|
537
627
|
},
|
|
538
628
|
{
|
|
539
629
|
id: "secret-exfiltration",
|
|
540
|
-
description: "Block reading a credential/key file and
|
|
630
|
+
description: "Block reading a credential/key file or env secret and sending it off the machine.",
|
|
541
631
|
match: (c) => {
|
|
542
632
|
const cmd = norm(cmdOf(c));
|
|
543
|
-
if (cmd
|
|
544
|
-
|
|
545
|
-
|
|
633
|
+
if (!cmd || !NET_SINK.test(cmd)) return null;
|
|
634
|
+
if (SECRET_PATHS.test(cmd)) return block("secret-exfiltration", "Reading secrets and sending them off the machine");
|
|
635
|
+
if (anyMatch(ENV_EXFIL, cmd) || ENV_VAR_IN_BODY.test(cmd)) return block("secret-exfiltration", "Sending environment variables / secrets off the machine");
|
|
546
636
|
return null;
|
|
547
637
|
}
|
|
548
638
|
},
|
|
549
639
|
{
|
|
550
|
-
id: "
|
|
551
|
-
description: "Block
|
|
640
|
+
id: "persistence",
|
|
641
|
+
description: "Block SSH-key implants; flag cron/rc/startup/service edits used for Trojan persistence.",
|
|
552
642
|
match: (c) => {
|
|
553
643
|
const cmd = norm(cmdOf(c));
|
|
554
|
-
|
|
644
|
+
if (/>>?[^\n]*\.ssh[\\/]authorized_keys/i.test(cmd)) return block("persistence", "Implanting an SSH key (backdoor)");
|
|
645
|
+
return anyMatch(PERSISTENCE, cmd) ? ask2("persistence", "Installing a persistent foothold (cron/startup/service)") : null;
|
|
555
646
|
}
|
|
556
647
|
},
|
|
557
|
-
{
|
|
558
|
-
id: "persistence",
|
|
559
|
-
description: "Flag cron/rc/startup/service edits used for Trojan persistence.",
|
|
560
|
-
match: (c) => anyMatch(PERSISTENCE, norm(cmdOf(c))) ? ask("persistence", "Installing a persistent foothold (cron/startup/service)") : null
|
|
561
|
-
},
|
|
562
648
|
{
|
|
563
649
|
id: "guardian-self-defense",
|
|
564
650
|
description: "Block any attempt to disable, uninstall, or rewrite Guardian's own config/state.",
|
|
565
651
|
match: (c) => {
|
|
566
|
-
if (anyMatch(GUARDIAN_TAMPER, norm(cmdOf(c))))
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
if (c.paths?.some((p) => /[\\/]\.oriro[\\/]guardian/i.test(p))) {
|
|
570
|
-
return block("guardian-self-defense", "Direct write to Guardian's own config/state");
|
|
571
|
-
}
|
|
652
|
+
if (anyMatch(GUARDIAN_TAMPER, norm(cmdOf(c)))) return block("guardian-self-defense", "Attempt to disable or tamper with Guardian itself");
|
|
653
|
+
if (c.paths?.some((p) => /[\\/]\.oriro[\\/]guardian/i.test(p))) return block("guardian-self-defense", "Direct write to Guardian's own config/state");
|
|
572
654
|
return null;
|
|
573
655
|
}
|
|
574
656
|
},
|
|
575
657
|
{
|
|
576
658
|
id: "security-tamper",
|
|
577
659
|
description: "Flag disabling firewall/Defender or wiping history.",
|
|
578
|
-
match: (c) => anyMatch(TAMPER, norm(cmdOf(c))) ?
|
|
660
|
+
match: (c) => anyMatch(TAMPER, norm(cmdOf(c))) ? ask2("security-tamper", "Disabling security controls or covering tracks") : null
|
|
579
661
|
},
|
|
580
662
|
{
|
|
581
663
|
id: "malware-signature",
|
|
@@ -584,9 +666,10 @@ var DEFAULT_RULES = [
|
|
|
584
666
|
},
|
|
585
667
|
{
|
|
586
668
|
id: "v3lite",
|
|
587
|
-
description: "Guardian V3 Lite
|
|
669
|
+
description: "Guardian V3 Lite on the tool call: IOC catalog + hidden-unicode (exec); + injection scan for untrusted MCP params.",
|
|
588
670
|
match: (c) => {
|
|
589
|
-
const r = scanToolCall(c.toolName, c.command ?? "", c.params)
|
|
671
|
+
const r = c.kind === "mcp" ? scanToolCall(c.toolName, c.command ?? "", c.params) : scanExecCommand(`${c.toolName}
|
|
672
|
+
${c.command ?? ""}`);
|
|
590
673
|
return r.safe ? null : block("v3lite", `Guardian V3 Lite flagged ${r.threat}`);
|
|
591
674
|
}
|
|
592
675
|
},
|
|
@@ -596,10 +679,9 @@ var DEFAULT_RULES = [
|
|
|
596
679
|
match: (c) => {
|
|
597
680
|
if (c.kind !== "fs" || !c.paths?.length) return null;
|
|
598
681
|
const hit = c.paths.find(
|
|
599
|
-
(p) => SECRET_PATHS.test(p) || /[
|
|
600
|
-
/[\\\/](etc|boot|sys|windows[\\\/]system32)[\\\/]/i.test(p)
|
|
682
|
+
(p) => SECRET_PATHS.test(p) || /[\\/]\.ssh[\\/]/i.test(p) || /[\\/](etc|boot|sys|windows[\\/]system32)[\\/]/i.test(p)
|
|
601
683
|
);
|
|
602
|
-
return hit ?
|
|
684
|
+
return hit ? ask2("sensitive-path-write", `Writing to a sensitive location: ${hit}`) : null;
|
|
603
685
|
}
|
|
604
686
|
}
|
|
605
687
|
];
|
|
@@ -1046,7 +1128,7 @@ async function speak(text, opts = {}) {
|
|
|
1046
1128
|
}
|
|
1047
1129
|
|
|
1048
1130
|
// src/avatar/onboarding.ts
|
|
1049
|
-
import { stdin as stdin2, stdout as
|
|
1131
|
+
import { stdin as stdin2, stdout as stdout3 } from "process";
|
|
1050
1132
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
1051
1133
|
|
|
1052
1134
|
// src/avatar/system-voice.ts
|
|
@@ -1120,7 +1202,7 @@ var C3 = {
|
|
|
1120
1202
|
reset: "\x1B[0m"
|
|
1121
1203
|
};
|
|
1122
1204
|
async function previewAvatar(avatar) {
|
|
1123
|
-
|
|
1205
|
+
stdout3.write(
|
|
1124
1206
|
`
|
|
1125
1207
|
${C3.teal}\u25EF${C3.reset} ${C3.bold}${avatar.slug}${C3.reset} is now your terminal face. ${C3.dim}Change anytime with ${C3.reset}${C3.teal}oriro avatar${C3.reset}
|
|
1126
1208
|
`
|
|
@@ -1131,19 +1213,19 @@ async function previewAvatar(avatar) {
|
|
|
1131
1213
|
png = readCachedAvatar(avatar.slug);
|
|
1132
1214
|
} catch {
|
|
1133
1215
|
}
|
|
1134
|
-
|
|
1216
|
+
stdout3.write("\n" + renderAvatar(avatar, png) + "\n");
|
|
1135
1217
|
setupSystemVoice();
|
|
1136
1218
|
const spoke = await speak(`Hi, I'm ${avatar.slug}, your ORIRO terminal face. I'll speak your replies.`, {
|
|
1137
1219
|
voiceId: avatar.slug,
|
|
1138
1220
|
lang: "en-US"
|
|
1139
1221
|
});
|
|
1140
|
-
if (spoke)
|
|
1222
|
+
if (spoke) stdout3.write(` ${C3.dim}(spoken aloud in your terminal's voice)${C3.reset}
|
|
1141
1223
|
`);
|
|
1142
1224
|
}
|
|
1143
1225
|
async function selectAvatarInteractive() {
|
|
1144
|
-
const rl = createInterface2({ input: stdin2, output:
|
|
1226
|
+
const rl = createInterface2({ input: stdin2, output: stdout3 });
|
|
1145
1227
|
try {
|
|
1146
|
-
|
|
1228
|
+
stdout3.write(
|
|
1147
1229
|
`
|
|
1148
1230
|
${C3.teal}\u25EF${C3.reset} ${C3.bold}Choose your ORIRO avatar${C3.reset} ${C3.dim}\u2014 ${AVATAR_COUNT} faces, it floats in your terminal and speaks.${C3.reset}
|
|
1149
1231
|
|
|
@@ -1151,36 +1233,46 @@ async function selectAvatarInteractive() {
|
|
|
1151
1233
|
);
|
|
1152
1234
|
const cats = avatarCategories();
|
|
1153
1235
|
cats.forEach(
|
|
1154
|
-
(cat2, i) =>
|
|
1236
|
+
(cat2, i) => stdout3.write(
|
|
1155
1237
|
` ${C3.teal}${String(i + 1).padStart(2)}${C3.reset} ${cat2} ${C3.dim}(${avatarsInCategory(cat2).length})${C3.reset}
|
|
1156
1238
|
`
|
|
1157
1239
|
)
|
|
1158
1240
|
);
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1241
|
+
let cat;
|
|
1242
|
+
for (; ; ) {
|
|
1243
|
+
const ans = (await ask(rl, `
|
|
1244
|
+
${C3.teal}\u203A${C3.reset} Pick a category number ${C3.dim}(or Enter to skip)${C3.reset}: `)).trim();
|
|
1245
|
+
if (!ans) {
|
|
1246
|
+
stdout3.write(` ${C3.dim}Skipped \u2014 no avatar.${C3.reset}
|
|
1247
|
+
`);
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
const n = Number(ans);
|
|
1251
|
+
cat = Number.isInteger(n) ? cats[n - 1] : void 0;
|
|
1252
|
+
if (cat) break;
|
|
1253
|
+
stdout3.write(` ${C3.dim}Please enter a number from the list.${C3.reset}
|
|
1254
|
+
`);
|
|
1167
1255
|
}
|
|
1168
1256
|
const list = avatarsInCategory(cat);
|
|
1169
|
-
|
|
1257
|
+
stdout3.write("\n");
|
|
1170
1258
|
list.forEach(
|
|
1171
|
-
(a, i) =>
|
|
1259
|
+
(a, i) => stdout3.write(` ${C3.teal}${String(i + 1).padStart(2)}${C3.reset} ${a.slug}
|
|
1172
1260
|
`)
|
|
1173
1261
|
);
|
|
1174
|
-
|
|
1175
|
-
(await rl
|
|
1176
|
-
${C3.teal}\u203A${C3.reset} Pick an avatar number: `)).trim()
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1262
|
+
for (; ; ) {
|
|
1263
|
+
const ans = (await ask(rl, `
|
|
1264
|
+
${C3.teal}\u203A${C3.reset} Pick an avatar number ${C3.dim}(or Enter to skip)${C3.reset}: `)).trim();
|
|
1265
|
+
if (!ans) {
|
|
1266
|
+
stdout3.write(` ${C3.dim}Skipped \u2014 no avatar.${C3.reset}
|
|
1267
|
+
`);
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
const n = Number(ans);
|
|
1271
|
+
const chosen = Number.isInteger(n) ? list[n - 1] : void 0;
|
|
1272
|
+
if (chosen) return chosen;
|
|
1273
|
+
stdout3.write(` ${C3.dim}Please enter a number from the list.${C3.reset}
|
|
1274
|
+
`);
|
|
1182
1275
|
}
|
|
1183
|
-
return chosen;
|
|
1184
1276
|
} finally {
|
|
1185
1277
|
rl.close();
|
|
1186
1278
|
}
|
|
@@ -1237,22 +1329,22 @@ function hasScribeChoice() {
|
|
|
1237
1329
|
|
|
1238
1330
|
// src/onboarding/wrapper.ts
|
|
1239
1331
|
function isFirstRun() {
|
|
1240
|
-
return !isLanguageConfigured();
|
|
1332
|
+
return !isLanguageConfigured() || !hasScribeChoice();
|
|
1241
1333
|
}
|
|
1242
1334
|
async function askYesNo(question) {
|
|
1243
|
-
const rl = createInterface3({ input: stdin3, output:
|
|
1335
|
+
const rl = createInterface3({ input: stdin3, output: stdout4 });
|
|
1244
1336
|
try {
|
|
1245
|
-
const a = (await rl
|
|
1337
|
+
const a = (await ask(rl, `${question} ${dim("[Y/n]")} `)).trim().toLowerCase();
|
|
1246
1338
|
return a === "" || a === "y" || a === "yes";
|
|
1247
1339
|
} finally {
|
|
1248
1340
|
rl.close();
|
|
1249
1341
|
}
|
|
1250
1342
|
}
|
|
1251
1343
|
async function runOnboarding() {
|
|
1252
|
-
|
|
1344
|
+
stdout4.write(banner());
|
|
1253
1345
|
await runLanguageOnboarding();
|
|
1254
1346
|
await activateGuardian();
|
|
1255
|
-
|
|
1347
|
+
stdout4.write(` ${accent("\u{1F6E1} Guardian V3")} is on by default. ${accent("\u{1F9ED} Head")} is ready.
|
|
1256
1348
|
|
|
1257
1349
|
`);
|
|
1258
1350
|
if (!isAvatarConfigured()) await runAvatarOnboarding();
|
|
@@ -1261,11 +1353,11 @@ async function runOnboarding() {
|
|
|
1261
1353
|
"Remember with me? The Scriber keeps your work in context on THIS machine only \u2014 it never leaves it."
|
|
1262
1354
|
);
|
|
1263
1355
|
setScribeConsent(yes);
|
|
1264
|
-
|
|
1356
|
+
stdout4.write(yes ? ` ${accent("\u{1F4D3} Scriber")} on.
|
|
1265
1357
|
` : ` ${dim("Scriber off \u2014 `oriro scribe on` anytime.")}
|
|
1266
1358
|
`);
|
|
1267
1359
|
}
|
|
1268
|
-
|
|
1360
|
+
stdout4.write(`
|
|
1269
1361
|
${accent("ORIRO is ready.")} ${dim("Type to chat \xB7 /exit to leave")}
|
|
1270
1362
|
|
|
1271
1363
|
`);
|
|
@@ -1357,6 +1449,24 @@ var RouterMux = class {
|
|
|
1357
1449
|
for (const s of stats) if (this.stats.has(s.id)) this.stats.set(s.id, { ...s });
|
|
1358
1450
|
}
|
|
1359
1451
|
};
|
|
1452
|
+
function healthStatePath(dir) {
|
|
1453
|
+
return join10(dir, "routers", "health.json");
|
|
1454
|
+
}
|
|
1455
|
+
function saveMuxState(dir, stats) {
|
|
1456
|
+
const p = healthStatePath(dir);
|
|
1457
|
+
mkdirSync5(join10(dir, "routers"), { recursive: true });
|
|
1458
|
+
writeFileSync7(p, JSON.stringify(stats, null, 2), "utf8");
|
|
1459
|
+
}
|
|
1460
|
+
function loadMuxState(dir) {
|
|
1461
|
+
const p = healthStatePath(dir);
|
|
1462
|
+
if (!existsSync3(p)) return [];
|
|
1463
|
+
try {
|
|
1464
|
+
const stats = JSON.parse(readFileSync8(p, "utf8"));
|
|
1465
|
+
return stats.map((s) => ({ ...s, latencyMs: Number.isFinite(s.latencyMs) ? s.latencyMs : Number.POSITIVE_INFINITY }));
|
|
1466
|
+
} catch {
|
|
1467
|
+
return [];
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1360
1470
|
|
|
1361
1471
|
// src/routers/floor.ts
|
|
1362
1472
|
var KEYLESS_FLOOR = [
|
|
@@ -1425,7 +1535,7 @@ async function validateRouter(entry, key, modelId) {
|
|
|
1425
1535
|
try {
|
|
1426
1536
|
let res;
|
|
1427
1537
|
if (entry.api === "google-generative-ai") {
|
|
1428
|
-
const url = `${entry.baseUrl.replace(/\/$/, "")}/models/${model}:generateContent${key ? `?key=${key}` : ""}`;
|
|
1538
|
+
const url = `${entry.baseUrl.replace(/\/$/, "")}/models/${model}:generateContent${key ? `?key=${encodeURIComponent(key)}` : ""}`;
|
|
1429
1539
|
res = await fetch(url, {
|
|
1430
1540
|
method: "POST",
|
|
1431
1541
|
headers: { "content-type": "application/json" },
|
|
@@ -1484,6 +1594,9 @@ async function addRouter(entry, opts) {
|
|
|
1484
1594
|
if (entry.comingSoon) {
|
|
1485
1595
|
return { ok: false, validation: { ok: false, latencyMs: 0, model: "", error: "coming soon" } };
|
|
1486
1596
|
}
|
|
1597
|
+
if (entry.kind && entry.kind !== "chat") {
|
|
1598
|
+
return { ok: false, validation: { ok: false, latencyMs: 0, model: "", error: `'${entry.id}' is a ${entry.kind} router, not a chat router` } };
|
|
1599
|
+
}
|
|
1487
1600
|
const key = opts?.key ?? (entry.keyless ? KEYLESS_SENTINEL : void 0);
|
|
1488
1601
|
const v = await validateRouter(entry, key, opts?.modelId);
|
|
1489
1602
|
if (!v.ok) return { ok: false, validation: v };
|
|
@@ -1501,7 +1614,11 @@ async function addRouter(entry, opts) {
|
|
|
1501
1614
|
return { ok: true, validation: v };
|
|
1502
1615
|
}
|
|
1503
1616
|
function useRouters(ids) {
|
|
1504
|
-
|
|
1617
|
+
const reg = readReg();
|
|
1618
|
+
const applied = ids.filter((id) => reg[id]);
|
|
1619
|
+
const unknown = ids.filter((id) => !reg[id]);
|
|
1620
|
+
if (applied.length > 0) savePool(oriroDir(), applied);
|
|
1621
|
+
return { applied, unknown };
|
|
1505
1622
|
}
|
|
1506
1623
|
function resolvePool() {
|
|
1507
1624
|
const reg = readReg();
|
|
@@ -1581,955 +1698,1040 @@ function sanitizeEventToolCalls(ev) {
|
|
|
1581
1698
|
return next;
|
|
1582
1699
|
}
|
|
1583
1700
|
|
|
1584
|
-
// src/
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1701
|
+
// src/scribe/scribe-pi.ts
|
|
1702
|
+
import { existsSync as existsSync9, readFileSync as readFileSync16 } from "fs";
|
|
1703
|
+
import { Type } from "typebox";
|
|
1704
|
+
|
|
1705
|
+
// src/scribe/capture.ts
|
|
1706
|
+
import { closeSync as closeSync2, fsyncSync as fsyncSync2, mkdirSync as mkdirSync10, openSync as openSync2, writeSync as writeSync2 } from "fs";
|
|
1707
|
+
import { join as join14 } from "path";
|
|
1708
|
+
|
|
1709
|
+
// src/scribe/digest.ts
|
|
1710
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
|
|
1711
|
+
|
|
1712
|
+
// src/scribe/paths.ts
|
|
1713
|
+
import { join as join13 } from "path";
|
|
1714
|
+
function scribeDir() {
|
|
1715
|
+
const override = process.env.ORIRO_SCRIBE_DIR?.trim();
|
|
1716
|
+
return override && override.length > 0 ? override : join13(CONFIG_DIR, "scribe");
|
|
1590
1717
|
}
|
|
1591
|
-
function
|
|
1592
|
-
return {
|
|
1593
|
-
role: "assistant",
|
|
1594
|
-
content: [],
|
|
1595
|
-
api: "openai-completions",
|
|
1596
|
-
provider: MUX_PROVIDER,
|
|
1597
|
-
model: MUX_MODEL,
|
|
1598
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
1599
|
-
stopReason: "error",
|
|
1600
|
-
timestamp: Date.now(),
|
|
1601
|
-
errorMessage: message
|
|
1602
|
-
};
|
|
1718
|
+
function journalFile(date) {
|
|
1719
|
+
return join13(scribeDir(), `${date}.md`);
|
|
1603
1720
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
for (const id of mux.ranked()) {
|
|
1607
|
-
const router = byId.get(id);
|
|
1608
|
-
if (!router) continue;
|
|
1609
|
-
const t0 = Date.now();
|
|
1610
|
-
let committed = false;
|
|
1611
|
-
let lastPartial;
|
|
1612
|
-
try {
|
|
1613
|
-
const inner = piStreamSimple(routerModel(router), context, {
|
|
1614
|
-
...options ?? {},
|
|
1615
|
-
apiKey: router.apiKey
|
|
1616
|
-
});
|
|
1617
|
-
let failedBeforeContent = false;
|
|
1618
|
-
for await (const ev of inner) {
|
|
1619
|
-
if (ev.type === "error") {
|
|
1620
|
-
mux.recordFailure(id, errToCallError(ev.error));
|
|
1621
|
-
if (!committed) {
|
|
1622
|
-
lastError = ev.error;
|
|
1623
|
-
failedBeforeContent = true;
|
|
1624
|
-
break;
|
|
1625
|
-
}
|
|
1626
|
-
out.push(ev);
|
|
1627
|
-
out.end(ev.error);
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
committed = true;
|
|
1631
|
-
if (ev.type === "done") {
|
|
1632
|
-
mux.recordSuccess(id, Date.now() - t0);
|
|
1633
|
-
const clean = sanitizeMessageToolCalls(scrubMessageIdentity(ev.message));
|
|
1634
|
-
out.push({ type: "done", reason: ev.reason, message: clean });
|
|
1635
|
-
out.end(clean);
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
lastPartial = ev.partial;
|
|
1639
|
-
out.push(sanitizeEventToolCalls(ev));
|
|
1640
|
-
}
|
|
1641
|
-
if (failedBeforeContent) continue;
|
|
1642
|
-
mux.recordSuccess(id, Date.now() - t0);
|
|
1643
|
-
out.end(lastPartial ? sanitizeMessageToolCalls(scrubMessageIdentity(lastPartial)) : void 0);
|
|
1644
|
-
return;
|
|
1645
|
-
} catch (e) {
|
|
1646
|
-
mux.recordFailure(id, e);
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
const msg = lastError ?? buildErrorMessage(
|
|
1650
|
-
"All keyless routers are unavailable. Add a BYOK key, select more free routers, or retry shortly."
|
|
1651
|
-
);
|
|
1652
|
-
out.push({ type: "error", reason: "error", error: msg });
|
|
1653
|
-
out.end(msg);
|
|
1721
|
+
function digestFile() {
|
|
1722
|
+
return join13(scribeDir(), "_digest.md");
|
|
1654
1723
|
}
|
|
1655
|
-
function
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
const mux = new RouterMux(routers.map((r) => r.id));
|
|
1661
|
-
registry.registerProvider(MUX_PROVIDER, {
|
|
1662
|
-
name: "ORIRO Free (keyless Mux)",
|
|
1663
|
-
api: "openai-completions",
|
|
1664
|
-
apiKey: "oriro-keyless",
|
|
1665
|
-
// Placeholder — required by registry validation but never used: our custom streamSimple
|
|
1666
|
-
// routes to the real keyless floor endpoints itself (see driveMux).
|
|
1667
|
-
baseUrl: "http://oriro-mux.local",
|
|
1668
|
-
models: [
|
|
1669
|
-
{
|
|
1670
|
-
id: MUX_MODEL,
|
|
1671
|
-
name: "ORIRO Free (best-router)",
|
|
1672
|
-
api: "openai-completions",
|
|
1673
|
-
baseUrl: "http://oriro-mux.local",
|
|
1674
|
-
reasoning: false,
|
|
1675
|
-
input: ["text"],
|
|
1676
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1677
|
-
contextWindow: 128e3,
|
|
1678
|
-
maxTokens: 4096
|
|
1679
|
-
}
|
|
1680
|
-
],
|
|
1681
|
-
streamSimple: (_model, context, options) => {
|
|
1682
|
-
const out = createAssistantMessageEventStream();
|
|
1683
|
-
void driveMux(out, mux, byId, applyIdentity(context), options);
|
|
1684
|
-
return out;
|
|
1685
|
-
}
|
|
1686
|
-
});
|
|
1687
|
-
return registry.find(MUX_PROVIDER, MUX_MODEL);
|
|
1724
|
+
function timelineFile() {
|
|
1725
|
+
return join13(scribeDir(), "_timeline.md");
|
|
1726
|
+
}
|
|
1727
|
+
function artifactsDir() {
|
|
1728
|
+
return join13(scribeDir(), "artifacts");
|
|
1688
1729
|
}
|
|
1689
1730
|
|
|
1690
|
-
// src/
|
|
1691
|
-
|
|
1731
|
+
// src/scribe/digest.ts
|
|
1732
|
+
var DIGEST_CAP = 8192;
|
|
1733
|
+
var TIMELINE_DAY_CAP = 400;
|
|
1734
|
+
function read(file4) {
|
|
1735
|
+
return existsSync5(file4) ? readFileSync11(file4, "utf8") : "";
|
|
1736
|
+
}
|
|
1737
|
+
function updateDigest(summary, context) {
|
|
1738
|
+
mkdirSync8(scribeDir(), { recursive: true });
|
|
1739
|
+
const existing = read(digestFile());
|
|
1740
|
+
let contextBlock = context?.trim();
|
|
1741
|
+
if (!contextBlock) {
|
|
1742
|
+
const m = existing.match(/## Context\n([\s\S]*?)\n## /);
|
|
1743
|
+
contextBlock = m?.[1]?.trim() ?? "_(not set yet)_";
|
|
1744
|
+
}
|
|
1745
|
+
const recentMatch = existing.match(/## Recent activity[^\n]*\n([\s\S]*)$/);
|
|
1746
|
+
const priorRecent = recentMatch?.[1]?.trim() ?? "";
|
|
1747
|
+
let recent = summary.trim() ? `- ${summary.trim()}
|
|
1748
|
+
${priorRecent}` : priorRecent;
|
|
1749
|
+
const header2 = `# ORIRO Scribe \u2014 Digest
|
|
1692
1750
|
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
{
|
|
1703
|
-
type: "navigation",
|
|
1704
|
-
label: "Navigation",
|
|
1705
|
-
priority: "CRITICAL",
|
|
1706
|
-
markup: [/<nav[\s>]/, /role=["']navigation["']/],
|
|
1707
|
-
recommend: "Add a top navigation so visitors can reach key sections."
|
|
1708
|
-
},
|
|
1709
|
-
{
|
|
1710
|
-
type: "features",
|
|
1711
|
-
label: "Features",
|
|
1712
|
-
priority: "CRITICAL",
|
|
1713
|
-
text: [/\bfeatures?\b/, /\bwhat you (?:can|get)\b/, /\bcapabilit/],
|
|
1714
|
-
recommend: "Add a features section that spells out concrete capabilities, not adjectives."
|
|
1715
|
-
},
|
|
1716
|
-
{
|
|
1717
|
-
type: "pricing",
|
|
1718
|
-
label: "Pricing",
|
|
1719
|
-
priority: "CRITICAL",
|
|
1720
|
-
text: [/\bpricing\b/, /\bper month\b/, /\b\/mo\b/, /\bfree plan\b/, /\$\d/, /₹\d/, /€\d/],
|
|
1721
|
-
recommend: 'Add transparent pricing \u2014 a critical conversion element; even a single "Free" tier helps.'
|
|
1722
|
-
},
|
|
1723
|
-
{
|
|
1724
|
-
type: "cta",
|
|
1725
|
-
label: "Call-to-Action",
|
|
1726
|
-
priority: "CRITICAL",
|
|
1727
|
-
text: [/\bget started\b/, /\bsign up\b/, /\bstart (?:free|now|building)\b/, /\btry (?:it|now|free)\b/, /\bbook a demo\b/, /\bget a demo\b/],
|
|
1728
|
-
recommend: 'Add a strong, repeated primary CTA ("Get started") so the next step is obvious.'
|
|
1729
|
-
},
|
|
1730
|
-
{
|
|
1731
|
-
type: "testimonials",
|
|
1732
|
-
label: "Testimonials",
|
|
1733
|
-
priority: "HIGH",
|
|
1734
|
-
text: [/\btestimonial/, /\bwhat (?:our )?(?:customers|users) say\b/, /\bloved by\b/, /\breview(?:s|ed)\b/],
|
|
1735
|
-
recommend: "Add 2\u20133 customer testimonials with names/photos to build trust."
|
|
1736
|
-
},
|
|
1737
|
-
{
|
|
1738
|
-
type: "stats",
|
|
1739
|
-
label: "Stats / Metrics",
|
|
1740
|
-
priority: "HIGH",
|
|
1741
|
-
text: [/\b\d[\d,.]*\s*[kkmm]\+?\s*(?:users|customers|developers|downloads|teams)\b/, /\b9\d(?:\.\d+)?%\b/, /\buptime\b/],
|
|
1742
|
-
recommend: 'Add impressive metrics ("10K+ users", "99.9% uptime") as social proof.'
|
|
1743
|
-
},
|
|
1744
|
-
{
|
|
1745
|
-
type: "video",
|
|
1746
|
-
label: "Video",
|
|
1747
|
-
priority: "HIGH",
|
|
1748
|
-
markup: [/<video[\s>]/, /youtube\.com\/embed/, /player\.vimeo\.com/, /<iframe[^>]+(?:youtube|vimeo)/],
|
|
1749
|
-
text: [/\bwatch the (?:video|demo)\b/],
|
|
1750
|
-
recommend: "Add a short explainer/demo video \u2014 it lifts conversion on landing pages."
|
|
1751
|
-
},
|
|
1752
|
-
{
|
|
1753
|
-
type: "demo",
|
|
1754
|
-
label: "Live Demo",
|
|
1755
|
-
priority: "HIGH",
|
|
1756
|
-
text: [/\btry it (?:now|live|free)\b/, /\bplayground\b/, /\binteractive demo\b/, /\blive demo\b/],
|
|
1757
|
-
recommend: 'Add a "try it" live demo or playground so visitors experience the product immediately.'
|
|
1758
|
-
},
|
|
1759
|
-
{
|
|
1760
|
-
type: "socialProof",
|
|
1761
|
-
label: "Social Proof",
|
|
1762
|
-
priority: "HIGH",
|
|
1763
|
-
text: [/\btrusted by\b/, /\bbacked by\b/, /\bused by\b/, /\bas seen (?:in|on)\b/, /\bcustomers include\b/],
|
|
1764
|
-
recommend: 'Add social proof (customer/investor logos, "trusted by \u2026") near the hero.'
|
|
1765
|
-
},
|
|
1766
|
-
{
|
|
1767
|
-
type: "faq",
|
|
1768
|
-
label: "FAQ",
|
|
1769
|
-
priority: "MEDIUM",
|
|
1770
|
-
text: [/\bfaq\b/, /\bfrequently asked\b/],
|
|
1771
|
-
markup: [/<details[\s>]/],
|
|
1772
|
-
recommend: "Add an FAQ that answers the top objections before they become exits."
|
|
1773
|
-
},
|
|
1774
|
-
{
|
|
1775
|
-
type: "integrations",
|
|
1776
|
-
label: "Integrations",
|
|
1777
|
-
priority: "MEDIUM",
|
|
1778
|
-
text: [/\bintegrations?\b/, /\bworks with\b/, /\bconnect your\b/],
|
|
1779
|
-
recommend: "Add an integrations section showing what the product connects to."
|
|
1780
|
-
},
|
|
1781
|
-
{
|
|
1782
|
-
type: "newsletter",
|
|
1783
|
-
label: "Newsletter / Capture",
|
|
1784
|
-
priority: "MEDIUM",
|
|
1785
|
-
text: [/\bsubscribe\b/, /\bnewsletter\b/, /\bjoin (?:the )?waitlist\b/],
|
|
1786
|
-
markup: [/type=["']email["']/],
|
|
1787
|
-
recommend: "Add an email capture (newsletter/waitlist) so non-converting visitors are not lost."
|
|
1788
|
-
},
|
|
1789
|
-
{
|
|
1790
|
-
type: "comparison",
|
|
1791
|
-
label: "Comparison",
|
|
1792
|
-
priority: "MEDIUM",
|
|
1793
|
-
text: [/\bcompare\b/, /\bcomparison\b/, /\b vs\.? \b/, /\bwhy choose\b/],
|
|
1794
|
-
recommend: 'Add a comparison ("us vs alternatives") to win evaluators who are shopping around.'
|
|
1795
|
-
},
|
|
1796
|
-
{
|
|
1797
|
-
type: "team",
|
|
1798
|
-
label: "Team / About",
|
|
1799
|
-
priority: "LOW",
|
|
1800
|
-
text: [/\bour team\b/, /\bmeet the team\b/, /\bfounders?\b/, /\babout us\b/],
|
|
1801
|
-
recommend: "Add a brief team/about section to humanize the brand."
|
|
1751
|
+
## Context
|
|
1752
|
+
${contextBlock}
|
|
1753
|
+
|
|
1754
|
+
## Recent activity (newest first)
|
|
1755
|
+
`;
|
|
1756
|
+
let out = header2 + recent;
|
|
1757
|
+
while (Buffer.byteLength(out, "utf8") > DIGEST_CAP && recent.includes("\n")) {
|
|
1758
|
+
recent = recent.slice(0, recent.lastIndexOf("\n")).trimEnd();
|
|
1759
|
+
out = header2 + recent;
|
|
1802
1760
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
const
|
|
1810
|
-
const
|
|
1811
|
-
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");
|
|
1812
1800
|
try {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
headers: { "user-agent": UA, accept: "text/html,application/xhtml+xml" }
|
|
1817
|
-
});
|
|
1818
|
-
const html = await res.text();
|
|
1819
|
-
return { html, ms: Date.now() - start, status: res.status, ok: res.ok, error: "" };
|
|
1820
|
-
} catch (err) {
|
|
1821
|
-
return { html: "", ms: Date.now() - start, status: 0, ok: false, error: err instanceof Error ? err.message : "fetch failed" };
|
|
1801
|
+
writeSync(fd, content.endsWith("\n") ? content : `${content}
|
|
1802
|
+
`);
|
|
1803
|
+
fsyncSync(fd);
|
|
1822
1804
|
} finally {
|
|
1823
|
-
|
|
1805
|
+
closeSync(fd);
|
|
1824
1806
|
}
|
|
1825
1807
|
}
|
|
1826
|
-
function
|
|
1827
|
-
|
|
1808
|
+
function readJournal(date) {
|
|
1809
|
+
const f = journalFile(date);
|
|
1810
|
+
return existsSync6(f) ? readFileSync12(f, "utf8") : "";
|
|
1828
1811
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1812
|
+
|
|
1813
|
+
// src/scribe/redact.ts
|
|
1814
|
+
var RULES = [
|
|
1815
|
+
{
|
|
1816
|
+
label: "private-key",
|
|
1817
|
+
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
|
|
1818
|
+
},
|
|
1819
|
+
// Lone PEM markers — a key SPLIT across fields/turns leaves only a BEGIN-head or an END-tail in
|
|
1820
|
+
// one field. A field carrying either marker is key material: redact the marker + its adjacent body
|
|
1821
|
+
// (forward from BEGIN, backward to END) so no sub-threshold fragment can ever sit on disk.
|
|
1822
|
+
{ label: "private-key", re: /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*/g },
|
|
1823
|
+
{ label: "private-key", re: /[\s\S]*-----END[A-Z ]*PRIVATE KEY-----/g },
|
|
1824
|
+
{ label: "anthropic-key", re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
|
|
1825
|
+
{ label: "openrouter-key", re: /sk-or-v1-[A-Za-z0-9]{20,}/g },
|
|
1826
|
+
// Stripe-style keys (sk_live_/pk_live_/rk_test_/…), underscore segments.
|
|
1827
|
+
{ label: "stripe-key", re: /\b[srp]k_(?:live|test)_[A-Za-z0-9]{16,}/g },
|
|
1828
|
+
// Generic sk- secret keys — allow hyphenated segments (sk-live-…, sk-proj-…) so a second
|
|
1829
|
+
// hyphen no longer breaks the match (the gap the Scriber spike caught).
|
|
1830
|
+
{ label: "secret-key-sk", re: /sk[-_][A-Za-z0-9][A-Za-z0-9-]{14,}/g },
|
|
1831
|
+
{ label: "google-key", re: /AIza[0-9A-Za-z_-]{30,}/g },
|
|
1832
|
+
{ label: "groq-key", re: /gsk_[A-Za-z0-9]{20,}/g },
|
|
1833
|
+
{ label: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
1834
|
+
{ label: "github-token", re: /gh[posr]_[A-Za-z0-9]{30,}/g },
|
|
1835
|
+
{ label: "xai-key", re: /xai-[A-Za-z0-9]{20,}/g },
|
|
1836
|
+
{ label: "aws-key", re: /AKIA[0-9A-Z]{16}/g },
|
|
1837
|
+
{ label: "jwt", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g },
|
|
1838
|
+
{ label: "telegram-token", re: /\b\d{8,10}:[A-Za-z0-9_-]{30,}\b/g },
|
|
1839
|
+
// Auth headers / inline credentials (any provider) — the audit found these leaked.
|
|
1840
|
+
{ label: "bearer-token", re: /\bbearer\s+[A-Za-z0-9._~+/=-]{12,}/gi },
|
|
1841
|
+
{ label: "basic-auth", re: /\bbasic\s+[A-Za-z0-9+/=]{12,}/gi },
|
|
1842
|
+
// key: value / key=value secrets (password, token, secret, api_key, access_key, …).
|
|
1843
|
+
{ label: "secret-kv", re: /\b(?:pass(?:word|wd)?|pwd|secret|token|api[_-]?key|access[_-]?key|auth)\s*[:=]\s*\S{3,}/gi },
|
|
1844
|
+
// Credentials embedded in a URL: scheme://user:PASSWORD@host → redact the password.
|
|
1845
|
+
{ label: "url-credential", re: /\b([a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:)[^/\s@]+(@)/gi },
|
|
1846
|
+
{ label: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g },
|
|
1847
|
+
{ label: "phone", re: /(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}/g }
|
|
1848
|
+
];
|
|
1849
|
+
function marker(label) {
|
|
1850
|
+
return `\u27E8REDACTED:${label}\u27E9`;
|
|
1834
1851
|
}
|
|
1835
|
-
function
|
|
1836
|
-
const
|
|
1837
|
-
for (const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
evidence = hit;
|
|
1843
|
-
break;
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
if (!evidence) {
|
|
1847
|
-
for (const re of rule.text ?? []) {
|
|
1848
|
-
const hit = firstMatch(re, text);
|
|
1849
|
-
if (hit) {
|
|
1850
|
-
evidence = hit;
|
|
1851
|
-
break;
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
if (evidence) found.push({ type: rule.type, label: rule.label, priority: rule.priority, evidence });
|
|
1852
|
+
function entropy(s) {
|
|
1853
|
+
const freq = /* @__PURE__ */ new Map();
|
|
1854
|
+
for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
1855
|
+
let h = 0;
|
|
1856
|
+
for (const n of freq.values()) {
|
|
1857
|
+
const p = n / s.length;
|
|
1858
|
+
h -= p * Math.log2(p);
|
|
1856
1859
|
}
|
|
1857
|
-
return
|
|
1860
|
+
return h;
|
|
1858
1861
|
}
|
|
1859
|
-
function
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1862
|
+
function looksLikeUnknownSecret(token) {
|
|
1863
|
+
if (token.length < 32) return false;
|
|
1864
|
+
if (token.includes("\u27E8REDACTED:")) return false;
|
|
1865
|
+
if (/^[0-9a-f]+$/i.test(token)) return false;
|
|
1866
|
+
const classes = (/[a-z]/.test(token) ? 1 : 0) + (/[A-Z]/.test(token) ? 1 : 0) + (/[0-9]/.test(token) ? 1 : 0);
|
|
1867
|
+
if (classes < 2) return false;
|
|
1868
|
+
return entropy(token) >= 4.2;
|
|
1869
|
+
}
|
|
1870
|
+
function redact(input) {
|
|
1871
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1872
|
+
let text = input;
|
|
1873
|
+
for (const rule of RULES) {
|
|
1874
|
+
text = text.replace(rule.re, () => {
|
|
1875
|
+
counts.set(rule.label, (counts.get(rule.label) ?? 0) + 1);
|
|
1876
|
+
return marker(rule.label);
|
|
1877
|
+
});
|
|
1865
1878
|
}
|
|
1866
|
-
|
|
1879
|
+
text = text.split(/(\s+)/).map((tok) => {
|
|
1880
|
+
if (looksLikeUnknownSecret(tok)) {
|
|
1881
|
+
counts.set("high-entropy", (counts.get("high-entropy") ?? 0) + 1);
|
|
1882
|
+
return marker("high-entropy");
|
|
1883
|
+
}
|
|
1884
|
+
return tok;
|
|
1885
|
+
}).join("");
|
|
1886
|
+
const redactions = [...counts.entries()].map(([label, count]) => ({
|
|
1887
|
+
label,
|
|
1888
|
+
count
|
|
1889
|
+
}));
|
|
1890
|
+
return { text, redactions };
|
|
1867
1891
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
const text = toText(html);
|
|
1873
|
-
const titleM = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
|
|
1874
|
-
const title = (titleM?.[1] ?? "").replace(/\s+/g, " ").trim();
|
|
1875
|
-
const descM = /<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["']/i.exec(html) ?? /<meta[^>]+content=["']([^"']*)["'][^>]+name=["']description["']/i.exec(html);
|
|
1876
|
-
const description = (descM?.[1] ?? "").replace(/\s+/g, " ").trim();
|
|
1877
|
-
const headings = extractMatches(/<h[1-3][^>]*>([\s\S]*?)<\/h[1-3]>/gi, html, 12);
|
|
1878
|
-
const ctaAll = extractMatches(/<(?:a|button)[^>]*>([\s\S]*?)<\/(?:a|button)>/gi, html, 80);
|
|
1879
|
-
const ctas = [];
|
|
1880
|
-
for (const c of ctaAll) {
|
|
1881
|
-
if (CTA_WORDS.test(c) && !ctas.includes(c)) ctas.push(c);
|
|
1882
|
-
if (ctas.length >= 10) break;
|
|
1892
|
+
function containsSecret(text) {
|
|
1893
|
+
for (const rule of RULES) {
|
|
1894
|
+
rule.re.lastIndex = 0;
|
|
1895
|
+
if (rule.re.test(text)) return true;
|
|
1883
1896
|
}
|
|
1884
|
-
const
|
|
1885
|
-
|
|
1886
|
-
const images = (lowerHtml.match(/<img[\s>]/g) ?? []).length;
|
|
1887
|
-
const hasVideo = /<video[\s>]/.test(lowerHtml) || /(?:youtube\.com\/embed|player\.vimeo\.com)/.test(lowerHtml);
|
|
1888
|
-
const domNodes = (html.match(/<[a-z!\/]/gi) ?? []).length;
|
|
1889
|
-
let note = "";
|
|
1890
|
-
if (fr.ok && text.length < 400 && domNodes < 60) {
|
|
1891
|
-
note = "Sparse HTML \u2014 likely a client-rendered (SPA) page; structure may be under-detected without a JS render.";
|
|
1897
|
+
for (const tok of text.split(/\s+/)) {
|
|
1898
|
+
if (looksLikeUnknownSecret(tok)) return true;
|
|
1892
1899
|
}
|
|
1893
|
-
return
|
|
1894
|
-
url,
|
|
1895
|
-
title,
|
|
1896
|
-
description,
|
|
1897
|
-
sections: detectSections(lowerHtml, text),
|
|
1898
|
-
headings,
|
|
1899
|
-
ctas,
|
|
1900
|
-
forms,
|
|
1901
|
-
links,
|
|
1902
|
-
images,
|
|
1903
|
-
hasVideo,
|
|
1904
|
-
metrics: { htmlBytes: html.length, domNodes, fetchMs: fr.ms, status: fr.status },
|
|
1905
|
-
ok: fr.ok && html.length > 0,
|
|
1906
|
-
note: fr.ok ? note : `Could not load: ${fr.error || `HTTP ${fr.status}`}`
|
|
1907
|
-
};
|
|
1908
|
-
}
|
|
1909
|
-
function ruleFor(type) {
|
|
1910
|
-
return SECTION_RULES.find((r) => r.type === type) ?? SECTION_RULES[0];
|
|
1911
|
-
}
|
|
1912
|
-
function analyzeGaps(target, competitors) {
|
|
1913
|
-
const targetTypes = new Set(target.sections.map((s) => s.type));
|
|
1914
|
-
const compPresence = /* @__PURE__ */ new Map();
|
|
1915
|
-
for (const comp of competitors) {
|
|
1916
|
-
if (!comp.ok) continue;
|
|
1917
|
-
for (const s of comp.sections) {
|
|
1918
|
-
const list = compPresence.get(s.type) ?? [];
|
|
1919
|
-
if (!list.includes(comp.url)) list.push(comp.url);
|
|
1920
|
-
compPresence.set(s.type, list);
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
const missing = [];
|
|
1924
|
-
const parity = [];
|
|
1925
|
-
for (const [type, presentOn] of compPresence) {
|
|
1926
|
-
if (targetTypes.has(type)) {
|
|
1927
|
-
parity.push(type);
|
|
1928
|
-
} else {
|
|
1929
|
-
const rule = ruleFor(type);
|
|
1930
|
-
missing.push({ section: type, label: rule.label, priority: rule.priority, presentOn, recommendation: rule.recommend });
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
missing.sort((a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || b.presentOn.length - a.presentOn.length);
|
|
1934
|
-
const advantages = target.sections.filter((s) => !compPresence.has(s.type));
|
|
1935
|
-
return { missing, advantages, parity };
|
|
1936
|
-
}
|
|
1937
|
-
function generateActionItems(missing) {
|
|
1938
|
-
return missing.map((g) => ({
|
|
1939
|
-
title: `Add a ${g.label} section`,
|
|
1940
|
-
priority: g.priority,
|
|
1941
|
-
effort: PRIORITY_EFFORT[g.priority],
|
|
1942
|
-
rationale: `${g.presentOn.length} of the compared page(s) have it; you don't. ${g.recommendation}`
|
|
1943
|
-
}));
|
|
1900
|
+
return false;
|
|
1944
1901
|
}
|
|
1945
|
-
|
|
1902
|
+
|
|
1903
|
+
// src/scribe/capture.ts
|
|
1904
|
+
var INLINE_CAP = 4e3;
|
|
1905
|
+
function sideFile(date, ts, kind, full) {
|
|
1906
|
+
mkdirSync10(artifactsDir(), { recursive: true });
|
|
1907
|
+
const name = `${date}_${ts.replace(/[:.]/g, "-")}_${kind}.md`;
|
|
1908
|
+
const p = join14(artifactsDir(), name);
|
|
1909
|
+
const fd = openSync2(p, "w");
|
|
1946
1910
|
try {
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1911
|
+
writeSync2(fd, full);
|
|
1912
|
+
fsyncSync2(fd);
|
|
1913
|
+
} finally {
|
|
1914
|
+
closeSync2(fd);
|
|
1950
1915
|
}
|
|
1916
|
+
return p;
|
|
1951
1917
|
}
|
|
1952
|
-
function
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
if (gaps.missing.length === 0) {
|
|
1962
|
-
parts.push("No structural gaps found \u2014 you cover everything they do.");
|
|
1963
|
-
} else {
|
|
1964
|
-
parts.push(`${gaps.missing.length} gap(s) found.`);
|
|
1965
|
-
if (crit.length) parts.push(`Critical: ${crit.join(", ")}.`);
|
|
1966
|
-
if (high.length) parts.push(`High: ${high.join(", ")}.`);
|
|
1918
|
+
function field(date, ts, label, value) {
|
|
1919
|
+
if (!value || !value.trim()) return "";
|
|
1920
|
+
if (value.length > INLINE_CAP) {
|
|
1921
|
+
const ref = sideFile(date, ts, label.toLowerCase().replace(/\s+/g, "-"), value);
|
|
1922
|
+
return `**${label}** (full \u2192 ${ref}):
|
|
1923
|
+
${value.slice(0, INLINE_CAP)}
|
|
1924
|
+
\u2026(truncated; full content in artifact)
|
|
1925
|
+
|
|
1926
|
+
`;
|
|
1967
1927
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1928
|
+
return `**${label}:**
|
|
1929
|
+
${value}
|
|
1930
|
+
|
|
1931
|
+
`;
|
|
1970
1932
|
}
|
|
1971
|
-
function
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1933
|
+
function renderTurn(rec) {
|
|
1934
|
+
let md = `## ${rec.ts}
|
|
1935
|
+
|
|
1936
|
+
`;
|
|
1937
|
+
md += field(rec.date, rec.ts, "User", rec.user);
|
|
1938
|
+
md += field(rec.date, rec.ts, "Router", rec.router);
|
|
1939
|
+
if (rec.tools?.length) md += `**Tools:** ${rec.tools.join(", ")}
|
|
1940
|
+
|
|
1941
|
+
`;
|
|
1942
|
+
if (rec.files?.length) md += `**Files:** ${rec.files.join(", ")}
|
|
1943
|
+
|
|
1944
|
+
`;
|
|
1945
|
+
md += field(rec.date, rec.ts, "Note", rec.note);
|
|
1946
|
+
return `${md}---
|
|
1947
|
+
`;
|
|
1975
1948
|
}
|
|
1976
|
-
|
|
1977
|
-
const
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
const
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1949
|
+
function oneLineSummary(rec) {
|
|
1950
|
+
const bits = [];
|
|
1951
|
+
if (rec.user) bits.push(rec.user.replace(/\s+/g, " ").slice(0, 80));
|
|
1952
|
+
if (rec.files?.length) bits.push(`files: ${rec.files.slice(0, 3).join(", ")}`);
|
|
1953
|
+
if (rec.note) bits.push(rec.note.replace(/\s+/g, " ").slice(0, 60));
|
|
1954
|
+
return bits.join(" \xB7 ") || "(activity)";
|
|
1955
|
+
}
|
|
1956
|
+
function redactRecord(rec) {
|
|
1957
|
+
const tally = /* @__PURE__ */ new Map();
|
|
1958
|
+
const rd = (s) => {
|
|
1959
|
+
if (!s) return s;
|
|
1960
|
+
const r = redact(s);
|
|
1961
|
+
for (const x of r.redactions) tally.set(x.label, (tally.get(x.label) ?? 0) + x.count);
|
|
1962
|
+
return r.text;
|
|
1963
|
+
};
|
|
1964
|
+
const safeRec = {
|
|
1965
|
+
...rec,
|
|
1966
|
+
user: rd(rec.user),
|
|
1967
|
+
note: rd(rec.note),
|
|
1968
|
+
router: rd(rec.router),
|
|
1969
|
+
context: rd(rec.context),
|
|
1970
|
+
files: rec.files?.map((f) => rd(f) ?? f)
|
|
1971
|
+
};
|
|
1972
|
+
return { rec: safeRec, redactions: [...tally.entries()].map(([label, count]) => ({ label, count })) };
|
|
1973
|
+
}
|
|
1974
|
+
function captureTurn(rec) {
|
|
1975
|
+
const { rec: safeRec, redactions } = redactRecord(rec);
|
|
1976
|
+
const journal = renderTurn(safeRec);
|
|
1977
|
+
appendJournal(rec.date, `${journal}
|
|
1978
|
+
`);
|
|
1979
|
+
updateDigest(`${safeRec.ts} \xB7 ${oneLineSummary(safeRec)}`, safeRec.context);
|
|
1980
|
+
updateTimeline(safeRec.date, oneLineSummary(safeRec));
|
|
1981
|
+
const auditClean = !containsSecret(readJournal(rec.date)) && !containsSecret(readDigest() ?? "");
|
|
1988
1982
|
return {
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
parity: gaps.parity,
|
|
1994
|
-
actionItems: generateActionItems(gaps.missing),
|
|
1995
|
-
summary: generateSummary(target, competitors, gaps)
|
|
1983
|
+
journalDate: rec.date,
|
|
1984
|
+
redactions,
|
|
1985
|
+
bytes: Buffer.byteLength(journal, "utf8"),
|
|
1986
|
+
auditClean
|
|
1996
1987
|
};
|
|
1997
1988
|
}
|
|
1998
1989
|
|
|
1999
|
-
// src/
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
for (const a of report.actionItems.slice(0, 12)) lines.push(` \u2192 ${a.title} [${a.priority}/${a.effort}] \u2014 ${a.rationale}`);
|
|
2013
|
-
}
|
|
2014
|
-
return lines.join("\n");
|
|
2015
|
-
}
|
|
2016
|
-
var InspectSiteParams = Type.Object({
|
|
2017
|
-
url: Type.String({ description: "The target website URL to inspect or rebuild from." }),
|
|
2018
|
-
competitors: Type.Optional(
|
|
2019
|
-
Type.Array(Type.String(), { description: "Optional competitor/reference URLs to compare the target against." })
|
|
2020
|
-
)
|
|
2021
|
-
});
|
|
2022
|
-
function registerHead(pi) {
|
|
2023
|
-
pi.registerTool({
|
|
2024
|
-
name: "inspect_site",
|
|
2025
|
-
label: "ORIRO Head",
|
|
2026
|
-
description: "Go out to a live website and SEE it: its sections, CTAs, structure, and any gaps versus competitor URLs. Returns a structured report to build from. Call this whenever the user wants to look at, compare against, or rebuild a website/page.",
|
|
2027
|
-
parameters: InspectSiteParams,
|
|
2028
|
-
async execute(_toolCallId, params) {
|
|
2029
|
-
const target = params.url;
|
|
2030
|
-
const competitors = params.competitors?.length ? params.competitors : [target];
|
|
2031
|
-
const report = await comparePages({ targetUrl: target, competitorUrls: competitors });
|
|
2032
|
-
return { content: [{ type: "text", text: summarizeForCoder(report) }], details: report };
|
|
2033
|
-
}
|
|
2034
|
-
});
|
|
1990
|
+
// src/scribe/health.ts
|
|
1991
|
+
import {
|
|
1992
|
+
closeSync as closeSync3,
|
|
1993
|
+
fsyncSync as fsyncSync3,
|
|
1994
|
+
mkdirSync as mkdirSync11,
|
|
1995
|
+
openSync as openSync3,
|
|
1996
|
+
readFileSync as readFileSync13,
|
|
1997
|
+
writeFileSync as writeFileSync11,
|
|
1998
|
+
writeSync as writeSync3
|
|
1999
|
+
} from "fs";
|
|
2000
|
+
import { join as join15 } from "path";
|
|
2001
|
+
function healthFile() {
|
|
2002
|
+
return join15(scribeDir(), "_health.json");
|
|
2035
2003
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
import { existsSync as existsSync9, readFileSync as readFileSync16 } from "fs";
|
|
2039
|
-
import { Type as Type2 } from "typebox";
|
|
2040
|
-
|
|
2041
|
-
// src/scribe/capture.ts
|
|
2042
|
-
import { closeSync as closeSync2, fsyncSync as fsyncSync2, mkdirSync as mkdirSync10, openSync as openSync2, writeSync as writeSync2 } from "fs";
|
|
2043
|
-
import { join as join14 } from "path";
|
|
2044
|
-
|
|
2045
|
-
// src/scribe/digest.ts
|
|
2046
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
|
|
2047
|
-
|
|
2048
|
-
// src/scribe/paths.ts
|
|
2049
|
-
import { join as join13 } from "path";
|
|
2050
|
-
function scribeDir() {
|
|
2051
|
-
const override = process.env.ORIRO_SCRIBE_DIR?.trim();
|
|
2052
|
-
return override && override.length > 0 ? override : join13(CONFIG_DIR, "scribe");
|
|
2004
|
+
function faultLogFile() {
|
|
2005
|
+
return join15(scribeDir(), "_faults.log");
|
|
2053
2006
|
}
|
|
2054
|
-
function
|
|
2055
|
-
|
|
2007
|
+
function read2() {
|
|
2008
|
+
try {
|
|
2009
|
+
return JSON.parse(readFileSync13(healthFile(), "utf8"));
|
|
2010
|
+
} catch {
|
|
2011
|
+
return { faultCount: 0 };
|
|
2012
|
+
}
|
|
2056
2013
|
}
|
|
2057
|
-
function
|
|
2058
|
-
|
|
2014
|
+
function write(h) {
|
|
2015
|
+
mkdirSync11(scribeDir(), { recursive: true });
|
|
2016
|
+
writeFileSync11(healthFile(), `${JSON.stringify(h, null, 2)}
|
|
2017
|
+
`, "utf8");
|
|
2059
2018
|
}
|
|
2060
|
-
function
|
|
2061
|
-
|
|
2019
|
+
function recordHealth() {
|
|
2020
|
+
const h = read2();
|
|
2021
|
+
h.lastWriteAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2022
|
+
write(h);
|
|
2062
2023
|
}
|
|
2063
|
-
function
|
|
2064
|
-
|
|
2024
|
+
function recordFault(role, err) {
|
|
2025
|
+
try {
|
|
2026
|
+
mkdirSync11(scribeDir(), { recursive: true });
|
|
2027
|
+
const msg = `${(/* @__PURE__ */ new Date()).toISOString()} [${role}] ${err instanceof Error ? err.message : String(err)}`;
|
|
2028
|
+
const fd = openSync3(faultLogFile(), "a");
|
|
2029
|
+
try {
|
|
2030
|
+
writeSync3(fd, `${msg}
|
|
2031
|
+
`);
|
|
2032
|
+
fsyncSync3(fd);
|
|
2033
|
+
} finally {
|
|
2034
|
+
closeSync3(fd);
|
|
2035
|
+
}
|
|
2036
|
+
const h = read2();
|
|
2037
|
+
h.faultCount = (h.faultCount ?? 0) + 1;
|
|
2038
|
+
h.lastFault = msg;
|
|
2039
|
+
write(h);
|
|
2040
|
+
} catch {
|
|
2041
|
+
}
|
|
2065
2042
|
}
|
|
2066
2043
|
|
|
2067
|
-
// src/scribe/
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2044
|
+
// src/scribe/wal.ts
|
|
2045
|
+
import {
|
|
2046
|
+
closeSync as closeSync4,
|
|
2047
|
+
existsSync as existsSync7,
|
|
2048
|
+
fsyncSync as fsyncSync4,
|
|
2049
|
+
mkdirSync as mkdirSync12,
|
|
2050
|
+
openSync as openSync4,
|
|
2051
|
+
readFileSync as readFileSync14,
|
|
2052
|
+
writeFileSync as writeFileSync12,
|
|
2053
|
+
writeSync as writeSync4
|
|
2054
|
+
} from "fs";
|
|
2055
|
+
import { join as join16 } from "path";
|
|
2056
|
+
function walFile() {
|
|
2057
|
+
return join16(scribeDir(), "_wal.jsonl");
|
|
2072
2058
|
}
|
|
2073
|
-
function
|
|
2074
|
-
|
|
2075
|
-
const
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
const priorRecent = recentMatch?.[1]?.trim() ?? "";
|
|
2083
|
-
let recent = summary.trim() ? `- ${summary.trim()}
|
|
2084
|
-
${priorRecent}` : priorRecent;
|
|
2085
|
-
const header2 = `# ORIRO Scribe \u2014 Digest
|
|
2086
|
-
|
|
2087
|
-
## Context
|
|
2088
|
-
${contextBlock}
|
|
2089
|
-
|
|
2090
|
-
## Recent activity (newest first)
|
|
2091
|
-
`;
|
|
2092
|
-
let out = header2 + recent;
|
|
2093
|
-
while (Buffer.byteLength(out, "utf8") > DIGEST_CAP && recent.includes("\n")) {
|
|
2094
|
-
recent = recent.slice(0, recent.lastIndexOf("\n")).trimEnd();
|
|
2095
|
-
out = header2 + recent;
|
|
2059
|
+
function appendLine(obj) {
|
|
2060
|
+
mkdirSync12(scribeDir(), { recursive: true });
|
|
2061
|
+
const fd = openSync4(walFile(), "a");
|
|
2062
|
+
try {
|
|
2063
|
+
writeSync4(fd, `${JSON.stringify(obj)}
|
|
2064
|
+
`);
|
|
2065
|
+
fsyncSync4(fd);
|
|
2066
|
+
} finally {
|
|
2067
|
+
closeSync4(fd);
|
|
2096
2068
|
}
|
|
2097
|
-
writeFileSync10(digestFile(), out, "utf8");
|
|
2098
2069
|
}
|
|
2099
|
-
function
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2070
|
+
function walAppend(id, rec) {
|
|
2071
|
+
appendLine({ t: "add", id, rec });
|
|
2072
|
+
}
|
|
2073
|
+
function walCommit(id) {
|
|
2074
|
+
appendLine({ t: "commit", id });
|
|
2075
|
+
}
|
|
2076
|
+
function walPending() {
|
|
2077
|
+
if (!existsSync7(walFile())) return [];
|
|
2078
|
+
const committed = /* @__PURE__ */ new Set();
|
|
2079
|
+
const adds = /* @__PURE__ */ new Map();
|
|
2080
|
+
for (const line of readFileSync14(walFile(), "utf8").split("\n")) {
|
|
2081
|
+
if (!line.trim()) continue;
|
|
2082
|
+
try {
|
|
2083
|
+
const e = JSON.parse(line);
|
|
2084
|
+
if (e.t === "commit") committed.add(e.id);
|
|
2085
|
+
else if (e.t === "add" && e.rec) adds.set(e.id, e.rec);
|
|
2086
|
+
} catch {
|
|
2087
|
+
}
|
|
2113
2088
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2089
|
+
const out = [];
|
|
2090
|
+
for (const [id, rec] of adds) {
|
|
2091
|
+
if (!committed.has(id)) out.push({ id, rec });
|
|
2092
|
+
}
|
|
2093
|
+
return out;
|
|
2094
|
+
}
|
|
2095
|
+
function walCompact() {
|
|
2096
|
+
if (!existsSync7(walFile())) return;
|
|
2097
|
+
const pending = walPending();
|
|
2098
|
+
const body = pending.map((p) => JSON.stringify({ t: "add", id: p.id, rec: p.rec })).join("\n");
|
|
2099
|
+
writeFileSync12(walFile(), body ? `${body}
|
|
2100
|
+
` : "", "utf8");
|
|
2118
2101
|
}
|
|
2119
2102
|
|
|
2120
|
-
// src/scribe/
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
writeSync
|
|
2129
|
-
} from "fs";
|
|
2130
|
-
function appendJournal(date, content) {
|
|
2131
|
-
mkdirSync9(scribeDir(), { recursive: true });
|
|
2132
|
-
const fd = openSync(journalFile(date), "a");
|
|
2103
|
+
// src/scribe/supervisor.ts
|
|
2104
|
+
var draining = false;
|
|
2105
|
+
function uid(ts) {
|
|
2106
|
+
return `${ts}-${Math.random().toString(36).slice(2, 9)}`;
|
|
2107
|
+
}
|
|
2108
|
+
function drainBacklog() {
|
|
2109
|
+
if (draining) return;
|
|
2110
|
+
draining = true;
|
|
2133
2111
|
try {
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2112
|
+
let drained = 0;
|
|
2113
|
+
for (const e of walPending()) {
|
|
2114
|
+
try {
|
|
2115
|
+
captureTurn(e.rec);
|
|
2116
|
+
walCommit(e.id);
|
|
2117
|
+
drained++;
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
recordFault("standby-replay", err);
|
|
2120
|
+
break;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
if (drained > 0) walCompact();
|
|
2137
2124
|
} finally {
|
|
2138
|
-
|
|
2125
|
+
draining = false;
|
|
2139
2126
|
}
|
|
2140
2127
|
}
|
|
2141
|
-
function
|
|
2142
|
-
|
|
2143
|
-
|
|
2128
|
+
function supervisedCapture(rec) {
|
|
2129
|
+
try {
|
|
2130
|
+
drainBacklog();
|
|
2131
|
+
const id = uid(rec.ts);
|
|
2132
|
+
const safe = redactRecord(rec).rec;
|
|
2133
|
+
walAppend(id, safe);
|
|
2134
|
+
try {
|
|
2135
|
+
const res = captureTurn(safe);
|
|
2136
|
+
walCommit(id);
|
|
2137
|
+
walCompact();
|
|
2138
|
+
recordHealth();
|
|
2139
|
+
return res;
|
|
2140
|
+
} catch (primaryErr) {
|
|
2141
|
+
recordFault("primary", primaryErr);
|
|
2142
|
+
try {
|
|
2143
|
+
const res = captureTurn(safe);
|
|
2144
|
+
walCommit(id);
|
|
2145
|
+
walCompact();
|
|
2146
|
+
recordHealth();
|
|
2147
|
+
return res;
|
|
2148
|
+
} catch (standbyErr) {
|
|
2149
|
+
recordFault("standby", standbyErr);
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
} catch (fatal) {
|
|
2154
|
+
recordFault("supervisor", fatal);
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2144
2157
|
}
|
|
2145
2158
|
|
|
2146
|
-
// src/scribe/
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
}
|
|
2152
|
-
{ label: "anthropic-key", re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
|
|
2153
|
-
{ label: "openrouter-key", re: /sk-or-v1-[A-Za-z0-9]{20,}/g },
|
|
2154
|
-
// Stripe-style keys (sk_live_/pk_live_/rk_test_/…), underscore segments.
|
|
2155
|
-
{ label: "stripe-key", re: /\b[srp]k_(?:live|test)_[A-Za-z0-9]{16,}/g },
|
|
2156
|
-
// Generic sk- secret keys — allow hyphenated segments (sk-live-…, sk-proj-…) so a second
|
|
2157
|
-
// hyphen no longer breaks the match (the gap the Scriber spike caught).
|
|
2158
|
-
{ label: "secret-key-sk", re: /sk[-_][A-Za-z0-9][A-Za-z0-9-]{14,}/g },
|
|
2159
|
-
{ label: "google-key", re: /AIza[0-9A-Za-z_-]{30,}/g },
|
|
2160
|
-
{ label: "groq-key", re: /gsk_[A-Za-z0-9]{20,}/g },
|
|
2161
|
-
{ label: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
2162
|
-
{ label: "github-token", re: /gh[posr]_[A-Za-z0-9]{30,}/g },
|
|
2163
|
-
{ label: "xai-key", re: /xai-[A-Za-z0-9]{20,}/g },
|
|
2164
|
-
{ label: "aws-key", re: /AKIA[0-9A-Z]{16}/g },
|
|
2165
|
-
{ label: "jwt", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g },
|
|
2166
|
-
{ label: "telegram-token", re: /\b\d{8,10}:[A-Za-z0-9_-]{30,}\b/g },
|
|
2167
|
-
{ label: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g },
|
|
2168
|
-
{ label: "phone", re: /(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}/g }
|
|
2169
|
-
];
|
|
2170
|
-
function marker(label) {
|
|
2171
|
-
return `\u27E8REDACTED:${label}\u27E9`;
|
|
2159
|
+
// src/scribe/retrieval.ts
|
|
2160
|
+
import { existsSync as existsSync8, readFileSync as readFileSync15, readdirSync } from "fs";
|
|
2161
|
+
function listDays() {
|
|
2162
|
+
const dir = scribeDir();
|
|
2163
|
+
if (!existsSync8(dir)) return [];
|
|
2164
|
+
return readdirSync(dir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).map((f) => f.replace(/\.md$/, "")).sort();
|
|
2172
2165
|
}
|
|
2173
|
-
function
|
|
2174
|
-
const
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2166
|
+
function readDay(date) {
|
|
2167
|
+
const f = journalFile(date);
|
|
2168
|
+
return existsSync8(f) ? readFileSync15(f, "utf8") : "";
|
|
2169
|
+
}
|
|
2170
|
+
function searchScribe(query, limit = 100) {
|
|
2171
|
+
const q = query.toLowerCase().trim();
|
|
2172
|
+
if (!q) return [];
|
|
2173
|
+
const hits = [];
|
|
2174
|
+
for (const date of listDays().reverse()) {
|
|
2175
|
+
const lines = readDay(date).split("\n");
|
|
2176
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2177
|
+
const ln = lines[i];
|
|
2178
|
+
if (ln && ln.toLowerCase().includes(q)) {
|
|
2179
|
+
hits.push({ date, line: i + 1, text: ln.trim().slice(0, 200) });
|
|
2180
|
+
if (hits.length >= limit) return hits;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2180
2183
|
}
|
|
2181
|
-
return
|
|
2184
|
+
return hits;
|
|
2182
2185
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
if (
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
return entropy(token) >= 4.2;
|
|
2186
|
+
|
|
2187
|
+
// src/scribe/scribe-pi.ts
|
|
2188
|
+
function scribeTurn(input) {
|
|
2189
|
+
if (!isScribeEnabled()) return;
|
|
2190
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2191
|
+
supervisedCapture({ ts, date: ts.slice(0, 10), ...input });
|
|
2190
2192
|
}
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
for (const rule of RULES) {
|
|
2195
|
-
text = text.replace(rule.re, () => {
|
|
2196
|
-
counts.set(rule.label, (counts.get(rule.label) ?? 0) + 1);
|
|
2197
|
-
return marker(rule.label);
|
|
2198
|
-
});
|
|
2199
|
-
}
|
|
2200
|
-
text = text.split(/(\s+)/).map((tok) => {
|
|
2201
|
-
if (looksLikeUnknownSecret(tok)) {
|
|
2202
|
-
counts.set("high-entropy", (counts.get("high-entropy") ?? 0) + 1);
|
|
2203
|
-
return marker("high-entropy");
|
|
2204
|
-
}
|
|
2205
|
-
return tok;
|
|
2206
|
-
}).join("");
|
|
2207
|
-
const redactions = [...counts.entries()].map(([label, count]) => ({
|
|
2208
|
-
label,
|
|
2209
|
-
count
|
|
2210
|
-
}));
|
|
2211
|
-
return { text, redactions };
|
|
2193
|
+
var pendingUserInput = "";
|
|
2194
|
+
function noteUserInput(text) {
|
|
2195
|
+
pendingUserInput = text;
|
|
2212
2196
|
}
|
|
2213
|
-
function
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2197
|
+
function takePendingUserInput() {
|
|
2198
|
+
const u = pendingUserInput;
|
|
2199
|
+
pendingUserInput = "";
|
|
2200
|
+
return u;
|
|
2201
|
+
}
|
|
2202
|
+
function buildScribeContext() {
|
|
2203
|
+
if (!isScribeEnabled()) return "";
|
|
2204
|
+
const parts = [];
|
|
2205
|
+
try {
|
|
2206
|
+
const t = timelineFile();
|
|
2207
|
+
if (existsSync9(t)) parts.push(`# Work history \u2014 every day so far
|
|
2208
|
+
${readFileSync16(t, "utf8").trim()}`);
|
|
2209
|
+
} catch {
|
|
2217
2210
|
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2211
|
+
try {
|
|
2212
|
+
const d = readDigest();
|
|
2213
|
+
if (d?.trim()) parts.push(`# Current context (recent)
|
|
2214
|
+
${d.trim()}`);
|
|
2215
|
+
} catch {
|
|
2216
|
+
}
|
|
2217
|
+
if (!parts.length) return "";
|
|
2218
|
+
return `${parts.join("\n\n")}
|
|
2219
|
+
|
|
2220
|
+
(Call scribe_recall to fetch the full text of any past day or topic.)`;
|
|
2221
|
+
}
|
|
2222
|
+
function registerScribe(pi) {
|
|
2223
|
+
pi.registerTool({
|
|
2224
|
+
name: "scribe_recall",
|
|
2225
|
+
label: "ORIRO Scribe",
|
|
2226
|
+
description: "Recall the user's past work from the on-device journal: search by keyword, or read a specific day (YYYY-MM-DD). Use to recover decisions, code, files, and context from earlier sessions.",
|
|
2227
|
+
parameters: Type.Object({
|
|
2228
|
+
query: Type.Optional(Type.String({ description: "Keyword/topic to search across all journals." })),
|
|
2229
|
+
day: Type.Optional(Type.String({ description: "A specific day YYYY-MM-DD to read in full." }))
|
|
2230
|
+
}),
|
|
2231
|
+
async execute(_id, params) {
|
|
2232
|
+
let text;
|
|
2233
|
+
const details = {};
|
|
2234
|
+
if (!isScribeEnabled()) {
|
|
2235
|
+
text = "Scribe is off (the user has not enabled it).";
|
|
2236
|
+
} else if (params.day) {
|
|
2237
|
+
text = readDay(params.day) || `No journal for ${params.day}. Days: ${listDays().join(", ") || "none"}`;
|
|
2238
|
+
details.day = params.day;
|
|
2239
|
+
} else {
|
|
2240
|
+
const hits = params.query ? searchScribe(params.query) : [];
|
|
2241
|
+
details.hits = hits;
|
|
2242
|
+
text = hits.length ? hits.map((h) => `${h.date}:${h.line} ${h.text}`).join("\n") : `No matches${params.query ? ` for "${params.query}"` : ""}. Days recorded: ${listDays().join(", ") || "none"}`;
|
|
2243
|
+
}
|
|
2244
|
+
return { content: [{ type: "text", text }], details };
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
function attachScribe(session) {
|
|
2249
|
+
let user = "";
|
|
2250
|
+
let assistant = "";
|
|
2251
|
+
const tools = /* @__PURE__ */ new Set();
|
|
2252
|
+
session.subscribe((e) => {
|
|
2253
|
+
if (!isScribeEnabled()) return;
|
|
2254
|
+
if (e?.type === "user_message" || e?.type === "session_user_message") user = String(e.text ?? e.message ?? user);
|
|
2255
|
+
if (e?.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") assistant += e.assistantMessageEvent.delta ?? "";
|
|
2256
|
+
if ((e?.type === "tool_call" || e?.type === "tool_execution_start") && e.toolName) tools.add(String(e.toolName));
|
|
2257
|
+
if (e?.type === "agent_end") {
|
|
2258
|
+
const userText = takePendingUserInput() || user;
|
|
2259
|
+
scribeTurn({ user: userText || void 0, router: "oriro-free", tools: [...tools], note: assistant.slice(0, 4e3) || void 0 });
|
|
2260
|
+
user = "";
|
|
2261
|
+
assistant = "";
|
|
2262
|
+
tools.clear();
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// src/routers/mux-provider.ts
|
|
2268
|
+
var MUX_PROVIDER = "oriro-mux";
|
|
2269
|
+
var MUX_MODEL = "oriro-free";
|
|
2270
|
+
function errToCallError(msg) {
|
|
2271
|
+
const text = msg.errorMessage ?? "";
|
|
2272
|
+
return /\b429\b|rate.?limit|too many requests/i.test(text) ? { status: 429 } : {};
|
|
2273
|
+
}
|
|
2274
|
+
function buildErrorMessage(message) {
|
|
2275
|
+
return {
|
|
2276
|
+
role: "assistant",
|
|
2277
|
+
content: [],
|
|
2278
|
+
api: "openai-completions",
|
|
2279
|
+
provider: MUX_PROVIDER,
|
|
2280
|
+
model: MUX_MODEL,
|
|
2281
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
2282
|
+
stopReason: "error",
|
|
2283
|
+
timestamp: Date.now(),
|
|
2284
|
+
errorMessage: message
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
async function driveMux(out, mux, byId, context, options) {
|
|
2288
|
+
let lastError;
|
|
2289
|
+
for (const id of mux.ranked()) {
|
|
2290
|
+
const router = byId.get(id);
|
|
2291
|
+
if (!router) continue;
|
|
2292
|
+
const t0 = Date.now();
|
|
2293
|
+
let committed = false;
|
|
2294
|
+
let lastPartial;
|
|
2295
|
+
try {
|
|
2296
|
+
const inner = piStreamSimple(routerModel(router), context, {
|
|
2297
|
+
...options ?? {},
|
|
2298
|
+
apiKey: router.apiKey
|
|
2299
|
+
});
|
|
2300
|
+
let failedBeforeContent = false;
|
|
2301
|
+
for await (const ev of inner) {
|
|
2302
|
+
if (ev.type === "error") {
|
|
2303
|
+
mux.recordFailure(id, errToCallError(ev.error));
|
|
2304
|
+
if (!committed) {
|
|
2305
|
+
lastError = ev.error;
|
|
2306
|
+
failedBeforeContent = true;
|
|
2307
|
+
break;
|
|
2308
|
+
}
|
|
2309
|
+
out.push(ev);
|
|
2310
|
+
out.end(ev.error);
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
committed = true;
|
|
2314
|
+
if (ev.type === "done") {
|
|
2315
|
+
mux.recordSuccess(id, Date.now() - t0);
|
|
2316
|
+
const clean = sanitizeMessageToolCalls(scrubMessageIdentity(ev.message));
|
|
2317
|
+
out.push({ type: "done", reason: ev.reason, message: clean });
|
|
2318
|
+
out.end(clean);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
lastPartial = ev.partial;
|
|
2322
|
+
out.push(sanitizeEventToolCalls(ev));
|
|
2323
|
+
}
|
|
2324
|
+
if (failedBeforeContent) continue;
|
|
2325
|
+
if (!committed) {
|
|
2326
|
+
mux.recordFailure(id, {});
|
|
2327
|
+
lastError ??= buildErrorMessage("Router returned no output.");
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
mux.recordSuccess(id, Date.now() - t0);
|
|
2331
|
+
out.end(lastPartial ? sanitizeMessageToolCalls(scrubMessageIdentity(lastPartial)) : void 0);
|
|
2332
|
+
return;
|
|
2333
|
+
} catch (e) {
|
|
2334
|
+
mux.recordFailure(id, e);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
const msg = lastError ?? buildErrorMessage(
|
|
2338
|
+
"All keyless routers are unavailable. Add a BYOK key, select more free routers, or retry shortly."
|
|
2339
|
+
);
|
|
2340
|
+
out.push({ type: "error", reason: "error", error: msg });
|
|
2341
|
+
out.end(msg);
|
|
2342
|
+
}
|
|
2343
|
+
function registerOriroMux(registry, opts = {}) {
|
|
2344
|
+
registerOpenAICompletions();
|
|
2345
|
+
const pooled = resolvePool();
|
|
2346
|
+
const routers = opts.routers ?? (pooled.length > 0 ? pooled : KEYLESS_FLOOR);
|
|
2347
|
+
const byId = new Map(routers.map((r) => [r.id, r]));
|
|
2348
|
+
const mux = new RouterMux(routers.map((r) => r.id));
|
|
2349
|
+
try {
|
|
2350
|
+
mux.load(loadMuxState(oriroDir()));
|
|
2351
|
+
} catch {
|
|
2352
|
+
}
|
|
2353
|
+
registry.registerProvider(MUX_PROVIDER, {
|
|
2354
|
+
name: "ORIRO Free (keyless Mux)",
|
|
2355
|
+
api: "openai-completions",
|
|
2356
|
+
apiKey: "oriro-keyless",
|
|
2357
|
+
// Placeholder — required by registry validation but never used: our custom streamSimple
|
|
2358
|
+
// routes to the real keyless floor endpoints itself (see driveMux).
|
|
2359
|
+
baseUrl: "http://oriro-mux.local",
|
|
2360
|
+
models: [
|
|
2361
|
+
{
|
|
2362
|
+
id: MUX_MODEL,
|
|
2363
|
+
name: "ORIRO Free (best-router)",
|
|
2364
|
+
api: "openai-completions",
|
|
2365
|
+
baseUrl: "http://oriro-mux.local",
|
|
2366
|
+
reasoning: false,
|
|
2367
|
+
input: ["text"],
|
|
2368
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2369
|
+
contextWindow: 128e3,
|
|
2370
|
+
maxTokens: 4096
|
|
2371
|
+
}
|
|
2372
|
+
],
|
|
2373
|
+
streamSimple: (_model, context, options) => {
|
|
2374
|
+
const out = createAssistantMessageEventStream();
|
|
2375
|
+
const ctx = applyIdentity(context);
|
|
2376
|
+
const memory = buildScribeContext();
|
|
2377
|
+
const withMemory = memory ? { ...ctx, systemPrompt: `${ctx.systemPrompt}
|
|
2378
|
+
|
|
2379
|
+
${memory}` } : ctx;
|
|
2380
|
+
void driveMux(out, mux, byId, withMemory, options).finally(() => {
|
|
2381
|
+
try {
|
|
2382
|
+
saveMuxState(oriroDir(), mux.snapshot());
|
|
2383
|
+
} catch {
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
return out;
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
return registry.find(MUX_PROVIDER, MUX_MODEL);
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// src/head/pi-tool.ts
|
|
2393
|
+
import { Type as Type2 } from "typebox";
|
|
2394
|
+
|
|
2395
|
+
// src/head/comparison-engine.ts
|
|
2396
|
+
var SECTION_RULES = [
|
|
2397
|
+
{
|
|
2398
|
+
type: "hero",
|
|
2399
|
+
label: "Hero",
|
|
2400
|
+
priority: "CRITICAL",
|
|
2401
|
+
markup: [/<h1[\s>]/],
|
|
2402
|
+
recommend: "Add a clear above-the-fold hero \u2014 one headline that states the value + one primary CTA."
|
|
2403
|
+
},
|
|
2404
|
+
{
|
|
2405
|
+
type: "navigation",
|
|
2406
|
+
label: "Navigation",
|
|
2407
|
+
priority: "CRITICAL",
|
|
2408
|
+
markup: [/<nav[\s>]/, /role=["']navigation["']/],
|
|
2409
|
+
recommend: "Add a top navigation so visitors can reach key sections."
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
type: "features",
|
|
2413
|
+
label: "Features",
|
|
2414
|
+
priority: "CRITICAL",
|
|
2415
|
+
text: [/\bfeatures?\b/, /\bwhat you (?:can|get)\b/, /\bcapabilit/],
|
|
2416
|
+
recommend: "Add a features section that spells out concrete capabilities, not adjectives."
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
type: "pricing",
|
|
2420
|
+
label: "Pricing",
|
|
2421
|
+
priority: "CRITICAL",
|
|
2422
|
+
text: [/\bpricing\b/, /\bper month\b/, /\b\/mo\b/, /\bfree plan\b/, /\$\d/, /₹\d/, /€\d/],
|
|
2423
|
+
recommend: 'Add transparent pricing \u2014 a critical conversion element; even a single "Free" tier helps.'
|
|
2424
|
+
},
|
|
2425
|
+
{
|
|
2426
|
+
type: "cta",
|
|
2427
|
+
label: "Call-to-Action",
|
|
2428
|
+
priority: "CRITICAL",
|
|
2429
|
+
text: [/\bget started\b/, /\bsign up\b/, /\bstart (?:free|now|building)\b/, /\btry (?:it|now|free)\b/, /\bbook a demo\b/, /\bget a demo\b/],
|
|
2430
|
+
recommend: 'Add a strong, repeated primary CTA ("Get started") so the next step is obvious.'
|
|
2431
|
+
},
|
|
2432
|
+
{
|
|
2433
|
+
type: "testimonials",
|
|
2434
|
+
label: "Testimonials",
|
|
2435
|
+
priority: "HIGH",
|
|
2436
|
+
text: [/\btestimonial/, /\bwhat (?:our )?(?:customers|users) say\b/, /\bloved by\b/, /\breview(?:s|ed)\b/],
|
|
2437
|
+
recommend: "Add 2\u20133 customer testimonials with names/photos to build trust."
|
|
2438
|
+
},
|
|
2439
|
+
{
|
|
2440
|
+
type: "stats",
|
|
2441
|
+
label: "Stats / Metrics",
|
|
2442
|
+
priority: "HIGH",
|
|
2443
|
+
text: [/\b\d[\d,.]*\s*[kkmm]\+?\s*(?:users|customers|developers|downloads|teams)\b/, /\b9\d(?:\.\d+)?%\b/, /\buptime\b/],
|
|
2444
|
+
recommend: 'Add impressive metrics ("10K+ users", "99.9% uptime") as social proof.'
|
|
2445
|
+
},
|
|
2446
|
+
{
|
|
2447
|
+
type: "video",
|
|
2448
|
+
label: "Video",
|
|
2449
|
+
priority: "HIGH",
|
|
2450
|
+
markup: [/<video[\s>]/, /youtube\.com\/embed/, /player\.vimeo\.com/, /<iframe[^>]+(?:youtube|vimeo)/],
|
|
2451
|
+
text: [/\bwatch the (?:video|demo)\b/],
|
|
2452
|
+
recommend: "Add a short explainer/demo video \u2014 it lifts conversion on landing pages."
|
|
2453
|
+
},
|
|
2454
|
+
{
|
|
2455
|
+
type: "demo",
|
|
2456
|
+
label: "Live Demo",
|
|
2457
|
+
priority: "HIGH",
|
|
2458
|
+
text: [/\btry it (?:now|live|free)\b/, /\bplayground\b/, /\binteractive demo\b/, /\blive demo\b/],
|
|
2459
|
+
recommend: 'Add a "try it" live demo or playground so visitors experience the product immediately.'
|
|
2460
|
+
},
|
|
2461
|
+
{
|
|
2462
|
+
type: "socialProof",
|
|
2463
|
+
label: "Social Proof",
|
|
2464
|
+
priority: "HIGH",
|
|
2465
|
+
text: [/\btrusted by\b/, /\bbacked by\b/, /\bused by\b/, /\bas seen (?:in|on)\b/, /\bcustomers include\b/],
|
|
2466
|
+
recommend: 'Add social proof (customer/investor logos, "trusted by \u2026") near the hero.'
|
|
2467
|
+
},
|
|
2468
|
+
{
|
|
2469
|
+
type: "faq",
|
|
2470
|
+
label: "FAQ",
|
|
2471
|
+
priority: "MEDIUM",
|
|
2472
|
+
text: [/\bfaq\b/, /\bfrequently asked\b/],
|
|
2473
|
+
markup: [/<details[\s>]/],
|
|
2474
|
+
recommend: "Add an FAQ that answers the top objections before they become exits."
|
|
2475
|
+
},
|
|
2476
|
+
{
|
|
2477
|
+
type: "integrations",
|
|
2478
|
+
label: "Integrations",
|
|
2479
|
+
priority: "MEDIUM",
|
|
2480
|
+
text: [/\bintegrations?\b/, /\bworks with\b/, /\bconnect your\b/],
|
|
2481
|
+
recommend: "Add an integrations section showing what the product connects to."
|
|
2482
|
+
},
|
|
2483
|
+
{
|
|
2484
|
+
type: "newsletter",
|
|
2485
|
+
label: "Newsletter / Capture",
|
|
2486
|
+
priority: "MEDIUM",
|
|
2487
|
+
text: [/\bsubscribe\b/, /\bnewsletter\b/, /\bjoin (?:the )?waitlist\b/],
|
|
2488
|
+
markup: [/type=["']email["']/],
|
|
2489
|
+
recommend: "Add an email capture (newsletter/waitlist) so non-converting visitors are not lost."
|
|
2490
|
+
},
|
|
2491
|
+
{
|
|
2492
|
+
type: "comparison",
|
|
2493
|
+
label: "Comparison",
|
|
2494
|
+
priority: "MEDIUM",
|
|
2495
|
+
text: [/\bcompare\b/, /\bcomparison\b/, /\b vs\.? \b/, /\bwhy choose\b/],
|
|
2496
|
+
recommend: 'Add a comparison ("us vs alternatives") to win evaluators who are shopping around.'
|
|
2497
|
+
},
|
|
2498
|
+
{
|
|
2499
|
+
type: "team",
|
|
2500
|
+
label: "Team / About",
|
|
2501
|
+
priority: "LOW",
|
|
2502
|
+
text: [/\bour team\b/, /\bmeet the team\b/, /\bfounders?\b/, /\babout us\b/],
|
|
2503
|
+
recommend: "Add a brief team/about section to humanize the brand."
|
|
2220
2504
|
}
|
|
2221
|
-
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
var
|
|
2226
|
-
function
|
|
2227
|
-
|
|
2228
|
-
const
|
|
2229
|
-
const
|
|
2230
|
-
const fd = openSync2(p, "w");
|
|
2505
|
+
];
|
|
2506
|
+
var PRIORITY_RANK = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
|
2507
|
+
var PRIORITY_EFFORT = { CRITICAL: "L", HIGH: "M", MEDIUM: "M", LOW: "S" };
|
|
2508
|
+
var FETCH_TIMEOUT_MS = 12e3;
|
|
2509
|
+
var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36 ORIRO-Inspector";
|
|
2510
|
+
async function fetchPage(url) {
|
|
2511
|
+
const controller = new AbortController();
|
|
2512
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
2513
|
+
const start = Date.now();
|
|
2231
2514
|
try {
|
|
2232
|
-
|
|
2233
|
-
|
|
2515
|
+
const res = await fetch(url, {
|
|
2516
|
+
signal: controller.signal,
|
|
2517
|
+
redirect: "follow",
|
|
2518
|
+
headers: { "user-agent": UA, accept: "text/html,application/xhtml+xml" }
|
|
2519
|
+
});
|
|
2520
|
+
const html = await res.text();
|
|
2521
|
+
return { html, ms: Date.now() - start, status: res.status, ok: res.ok, error: "" };
|
|
2522
|
+
} catch (err) {
|
|
2523
|
+
return { html: "", ms: Date.now() - start, status: 0, ok: false, error: err instanceof Error ? err.message : "fetch failed" };
|
|
2234
2524
|
} finally {
|
|
2235
|
-
|
|
2236
|
-
}
|
|
2237
|
-
return p;
|
|
2238
|
-
}
|
|
2239
|
-
function field(date, ts, label, value) {
|
|
2240
|
-
if (!value || !value.trim()) return "";
|
|
2241
|
-
if (value.length > INLINE_CAP) {
|
|
2242
|
-
const ref = sideFile(date, ts, label.toLowerCase().replace(/\s+/g, "-"), value);
|
|
2243
|
-
return `**${label}** (full \u2192 ${ref}):
|
|
2244
|
-
${value.slice(0, INLINE_CAP)}
|
|
2245
|
-
\u2026(truncated; full content in artifact)
|
|
2246
|
-
|
|
2247
|
-
`;
|
|
2248
|
-
}
|
|
2249
|
-
return `**${label}:**
|
|
2250
|
-
${value}
|
|
2251
|
-
|
|
2252
|
-
`;
|
|
2253
|
-
}
|
|
2254
|
-
function renderTurn(rec) {
|
|
2255
|
-
let md = `## ${rec.ts}
|
|
2256
|
-
|
|
2257
|
-
`;
|
|
2258
|
-
md += field(rec.date, rec.ts, "User", rec.user);
|
|
2259
|
-
md += field(rec.date, rec.ts, "Router", rec.router);
|
|
2260
|
-
if (rec.tools?.length) md += `**Tools:** ${rec.tools.join(", ")}
|
|
2261
|
-
|
|
2262
|
-
`;
|
|
2263
|
-
if (rec.files?.length) md += `**Files:** ${rec.files.join(", ")}
|
|
2264
|
-
|
|
2265
|
-
`;
|
|
2266
|
-
md += field(rec.date, rec.ts, "Note", rec.note);
|
|
2267
|
-
return `${md}---
|
|
2268
|
-
`;
|
|
2269
|
-
}
|
|
2270
|
-
function oneLineSummary(rec) {
|
|
2271
|
-
const bits = [];
|
|
2272
|
-
if (rec.user) bits.push(rec.user.replace(/\s+/g, " ").slice(0, 80));
|
|
2273
|
-
if (rec.files?.length) bits.push(`files: ${rec.files.slice(0, 3).join(", ")}`);
|
|
2274
|
-
if (rec.note) bits.push(rec.note.replace(/\s+/g, " ").slice(0, 60));
|
|
2275
|
-
return bits.join(" \xB7 ") || "(activity)";
|
|
2276
|
-
}
|
|
2277
|
-
function captureTurn(rec) {
|
|
2278
|
-
const safe = redact(renderTurn(rec));
|
|
2279
|
-
appendJournal(rec.date, `${safe.text}
|
|
2280
|
-
`);
|
|
2281
|
-
const summary = redact(`${rec.ts} \xB7 ${oneLineSummary(rec)}`).text;
|
|
2282
|
-
updateDigest(summary, rec.context ? redact(rec.context).text : void 0);
|
|
2283
|
-
updateTimeline(rec.date, redact(oneLineSummary(rec)).text);
|
|
2284
|
-
const auditClean = !containsSecret(readJournal(rec.date));
|
|
2285
|
-
return {
|
|
2286
|
-
journalDate: rec.date,
|
|
2287
|
-
redactions: safe.redactions,
|
|
2288
|
-
bytes: Buffer.byteLength(safe.text, "utf8"),
|
|
2289
|
-
auditClean
|
|
2290
|
-
};
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
// src/scribe/health.ts
|
|
2294
|
-
import {
|
|
2295
|
-
closeSync as closeSync3,
|
|
2296
|
-
fsyncSync as fsyncSync3,
|
|
2297
|
-
mkdirSync as mkdirSync11,
|
|
2298
|
-
openSync as openSync3,
|
|
2299
|
-
readFileSync as readFileSync13,
|
|
2300
|
-
writeFileSync as writeFileSync11,
|
|
2301
|
-
writeSync as writeSync3
|
|
2302
|
-
} from "fs";
|
|
2303
|
-
import { join as join15 } from "path";
|
|
2304
|
-
function healthFile() {
|
|
2305
|
-
return join15(scribeDir(), "_health.json");
|
|
2306
|
-
}
|
|
2307
|
-
function faultLogFile() {
|
|
2308
|
-
return join15(scribeDir(), "_faults.log");
|
|
2309
|
-
}
|
|
2310
|
-
function read2() {
|
|
2311
|
-
try {
|
|
2312
|
-
return JSON.parse(readFileSync13(healthFile(), "utf8"));
|
|
2313
|
-
} catch {
|
|
2314
|
-
return { faultCount: 0 };
|
|
2525
|
+
clearTimeout(timer);
|
|
2315
2526
|
}
|
|
2316
2527
|
}
|
|
2317
|
-
function
|
|
2318
|
-
|
|
2319
|
-
writeFileSync11(healthFile(), `${JSON.stringify(h, null, 2)}
|
|
2320
|
-
`, "utf8");
|
|
2528
|
+
function toText(html) {
|
|
2529
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /gi, " ").replace(/\s+/g, " ").toLowerCase().trim();
|
|
2321
2530
|
}
|
|
2322
|
-
function
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2531
|
+
function firstMatch(re, hay) {
|
|
2532
|
+
const m = re.exec(hay);
|
|
2533
|
+
if (!m) return "";
|
|
2534
|
+
const slice = (m[0] ?? "").trim();
|
|
2535
|
+
return slice.length > 80 ? `${slice.slice(0, 77)}\u2026` : slice;
|
|
2326
2536
|
}
|
|
2327
|
-
function
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
const
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
closeSync3(fd);
|
|
2537
|
+
function detectSections(rawHtmlLower, text) {
|
|
2538
|
+
const found = [];
|
|
2539
|
+
for (const rule of SECTION_RULES) {
|
|
2540
|
+
let evidence = "";
|
|
2541
|
+
for (const re of rule.markup ?? []) {
|
|
2542
|
+
const hit = firstMatch(re, rawHtmlLower);
|
|
2543
|
+
if (hit) {
|
|
2544
|
+
evidence = hit;
|
|
2545
|
+
break;
|
|
2546
|
+
}
|
|
2338
2547
|
}
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2548
|
+
if (!evidence) {
|
|
2549
|
+
for (const re of rule.text ?? []) {
|
|
2550
|
+
const hit = firstMatch(re, text);
|
|
2551
|
+
if (hit) {
|
|
2552
|
+
evidence = hit;
|
|
2553
|
+
break;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
if (evidence) found.push({ type: rule.type, label: rule.label, priority: rule.priority, evidence });
|
|
2344
2558
|
}
|
|
2559
|
+
return found;
|
|
2345
2560
|
}
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
mkdirSync as mkdirSync12,
|
|
2353
|
-
openSync as openSync4,
|
|
2354
|
-
readFileSync as readFileSync14,
|
|
2355
|
-
writeFileSync as writeFileSync12,
|
|
2356
|
-
writeSync as writeSync4
|
|
2357
|
-
} from "fs";
|
|
2358
|
-
import { join as join16 } from "path";
|
|
2359
|
-
function walFile() {
|
|
2360
|
-
return join16(scribeDir(), "_wal.jsonl");
|
|
2361
|
-
}
|
|
2362
|
-
function appendLine(obj) {
|
|
2363
|
-
mkdirSync12(scribeDir(), { recursive: true });
|
|
2364
|
-
const fd = openSync4(walFile(), "a");
|
|
2365
|
-
try {
|
|
2366
|
-
writeSync4(fd, `${JSON.stringify(obj)}
|
|
2367
|
-
`);
|
|
2368
|
-
fsyncSync4(fd);
|
|
2369
|
-
} finally {
|
|
2370
|
-
closeSync4(fd);
|
|
2561
|
+
function extractMatches(re, html, max) {
|
|
2562
|
+
const out = [];
|
|
2563
|
+
for (const m of html.matchAll(re)) {
|
|
2564
|
+
const inner = (m[1] ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
2565
|
+
if (inner && !out.includes(inner)) out.push(inner);
|
|
2566
|
+
if (out.length >= max) break;
|
|
2371
2567
|
}
|
|
2568
|
+
return out;
|
|
2372
2569
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2570
|
+
var CTA_WORDS = /\b(get started|sign up|start free|start now|start building|try (?:it|now|free)|book a demo|get a demo|request access|join (?:the )?waitlist|download)\b/i;
|
|
2571
|
+
function extractStructure(url, fr) {
|
|
2572
|
+
const html = fr.html;
|
|
2573
|
+
const lowerHtml = html.toLowerCase();
|
|
2574
|
+
const text = toText(html);
|
|
2575
|
+
const titleM = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
|
|
2576
|
+
const title = (titleM?.[1] ?? "").replace(/\s+/g, " ").trim();
|
|
2577
|
+
const descM = /<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["']/i.exec(html) ?? /<meta[^>]+content=["']([^"']*)["'][^>]+name=["']description["']/i.exec(html);
|
|
2578
|
+
const description = (descM?.[1] ?? "").replace(/\s+/g, " ").trim();
|
|
2579
|
+
const headings = extractMatches(/<h[1-3][^>]*>([\s\S]*?)<\/h[1-3]>/gi, html, 12);
|
|
2580
|
+
const ctaAll = extractMatches(/<(?:a|button)[^>]*>([\s\S]*?)<\/(?:a|button)>/gi, html, 80);
|
|
2581
|
+
const ctas = [];
|
|
2582
|
+
for (const c of ctaAll) {
|
|
2583
|
+
if (CTA_WORDS.test(c) && !ctas.includes(c)) ctas.push(c);
|
|
2584
|
+
if (ctas.length >= 10) break;
|
|
2585
|
+
}
|
|
2586
|
+
const forms = (lowerHtml.match(/<form[\s>]/g) ?? []).length;
|
|
2587
|
+
const links = (lowerHtml.match(/<a[\s>]/g) ?? []).length;
|
|
2588
|
+
const images = (lowerHtml.match(/<img[\s>]/g) ?? []).length;
|
|
2589
|
+
const hasVideo = /<video[\s>]/.test(lowerHtml) || /(?:youtube\.com\/embed|player\.vimeo\.com)/.test(lowerHtml);
|
|
2590
|
+
const domNodes = (html.match(/<[a-z!\/]/gi) ?? []).length;
|
|
2591
|
+
let note = "";
|
|
2592
|
+
if (fr.ok && text.length < 400 && domNodes < 60) {
|
|
2593
|
+
note = "Sparse HTML \u2014 likely a client-rendered (SPA) page; structure may be under-detected without a JS render.";
|
|
2594
|
+
}
|
|
2595
|
+
return {
|
|
2596
|
+
url,
|
|
2597
|
+
title,
|
|
2598
|
+
description,
|
|
2599
|
+
sections: detectSections(lowerHtml, text),
|
|
2600
|
+
headings,
|
|
2601
|
+
ctas,
|
|
2602
|
+
forms,
|
|
2603
|
+
links,
|
|
2604
|
+
images,
|
|
2605
|
+
hasVideo,
|
|
2606
|
+
metrics: { htmlBytes: html.length, domNodes, fetchMs: fr.ms, status: fr.status },
|
|
2607
|
+
ok: fr.ok && html.length > 0,
|
|
2608
|
+
note: fr.ok ? note : `Could not load: ${fr.error || `HTTP ${fr.status}`}`
|
|
2609
|
+
};
|
|
2378
2610
|
}
|
|
2379
|
-
function
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2611
|
+
function ruleFor(type) {
|
|
2612
|
+
return SECTION_RULES.find((r) => r.type === type) ?? SECTION_RULES[0];
|
|
2613
|
+
}
|
|
2614
|
+
function analyzeGaps(target, competitors) {
|
|
2615
|
+
const targetTypes = new Set(target.sections.map((s) => s.type));
|
|
2616
|
+
const compPresence = /* @__PURE__ */ new Map();
|
|
2617
|
+
for (const comp of competitors) {
|
|
2618
|
+
if (!comp.ok) continue;
|
|
2619
|
+
for (const s of comp.sections) {
|
|
2620
|
+
const list = compPresence.get(s.type) ?? [];
|
|
2621
|
+
if (!list.includes(comp.url)) list.push(comp.url);
|
|
2622
|
+
compPresence.set(s.type, list);
|
|
2390
2623
|
}
|
|
2391
2624
|
}
|
|
2392
|
-
const
|
|
2393
|
-
|
|
2394
|
-
|
|
2625
|
+
const missing = [];
|
|
2626
|
+
const parity = [];
|
|
2627
|
+
for (const [type, presentOn] of compPresence) {
|
|
2628
|
+
if (targetTypes.has(type)) {
|
|
2629
|
+
parity.push(type);
|
|
2630
|
+
} else {
|
|
2631
|
+
const rule = ruleFor(type);
|
|
2632
|
+
missing.push({ section: type, label: rule.label, priority: rule.priority, presentOn, recommendation: rule.recommend });
|
|
2633
|
+
}
|
|
2395
2634
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
if (!existsSync7(walFile())) return;
|
|
2400
|
-
const pending = walPending();
|
|
2401
|
-
const body = pending.map((p) => JSON.stringify({ t: "add", id: p.id, rec: p.rec })).join("\n");
|
|
2402
|
-
writeFileSync12(walFile(), body ? `${body}
|
|
2403
|
-
` : "", "utf8");
|
|
2635
|
+
missing.sort((a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || b.presentOn.length - a.presentOn.length);
|
|
2636
|
+
const advantages = target.sections.filter((s) => !compPresence.has(s.type));
|
|
2637
|
+
return { missing, advantages, parity };
|
|
2404
2638
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2639
|
+
function generateActionItems(missing) {
|
|
2640
|
+
return missing.map((g) => ({
|
|
2641
|
+
title: `Add a ${g.label} section`,
|
|
2642
|
+
priority: g.priority,
|
|
2643
|
+
effort: PRIORITY_EFFORT[g.priority],
|
|
2644
|
+
rationale: `${g.presentOn.length} of the compared page(s) have it; you don't. ${g.recommendation}`
|
|
2645
|
+
}));
|
|
2410
2646
|
}
|
|
2411
|
-
function
|
|
2412
|
-
if (draining) return;
|
|
2413
|
-
draining = true;
|
|
2647
|
+
function hostOf(url) {
|
|
2414
2648
|
try {
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
captureTurn(e.rec);
|
|
2419
|
-
walCommit(e.id);
|
|
2420
|
-
drained++;
|
|
2421
|
-
} catch (err) {
|
|
2422
|
-
recordFault("standby-replay", err);
|
|
2423
|
-
break;
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
if (drained > 0) walCompact();
|
|
2427
|
-
} finally {
|
|
2428
|
-
draining = false;
|
|
2649
|
+
return new URL(url).host.replace(/^www\./, "");
|
|
2650
|
+
} catch {
|
|
2651
|
+
return url;
|
|
2429
2652
|
}
|
|
2430
2653
|
}
|
|
2431
|
-
function
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
recordHealth();
|
|
2447
|
-
return res;
|
|
2448
|
-
} catch (standbyErr) {
|
|
2449
|
-
recordFault("standby", standbyErr);
|
|
2450
|
-
return null;
|
|
2451
|
-
}
|
|
2452
|
-
}
|
|
2453
|
-
} catch (fatal) {
|
|
2454
|
-
recordFault("supervisor", fatal);
|
|
2455
|
-
return null;
|
|
2654
|
+
function generateSummary(target, competitors, gaps) {
|
|
2655
|
+
const okComps = competitors.filter((c) => c.ok);
|
|
2656
|
+
const tName = hostOf(target.url);
|
|
2657
|
+
if (!target.ok) return `Could not load ${tName} (${target.note}). Nothing to compare against yet.`;
|
|
2658
|
+
if (okComps.length === 0) return `Loaded ${tName} (${target.sections.length} sections) but none of the comparison URLs could be loaded.`;
|
|
2659
|
+
const crit = gaps.missing.filter((m) => m.priority === "CRITICAL").map((m) => m.label);
|
|
2660
|
+
const high = gaps.missing.filter((m) => m.priority === "HIGH").map((m) => m.label);
|
|
2661
|
+
const parts = [];
|
|
2662
|
+
parts.push(`${tName} has ${target.sections.length} detectable sections; compared against ${okComps.length} page(s).`);
|
|
2663
|
+
if (gaps.missing.length === 0) {
|
|
2664
|
+
parts.push("No structural gaps found \u2014 you cover everything they do.");
|
|
2665
|
+
} else {
|
|
2666
|
+
parts.push(`${gaps.missing.length} gap(s) found.`);
|
|
2667
|
+
if (crit.length) parts.push(`Critical: ${crit.join(", ")}.`);
|
|
2668
|
+
if (high.length) parts.push(`High: ${high.join(", ")}.`);
|
|
2456
2669
|
}
|
|
2670
|
+
if (gaps.advantages.length) parts.push(`Your edge: ${gaps.advantages.map((a) => a.label).join(", ")}.`);
|
|
2671
|
+
return parts.join(" ");
|
|
2457
2672
|
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
const dir = scribeDir();
|
|
2463
|
-
if (!existsSync8(dir)) return [];
|
|
2464
|
-
return readdirSync(dir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).map((f) => f.replace(/\.md$/, "")).sort();
|
|
2465
|
-
}
|
|
2466
|
-
function readDay(date) {
|
|
2467
|
-
const f = journalFile(date);
|
|
2468
|
-
return existsSync8(f) ? readFileSync15(f, "utf8") : "";
|
|
2673
|
+
function normalizeUrl(u) {
|
|
2674
|
+
const t = (u || "").trim();
|
|
2675
|
+
if (!t) return t;
|
|
2676
|
+
return /^https?:\/\//i.test(t) ? t : `https://${t}`;
|
|
2469
2677
|
}
|
|
2470
|
-
function
|
|
2471
|
-
const
|
|
2472
|
-
|
|
2473
|
-
const
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2678
|
+
async function comparePages(opts) {
|
|
2679
|
+
const targetUrl = normalizeUrl(opts.targetUrl);
|
|
2680
|
+
const competitorUrls = (opts.competitorUrls ?? []).map(normalizeUrl).filter((u) => u.length > 0).slice(0, 30);
|
|
2681
|
+
const [targetFetch, ...compFetches] = await Promise.all([
|
|
2682
|
+
fetchPage(targetUrl),
|
|
2683
|
+
...competitorUrls.map((u) => fetchPage(u))
|
|
2684
|
+
]);
|
|
2685
|
+
const target = extractStructure(targetUrl, targetFetch ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" });
|
|
2686
|
+
const competitors = competitorUrls.map(
|
|
2687
|
+
(u, i) => extractStructure(u, compFetches[i] ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" })
|
|
2688
|
+
);
|
|
2689
|
+
const gaps = analyzeGaps(target, competitors);
|
|
2690
|
+
return {
|
|
2691
|
+
target,
|
|
2692
|
+
competitors,
|
|
2693
|
+
missing: gaps.missing,
|
|
2694
|
+
advantages: gaps.advantages,
|
|
2695
|
+
parity: gaps.parity,
|
|
2696
|
+
actionItems: generateActionItems(gaps.missing),
|
|
2697
|
+
summary: generateSummary(target, competitors, gaps)
|
|
2698
|
+
};
|
|
2485
2699
|
}
|
|
2486
2700
|
|
|
2487
|
-
// src/
|
|
2488
|
-
function
|
|
2489
|
-
|
|
2490
|
-
const
|
|
2491
|
-
|
|
2701
|
+
// src/head/pi-tool.ts
|
|
2702
|
+
function summarizeForCoder(report) {
|
|
2703
|
+
const lines = [report.summary];
|
|
2704
|
+
const page = (p) => ` \u2022 ${p.url} \u2014 ${p.ok ? `${p.sections.length} sections: ${p.sections.map((s) => s.type).join(", ")}` : `not readable (${p.note})`}`;
|
|
2705
|
+
lines.push("Pages seen:");
|
|
2706
|
+
lines.push(page(report.target));
|
|
2707
|
+
for (const c of report.competitors) if (c.url !== report.target.url) lines.push(page(c));
|
|
2708
|
+
if (report.missing.length) {
|
|
2709
|
+
lines.push("Missing on the target (gaps to build):");
|
|
2710
|
+
for (const g of report.missing.slice(0, 12)) lines.push(` \u2022 ${g.label} (${g.priority}) \u2014 ${g.recommendation}`);
|
|
2711
|
+
}
|
|
2712
|
+
if (report.actionItems.length) {
|
|
2713
|
+
lines.push("Suggested action items:");
|
|
2714
|
+
for (const a of report.actionItems.slice(0, 12)) lines.push(` \u2192 ${a.title} [${a.priority}/${a.effort}] \u2014 ${a.rationale}`);
|
|
2715
|
+
}
|
|
2716
|
+
return lines.join("\n");
|
|
2492
2717
|
}
|
|
2493
|
-
|
|
2718
|
+
var InspectSiteParams = Type2.Object({
|
|
2719
|
+
url: Type2.String({ description: "The target website URL to inspect or rebuild from." }),
|
|
2720
|
+
competitors: Type2.Optional(
|
|
2721
|
+
Type2.Array(Type2.String(), { description: "Optional competitor/reference URLs to compare the target against." })
|
|
2722
|
+
)
|
|
2723
|
+
});
|
|
2724
|
+
function registerHead(pi) {
|
|
2494
2725
|
pi.registerTool({
|
|
2495
|
-
name: "
|
|
2496
|
-
label: "ORIRO
|
|
2497
|
-
description: "
|
|
2498
|
-
parameters:
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
const details = {};
|
|
2505
|
-
if (!isScribeEnabled()) {
|
|
2506
|
-
text = "Scribe is off (the user has not enabled it).";
|
|
2507
|
-
} else if (params.day) {
|
|
2508
|
-
text = readDay(params.day) || `No journal for ${params.day}. Days: ${listDays().join(", ") || "none"}`;
|
|
2509
|
-
details.day = params.day;
|
|
2510
|
-
} else {
|
|
2511
|
-
const hits = params.query ? searchScribe(params.query) : [];
|
|
2512
|
-
details.hits = hits;
|
|
2513
|
-
text = hits.length ? hits.map((h) => `${h.date}:${h.line} ${h.text}`).join("\n") : `No matches${params.query ? ` for "${params.query}"` : ""}. Days recorded: ${listDays().join(", ") || "none"}`;
|
|
2514
|
-
}
|
|
2515
|
-
return { content: [{ type: "text", text }], details };
|
|
2516
|
-
}
|
|
2517
|
-
});
|
|
2518
|
-
}
|
|
2519
|
-
function attachScribe(session) {
|
|
2520
|
-
let user = "";
|
|
2521
|
-
let assistant = "";
|
|
2522
|
-
const tools = /* @__PURE__ */ new Set();
|
|
2523
|
-
session.subscribe((e) => {
|
|
2524
|
-
if (!isScribeEnabled()) return;
|
|
2525
|
-
if (e?.type === "user_message" || e?.type === "session_user_message") user = String(e.text ?? e.message ?? user);
|
|
2526
|
-
if (e?.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") assistant += e.assistantMessageEvent.delta ?? "";
|
|
2527
|
-
if ((e?.type === "tool_call" || e?.type === "tool_execution_start") && e.toolName) tools.add(String(e.toolName));
|
|
2528
|
-
if (e?.type === "agent_end") {
|
|
2529
|
-
scribeTurn({ user: user || void 0, router: "oriro-free", tools: [...tools], note: assistant.slice(0, 4e3) || void 0 });
|
|
2530
|
-
user = "";
|
|
2531
|
-
assistant = "";
|
|
2532
|
-
tools.clear();
|
|
2726
|
+
name: "inspect_site",
|
|
2727
|
+
label: "ORIRO Head",
|
|
2728
|
+
description: "Go out to a live website and SEE it: its sections, CTAs, structure, and any gaps versus competitor URLs. Returns a structured report to build from. Call this whenever the user wants to look at, compare against, or rebuild a website/page.",
|
|
2729
|
+
parameters: InspectSiteParams,
|
|
2730
|
+
async execute(_toolCallId, params) {
|
|
2731
|
+
const target = params.url;
|
|
2732
|
+
const competitors = params.competitors?.length ? params.competitors : [target];
|
|
2733
|
+
const report = await comparePages({ targetUrl: target, competitorUrls: competitors });
|
|
2734
|
+
return { content: [{ type: "text", text: summarizeForCoder(report) }], details: report };
|
|
2533
2735
|
}
|
|
2534
2736
|
});
|
|
2535
2737
|
}
|
|
@@ -2824,6 +3026,28 @@ function setupNllbTranslator(opts) {
|
|
|
2824
3026
|
return instance;
|
|
2825
3027
|
}
|
|
2826
3028
|
|
|
3029
|
+
// src/language/gateway.ts
|
|
3030
|
+
var isEnglish2 = (code) => !code || code.toLowerCase().startsWith("en");
|
|
3031
|
+
var isCommand = (text) => text.trimStart().startsWith("/");
|
|
3032
|
+
async function ensureReady() {
|
|
3033
|
+
try {
|
|
3034
|
+
await setupNllbTranslator().load();
|
|
3035
|
+
} catch {
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
async function translateIncoming(message) {
|
|
3039
|
+
const lang = getTerminalLanguage().code;
|
|
3040
|
+
if (isEnglish2(lang) || !message.trim() || isCommand(message)) return message;
|
|
3041
|
+
await ensureReady();
|
|
3042
|
+
return translateForCoder(message, lang);
|
|
3043
|
+
}
|
|
3044
|
+
async function translateOutgoing(text) {
|
|
3045
|
+
const lang = getTerminalLanguage().code;
|
|
3046
|
+
if (isEnglish2(lang) || !text.trim()) return text;
|
|
3047
|
+
await ensureReady();
|
|
3048
|
+
return translateForUser(text, lang);
|
|
3049
|
+
}
|
|
3050
|
+
|
|
2827
3051
|
// src/repl.ts
|
|
2828
3052
|
function replHelp() {
|
|
2829
3053
|
return `
|
|
@@ -2838,12 +3062,26 @@ function replHelp() {
|
|
|
2838
3062
|
}
|
|
2839
3063
|
async function runRepl() {
|
|
2840
3064
|
if (isFirstRun()) await runOnboarding();
|
|
2841
|
-
else
|
|
2842
|
-
const
|
|
2843
|
-
const isEnglish2 = lang.toLowerCase().startsWith("en");
|
|
2844
|
-
if (!isEnglish2) setupNllbTranslator();
|
|
3065
|
+
else stdout5.write(banner());
|
|
3066
|
+
const isEnglish3 = getTerminalLanguage().code.toLowerCase().startsWith("en");
|
|
2845
3067
|
const { session } = await assembleOriroSession();
|
|
2846
|
-
const rl = createInterface4({ input: stdin4, output:
|
|
3068
|
+
const rl = createInterface4({ input: stdin4, output: stdout5 });
|
|
3069
|
+
let closing = false;
|
|
3070
|
+
const onSigint = () => {
|
|
3071
|
+
if (closing) return;
|
|
3072
|
+
closing = true;
|
|
3073
|
+
stdout5.write(dim("\nBye.\n"));
|
|
3074
|
+
try {
|
|
3075
|
+
rl.close();
|
|
3076
|
+
} catch {
|
|
3077
|
+
}
|
|
3078
|
+
try {
|
|
3079
|
+
session.dispose();
|
|
3080
|
+
} catch {
|
|
3081
|
+
}
|
|
3082
|
+
process.exit(0);
|
|
3083
|
+
};
|
|
3084
|
+
process.on("SIGINT", onSigint);
|
|
2847
3085
|
try {
|
|
2848
3086
|
for (; ; ) {
|
|
2849
3087
|
let line;
|
|
@@ -2853,18 +3091,20 @@ async function runRepl() {
|
|
|
2853
3091
|
break;
|
|
2854
3092
|
}
|
|
2855
3093
|
if (!line) continue;
|
|
2856
|
-
|
|
2857
|
-
if (
|
|
2858
|
-
|
|
3094
|
+
const slash = line.toLowerCase();
|
|
3095
|
+
if (slash === "/exit" || slash === "/quit") break;
|
|
3096
|
+
if (slash === "/help" || slash === "/?") {
|
|
3097
|
+
stdout5.write(replHelp());
|
|
2859
3098
|
continue;
|
|
2860
3099
|
}
|
|
2861
|
-
const english = await
|
|
3100
|
+
const english = await translateIncoming(line);
|
|
3101
|
+
noteUserInput(line);
|
|
2862
3102
|
let out = "";
|
|
2863
3103
|
const unsub = session.subscribe((e) => {
|
|
2864
3104
|
if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
|
|
2865
3105
|
const d = e.assistantMessageEvent.delta ?? "";
|
|
2866
3106
|
out += d;
|
|
2867
|
-
if (
|
|
3107
|
+
if (isEnglish3) stdout5.write(d);
|
|
2868
3108
|
}
|
|
2869
3109
|
});
|
|
2870
3110
|
try {
|
|
@@ -2872,15 +3112,18 @@ async function runRepl() {
|
|
|
2872
3112
|
} finally {
|
|
2873
3113
|
unsub();
|
|
2874
3114
|
}
|
|
2875
|
-
if (
|
|
2876
|
-
else
|
|
3115
|
+
if (isEnglish3) stdout5.write("\n\n");
|
|
3116
|
+
else stdout5.write(`${await translateOutgoing(out.trim())}
|
|
2877
3117
|
|
|
2878
3118
|
`);
|
|
2879
3119
|
}
|
|
2880
3120
|
} finally {
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3121
|
+
process.removeListener("SIGINT", onSigint);
|
|
3122
|
+
if (!closing) {
|
|
3123
|
+
rl.close();
|
|
3124
|
+
session.dispose();
|
|
3125
|
+
stdout5.write(dim("\nBye.\n"));
|
|
3126
|
+
}
|
|
2884
3127
|
}
|
|
2885
3128
|
}
|
|
2886
3129
|
|
|
@@ -3247,9 +3490,12 @@ var heading = (s) => {
|
|
|
3247
3490
|
${bold(accent(s))}
|
|
3248
3491
|
`);
|
|
3249
3492
|
};
|
|
3493
|
+
var DieError = class extends Error {
|
|
3494
|
+
};
|
|
3250
3495
|
function die(msg) {
|
|
3251
3496
|
fail(msg);
|
|
3252
|
-
process.
|
|
3497
|
+
process.exitCode = 1;
|
|
3498
|
+
throw new DieError(msg);
|
|
3253
3499
|
}
|
|
3254
3500
|
|
|
3255
3501
|
// src/commands/routers.ts
|
|
@@ -3278,11 +3524,12 @@ function registerRoutersCommand(program2) {
|
|
|
3278
3524
|
ok(`added ${accent(slug)} (${res.validation.latencyMs}ms, model ${res.validation.model}) \u2192 active pool`);
|
|
3279
3525
|
});
|
|
3280
3526
|
routers.command("use <slugs...>").description("set the active router pool (ids must be added first)").action((slugs) => {
|
|
3281
|
-
useRouters(slugs);
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3527
|
+
const { applied, unknown } = useRouters(slugs);
|
|
3528
|
+
if (!applied.length) {
|
|
3529
|
+
die(`none of those are added yet: ${unknown.join(", ")} \u2014 run \`oriro routers add <slug>\` first`);
|
|
3530
|
+
}
|
|
3531
|
+
ok(`pool set: ${applied.join(", ")}`);
|
|
3532
|
+
if (unknown.length) info(`skipped (not added yet \u2014 run \`oriro routers add\`): ${unknown.join(", ")}`);
|
|
3286
3533
|
});
|
|
3287
3534
|
}
|
|
3288
3535
|
|
|
@@ -4314,6 +4561,9 @@ function listConnectors(category) {
|
|
|
4314
4561
|
function connectorCategories() {
|
|
4315
4562
|
return [...new Set(CONNECTOR_CATALOG.map((c) => c.category))].sort();
|
|
4316
4563
|
}
|
|
4564
|
+
function isConnectorAdded(slug) {
|
|
4565
|
+
return readAdded().includes(slug);
|
|
4566
|
+
}
|
|
4317
4567
|
function addConnector(slug) {
|
|
4318
4568
|
const entry = connectorBySlug(slug);
|
|
4319
4569
|
if (!entry) return { ok: false, error: `unknown connector '${slug}' \u2014 run \`oriro connectors list\`` };
|
|
@@ -4338,23 +4588,30 @@ function registerConnectorsCommand(program2) {
|
|
|
4338
4588
|
const connectors = program2.command("connectors").description("MCP connectors \u2014 add external tools/services (inert until used)");
|
|
4339
4589
|
connectors.command("list [category]").description("list the connector catalog (optionally filtered by category)").action((category) => {
|
|
4340
4590
|
if (category && !connectorCategories().includes(category)) {
|
|
4341
|
-
|
|
4342
|
-
return;
|
|
4591
|
+
die(`unknown category '${category}' \u2014 categories: ${connectorCategories().join(", ")}`);
|
|
4343
4592
|
}
|
|
4344
4593
|
const entries = listConnectors(category);
|
|
4345
4594
|
const added = new Set(addedConnectors().map((c) => c.slug));
|
|
4346
4595
|
heading(category ? `Connectors \xB7 ${category}` : "Connectors");
|
|
4596
|
+
let addable = 0;
|
|
4347
4597
|
for (const c of entries) {
|
|
4348
|
-
const
|
|
4349
|
-
|
|
4598
|
+
const canAdd = !!c.mcpUrl;
|
|
4599
|
+
if (canAdd) addable++;
|
|
4600
|
+
const mark = !canAdd ? dim("\xB7") : added.has(c.slug) ? accent("\u25CF") : dim("\u25CB");
|
|
4601
|
+
const name = canAdd ? c.name.padEnd(22) : dim(`${c.name} (coming soon)`.padEnd(22));
|
|
4602
|
+
process.stdout.write(` ${mark} ${(canAdd ? accent : dim)(c.slug.padEnd(20))} ${name} ${dim(c.category)}
|
|
4350
4603
|
`);
|
|
4351
4604
|
}
|
|
4352
|
-
info(`${
|
|
4605
|
+
info(`${addable} addable${category ? ` in '${category}'` : ""} \xB7 ${added.size} added \xB7 ${entries.length - addable} coming soon`);
|
|
4353
4606
|
});
|
|
4354
4607
|
connectors.command("add <slug>").description("add a connector (validate + record; connects only when used)").action((slug) => {
|
|
4608
|
+
if (isConnectorAdded(slug)) {
|
|
4609
|
+
info(`${slug} is already added`);
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4355
4612
|
const res = addConnector(slug);
|
|
4356
4613
|
if (!res.ok) die(res.error ?? `could not add '${slug}'`);
|
|
4357
|
-
ok(`added ${accent(slug)} \u2014
|
|
4614
|
+
ok(`added ${accent(slug)} \u2014 recorded locally`);
|
|
4358
4615
|
});
|
|
4359
4616
|
connectors.command("remove <slug>").description("remove a connector").action((slug) => {
|
|
4360
4617
|
if (removeConnector(slug)) ok(`removed ${accent(slug)}`);
|
|
@@ -4411,6 +4668,7 @@ var OriroChannelHost = class {
|
|
|
4411
4668
|
if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") out += e.assistantMessageEvent.delta ?? "";
|
|
4412
4669
|
});
|
|
4413
4670
|
try {
|
|
4671
|
+
noteUserInput(text);
|
|
4414
4672
|
await session.prompt(text);
|
|
4415
4673
|
} finally {
|
|
4416
4674
|
unsub();
|
|
@@ -4591,7 +4849,7 @@ function registerChannelsCommand(program2) {
|
|
|
4591
4849
|
if (!isKind(kind)) die(`unknown channel '${kind}' \u2014 one of: ${KINDS.join(", ")}`);
|
|
4592
4850
|
if (kind === "whatsapp") {
|
|
4593
4851
|
if (!opts.acceptRisk) {
|
|
4594
|
-
|
|
4852
|
+
info("WhatsApp uses Baileys, which pairs a REAL WhatsApp account and may violate WhatsApp's ToS (ban risk).");
|
|
4595
4853
|
info("If you accept that risk, re-run: `oriro channels start whatsapp --accept-risk`");
|
|
4596
4854
|
return;
|
|
4597
4855
|
}
|
|
@@ -4709,7 +4967,12 @@ var version = createRequire(import.meta.url)("../package.json").version;
|
|
|
4709
4967
|
var program = new Command();
|
|
4710
4968
|
program.name("oriro").description("ORIRO \u2014 a free, on-device-friendly terminal AI agent.").version(version, "-v, --version").action(async (_options, command) => {
|
|
4711
4969
|
if (command.args.length > 0) {
|
|
4712
|
-
|
|
4970
|
+
const arg = command.args[0];
|
|
4971
|
+
if (arg === "help") {
|
|
4972
|
+
command.outputHelp();
|
|
4973
|
+
return;
|
|
4974
|
+
}
|
|
4975
|
+
process.stderr.write(`error: unknown command '${arg}'
|
|
4713
4976
|
Run 'oriro --help' to see available commands.
|
|
4714
4977
|
`);
|
|
4715
4978
|
process.exitCode = 1;
|
|
@@ -4725,6 +4988,7 @@ registerSkillsCommand(program);
|
|
|
4725
4988
|
registerLanguageCommand(program);
|
|
4726
4989
|
registerAvatarCommand(program);
|
|
4727
4990
|
program.parseAsync().catch((e) => {
|
|
4991
|
+
if (e instanceof DieError) return;
|
|
4728
4992
|
process.stderr.write(`
|
|
4729
4993
|
ORIRO error: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
4730
4994
|
`);
|