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