@oriro/orirocli 0.1.5 → 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 +2331 -1891
- package/package.json +1 -1
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 = [
|
|
@@ -1327,1819 +1327,25 @@ function hasScribeChoice() {
|
|
|
1327
1327
|
}
|
|
1328
1328
|
}
|
|
1329
1329
|
|
|
1330
|
-
// src/onboarding
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
const rl = createInterface3({ input: stdin3, output: stdout4 });
|
|
1336
|
-
try {
|
|
1337
|
-
const a = (await ask(rl, `${question} ${dim("[Y/n]")} `)).trim().toLowerCase();
|
|
1338
|
-
return a === "" || a === "y" || a === "yes";
|
|
1339
|
-
} finally {
|
|
1340
|
-
rl.close();
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
async function runOnboarding() {
|
|
1344
|
-
stdout4.write(banner());
|
|
1345
|
-
await runLanguageOnboarding();
|
|
1346
|
-
await activateGuardian();
|
|
1347
|
-
stdout4.write(` ${accent("\u{1F6E1} Guardian V3")} is on by default. ${accent("\u{1F9ED} Head")} is ready.
|
|
1348
|
-
|
|
1349
|
-
`);
|
|
1350
|
-
if (!isAvatarConfigured()) await runAvatarOnboarding();
|
|
1351
|
-
if (!hasScribeChoice()) {
|
|
1352
|
-
const yes = await askYesNo(
|
|
1353
|
-
"Remember with me? The Scriber keeps your work in context on THIS machine only \u2014 it never leaves it."
|
|
1354
|
-
);
|
|
1355
|
-
setScribeConsent(yes);
|
|
1356
|
-
stdout4.write(yes ? ` ${accent("\u{1F4D3} Scriber")} on.
|
|
1357
|
-
` : ` ${dim("Scriber off \u2014 `oriro scribe on` anytime.")}
|
|
1358
|
-
`);
|
|
1359
|
-
}
|
|
1360
|
-
stdout4.write(`
|
|
1361
|
-
${accent("ORIRO is ready.")} ${dim("Type to chat \xB7 /exit to leave")}
|
|
1362
|
-
|
|
1363
|
-
`);
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
// src/onboarding/assemble.ts
|
|
1367
|
-
import {
|
|
1368
|
-
createAgentSession as createAgentSession2,
|
|
1369
|
-
AuthStorage as AuthStorage2,
|
|
1370
|
-
ModelRegistry as ModelRegistry2,
|
|
1371
|
-
SessionManager as SessionManager2,
|
|
1372
|
-
SettingsManager,
|
|
1373
|
-
DefaultResourceLoader,
|
|
1374
|
-
getAgentDir
|
|
1375
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1376
|
-
|
|
1377
|
-
// src/routers/mux-provider.ts
|
|
1378
|
-
import { streamSimple as piStreamSimple, createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
1379
|
-
import { register as registerOpenAICompletions } from "@earendil-works/pi-ai/openai-completions";
|
|
1380
|
-
|
|
1381
|
-
// src/routers/mux.ts
|
|
1382
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
1383
|
-
import { join as join10 } from "path";
|
|
1384
|
-
var COOLDOWN_DEFAULT_MS = 6e4;
|
|
1385
|
-
var UNHEALTHY_AFTER = 3;
|
|
1386
|
-
var RouterMux = class {
|
|
1387
|
-
stats = /* @__PURE__ */ new Map();
|
|
1388
|
-
now;
|
|
1389
|
-
constructor(routerIds, now = () => Date.now()) {
|
|
1390
|
-
this.now = now;
|
|
1391
|
-
for (const id of routerIds) {
|
|
1392
|
-
this.stats.set(id, {
|
|
1393
|
-
id,
|
|
1394
|
-
latencyMs: Number.POSITIVE_INFINITY,
|
|
1395
|
-
healthy: true,
|
|
1396
|
-
cooldownUntil: 0,
|
|
1397
|
-
consecutiveErrors: 0
|
|
1398
|
-
});
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
/** Available routers, best-first (healthy, not cooling down, lowest latency). */
|
|
1402
|
-
ranked() {
|
|
1403
|
-
const t = this.now();
|
|
1404
|
-
return [...this.stats.values()].filter((s) => s.healthy && s.cooldownUntil <= t).sort((a, b) => a.latencyMs - b.latencyMs).map((s) => s.id);
|
|
1405
|
-
}
|
|
1406
|
-
recordSuccess(id, latencyMs) {
|
|
1407
|
-
const s = this.stats.get(id);
|
|
1408
|
-
if (!s) return;
|
|
1409
|
-
s.latencyMs = s.latencyMs === Number.POSITIVE_INFINITY ? latencyMs : 0.7 * s.latencyMs + 0.3 * latencyMs;
|
|
1410
|
-
s.consecutiveErrors = 0;
|
|
1411
|
-
s.healthy = true;
|
|
1412
|
-
}
|
|
1413
|
-
recordFailure(id, err) {
|
|
1414
|
-
const s = this.stats.get(id);
|
|
1415
|
-
if (!s) return;
|
|
1416
|
-
s.consecutiveErrors += 1;
|
|
1417
|
-
if (err?.status === 429) {
|
|
1418
|
-
s.cooldownUntil = this.now() + (err.retryAfterMs ?? COOLDOWN_DEFAULT_MS);
|
|
1419
|
-
}
|
|
1420
|
-
if (s.consecutiveErrors >= UNHEALTHY_AFTER) s.healthy = false;
|
|
1421
|
-
}
|
|
1422
|
-
/** Run a call through the best router, failing over on error. Throws only if all exhausted. */
|
|
1423
|
-
async run(call) {
|
|
1424
|
-
const order = this.ranked();
|
|
1425
|
-
if (order.length === 0) {
|
|
1426
|
-
throw new Error(
|
|
1427
|
-
"All selected routers are rate-limited or unavailable. Add a BYOK key, select more free routers, or retry shortly."
|
|
1428
|
-
);
|
|
1429
|
-
}
|
|
1430
|
-
let lastErr;
|
|
1431
|
-
for (const id of order) {
|
|
1432
|
-
const t0 = this.now();
|
|
1433
|
-
try {
|
|
1434
|
-
const result = await call(id);
|
|
1435
|
-
this.recordSuccess(id, this.now() - t0);
|
|
1436
|
-
return { result, routerId: id };
|
|
1437
|
-
} catch (e) {
|
|
1438
|
-
const err = e;
|
|
1439
|
-
this.recordFailure(id, { status: err?.status, retryAfterMs: err?.retryAfterMs });
|
|
1440
|
-
lastErr = e;
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
throw lastErr instanceof Error ? lastErr : new Error("All selected routers failed this request.");
|
|
1444
|
-
}
|
|
1445
|
-
snapshot() {
|
|
1446
|
-
return [...this.stats.values()].map((s) => ({ ...s }));
|
|
1447
|
-
}
|
|
1448
|
-
load(stats) {
|
|
1449
|
-
for (const s of stats) if (this.stats.has(s.id)) this.stats.set(s.id, { ...s });
|
|
1450
|
-
}
|
|
1451
|
-
};
|
|
1452
|
-
function healthStatePath(dir) {
|
|
1453
|
-
return join10(dir, "routers", "health.json");
|
|
1454
|
-
}
|
|
1455
|
-
function saveMuxState(dir, stats) {
|
|
1456
|
-
const p = healthStatePath(dir);
|
|
1457
|
-
mkdirSync5(join10(dir, "routers"), { recursive: true });
|
|
1458
|
-
writeFileSync7(p, JSON.stringify(stats, null, 2), "utf8");
|
|
1459
|
-
}
|
|
1460
|
-
function loadMuxState(dir) {
|
|
1461
|
-
const p = healthStatePath(dir);
|
|
1462
|
-
if (!existsSync3(p)) return [];
|
|
1463
|
-
try {
|
|
1464
|
-
const stats = JSON.parse(readFileSync8(p, "utf8"));
|
|
1465
|
-
return stats.map((s) => ({ ...s, latencyMs: Number.isFinite(s.latencyMs) ? s.latencyMs : Number.POSITIVE_INFINITY }));
|
|
1466
|
-
} catch {
|
|
1467
|
-
return [];
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
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";
|
|
1470
1335
|
|
|
1471
|
-
// src/routers/
|
|
1472
|
-
var
|
|
1473
|
-
|
|
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({
|
|
1474
1347
|
id: "pollinations",
|
|
1475
|
-
|
|
1476
|
-
baseUrl: "https://text.pollinations.ai/openai",
|
|
1477
|
-
model: "openai",
|
|
1478
|
-
apiKey: "oriro-keyless"
|
|
1479
|
-
},
|
|
1480
|
-
{
|
|
1481
|
-
id: "ollama-local",
|
|
1482
|
-
name: "Ollama (on-device)",
|
|
1483
|
-
baseUrl: "http://localhost:11434/v1",
|
|
1484
|
-
model: "llama3.2",
|
|
1485
|
-
apiKey: "ollama"
|
|
1486
|
-
}
|
|
1487
|
-
];
|
|
1488
|
-
function routerModel(r) {
|
|
1489
|
-
return {
|
|
1490
|
-
id: r.model,
|
|
1491
|
-
name: r.name,
|
|
1492
|
-
api: "openai-completions",
|
|
1493
|
-
provider: r.id,
|
|
1494
|
-
baseUrl: r.baseUrl,
|
|
1495
|
-
reasoning: false,
|
|
1496
|
-
input: ["text"],
|
|
1497
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1498
|
-
contextWindow: 128e3,
|
|
1499
|
-
maxTokens: 4096
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
// src/routers/router-pool.ts
|
|
1504
|
-
import { mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
|
|
1505
|
-
import { join as join12 } from "path";
|
|
1506
|
-
|
|
1507
|
-
// src/routers/pool.ts
|
|
1508
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
1509
|
-
import { join as join11 } from "path";
|
|
1510
|
-
function poolFile(dir) {
|
|
1511
|
-
return join11(dir, "routers", "selected.json");
|
|
1512
|
-
}
|
|
1513
|
-
function loadPool(dir) {
|
|
1514
|
-
const p = poolFile(dir);
|
|
1515
|
-
if (!existsSync4(p)) return [];
|
|
1516
|
-
try {
|
|
1517
|
-
const v = JSON.parse(readFileSync9(p, "utf8"));
|
|
1518
|
-
return Array.isArray(v) ? v : [];
|
|
1519
|
-
} catch {
|
|
1520
|
-
return [];
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
function savePool(dir, ids) {
|
|
1524
|
-
mkdirSync6(join11(dir, "routers"), { recursive: true });
|
|
1525
|
-
writeFileSync8(poolFile(dir), JSON.stringify([...new Set(ids)], null, 2), "utf8");
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
// src/routers/validate.ts
|
|
1529
|
-
var PROBE_TIMEOUT_MS = 12e3;
|
|
1530
|
-
async function validateRouter(entry, key, modelId) {
|
|
1531
|
-
const model = modelId ?? entry.freeModels[0] ?? "";
|
|
1532
|
-
const t0 = Date.now();
|
|
1533
|
-
const controller = new AbortController();
|
|
1534
|
-
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
1535
|
-
try {
|
|
1536
|
-
let res;
|
|
1537
|
-
if (entry.api === "google-generative-ai") {
|
|
1538
|
-
const url = `${entry.baseUrl.replace(/\/$/, "")}/models/${model}:generateContent${key ? `?key=${encodeURIComponent(key)}` : ""}`;
|
|
1539
|
-
res = await fetch(url, {
|
|
1540
|
-
method: "POST",
|
|
1541
|
-
headers: { "content-type": "application/json" },
|
|
1542
|
-
body: JSON.stringify({ contents: [{ parts: [{ text: "ping" }] }] }),
|
|
1543
|
-
signal: controller.signal
|
|
1544
|
-
});
|
|
1545
|
-
} else {
|
|
1546
|
-
const headers = { "content-type": "application/json" };
|
|
1547
|
-
if (key) headers.authorization = `Bearer ${key}`;
|
|
1548
|
-
res = await fetch(`${entry.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
1549
|
-
method: "POST",
|
|
1550
|
-
headers,
|
|
1551
|
-
body: JSON.stringify({
|
|
1552
|
-
model,
|
|
1553
|
-
messages: [{ role: "user", content: "ping" }],
|
|
1554
|
-
max_tokens: 1
|
|
1555
|
-
}),
|
|
1556
|
-
signal: controller.signal
|
|
1557
|
-
});
|
|
1558
|
-
}
|
|
1559
|
-
return {
|
|
1560
|
-
ok: res.ok,
|
|
1561
|
-
latencyMs: Date.now() - t0,
|
|
1562
|
-
model,
|
|
1563
|
-
error: res.ok ? void 0 : `HTTP ${res.status}`
|
|
1564
|
-
};
|
|
1565
|
-
} catch (e) {
|
|
1566
|
-
return {
|
|
1567
|
-
ok: false,
|
|
1568
|
-
latencyMs: Date.now() - t0,
|
|
1569
|
-
model,
|
|
1570
|
-
error: e instanceof Error ? e.message : String(e)
|
|
1571
|
-
};
|
|
1572
|
-
} finally {
|
|
1573
|
-
clearTimeout(timer);
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
// src/routers/router-pool.ts
|
|
1578
|
-
var KEYLESS_SENTINEL = "oriro-keyless-no-key-required";
|
|
1579
|
-
function regFile() {
|
|
1580
|
-
return join12(oriroDir(), "routers", "registered.json");
|
|
1581
|
-
}
|
|
1582
|
-
function readReg() {
|
|
1583
|
-
try {
|
|
1584
|
-
return JSON.parse(readFileSync10(regFile(), "utf8"));
|
|
1585
|
-
} catch {
|
|
1586
|
-
return {};
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
function writeReg(m) {
|
|
1590
|
-
mkdirSync7(join12(oriroDir(), "routers"), { recursive: true });
|
|
1591
|
-
writeFileSync9(regFile(), JSON.stringify(m, null, 2), "utf8");
|
|
1592
|
-
}
|
|
1593
|
-
async function addRouter(entry, opts) {
|
|
1594
|
-
if (entry.comingSoon) {
|
|
1595
|
-
return { ok: false, validation: { ok: false, latencyMs: 0, model: "", error: "coming soon" } };
|
|
1596
|
-
}
|
|
1597
|
-
if (entry.kind && entry.kind !== "chat") {
|
|
1598
|
-
return { ok: false, validation: { ok: false, latencyMs: 0, model: "", error: `'${entry.id}' is a ${entry.kind} router, not a chat router` } };
|
|
1599
|
-
}
|
|
1600
|
-
const key = opts?.key ?? (entry.keyless ? KEYLESS_SENTINEL : void 0);
|
|
1601
|
-
const v = await validateRouter(entry, key, opts?.modelId);
|
|
1602
|
-
if (!v.ok) return { ok: false, validation: v };
|
|
1603
|
-
const router = {
|
|
1604
|
-
id: entry.id,
|
|
1605
|
-
name: entry.displayName,
|
|
1606
|
-
baseUrl: entry.baseUrl,
|
|
1607
|
-
model: opts?.modelId ?? v.model ?? entry.freeModels[0] ?? "",
|
|
1608
|
-
apiKey: key ?? KEYLESS_SENTINEL
|
|
1609
|
-
};
|
|
1610
|
-
const reg = readReg();
|
|
1611
|
-
reg[entry.id] = router;
|
|
1612
|
-
writeReg(reg);
|
|
1613
|
-
savePool(oriroDir(), [...loadPool(oriroDir()), entry.id]);
|
|
1614
|
-
return { ok: true, validation: v };
|
|
1615
|
-
}
|
|
1616
|
-
function useRouters(ids) {
|
|
1617
|
-
const reg = readReg();
|
|
1618
|
-
const applied = ids.filter((id) => reg[id]);
|
|
1619
|
-
const unknown = ids.filter((id) => !reg[id]);
|
|
1620
|
-
if (applied.length > 0) savePool(oriroDir(), applied);
|
|
1621
|
-
return { applied, unknown };
|
|
1622
|
-
}
|
|
1623
|
-
function resolvePool() {
|
|
1624
|
-
const reg = readReg();
|
|
1625
|
-
return loadPool(oriroDir()).map((id) => reg[id]).filter((r) => Boolean(r));
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
// src/identity/filter.ts
|
|
1629
|
-
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.";
|
|
1630
|
-
var BANNED_TEST = /\b(qwen|llama|mistral|mixtral|deepseek|gpt(?:-?\d(?:\.\d)?)?|claude|gemini|openai|anthropic|google|meta\s?ai|alibaba)\b/i;
|
|
1631
|
-
var BANNED_REPLACE = new RegExp(BANNED_TEST.source, "gi");
|
|
1632
|
-
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;
|
|
1633
|
-
var SELF_INTRO = /\b(i am|i'm)\s+(a|an)\b/i;
|
|
1634
|
-
var AI_NOUN = /\b(assistant|ai|model|language model|bot|agent|chatbot)\b/i;
|
|
1635
|
-
function applyIdentity(context) {
|
|
1636
|
-
const sys = context.systemPrompt ? `${ORIRO_IDENTITY}
|
|
1637
|
-
|
|
1638
|
-
${context.systemPrompt}` : ORIRO_IDENTITY;
|
|
1639
|
-
return { ...context, systemPrompt: sys };
|
|
1640
|
-
}
|
|
1641
|
-
function scrubIdentity(text) {
|
|
1642
|
-
return text.replace(/[^.?!\n]+[.?!]?/g, (sentence) => {
|
|
1643
|
-
let s = SELF_REF.test(sentence) && BANNED_TEST.test(sentence) ? sentence.replace(BANNED_REPLACE, "ORIRO") : sentence;
|
|
1644
|
-
if (!/\boriro\b/i.test(s) && SELF_INTRO.test(s) && AI_NOUN.test(s)) {
|
|
1645
|
-
s = s.replace(SELF_INTRO, "I am ORIRO, $2");
|
|
1646
|
-
}
|
|
1647
|
-
return s;
|
|
1648
|
-
});
|
|
1649
|
-
}
|
|
1650
|
-
function scrubMessageIdentity(msg) {
|
|
1651
|
-
return {
|
|
1652
|
-
...msg,
|
|
1653
|
-
content: msg.content.map(
|
|
1654
|
-
(c) => c.type === "text" ? { ...c, text: scrubIdentity(c.text) } : c
|
|
1655
|
-
)
|
|
1656
|
-
};
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
// src/routers/tool-sanitize.ts
|
|
1660
|
-
var CONTROL_TOKEN = /<\|[^|]*\|>/g;
|
|
1661
|
-
var RECIPIENT_PREFIX = /^(?:to=)?(?:functions?|tools?|recipient)[.=]/i;
|
|
1662
|
-
var RECIPIENT = /(?:to=)?(?:functions?|tools?|recipient)[.=]([A-Za-z0-9_.:-]+)/i;
|
|
1663
|
-
var CLEAN_NAME = /^[A-Za-z0-9_.:-]+$/;
|
|
1664
|
-
function sanitizeToolName(raw) {
|
|
1665
|
-
if (!raw) return raw;
|
|
1666
|
-
if (!raw.includes("<|") && !RECIPIENT_PREFIX.test(raw)) return raw;
|
|
1667
|
-
const base = (raw.split("<|")[0] ?? "").replace(RECIPIENT_PREFIX, "").trim();
|
|
1668
|
-
if (base && CLEAN_NAME.test(base)) return base;
|
|
1669
|
-
const recip = raw.match(RECIPIENT);
|
|
1670
|
-
if (recip?.[1]) return recip[1];
|
|
1671
|
-
const m = raw.replace(CONTROL_TOKEN, " ").match(/[A-Za-z_][A-Za-z0-9_.:-]*/);
|
|
1672
|
-
return m ? m[0] : raw;
|
|
1673
|
-
}
|
|
1674
|
-
function sanitizeMessageToolCalls(msg) {
|
|
1675
|
-
let changed = false;
|
|
1676
|
-
const content = msg.content.map((c) => {
|
|
1677
|
-
if (c.type === "toolCall") {
|
|
1678
|
-
const name = sanitizeToolName(c.name);
|
|
1679
|
-
if (name !== c.name) {
|
|
1680
|
-
changed = true;
|
|
1681
|
-
return { ...c, name };
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
return c;
|
|
1685
|
-
});
|
|
1686
|
-
return changed ? { ...msg, content } : msg;
|
|
1687
|
-
}
|
|
1688
|
-
function sanitizeEventToolCalls(ev) {
|
|
1689
|
-
let next = ev;
|
|
1690
|
-
if ("partial" in next && next.partial) {
|
|
1691
|
-
const partial = sanitizeMessageToolCalls(next.partial);
|
|
1692
|
-
if (partial !== next.partial) next = { ...next, partial };
|
|
1693
|
-
}
|
|
1694
|
-
if (next.type === "toolcall_end" && next.toolCall) {
|
|
1695
|
-
const name = sanitizeToolName(next.toolCall.name);
|
|
1696
|
-
if (name !== next.toolCall.name) next = { ...next, toolCall: { ...next.toolCall, name } };
|
|
1697
|
-
}
|
|
1698
|
-
return next;
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
// src/scribe/scribe-pi.ts
|
|
1702
|
-
import { existsSync as existsSync9, readFileSync as readFileSync16 } from "fs";
|
|
1703
|
-
import { Type } from "typebox";
|
|
1704
|
-
|
|
1705
|
-
// src/scribe/capture.ts
|
|
1706
|
-
import { closeSync as closeSync2, fsyncSync as fsyncSync2, mkdirSync as mkdirSync10, openSync as openSync2, writeSync as writeSync2 } from "fs";
|
|
1707
|
-
import { join as join14 } from "path";
|
|
1708
|
-
|
|
1709
|
-
// src/scribe/digest.ts
|
|
1710
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
|
|
1711
|
-
|
|
1712
|
-
// src/scribe/paths.ts
|
|
1713
|
-
import { join as join13 } from "path";
|
|
1714
|
-
function scribeDir() {
|
|
1715
|
-
const override = process.env.ORIRO_SCRIBE_DIR?.trim();
|
|
1716
|
-
return override && override.length > 0 ? override : join13(CONFIG_DIR, "scribe");
|
|
1717
|
-
}
|
|
1718
|
-
function journalFile(date) {
|
|
1719
|
-
return join13(scribeDir(), `${date}.md`);
|
|
1720
|
-
}
|
|
1721
|
-
function digestFile() {
|
|
1722
|
-
return join13(scribeDir(), "_digest.md");
|
|
1723
|
-
}
|
|
1724
|
-
function timelineFile() {
|
|
1725
|
-
return join13(scribeDir(), "_timeline.md");
|
|
1726
|
-
}
|
|
1727
|
-
function artifactsDir() {
|
|
1728
|
-
return join13(scribeDir(), "artifacts");
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
// src/scribe/digest.ts
|
|
1732
|
-
var DIGEST_CAP = 8192;
|
|
1733
|
-
var TIMELINE_DAY_CAP = 400;
|
|
1734
|
-
function read(file4) {
|
|
1735
|
-
return existsSync5(file4) ? readFileSync11(file4, "utf8") : "";
|
|
1736
|
-
}
|
|
1737
|
-
function updateDigest(summary, context) {
|
|
1738
|
-
mkdirSync8(scribeDir(), { recursive: true });
|
|
1739
|
-
const existing = read(digestFile());
|
|
1740
|
-
let contextBlock = context?.trim();
|
|
1741
|
-
if (!contextBlock) {
|
|
1742
|
-
const m = existing.match(/## Context\n([\s\S]*?)\n## /);
|
|
1743
|
-
contextBlock = m?.[1]?.trim() ?? "_(not set yet)_";
|
|
1744
|
-
}
|
|
1745
|
-
const recentMatch = existing.match(/## Recent activity[^\n]*\n([\s\S]*)$/);
|
|
1746
|
-
const priorRecent = recentMatch?.[1]?.trim() ?? "";
|
|
1747
|
-
let recent = summary.trim() ? `- ${summary.trim()}
|
|
1748
|
-
${priorRecent}` : priorRecent;
|
|
1749
|
-
const header2 = `# ORIRO Scribe \u2014 Digest
|
|
1750
|
-
|
|
1751
|
-
## Context
|
|
1752
|
-
${contextBlock}
|
|
1753
|
-
|
|
1754
|
-
## Recent activity (newest first)
|
|
1755
|
-
`;
|
|
1756
|
-
let out = header2 + recent;
|
|
1757
|
-
while (Buffer.byteLength(out, "utf8") > DIGEST_CAP && recent.includes("\n")) {
|
|
1758
|
-
recent = recent.slice(0, recent.lastIndexOf("\n")).trimEnd();
|
|
1759
|
-
out = header2 + recent;
|
|
1760
|
-
}
|
|
1761
|
-
writeFileSync10(digestFile(), out, "utf8");
|
|
1762
|
-
}
|
|
1763
|
-
function updateTimeline(date, topic) {
|
|
1764
|
-
mkdirSync8(scribeDir(), { recursive: true });
|
|
1765
|
-
const clean = topic.replace(/\s+/g, " ").trim();
|
|
1766
|
-
if (!clean) return;
|
|
1767
|
-
const lines = read(timelineFile()).split("\n").filter(Boolean);
|
|
1768
|
-
const header2 = "# ORIRO Scribe \u2014 Timeline";
|
|
1769
|
-
const body = lines.filter((l) => l !== header2);
|
|
1770
|
-
const idx = body.findIndex((l) => l.startsWith(`- ${date} \xB7`));
|
|
1771
|
-
if (idx === -1) {
|
|
1772
|
-
body.push(`- ${date} \xB7 ${clean}`.slice(0, TIMELINE_DAY_CAP + date.length + 6));
|
|
1773
|
-
} else {
|
|
1774
|
-
let merged = `${body[idx]}; ${clean}`;
|
|
1775
|
-
if (merged.length > TIMELINE_DAY_CAP) merged = `${merged.slice(0, TIMELINE_DAY_CAP)}\u2026`;
|
|
1776
|
-
body[idx] = merged;
|
|
1777
|
-
}
|
|
1778
|
-
body.sort();
|
|
1779
|
-
writeFileSync10(timelineFile(), `${header2}
|
|
1780
|
-
${body.join("\n")}
|
|
1781
|
-
`, "utf8");
|
|
1782
|
-
}
|
|
1783
|
-
function readDigest() {
|
|
1784
|
-
return read(digestFile());
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
// src/scribe/journal.ts
|
|
1788
|
-
import {
|
|
1789
|
-
closeSync,
|
|
1790
|
-
existsSync as existsSync6,
|
|
1791
|
-
fsyncSync,
|
|
1792
|
-
mkdirSync as mkdirSync9,
|
|
1793
|
-
openSync,
|
|
1794
|
-
readFileSync as readFileSync12,
|
|
1795
|
-
writeSync
|
|
1796
|
-
} from "fs";
|
|
1797
|
-
function appendJournal(date, content) {
|
|
1798
|
-
mkdirSync9(scribeDir(), { recursive: true });
|
|
1799
|
-
const fd = openSync(journalFile(date), "a");
|
|
1800
|
-
try {
|
|
1801
|
-
writeSync(fd, content.endsWith("\n") ? content : `${content}
|
|
1802
|
-
`);
|
|
1803
|
-
fsyncSync(fd);
|
|
1804
|
-
} finally {
|
|
1805
|
-
closeSync(fd);
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
function readJournal(date) {
|
|
1809
|
-
const f = journalFile(date);
|
|
1810
|
-
return existsSync6(f) ? readFileSync12(f, "utf8") : "";
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
// src/scribe/redact.ts
|
|
1814
|
-
var RULES = [
|
|
1815
|
-
{
|
|
1816
|
-
label: "private-key",
|
|
1817
|
-
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
|
|
1818
|
-
},
|
|
1819
|
-
// Lone PEM markers — a key SPLIT across fields/turns leaves only a BEGIN-head or an END-tail in
|
|
1820
|
-
// one field. A field carrying either marker is key material: redact the marker + its adjacent body
|
|
1821
|
-
// (forward from BEGIN, backward to END) so no sub-threshold fragment can ever sit on disk.
|
|
1822
|
-
{ label: "private-key", re: /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*/g },
|
|
1823
|
-
{ label: "private-key", re: /[\s\S]*-----END[A-Z ]*PRIVATE KEY-----/g },
|
|
1824
|
-
{ label: "anthropic-key", re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
|
|
1825
|
-
{ label: "openrouter-key", re: /sk-or-v1-[A-Za-z0-9]{20,}/g },
|
|
1826
|
-
// Stripe-style keys (sk_live_/pk_live_/rk_test_/…), underscore segments.
|
|
1827
|
-
{ label: "stripe-key", re: /\b[srp]k_(?:live|test)_[A-Za-z0-9]{16,}/g },
|
|
1828
|
-
// Generic sk- secret keys — allow hyphenated segments (sk-live-…, sk-proj-…) so a second
|
|
1829
|
-
// hyphen no longer breaks the match (the gap the Scriber spike caught).
|
|
1830
|
-
{ label: "secret-key-sk", re: /sk[-_][A-Za-z0-9][A-Za-z0-9-]{14,}/g },
|
|
1831
|
-
{ label: "google-key", re: /AIza[0-9A-Za-z_-]{30,}/g },
|
|
1832
|
-
{ label: "groq-key", re: /gsk_[A-Za-z0-9]{20,}/g },
|
|
1833
|
-
{ label: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
1834
|
-
{ label: "github-token", re: /gh[posr]_[A-Za-z0-9]{30,}/g },
|
|
1835
|
-
{ label: "xai-key", re: /xai-[A-Za-z0-9]{20,}/g },
|
|
1836
|
-
{ label: "aws-key", re: /AKIA[0-9A-Z]{16}/g },
|
|
1837
|
-
{ label: "jwt", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g },
|
|
1838
|
-
{ label: "telegram-token", re: /\b\d{8,10}:[A-Za-z0-9_-]{30,}\b/g },
|
|
1839
|
-
// Auth headers / inline credentials (any provider) — the audit found these leaked.
|
|
1840
|
-
{ label: "bearer-token", re: /\bbearer\s+[A-Za-z0-9._~+/=-]{12,}/gi },
|
|
1841
|
-
{ label: "basic-auth", re: /\bbasic\s+[A-Za-z0-9+/=]{12,}/gi },
|
|
1842
|
-
// key: value / key=value secrets (password, token, secret, api_key, access_key, …).
|
|
1843
|
-
{ label: "secret-kv", re: /\b(?:pass(?:word|wd)?|pwd|secret|token|api[_-]?key|access[_-]?key|auth)\s*[:=]\s*\S{3,}/gi },
|
|
1844
|
-
// Credentials embedded in a URL: scheme://user:PASSWORD@host → redact the password.
|
|
1845
|
-
{ label: "url-credential", re: /\b([a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:)[^/\s@]+(@)/gi },
|
|
1846
|
-
{ label: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g },
|
|
1847
|
-
{ label: "phone", re: /(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}/g }
|
|
1848
|
-
];
|
|
1849
|
-
function marker(label) {
|
|
1850
|
-
return `\u27E8REDACTED:${label}\u27E9`;
|
|
1851
|
-
}
|
|
1852
|
-
function entropy(s) {
|
|
1853
|
-
const freq = /* @__PURE__ */ new Map();
|
|
1854
|
-
for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
1855
|
-
let h = 0;
|
|
1856
|
-
for (const n of freq.values()) {
|
|
1857
|
-
const p = n / s.length;
|
|
1858
|
-
h -= p * Math.log2(p);
|
|
1859
|
-
}
|
|
1860
|
-
return h;
|
|
1861
|
-
}
|
|
1862
|
-
function looksLikeUnknownSecret(token) {
|
|
1863
|
-
if (token.length < 32) return false;
|
|
1864
|
-
if (token.includes("\u27E8REDACTED:")) return false;
|
|
1865
|
-
if (/^[0-9a-f]+$/i.test(token)) return false;
|
|
1866
|
-
const classes = (/[a-z]/.test(token) ? 1 : 0) + (/[A-Z]/.test(token) ? 1 : 0) + (/[0-9]/.test(token) ? 1 : 0);
|
|
1867
|
-
if (classes < 2) return false;
|
|
1868
|
-
return entropy(token) >= 4.2;
|
|
1869
|
-
}
|
|
1870
|
-
function redact(input) {
|
|
1871
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1872
|
-
let text = input;
|
|
1873
|
-
for (const rule of RULES) {
|
|
1874
|
-
text = text.replace(rule.re, () => {
|
|
1875
|
-
counts.set(rule.label, (counts.get(rule.label) ?? 0) + 1);
|
|
1876
|
-
return marker(rule.label);
|
|
1877
|
-
});
|
|
1878
|
-
}
|
|
1879
|
-
text = text.split(/(\s+)/).map((tok) => {
|
|
1880
|
-
if (looksLikeUnknownSecret(tok)) {
|
|
1881
|
-
counts.set("high-entropy", (counts.get("high-entropy") ?? 0) + 1);
|
|
1882
|
-
return marker("high-entropy");
|
|
1883
|
-
}
|
|
1884
|
-
return tok;
|
|
1885
|
-
}).join("");
|
|
1886
|
-
const redactions = [...counts.entries()].map(([label, count]) => ({
|
|
1887
|
-
label,
|
|
1888
|
-
count
|
|
1889
|
-
}));
|
|
1890
|
-
return { text, redactions };
|
|
1891
|
-
}
|
|
1892
|
-
function containsSecret(text) {
|
|
1893
|
-
for (const rule of RULES) {
|
|
1894
|
-
rule.re.lastIndex = 0;
|
|
1895
|
-
if (rule.re.test(text)) return true;
|
|
1896
|
-
}
|
|
1897
|
-
for (const tok of text.split(/\s+/)) {
|
|
1898
|
-
if (looksLikeUnknownSecret(tok)) return true;
|
|
1899
|
-
}
|
|
1900
|
-
return false;
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
// src/scribe/capture.ts
|
|
1904
|
-
var INLINE_CAP = 4e3;
|
|
1905
|
-
function sideFile(date, ts, kind, full) {
|
|
1906
|
-
mkdirSync10(artifactsDir(), { recursive: true });
|
|
1907
|
-
const name = `${date}_${ts.replace(/[:.]/g, "-")}_${kind}.md`;
|
|
1908
|
-
const p = join14(artifactsDir(), name);
|
|
1909
|
-
const fd = openSync2(p, "w");
|
|
1910
|
-
try {
|
|
1911
|
-
writeSync2(fd, full);
|
|
1912
|
-
fsyncSync2(fd);
|
|
1913
|
-
} finally {
|
|
1914
|
-
closeSync2(fd);
|
|
1915
|
-
}
|
|
1916
|
-
return p;
|
|
1917
|
-
}
|
|
1918
|
-
function field(date, ts, label, value) {
|
|
1919
|
-
if (!value || !value.trim()) return "";
|
|
1920
|
-
if (value.length > INLINE_CAP) {
|
|
1921
|
-
const ref = sideFile(date, ts, label.toLowerCase().replace(/\s+/g, "-"), value);
|
|
1922
|
-
return `**${label}** (full \u2192 ${ref}):
|
|
1923
|
-
${value.slice(0, INLINE_CAP)}
|
|
1924
|
-
\u2026(truncated; full content in artifact)
|
|
1925
|
-
|
|
1926
|
-
`;
|
|
1927
|
-
}
|
|
1928
|
-
return `**${label}:**
|
|
1929
|
-
${value}
|
|
1930
|
-
|
|
1931
|
-
`;
|
|
1932
|
-
}
|
|
1933
|
-
function renderTurn(rec) {
|
|
1934
|
-
let md = `## ${rec.ts}
|
|
1935
|
-
|
|
1936
|
-
`;
|
|
1937
|
-
md += field(rec.date, rec.ts, "User", rec.user);
|
|
1938
|
-
md += field(rec.date, rec.ts, "Router", rec.router);
|
|
1939
|
-
if (rec.tools?.length) md += `**Tools:** ${rec.tools.join(", ")}
|
|
1940
|
-
|
|
1941
|
-
`;
|
|
1942
|
-
if (rec.files?.length) md += `**Files:** ${rec.files.join(", ")}
|
|
1943
|
-
|
|
1944
|
-
`;
|
|
1945
|
-
md += field(rec.date, rec.ts, "Note", rec.note);
|
|
1946
|
-
return `${md}---
|
|
1947
|
-
`;
|
|
1948
|
-
}
|
|
1949
|
-
function oneLineSummary(rec) {
|
|
1950
|
-
const bits = [];
|
|
1951
|
-
if (rec.user) bits.push(rec.user.replace(/\s+/g, " ").slice(0, 80));
|
|
1952
|
-
if (rec.files?.length) bits.push(`files: ${rec.files.slice(0, 3).join(", ")}`);
|
|
1953
|
-
if (rec.note) bits.push(rec.note.replace(/\s+/g, " ").slice(0, 60));
|
|
1954
|
-
return bits.join(" \xB7 ") || "(activity)";
|
|
1955
|
-
}
|
|
1956
|
-
function redactRecord(rec) {
|
|
1957
|
-
const tally = /* @__PURE__ */ new Map();
|
|
1958
|
-
const rd = (s) => {
|
|
1959
|
-
if (!s) return s;
|
|
1960
|
-
const r = redact(s);
|
|
1961
|
-
for (const x of r.redactions) tally.set(x.label, (tally.get(x.label) ?? 0) + x.count);
|
|
1962
|
-
return r.text;
|
|
1963
|
-
};
|
|
1964
|
-
const safeRec = {
|
|
1965
|
-
...rec,
|
|
1966
|
-
user: rd(rec.user),
|
|
1967
|
-
note: rd(rec.note),
|
|
1968
|
-
router: rd(rec.router),
|
|
1969
|
-
context: rd(rec.context),
|
|
1970
|
-
files: rec.files?.map((f) => rd(f) ?? f)
|
|
1971
|
-
};
|
|
1972
|
-
return { rec: safeRec, redactions: [...tally.entries()].map(([label, count]) => ({ label, count })) };
|
|
1973
|
-
}
|
|
1974
|
-
function captureTurn(rec) {
|
|
1975
|
-
const { rec: safeRec, redactions } = redactRecord(rec);
|
|
1976
|
-
const journal = renderTurn(safeRec);
|
|
1977
|
-
appendJournal(rec.date, `${journal}
|
|
1978
|
-
`);
|
|
1979
|
-
updateDigest(`${safeRec.ts} \xB7 ${oneLineSummary(safeRec)}`, safeRec.context);
|
|
1980
|
-
updateTimeline(safeRec.date, oneLineSummary(safeRec));
|
|
1981
|
-
const auditClean = !containsSecret(readJournal(rec.date)) && !containsSecret(readDigest() ?? "");
|
|
1982
|
-
return {
|
|
1983
|
-
journalDate: rec.date,
|
|
1984
|
-
redactions,
|
|
1985
|
-
bytes: Buffer.byteLength(journal, "utf8"),
|
|
1986
|
-
auditClean
|
|
1987
|
-
};
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
// src/scribe/health.ts
|
|
1991
|
-
import {
|
|
1992
|
-
closeSync as closeSync3,
|
|
1993
|
-
fsyncSync as fsyncSync3,
|
|
1994
|
-
mkdirSync as mkdirSync11,
|
|
1995
|
-
openSync as openSync3,
|
|
1996
|
-
readFileSync as readFileSync13,
|
|
1997
|
-
writeFileSync as writeFileSync11,
|
|
1998
|
-
writeSync as writeSync3
|
|
1999
|
-
} from "fs";
|
|
2000
|
-
import { join as join15 } from "path";
|
|
2001
|
-
function healthFile() {
|
|
2002
|
-
return join15(scribeDir(), "_health.json");
|
|
2003
|
-
}
|
|
2004
|
-
function faultLogFile() {
|
|
2005
|
-
return join15(scribeDir(), "_faults.log");
|
|
2006
|
-
}
|
|
2007
|
-
function read2() {
|
|
2008
|
-
try {
|
|
2009
|
-
return JSON.parse(readFileSync13(healthFile(), "utf8"));
|
|
2010
|
-
} catch {
|
|
2011
|
-
return { faultCount: 0 };
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
function write(h) {
|
|
2015
|
-
mkdirSync11(scribeDir(), { recursive: true });
|
|
2016
|
-
writeFileSync11(healthFile(), `${JSON.stringify(h, null, 2)}
|
|
2017
|
-
`, "utf8");
|
|
2018
|
-
}
|
|
2019
|
-
function recordHealth() {
|
|
2020
|
-
const h = read2();
|
|
2021
|
-
h.lastWriteAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2022
|
-
write(h);
|
|
2023
|
-
}
|
|
2024
|
-
function recordFault(role, err) {
|
|
2025
|
-
try {
|
|
2026
|
-
mkdirSync11(scribeDir(), { recursive: true });
|
|
2027
|
-
const msg = `${(/* @__PURE__ */ new Date()).toISOString()} [${role}] ${err instanceof Error ? err.message : String(err)}`;
|
|
2028
|
-
const fd = openSync3(faultLogFile(), "a");
|
|
2029
|
-
try {
|
|
2030
|
-
writeSync3(fd, `${msg}
|
|
2031
|
-
`);
|
|
2032
|
-
fsyncSync3(fd);
|
|
2033
|
-
} finally {
|
|
2034
|
-
closeSync3(fd);
|
|
2035
|
-
}
|
|
2036
|
-
const h = read2();
|
|
2037
|
-
h.faultCount = (h.faultCount ?? 0) + 1;
|
|
2038
|
-
h.lastFault = msg;
|
|
2039
|
-
write(h);
|
|
2040
|
-
} catch {
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
// src/scribe/wal.ts
|
|
2045
|
-
import {
|
|
2046
|
-
closeSync as closeSync4,
|
|
2047
|
-
existsSync as existsSync7,
|
|
2048
|
-
fsyncSync as fsyncSync4,
|
|
2049
|
-
mkdirSync as mkdirSync12,
|
|
2050
|
-
openSync as openSync4,
|
|
2051
|
-
readFileSync as readFileSync14,
|
|
2052
|
-
writeFileSync as writeFileSync12,
|
|
2053
|
-
writeSync as writeSync4
|
|
2054
|
-
} from "fs";
|
|
2055
|
-
import { join as join16 } from "path";
|
|
2056
|
-
function walFile() {
|
|
2057
|
-
return join16(scribeDir(), "_wal.jsonl");
|
|
2058
|
-
}
|
|
2059
|
-
function appendLine(obj) {
|
|
2060
|
-
mkdirSync12(scribeDir(), { recursive: true });
|
|
2061
|
-
const fd = openSync4(walFile(), "a");
|
|
2062
|
-
try {
|
|
2063
|
-
writeSync4(fd, `${JSON.stringify(obj)}
|
|
2064
|
-
`);
|
|
2065
|
-
fsyncSync4(fd);
|
|
2066
|
-
} finally {
|
|
2067
|
-
closeSync4(fd);
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
function walAppend(id, rec) {
|
|
2071
|
-
appendLine({ t: "add", id, rec });
|
|
2072
|
-
}
|
|
2073
|
-
function walCommit(id) {
|
|
2074
|
-
appendLine({ t: "commit", id });
|
|
2075
|
-
}
|
|
2076
|
-
function walPending() {
|
|
2077
|
-
if (!existsSync7(walFile())) return [];
|
|
2078
|
-
const committed = /* @__PURE__ */ new Set();
|
|
2079
|
-
const adds = /* @__PURE__ */ new Map();
|
|
2080
|
-
for (const line of readFileSync14(walFile(), "utf8").split("\n")) {
|
|
2081
|
-
if (!line.trim()) continue;
|
|
2082
|
-
try {
|
|
2083
|
-
const e = JSON.parse(line);
|
|
2084
|
-
if (e.t === "commit") committed.add(e.id);
|
|
2085
|
-
else if (e.t === "add" && e.rec) adds.set(e.id, e.rec);
|
|
2086
|
-
} catch {
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
const out = [];
|
|
2090
|
-
for (const [id, rec] of adds) {
|
|
2091
|
-
if (!committed.has(id)) out.push({ id, rec });
|
|
2092
|
-
}
|
|
2093
|
-
return out;
|
|
2094
|
-
}
|
|
2095
|
-
function walCompact() {
|
|
2096
|
-
if (!existsSync7(walFile())) return;
|
|
2097
|
-
const pending = walPending();
|
|
2098
|
-
const body = pending.map((p) => JSON.stringify({ t: "add", id: p.id, rec: p.rec })).join("\n");
|
|
2099
|
-
writeFileSync12(walFile(), body ? `${body}
|
|
2100
|
-
` : "", "utf8");
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
// src/scribe/supervisor.ts
|
|
2104
|
-
var draining = false;
|
|
2105
|
-
function uid(ts) {
|
|
2106
|
-
return `${ts}-${Math.random().toString(36).slice(2, 9)}`;
|
|
2107
|
-
}
|
|
2108
|
-
function drainBacklog() {
|
|
2109
|
-
if (draining) return;
|
|
2110
|
-
draining = true;
|
|
2111
|
-
try {
|
|
2112
|
-
let drained = 0;
|
|
2113
|
-
for (const e of walPending()) {
|
|
2114
|
-
try {
|
|
2115
|
-
captureTurn(e.rec);
|
|
2116
|
-
walCommit(e.id);
|
|
2117
|
-
drained++;
|
|
2118
|
-
} catch (err) {
|
|
2119
|
-
recordFault("standby-replay", err);
|
|
2120
|
-
break;
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
if (drained > 0) walCompact();
|
|
2124
|
-
} finally {
|
|
2125
|
-
draining = false;
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
function supervisedCapture(rec) {
|
|
2129
|
-
try {
|
|
2130
|
-
drainBacklog();
|
|
2131
|
-
const id = uid(rec.ts);
|
|
2132
|
-
const safe = redactRecord(rec).rec;
|
|
2133
|
-
walAppend(id, safe);
|
|
2134
|
-
try {
|
|
2135
|
-
const res = captureTurn(safe);
|
|
2136
|
-
walCommit(id);
|
|
2137
|
-
walCompact();
|
|
2138
|
-
recordHealth();
|
|
2139
|
-
return res;
|
|
2140
|
-
} catch (primaryErr) {
|
|
2141
|
-
recordFault("primary", primaryErr);
|
|
2142
|
-
try {
|
|
2143
|
-
const res = captureTurn(safe);
|
|
2144
|
-
walCommit(id);
|
|
2145
|
-
walCompact();
|
|
2146
|
-
recordHealth();
|
|
2147
|
-
return res;
|
|
2148
|
-
} catch (standbyErr) {
|
|
2149
|
-
recordFault("standby", standbyErr);
|
|
2150
|
-
return null;
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
} catch (fatal) {
|
|
2154
|
-
recordFault("supervisor", fatal);
|
|
2155
|
-
return null;
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
// src/scribe/retrieval.ts
|
|
2160
|
-
import { existsSync as existsSync8, readFileSync as readFileSync15, readdirSync } from "fs";
|
|
2161
|
-
function listDays() {
|
|
2162
|
-
const dir = scribeDir();
|
|
2163
|
-
if (!existsSync8(dir)) return [];
|
|
2164
|
-
return readdirSync(dir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).map((f) => f.replace(/\.md$/, "")).sort();
|
|
2165
|
-
}
|
|
2166
|
-
function readDay(date) {
|
|
2167
|
-
const f = journalFile(date);
|
|
2168
|
-
return existsSync8(f) ? readFileSync15(f, "utf8") : "";
|
|
2169
|
-
}
|
|
2170
|
-
function searchScribe(query, limit = 100) {
|
|
2171
|
-
const q = query.toLowerCase().trim();
|
|
2172
|
-
if (!q) return [];
|
|
2173
|
-
const hits = [];
|
|
2174
|
-
for (const date of listDays().reverse()) {
|
|
2175
|
-
const lines = readDay(date).split("\n");
|
|
2176
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2177
|
-
const ln = lines[i];
|
|
2178
|
-
if (ln && ln.toLowerCase().includes(q)) {
|
|
2179
|
-
hits.push({ date, line: i + 1, text: ln.trim().slice(0, 200) });
|
|
2180
|
-
if (hits.length >= limit) return hits;
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
return hits;
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
// src/scribe/scribe-pi.ts
|
|
2188
|
-
function scribeTurn(input) {
|
|
2189
|
-
if (!isScribeEnabled()) return;
|
|
2190
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2191
|
-
supervisedCapture({ ts, date: ts.slice(0, 10), ...input });
|
|
2192
|
-
}
|
|
2193
|
-
var pendingUserInput = "";
|
|
2194
|
-
function noteUserInput(text) {
|
|
2195
|
-
pendingUserInput = text;
|
|
2196
|
-
}
|
|
2197
|
-
function takePendingUserInput() {
|
|
2198
|
-
const u = pendingUserInput;
|
|
2199
|
-
pendingUserInput = "";
|
|
2200
|
-
return u;
|
|
2201
|
-
}
|
|
2202
|
-
function buildScribeContext() {
|
|
2203
|
-
if (!isScribeEnabled()) return "";
|
|
2204
|
-
const parts = [];
|
|
2205
|
-
try {
|
|
2206
|
-
const t = timelineFile();
|
|
2207
|
-
if (existsSync9(t)) parts.push(`# Work history \u2014 every day so far
|
|
2208
|
-
${readFileSync16(t, "utf8").trim()}`);
|
|
2209
|
-
} catch {
|
|
2210
|
-
}
|
|
2211
|
-
try {
|
|
2212
|
-
const d = readDigest();
|
|
2213
|
-
if (d?.trim()) parts.push(`# Current context (recent)
|
|
2214
|
-
${d.trim()}`);
|
|
2215
|
-
} catch {
|
|
2216
|
-
}
|
|
2217
|
-
if (!parts.length) return "";
|
|
2218
|
-
return `${parts.join("\n\n")}
|
|
2219
|
-
|
|
2220
|
-
(Call scribe_recall to fetch the full text of any past day or topic.)`;
|
|
2221
|
-
}
|
|
2222
|
-
function registerScribe(pi) {
|
|
2223
|
-
pi.registerTool({
|
|
2224
|
-
name: "scribe_recall",
|
|
2225
|
-
label: "ORIRO Scribe",
|
|
2226
|
-
description: "Recall the user's past work from the on-device journal: search by keyword, or read a specific day (YYYY-MM-DD). Use to recover decisions, code, files, and context from earlier sessions.",
|
|
2227
|
-
parameters: Type.Object({
|
|
2228
|
-
query: Type.Optional(Type.String({ description: "Keyword/topic to search across all journals." })),
|
|
2229
|
-
day: Type.Optional(Type.String({ description: "A specific day YYYY-MM-DD to read in full." }))
|
|
2230
|
-
}),
|
|
2231
|
-
async execute(_id, params) {
|
|
2232
|
-
let text;
|
|
2233
|
-
const details = {};
|
|
2234
|
-
if (!isScribeEnabled()) {
|
|
2235
|
-
text = "Scribe is off (the user has not enabled it).";
|
|
2236
|
-
} else if (params.day) {
|
|
2237
|
-
text = readDay(params.day) || `No journal for ${params.day}. Days: ${listDays().join(", ") || "none"}`;
|
|
2238
|
-
details.day = params.day;
|
|
2239
|
-
} else {
|
|
2240
|
-
const hits = params.query ? searchScribe(params.query) : [];
|
|
2241
|
-
details.hits = hits;
|
|
2242
|
-
text = hits.length ? hits.map((h) => `${h.date}:${h.line} ${h.text}`).join("\n") : `No matches${params.query ? ` for "${params.query}"` : ""}. Days recorded: ${listDays().join(", ") || "none"}`;
|
|
2243
|
-
}
|
|
2244
|
-
return { content: [{ type: "text", text }], details };
|
|
2245
|
-
}
|
|
2246
|
-
});
|
|
2247
|
-
}
|
|
2248
|
-
function attachScribe(session) {
|
|
2249
|
-
let user = "";
|
|
2250
|
-
let assistant = "";
|
|
2251
|
-
const tools = /* @__PURE__ */ new Set();
|
|
2252
|
-
session.subscribe((e) => {
|
|
2253
|
-
if (!isScribeEnabled()) return;
|
|
2254
|
-
if (e?.type === "user_message" || e?.type === "session_user_message") user = String(e.text ?? e.message ?? user);
|
|
2255
|
-
if (e?.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") assistant += e.assistantMessageEvent.delta ?? "";
|
|
2256
|
-
if ((e?.type === "tool_call" || e?.type === "tool_execution_start") && e.toolName) tools.add(String(e.toolName));
|
|
2257
|
-
if (e?.type === "agent_end") {
|
|
2258
|
-
const userText = takePendingUserInput() || user;
|
|
2259
|
-
scribeTurn({ user: userText || void 0, router: "oriro-free", tools: [...tools], note: assistant.slice(0, 4e3) || void 0 });
|
|
2260
|
-
user = "";
|
|
2261
|
-
assistant = "";
|
|
2262
|
-
tools.clear();
|
|
2263
|
-
}
|
|
2264
|
-
});
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
// src/routers/mux-provider.ts
|
|
2268
|
-
var MUX_PROVIDER = "oriro-mux";
|
|
2269
|
-
var MUX_MODEL = "oriro-free";
|
|
2270
|
-
function errToCallError(msg) {
|
|
2271
|
-
const text = msg.errorMessage ?? "";
|
|
2272
|
-
return /\b429\b|rate.?limit|too many requests/i.test(text) ? { status: 429 } : {};
|
|
2273
|
-
}
|
|
2274
|
-
function buildErrorMessage(message) {
|
|
2275
|
-
return {
|
|
2276
|
-
role: "assistant",
|
|
2277
|
-
content: [],
|
|
2278
|
-
api: "openai-completions",
|
|
2279
|
-
provider: MUX_PROVIDER,
|
|
2280
|
-
model: MUX_MODEL,
|
|
2281
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
2282
|
-
stopReason: "error",
|
|
2283
|
-
timestamp: Date.now(),
|
|
2284
|
-
errorMessage: message
|
|
2285
|
-
};
|
|
2286
|
-
}
|
|
2287
|
-
async function driveMux(out, mux, byId, context, options) {
|
|
2288
|
-
let lastError;
|
|
2289
|
-
for (const id of mux.ranked()) {
|
|
2290
|
-
const router = byId.get(id);
|
|
2291
|
-
if (!router) continue;
|
|
2292
|
-
const t0 = Date.now();
|
|
2293
|
-
let committed = false;
|
|
2294
|
-
let lastPartial;
|
|
2295
|
-
try {
|
|
2296
|
-
const inner = piStreamSimple(routerModel(router), context, {
|
|
2297
|
-
...options ?? {},
|
|
2298
|
-
apiKey: router.apiKey
|
|
2299
|
-
});
|
|
2300
|
-
let failedBeforeContent = false;
|
|
2301
|
-
for await (const ev of inner) {
|
|
2302
|
-
if (ev.type === "error") {
|
|
2303
|
-
mux.recordFailure(id, errToCallError(ev.error));
|
|
2304
|
-
if (!committed) {
|
|
2305
|
-
lastError = ev.error;
|
|
2306
|
-
failedBeforeContent = true;
|
|
2307
|
-
break;
|
|
2308
|
-
}
|
|
2309
|
-
out.push(ev);
|
|
2310
|
-
out.end(ev.error);
|
|
2311
|
-
return;
|
|
2312
|
-
}
|
|
2313
|
-
committed = true;
|
|
2314
|
-
if (ev.type === "done") {
|
|
2315
|
-
mux.recordSuccess(id, Date.now() - t0);
|
|
2316
|
-
const clean = sanitizeMessageToolCalls(scrubMessageIdentity(ev.message));
|
|
2317
|
-
out.push({ type: "done", reason: ev.reason, message: clean });
|
|
2318
|
-
out.end(clean);
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
lastPartial = ev.partial;
|
|
2322
|
-
out.push(sanitizeEventToolCalls(ev));
|
|
2323
|
-
}
|
|
2324
|
-
if (failedBeforeContent) continue;
|
|
2325
|
-
if (!committed) {
|
|
2326
|
-
mux.recordFailure(id, {});
|
|
2327
|
-
lastError ??= buildErrorMessage("Router returned no output.");
|
|
2328
|
-
continue;
|
|
2329
|
-
}
|
|
2330
|
-
mux.recordSuccess(id, Date.now() - t0);
|
|
2331
|
-
out.end(lastPartial ? sanitizeMessageToolCalls(scrubMessageIdentity(lastPartial)) : void 0);
|
|
2332
|
-
return;
|
|
2333
|
-
} catch (e) {
|
|
2334
|
-
mux.recordFailure(id, e);
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
const msg = lastError ?? buildErrorMessage(
|
|
2338
|
-
"All keyless routers are unavailable. Add a BYOK key, select more free routers, or retry shortly."
|
|
2339
|
-
);
|
|
2340
|
-
out.push({ type: "error", reason: "error", error: msg });
|
|
2341
|
-
out.end(msg);
|
|
2342
|
-
}
|
|
2343
|
-
function registerOriroMux(registry, opts = {}) {
|
|
2344
|
-
registerOpenAICompletions();
|
|
2345
|
-
const pooled = resolvePool();
|
|
2346
|
-
const routers = opts.routers ?? (pooled.length > 0 ? pooled : KEYLESS_FLOOR);
|
|
2347
|
-
const byId = new Map(routers.map((r) => [r.id, r]));
|
|
2348
|
-
const mux = new RouterMux(routers.map((r) => r.id));
|
|
2349
|
-
try {
|
|
2350
|
-
mux.load(loadMuxState(oriroDir()));
|
|
2351
|
-
} catch {
|
|
2352
|
-
}
|
|
2353
|
-
registry.registerProvider(MUX_PROVIDER, {
|
|
2354
|
-
name: "ORIRO Free (keyless Mux)",
|
|
2355
|
-
api: "openai-completions",
|
|
2356
|
-
apiKey: "oriro-keyless",
|
|
2357
|
-
// Placeholder — required by registry validation but never used: our custom streamSimple
|
|
2358
|
-
// routes to the real keyless floor endpoints itself (see driveMux).
|
|
2359
|
-
baseUrl: "http://oriro-mux.local",
|
|
2360
|
-
models: [
|
|
2361
|
-
{
|
|
2362
|
-
id: MUX_MODEL,
|
|
2363
|
-
name: "ORIRO Free (best-router)",
|
|
2364
|
-
api: "openai-completions",
|
|
2365
|
-
baseUrl: "http://oriro-mux.local",
|
|
2366
|
-
reasoning: false,
|
|
2367
|
-
input: ["text"],
|
|
2368
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2369
|
-
contextWindow: 128e3,
|
|
2370
|
-
maxTokens: 4096
|
|
2371
|
-
}
|
|
2372
|
-
],
|
|
2373
|
-
streamSimple: (_model, context, options) => {
|
|
2374
|
-
const out = createAssistantMessageEventStream();
|
|
2375
|
-
const ctx = applyIdentity(context);
|
|
2376
|
-
const memory = buildScribeContext();
|
|
2377
|
-
const withMemory = memory ? { ...ctx, systemPrompt: `${ctx.systemPrompt}
|
|
2378
|
-
|
|
2379
|
-
${memory}` } : ctx;
|
|
2380
|
-
void driveMux(out, mux, byId, withMemory, options).finally(() => {
|
|
2381
|
-
try {
|
|
2382
|
-
saveMuxState(oriroDir(), mux.snapshot());
|
|
2383
|
-
} catch {
|
|
2384
|
-
}
|
|
2385
|
-
});
|
|
2386
|
-
return out;
|
|
2387
|
-
}
|
|
2388
|
-
});
|
|
2389
|
-
return registry.find(MUX_PROVIDER, MUX_MODEL);
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
// src/head/pi-tool.ts
|
|
2393
|
-
import { Type as Type2 } from "typebox";
|
|
2394
|
-
|
|
2395
|
-
// src/head/comparison-engine.ts
|
|
2396
|
-
var SECTION_RULES = [
|
|
2397
|
-
{
|
|
2398
|
-
type: "hero",
|
|
2399
|
-
label: "Hero",
|
|
2400
|
-
priority: "CRITICAL",
|
|
2401
|
-
markup: [/<h1[\s>]/],
|
|
2402
|
-
recommend: "Add a clear above-the-fold hero \u2014 one headline that states the value + one primary CTA."
|
|
2403
|
-
},
|
|
2404
|
-
{
|
|
2405
|
-
type: "navigation",
|
|
2406
|
-
label: "Navigation",
|
|
2407
|
-
priority: "CRITICAL",
|
|
2408
|
-
markup: [/<nav[\s>]/, /role=["']navigation["']/],
|
|
2409
|
-
recommend: "Add a top navigation so visitors can reach key sections."
|
|
2410
|
-
},
|
|
2411
|
-
{
|
|
2412
|
-
type: "features",
|
|
2413
|
-
label: "Features",
|
|
2414
|
-
priority: "CRITICAL",
|
|
2415
|
-
text: [/\bfeatures?\b/, /\bwhat you (?:can|get)\b/, /\bcapabilit/],
|
|
2416
|
-
recommend: "Add a features section that spells out concrete capabilities, not adjectives."
|
|
2417
|
-
},
|
|
2418
|
-
{
|
|
2419
|
-
type: "pricing",
|
|
2420
|
-
label: "Pricing",
|
|
2421
|
-
priority: "CRITICAL",
|
|
2422
|
-
text: [/\bpricing\b/, /\bper month\b/, /\b\/mo\b/, /\bfree plan\b/, /\$\d/, /₹\d/, /€\d/],
|
|
2423
|
-
recommend: 'Add transparent pricing \u2014 a critical conversion element; even a single "Free" tier helps.'
|
|
2424
|
-
},
|
|
2425
|
-
{
|
|
2426
|
-
type: "cta",
|
|
2427
|
-
label: "Call-to-Action",
|
|
2428
|
-
priority: "CRITICAL",
|
|
2429
|
-
text: [/\bget started\b/, /\bsign up\b/, /\bstart (?:free|now|building)\b/, /\btry (?:it|now|free)\b/, /\bbook a demo\b/, /\bget a demo\b/],
|
|
2430
|
-
recommend: 'Add a strong, repeated primary CTA ("Get started") so the next step is obvious.'
|
|
2431
|
-
},
|
|
2432
|
-
{
|
|
2433
|
-
type: "testimonials",
|
|
2434
|
-
label: "Testimonials",
|
|
2435
|
-
priority: "HIGH",
|
|
2436
|
-
text: [/\btestimonial/, /\bwhat (?:our )?(?:customers|users) say\b/, /\bloved by\b/, /\breview(?:s|ed)\b/],
|
|
2437
|
-
recommend: "Add 2\u20133 customer testimonials with names/photos to build trust."
|
|
2438
|
-
},
|
|
2439
|
-
{
|
|
2440
|
-
type: "stats",
|
|
2441
|
-
label: "Stats / Metrics",
|
|
2442
|
-
priority: "HIGH",
|
|
2443
|
-
text: [/\b\d[\d,.]*\s*[kkmm]\+?\s*(?:users|customers|developers|downloads|teams)\b/, /\b9\d(?:\.\d+)?%\b/, /\buptime\b/],
|
|
2444
|
-
recommend: 'Add impressive metrics ("10K+ users", "99.9% uptime") as social proof.'
|
|
2445
|
-
},
|
|
2446
|
-
{
|
|
2447
|
-
type: "video",
|
|
2448
|
-
label: "Video",
|
|
2449
|
-
priority: "HIGH",
|
|
2450
|
-
markup: [/<video[\s>]/, /youtube\.com\/embed/, /player\.vimeo\.com/, /<iframe[^>]+(?:youtube|vimeo)/],
|
|
2451
|
-
text: [/\bwatch the (?:video|demo)\b/],
|
|
2452
|
-
recommend: "Add a short explainer/demo video \u2014 it lifts conversion on landing pages."
|
|
2453
|
-
},
|
|
2454
|
-
{
|
|
2455
|
-
type: "demo",
|
|
2456
|
-
label: "Live Demo",
|
|
2457
|
-
priority: "HIGH",
|
|
2458
|
-
text: [/\btry it (?:now|live|free)\b/, /\bplayground\b/, /\binteractive demo\b/, /\blive demo\b/],
|
|
2459
|
-
recommend: 'Add a "try it" live demo or playground so visitors experience the product immediately.'
|
|
2460
|
-
},
|
|
2461
|
-
{
|
|
2462
|
-
type: "socialProof",
|
|
2463
|
-
label: "Social Proof",
|
|
2464
|
-
priority: "HIGH",
|
|
2465
|
-
text: [/\btrusted by\b/, /\bbacked by\b/, /\bused by\b/, /\bas seen (?:in|on)\b/, /\bcustomers include\b/],
|
|
2466
|
-
recommend: 'Add social proof (customer/investor logos, "trusted by \u2026") near the hero.'
|
|
2467
|
-
},
|
|
2468
|
-
{
|
|
2469
|
-
type: "faq",
|
|
2470
|
-
label: "FAQ",
|
|
2471
|
-
priority: "MEDIUM",
|
|
2472
|
-
text: [/\bfaq\b/, /\bfrequently asked\b/],
|
|
2473
|
-
markup: [/<details[\s>]/],
|
|
2474
|
-
recommend: "Add an FAQ that answers the top objections before they become exits."
|
|
2475
|
-
},
|
|
2476
|
-
{
|
|
2477
|
-
type: "integrations",
|
|
2478
|
-
label: "Integrations",
|
|
2479
|
-
priority: "MEDIUM",
|
|
2480
|
-
text: [/\bintegrations?\b/, /\bworks with\b/, /\bconnect your\b/],
|
|
2481
|
-
recommend: "Add an integrations section showing what the product connects to."
|
|
2482
|
-
},
|
|
2483
|
-
{
|
|
2484
|
-
type: "newsletter",
|
|
2485
|
-
label: "Newsletter / Capture",
|
|
2486
|
-
priority: "MEDIUM",
|
|
2487
|
-
text: [/\bsubscribe\b/, /\bnewsletter\b/, /\bjoin (?:the )?waitlist\b/],
|
|
2488
|
-
markup: [/type=["']email["']/],
|
|
2489
|
-
recommend: "Add an email capture (newsletter/waitlist) so non-converting visitors are not lost."
|
|
2490
|
-
},
|
|
2491
|
-
{
|
|
2492
|
-
type: "comparison",
|
|
2493
|
-
label: "Comparison",
|
|
2494
|
-
priority: "MEDIUM",
|
|
2495
|
-
text: [/\bcompare\b/, /\bcomparison\b/, /\b vs\.? \b/, /\bwhy choose\b/],
|
|
2496
|
-
recommend: 'Add a comparison ("us vs alternatives") to win evaluators who are shopping around.'
|
|
2497
|
-
},
|
|
2498
|
-
{
|
|
2499
|
-
type: "team",
|
|
2500
|
-
label: "Team / About",
|
|
2501
|
-
priority: "LOW",
|
|
2502
|
-
text: [/\bour team\b/, /\bmeet the team\b/, /\bfounders?\b/, /\babout us\b/],
|
|
2503
|
-
recommend: "Add a brief team/about section to humanize the brand."
|
|
2504
|
-
}
|
|
2505
|
-
];
|
|
2506
|
-
var PRIORITY_RANK = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
|
2507
|
-
var PRIORITY_EFFORT = { CRITICAL: "L", HIGH: "M", MEDIUM: "M", LOW: "S" };
|
|
2508
|
-
var FETCH_TIMEOUT_MS = 12e3;
|
|
2509
|
-
var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36 ORIRO-Inspector";
|
|
2510
|
-
async function fetchPage(url) {
|
|
2511
|
-
const controller = new AbortController();
|
|
2512
|
-
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
2513
|
-
const start = Date.now();
|
|
2514
|
-
try {
|
|
2515
|
-
const res = await fetch(url, {
|
|
2516
|
-
signal: controller.signal,
|
|
2517
|
-
redirect: "follow",
|
|
2518
|
-
headers: { "user-agent": UA, accept: "text/html,application/xhtml+xml" }
|
|
2519
|
-
});
|
|
2520
|
-
const html = await res.text();
|
|
2521
|
-
return { html, ms: Date.now() - start, status: res.status, ok: res.ok, error: "" };
|
|
2522
|
-
} catch (err) {
|
|
2523
|
-
return { html: "", ms: Date.now() - start, status: 0, ok: false, error: err instanceof Error ? err.message : "fetch failed" };
|
|
2524
|
-
} finally {
|
|
2525
|
-
clearTimeout(timer);
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
function toText(html) {
|
|
2529
|
-
return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /gi, " ").replace(/\s+/g, " ").toLowerCase().trim();
|
|
2530
|
-
}
|
|
2531
|
-
function firstMatch(re, hay) {
|
|
2532
|
-
const m = re.exec(hay);
|
|
2533
|
-
if (!m) return "";
|
|
2534
|
-
const slice = (m[0] ?? "").trim();
|
|
2535
|
-
return slice.length > 80 ? `${slice.slice(0, 77)}\u2026` : slice;
|
|
2536
|
-
}
|
|
2537
|
-
function detectSections(rawHtmlLower, text) {
|
|
2538
|
-
const found = [];
|
|
2539
|
-
for (const rule of SECTION_RULES) {
|
|
2540
|
-
let evidence = "";
|
|
2541
|
-
for (const re of rule.markup ?? []) {
|
|
2542
|
-
const hit = firstMatch(re, rawHtmlLower);
|
|
2543
|
-
if (hit) {
|
|
2544
|
-
evidence = hit;
|
|
2545
|
-
break;
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
if (!evidence) {
|
|
2549
|
-
for (const re of rule.text ?? []) {
|
|
2550
|
-
const hit = firstMatch(re, text);
|
|
2551
|
-
if (hit) {
|
|
2552
|
-
evidence = hit;
|
|
2553
|
-
break;
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
2557
|
-
if (evidence) found.push({ type: rule.type, label: rule.label, priority: rule.priority, evidence });
|
|
2558
|
-
}
|
|
2559
|
-
return found;
|
|
2560
|
-
}
|
|
2561
|
-
function extractMatches(re, html, max) {
|
|
2562
|
-
const out = [];
|
|
2563
|
-
for (const m of html.matchAll(re)) {
|
|
2564
|
-
const inner = (m[1] ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
2565
|
-
if (inner && !out.includes(inner)) out.push(inner);
|
|
2566
|
-
if (out.length >= max) break;
|
|
2567
|
-
}
|
|
2568
|
-
return out;
|
|
2569
|
-
}
|
|
2570
|
-
var CTA_WORDS = /\b(get started|sign up|start free|start now|start building|try (?:it|now|free)|book a demo|get a demo|request access|join (?:the )?waitlist|download)\b/i;
|
|
2571
|
-
function extractStructure(url, fr) {
|
|
2572
|
-
const html = fr.html;
|
|
2573
|
-
const lowerHtml = html.toLowerCase();
|
|
2574
|
-
const text = toText(html);
|
|
2575
|
-
const titleM = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
|
|
2576
|
-
const title = (titleM?.[1] ?? "").replace(/\s+/g, " ").trim();
|
|
2577
|
-
const descM = /<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["']/i.exec(html) ?? /<meta[^>]+content=["']([^"']*)["'][^>]+name=["']description["']/i.exec(html);
|
|
2578
|
-
const description = (descM?.[1] ?? "").replace(/\s+/g, " ").trim();
|
|
2579
|
-
const headings = extractMatches(/<h[1-3][^>]*>([\s\S]*?)<\/h[1-3]>/gi, html, 12);
|
|
2580
|
-
const ctaAll = extractMatches(/<(?:a|button)[^>]*>([\s\S]*?)<\/(?:a|button)>/gi, html, 80);
|
|
2581
|
-
const ctas = [];
|
|
2582
|
-
for (const c of ctaAll) {
|
|
2583
|
-
if (CTA_WORDS.test(c) && !ctas.includes(c)) ctas.push(c);
|
|
2584
|
-
if (ctas.length >= 10) break;
|
|
2585
|
-
}
|
|
2586
|
-
const forms = (lowerHtml.match(/<form[\s>]/g) ?? []).length;
|
|
2587
|
-
const links = (lowerHtml.match(/<a[\s>]/g) ?? []).length;
|
|
2588
|
-
const images = (lowerHtml.match(/<img[\s>]/g) ?? []).length;
|
|
2589
|
-
const hasVideo = /<video[\s>]/.test(lowerHtml) || /(?:youtube\.com\/embed|player\.vimeo\.com)/.test(lowerHtml);
|
|
2590
|
-
const domNodes = (html.match(/<[a-z!\/]/gi) ?? []).length;
|
|
2591
|
-
let note = "";
|
|
2592
|
-
if (fr.ok && text.length < 400 && domNodes < 60) {
|
|
2593
|
-
note = "Sparse HTML \u2014 likely a client-rendered (SPA) page; structure may be under-detected without a JS render.";
|
|
2594
|
-
}
|
|
2595
|
-
return {
|
|
2596
|
-
url,
|
|
2597
|
-
title,
|
|
2598
|
-
description,
|
|
2599
|
-
sections: detectSections(lowerHtml, text),
|
|
2600
|
-
headings,
|
|
2601
|
-
ctas,
|
|
2602
|
-
forms,
|
|
2603
|
-
links,
|
|
2604
|
-
images,
|
|
2605
|
-
hasVideo,
|
|
2606
|
-
metrics: { htmlBytes: html.length, domNodes, fetchMs: fr.ms, status: fr.status },
|
|
2607
|
-
ok: fr.ok && html.length > 0,
|
|
2608
|
-
note: fr.ok ? note : `Could not load: ${fr.error || `HTTP ${fr.status}`}`
|
|
2609
|
-
};
|
|
2610
|
-
}
|
|
2611
|
-
function ruleFor(type) {
|
|
2612
|
-
return SECTION_RULES.find((r) => r.type === type) ?? SECTION_RULES[0];
|
|
2613
|
-
}
|
|
2614
|
-
function analyzeGaps(target, competitors) {
|
|
2615
|
-
const targetTypes = new Set(target.sections.map((s) => s.type));
|
|
2616
|
-
const compPresence = /* @__PURE__ */ new Map();
|
|
2617
|
-
for (const comp of competitors) {
|
|
2618
|
-
if (!comp.ok) continue;
|
|
2619
|
-
for (const s of comp.sections) {
|
|
2620
|
-
const list = compPresence.get(s.type) ?? [];
|
|
2621
|
-
if (!list.includes(comp.url)) list.push(comp.url);
|
|
2622
|
-
compPresence.set(s.type, list);
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
const missing = [];
|
|
2626
|
-
const parity = [];
|
|
2627
|
-
for (const [type, presentOn] of compPresence) {
|
|
2628
|
-
if (targetTypes.has(type)) {
|
|
2629
|
-
parity.push(type);
|
|
2630
|
-
} else {
|
|
2631
|
-
const rule = ruleFor(type);
|
|
2632
|
-
missing.push({ section: type, label: rule.label, priority: rule.priority, presentOn, recommendation: rule.recommend });
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
missing.sort((a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || b.presentOn.length - a.presentOn.length);
|
|
2636
|
-
const advantages = target.sections.filter((s) => !compPresence.has(s.type));
|
|
2637
|
-
return { missing, advantages, parity };
|
|
2638
|
-
}
|
|
2639
|
-
function generateActionItems(missing) {
|
|
2640
|
-
return missing.map((g) => ({
|
|
2641
|
-
title: `Add a ${g.label} section`,
|
|
2642
|
-
priority: g.priority,
|
|
2643
|
-
effort: PRIORITY_EFFORT[g.priority],
|
|
2644
|
-
rationale: `${g.presentOn.length} of the compared page(s) have it; you don't. ${g.recommendation}`
|
|
2645
|
-
}));
|
|
2646
|
-
}
|
|
2647
|
-
function hostOf(url) {
|
|
2648
|
-
try {
|
|
2649
|
-
return new URL(url).host.replace(/^www\./, "");
|
|
2650
|
-
} catch {
|
|
2651
|
-
return url;
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
function generateSummary(target, competitors, gaps) {
|
|
2655
|
-
const okComps = competitors.filter((c) => c.ok);
|
|
2656
|
-
const tName = hostOf(target.url);
|
|
2657
|
-
if (!target.ok) return `Could not load ${tName} (${target.note}). Nothing to compare against yet.`;
|
|
2658
|
-
if (okComps.length === 0) return `Loaded ${tName} (${target.sections.length} sections) but none of the comparison URLs could be loaded.`;
|
|
2659
|
-
const crit = gaps.missing.filter((m) => m.priority === "CRITICAL").map((m) => m.label);
|
|
2660
|
-
const high = gaps.missing.filter((m) => m.priority === "HIGH").map((m) => m.label);
|
|
2661
|
-
const parts = [];
|
|
2662
|
-
parts.push(`${tName} has ${target.sections.length} detectable sections; compared against ${okComps.length} page(s).`);
|
|
2663
|
-
if (gaps.missing.length === 0) {
|
|
2664
|
-
parts.push("No structural gaps found \u2014 you cover everything they do.");
|
|
2665
|
-
} else {
|
|
2666
|
-
parts.push(`${gaps.missing.length} gap(s) found.`);
|
|
2667
|
-
if (crit.length) parts.push(`Critical: ${crit.join(", ")}.`);
|
|
2668
|
-
if (high.length) parts.push(`High: ${high.join(", ")}.`);
|
|
2669
|
-
}
|
|
2670
|
-
if (gaps.advantages.length) parts.push(`Your edge: ${gaps.advantages.map((a) => a.label).join(", ")}.`);
|
|
2671
|
-
return parts.join(" ");
|
|
2672
|
-
}
|
|
2673
|
-
function normalizeUrl(u) {
|
|
2674
|
-
const t = (u || "").trim();
|
|
2675
|
-
if (!t) return t;
|
|
2676
|
-
return /^https?:\/\//i.test(t) ? t : `https://${t}`;
|
|
2677
|
-
}
|
|
2678
|
-
async function comparePages(opts) {
|
|
2679
|
-
const targetUrl = normalizeUrl(opts.targetUrl);
|
|
2680
|
-
const competitorUrls = (opts.competitorUrls ?? []).map(normalizeUrl).filter((u) => u.length > 0).slice(0, 30);
|
|
2681
|
-
const [targetFetch, ...compFetches] = await Promise.all([
|
|
2682
|
-
fetchPage(targetUrl),
|
|
2683
|
-
...competitorUrls.map((u) => fetchPage(u))
|
|
2684
|
-
]);
|
|
2685
|
-
const target = extractStructure(targetUrl, targetFetch ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" });
|
|
2686
|
-
const competitors = competitorUrls.map(
|
|
2687
|
-
(u, i) => extractStructure(u, compFetches[i] ?? { html: "", ms: 0, status: 0, ok: false, error: "no fetch" })
|
|
2688
|
-
);
|
|
2689
|
-
const gaps = analyzeGaps(target, competitors);
|
|
2690
|
-
return {
|
|
2691
|
-
target,
|
|
2692
|
-
competitors,
|
|
2693
|
-
missing: gaps.missing,
|
|
2694
|
-
advantages: gaps.advantages,
|
|
2695
|
-
parity: gaps.parity,
|
|
2696
|
-
actionItems: generateActionItems(gaps.missing),
|
|
2697
|
-
summary: generateSummary(target, competitors, gaps)
|
|
2698
|
-
};
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
// src/head/pi-tool.ts
|
|
2702
|
-
function summarizeForCoder(report) {
|
|
2703
|
-
const lines = [report.summary];
|
|
2704
|
-
const page = (p) => ` \u2022 ${p.url} \u2014 ${p.ok ? `${p.sections.length} sections: ${p.sections.map((s) => s.type).join(", ")}` : `not readable (${p.note})`}`;
|
|
2705
|
-
lines.push("Pages seen:");
|
|
2706
|
-
lines.push(page(report.target));
|
|
2707
|
-
for (const c of report.competitors) if (c.url !== report.target.url) lines.push(page(c));
|
|
2708
|
-
if (report.missing.length) {
|
|
2709
|
-
lines.push("Missing on the target (gaps to build):");
|
|
2710
|
-
for (const g of report.missing.slice(0, 12)) lines.push(` \u2022 ${g.label} (${g.priority}) \u2014 ${g.recommendation}`);
|
|
2711
|
-
}
|
|
2712
|
-
if (report.actionItems.length) {
|
|
2713
|
-
lines.push("Suggested action items:");
|
|
2714
|
-
for (const a of report.actionItems.slice(0, 12)) lines.push(` \u2192 ${a.title} [${a.priority}/${a.effort}] \u2014 ${a.rationale}`);
|
|
2715
|
-
}
|
|
2716
|
-
return lines.join("\n");
|
|
2717
|
-
}
|
|
2718
|
-
var InspectSiteParams = Type2.Object({
|
|
2719
|
-
url: Type2.String({ description: "The target website URL to inspect or rebuild from." }),
|
|
2720
|
-
competitors: Type2.Optional(
|
|
2721
|
-
Type2.Array(Type2.String(), { description: "Optional competitor/reference URLs to compare the target against." })
|
|
2722
|
-
)
|
|
2723
|
-
});
|
|
2724
|
-
function registerHead(pi) {
|
|
2725
|
-
pi.registerTool({
|
|
2726
|
-
name: "inspect_site",
|
|
2727
|
-
label: "ORIRO Head",
|
|
2728
|
-
description: "Go out to a live website and SEE it: its sections, CTAs, structure, and any gaps versus competitor URLs. Returns a structured report to build from. Call this whenever the user wants to look at, compare against, or rebuild a website/page.",
|
|
2729
|
-
parameters: InspectSiteParams,
|
|
2730
|
-
async execute(_toolCallId, params) {
|
|
2731
|
-
const target = params.url;
|
|
2732
|
-
const competitors = params.competitors?.length ? params.competitors : [target];
|
|
2733
|
-
const report = await comparePages({ targetUrl: target, competitorUrls: competitors });
|
|
2734
|
-
return { content: [{ type: "text", text: summarizeForCoder(report) }], details: report };
|
|
2735
|
-
}
|
|
2736
|
-
});
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
// src/orchestrate.ts
|
|
2740
|
-
import { createAgentSession, AuthStorage, ModelRegistry, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2741
|
-
import { Type as Type3 } from "typebox";
|
|
2742
|
-
var MAX_AGENTS = 8;
|
|
2743
|
-
var MAX_CONCURRENCY = 4;
|
|
2744
|
-
async function runOnce(spec) {
|
|
2745
|
-
const authStorage = AuthStorage.inMemory();
|
|
2746
|
-
const modelRegistry = ModelRegistry.inMemory(authStorage);
|
|
2747
|
-
const model = registerOriroMux(modelRegistry);
|
|
2748
|
-
if (!model) return { ...spec, ok: false, output: "no free model available" };
|
|
2749
|
-
const { session } = await createAgentSession({
|
|
2750
|
-
model,
|
|
2751
|
-
authStorage,
|
|
2752
|
-
modelRegistry,
|
|
2753
|
-
sessionManager: SessionManager.inMemory(),
|
|
2754
|
-
noTools: "all"
|
|
2755
|
-
});
|
|
2756
|
-
let out = "";
|
|
2757
|
-
const unsub = session.subscribe((e) => {
|
|
2758
|
-
if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") out += e.assistantMessageEvent.delta ?? "";
|
|
2759
|
-
});
|
|
2760
|
-
try {
|
|
2761
|
-
await session.prompt(`You are the ${spec.role} sub-agent. ${spec.task}`);
|
|
2762
|
-
} catch (e) {
|
|
2763
|
-
return { ...spec, ok: false, output: e instanceof Error ? e.message : String(e) };
|
|
2764
|
-
} finally {
|
|
2765
|
-
unsub();
|
|
2766
|
-
session.dispose();
|
|
2767
|
-
}
|
|
2768
|
-
return { ...spec, ok: out.trim().length > 0, output: out.trim() };
|
|
2769
|
-
}
|
|
2770
|
-
async function runAgent(spec) {
|
|
2771
|
-
let last = await runOnce(spec);
|
|
2772
|
-
if (!last.ok) last = await runOnce(spec);
|
|
2773
|
-
return last;
|
|
2774
|
-
}
|
|
2775
|
-
async function runPool(items, n, fn) {
|
|
2776
|
-
const results = new Array(items.length);
|
|
2777
|
-
let i = 0;
|
|
2778
|
-
async function worker() {
|
|
2779
|
-
while (i < items.length) {
|
|
2780
|
-
const idx = i++;
|
|
2781
|
-
const item = items[idx];
|
|
2782
|
-
if (item === void 0) continue;
|
|
2783
|
-
results[idx] = await fn(item);
|
|
2784
|
-
}
|
|
2785
|
-
}
|
|
2786
|
-
await Promise.all(Array.from({ length: Math.min(n, items.length) }, () => worker()));
|
|
2787
|
-
return results;
|
|
2788
|
-
}
|
|
2789
|
-
async function orchestrate(opts) {
|
|
2790
|
-
const agents = opts.agents.slice(0, MAX_AGENTS);
|
|
2791
|
-
if ((opts.mode ?? "parallel") === "chain") {
|
|
2792
|
-
const results = [];
|
|
2793
|
-
let prev = "";
|
|
2794
|
-
for (const a of agents) {
|
|
2795
|
-
const r = await runAgent({ role: a.role, task: prev ? `${a.task}
|
|
2796
|
-
|
|
2797
|
-
Previous result:
|
|
2798
|
-
${prev}` : a.task });
|
|
2799
|
-
results.push(r);
|
|
2800
|
-
prev = r.output;
|
|
2801
|
-
}
|
|
2802
|
-
return results;
|
|
2803
|
-
}
|
|
2804
|
-
return runPool(agents, MAX_CONCURRENCY, runAgent);
|
|
2805
|
-
}
|
|
2806
|
-
function registerOrchestrator(pi) {
|
|
2807
|
-
pi.registerTool({
|
|
2808
|
-
name: "deploy_agents",
|
|
2809
|
-
label: "ORIRO Orchestrator",
|
|
2810
|
-
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.",
|
|
2811
|
-
parameters: Type3.Object({
|
|
2812
|
-
agents: Type3.Array(Type3.Object({ role: Type3.String(), task: Type3.String() }), {
|
|
2813
|
-
description: "The sub-agents to deploy (max 8)."
|
|
2814
|
-
}),
|
|
2815
|
-
mode: Type3.Optional(Type3.Union([Type3.Literal("parallel"), Type3.Literal("chain")]))
|
|
2816
|
-
}),
|
|
2817
|
-
async execute(_id, params) {
|
|
2818
|
-
const results = await orchestrate({ agents: params.agents, mode: params.mode });
|
|
2819
|
-
const text = results.map((r) => `[${r.role}] ${r.ok ? "\u2713" : "\u2717"} ${r.output.slice(0, 300)}`).join("\n");
|
|
2820
|
-
return { content: [{ type: "text", text }], details: { results } };
|
|
2821
|
-
}
|
|
2822
|
-
});
|
|
2823
|
-
}
|
|
2824
|
-
|
|
2825
|
-
// src/skills/loader.ts
|
|
2826
|
-
import { loadSkills, formatSkillsForPrompt } from "@earendil-works/pi-coding-agent";
|
|
2827
|
-
import { fileURLToPath } from "url";
|
|
2828
|
-
import { existsSync as existsSync10 } from "fs";
|
|
2829
|
-
import { dirname as dirname2, join as join17 } from "path";
|
|
2830
|
-
function packageRoot(start) {
|
|
2831
|
-
let dir = start;
|
|
2832
|
-
for (let i = 0; i < 10; i++) {
|
|
2833
|
-
if (existsSync10(join17(dir, "package.json"))) return dir;
|
|
2834
|
-
const parent = dirname2(dir);
|
|
2835
|
-
if (parent === dir) break;
|
|
2836
|
-
dir = parent;
|
|
2837
|
-
}
|
|
2838
|
-
return start;
|
|
2839
|
-
}
|
|
2840
|
-
function skillsDir() {
|
|
2841
|
-
if (process.env.ORIRO_SKILLS_DIR) return process.env.ORIRO_SKILLS_DIR;
|
|
2842
|
-
return join17(packageRoot(dirname2(fileURLToPath(import.meta.url))), "skills");
|
|
2843
|
-
}
|
|
2844
|
-
async function loadOriroSkills(dir = skillsDir()) {
|
|
2845
|
-
const result = await loadSkills({
|
|
2846
|
-
cwd: dir,
|
|
2847
|
-
agentDir: dir,
|
|
2848
|
-
skillPaths: [dir],
|
|
2849
|
-
includeDefaults: false
|
|
2850
|
-
});
|
|
2851
|
-
const all = Array.isArray(result) ? result : result.skills ?? [];
|
|
2852
|
-
return {
|
|
2853
|
-
all,
|
|
2854
|
-
core: all.filter((s) => !s.disableModelInvocation),
|
|
2855
|
-
tail: all.filter((s) => s.disableModelInvocation),
|
|
2856
|
-
prompt: formatSkillsForPrompt(all)
|
|
2857
|
-
};
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
// src/onboarding/assemble.ts
|
|
2861
|
-
async function assembleOriroSession(opts = {}) {
|
|
2862
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
2863
|
-
const authStorage = AuthStorage2.inMemory();
|
|
2864
|
-
const modelRegistry = ModelRegistry2.inMemory(authStorage);
|
|
2865
|
-
const settingsManager = SettingsManager.create(cwd);
|
|
2866
|
-
const model = registerOriroMux(modelRegistry);
|
|
2867
|
-
if (!model) throw new Error("ORIRO keyless model unavailable");
|
|
2868
|
-
const resourceLoader = new DefaultResourceLoader({
|
|
2869
|
-
cwd,
|
|
2870
|
-
agentDir: getAgentDir(),
|
|
2871
|
-
settingsManager,
|
|
2872
|
-
additionalSkillPaths: [skillsDir()],
|
|
2873
|
-
extensionFactories: [registerGuardian, registerHead, registerScribe, registerOrchestrator]
|
|
2874
|
-
});
|
|
2875
|
-
await resourceLoader.reload();
|
|
2876
|
-
const { session, extensionsResult } = await createAgentSession2({
|
|
2877
|
-
model,
|
|
2878
|
-
authStorage,
|
|
2879
|
-
modelRegistry,
|
|
2880
|
-
settingsManager,
|
|
2881
|
-
sessionManager: SessionManager2.inMemory(),
|
|
2882
|
-
resourceLoader
|
|
2883
|
-
});
|
|
2884
|
-
attachScribe(session);
|
|
2885
|
-
return { session, extensionsResult };
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
// src/language/nllb-translator.ts
|
|
2889
|
-
var NLLB_CODE = {
|
|
2890
|
-
en: "eng_Latn",
|
|
2891
|
-
zh: "zho_Hans",
|
|
2892
|
-
de: "deu_Latn",
|
|
2893
|
-
es: "spa_Latn",
|
|
2894
|
-
ru: "rus_Cyrl",
|
|
2895
|
-
ko: "kor_Hang",
|
|
2896
|
-
fr: "fra_Latn",
|
|
2897
|
-
ja: "jpn_Jpan",
|
|
2898
|
-
pt: "por_Latn",
|
|
2899
|
-
tr: "tur_Latn",
|
|
2900
|
-
pl: "pol_Latn",
|
|
2901
|
-
ca: "cat_Latn",
|
|
2902
|
-
nl: "nld_Latn",
|
|
2903
|
-
ar: "arb_Arab",
|
|
2904
|
-
sv: "swe_Latn",
|
|
2905
|
-
it: "ita_Latn",
|
|
2906
|
-
id: "ind_Latn",
|
|
2907
|
-
hi: "hin_Deva",
|
|
2908
|
-
fi: "fin_Latn",
|
|
2909
|
-
vi: "vie_Latn",
|
|
2910
|
-
he: "heb_Hebr",
|
|
2911
|
-
uk: "ukr_Cyrl",
|
|
2912
|
-
el: "ell_Grek",
|
|
2913
|
-
ms: "zsm_Latn",
|
|
2914
|
-
cs: "ces_Latn",
|
|
2915
|
-
ro: "ron_Latn",
|
|
2916
|
-
da: "dan_Latn",
|
|
2917
|
-
hu: "hun_Latn",
|
|
2918
|
-
ta: "tam_Taml",
|
|
2919
|
-
no: "nob_Latn",
|
|
2920
|
-
th: "tha_Thai",
|
|
2921
|
-
ur: "urd_Arab",
|
|
2922
|
-
hr: "hrv_Latn",
|
|
2923
|
-
bg: "bul_Cyrl",
|
|
2924
|
-
lt: "lit_Latn",
|
|
2925
|
-
mi: "mri_Latn",
|
|
2926
|
-
ml: "mal_Mlym",
|
|
2927
|
-
cy: "cym_Latn",
|
|
2928
|
-
sk: "slk_Latn",
|
|
2929
|
-
te: "tel_Telu",
|
|
2930
|
-
fa: "pes_Arab",
|
|
2931
|
-
lv: "lvs_Latn",
|
|
2932
|
-
bn: "ben_Beng",
|
|
2933
|
-
sr: "srp_Cyrl",
|
|
2934
|
-
az: "azj_Latn",
|
|
2935
|
-
sl: "slv_Latn",
|
|
2936
|
-
kn: "kan_Knda",
|
|
2937
|
-
et: "est_Latn",
|
|
2938
|
-
mk: "mkd_Cyrl",
|
|
2939
|
-
eu: "eus_Latn",
|
|
2940
|
-
is: "isl_Latn",
|
|
2941
|
-
hy: "hye_Armn",
|
|
2942
|
-
ne: "npi_Deva",
|
|
2943
|
-
mn: "khk_Cyrl",
|
|
2944
|
-
bs: "bos_Latn",
|
|
2945
|
-
kk: "kaz_Cyrl",
|
|
2946
|
-
sq: "als_Latn",
|
|
2947
|
-
sw: "swh_Latn",
|
|
2948
|
-
gl: "glg_Latn",
|
|
2949
|
-
mr: "mar_Deva",
|
|
2950
|
-
pa: "pan_Guru",
|
|
2951
|
-
si: "sin_Sinh",
|
|
2952
|
-
km: "khm_Khmr",
|
|
2953
|
-
sn: "sna_Latn",
|
|
2954
|
-
yo: "yor_Latn",
|
|
2955
|
-
so: "som_Latn",
|
|
2956
|
-
af: "afr_Latn",
|
|
2957
|
-
oc: "oci_Latn",
|
|
2958
|
-
ka: "kat_Geor",
|
|
2959
|
-
be: "bel_Cyrl",
|
|
2960
|
-
tg: "tgk_Cyrl",
|
|
2961
|
-
sd: "snd_Arab",
|
|
2962
|
-
gu: "guj_Gujr",
|
|
2963
|
-
am: "amh_Ethi",
|
|
2964
|
-
yi: "ydd_Hebr",
|
|
2965
|
-
lo: "lao_Laoo",
|
|
2966
|
-
uz: "uzn_Latn",
|
|
2967
|
-
fo: "fao_Latn",
|
|
2968
|
-
ht: "hat_Latn",
|
|
2969
|
-
ps: "pbt_Arab",
|
|
2970
|
-
tk: "tuk_Latn",
|
|
2971
|
-
nn: "nno_Latn",
|
|
2972
|
-
mt: "mlt_Latn",
|
|
2973
|
-
sa: "san_Deva",
|
|
2974
|
-
lb: "ltz_Latn",
|
|
2975
|
-
my: "mya_Mymr",
|
|
2976
|
-
bo: "bod_Tibt",
|
|
2977
|
-
tl: "tgl_Latn",
|
|
2978
|
-
mg: "plt_Latn",
|
|
2979
|
-
as: "asm_Beng",
|
|
2980
|
-
tt: "tat_Cyrl",
|
|
2981
|
-
ln: "lin_Latn",
|
|
2982
|
-
ha: "hau_Latn",
|
|
2983
|
-
ba: "bak_Cyrl",
|
|
2984
|
-
jw: "jav_Latn",
|
|
2985
|
-
su: "sun_Latn",
|
|
2986
|
-
yue: "yue_Hant"
|
|
2987
|
-
};
|
|
2988
|
-
var ENG = "eng_Latn";
|
|
2989
|
-
var toNllb = (iso) => NLLB_CODE[(iso || "").toLowerCase()] ?? ENG;
|
|
2990
|
-
var NllbTranslator = class {
|
|
2991
|
-
pipe = null;
|
|
2992
|
-
loading = null;
|
|
2993
|
-
ready() {
|
|
2994
|
-
return this.pipe !== null;
|
|
2995
|
-
}
|
|
2996
|
-
/** Lazy-load NLLB-200 once (first-use download + cache). Idempotent. */
|
|
2997
|
-
async load(modelId = "Xenova/nllb-200-distilled-600M") {
|
|
2998
|
-
if (this.pipe) return;
|
|
2999
|
-
if (this.loading) return this.loading;
|
|
3000
|
-
this.loading = (async () => {
|
|
3001
|
-
const { pipeline } = await import("@huggingface/transformers");
|
|
3002
|
-
this.pipe = await pipeline("translation", modelId);
|
|
3003
|
-
})();
|
|
3004
|
-
return this.loading;
|
|
3005
|
-
}
|
|
3006
|
-
async run(text, src, tgt) {
|
|
3007
|
-
if (!this.pipe) await this.load();
|
|
3008
|
-
if (!this.pipe) return text;
|
|
3009
|
-
const out = await this.pipe(text, { src_lang: src, tgt_lang: tgt });
|
|
3010
|
-
return out?.[0]?.translation_text?.trim() || text;
|
|
3011
|
-
}
|
|
3012
|
-
toEnglish(text, fromLang) {
|
|
3013
|
-
return this.run(text, toNllb(fromLang), ENG);
|
|
3014
|
-
}
|
|
3015
|
-
fromEnglish(english, toLang) {
|
|
3016
|
-
return this.run(english, ENG, toNllb(toLang));
|
|
3017
|
-
}
|
|
3018
|
-
};
|
|
3019
|
-
var instance = null;
|
|
3020
|
-
function setupNllbTranslator(opts) {
|
|
3021
|
-
if (!instance) {
|
|
3022
|
-
instance = new NllbTranslator();
|
|
3023
|
-
registerTranslator(instance);
|
|
3024
|
-
}
|
|
3025
|
-
if (opts?.preload) void instance.load();
|
|
3026
|
-
return instance;
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
// src/language/gateway.ts
|
|
3030
|
-
var isEnglish2 = (code) => !code || code.toLowerCase().startsWith("en");
|
|
3031
|
-
var isCommand = (text) => text.trimStart().startsWith("/");
|
|
3032
|
-
async function ensureReady() {
|
|
3033
|
-
try {
|
|
3034
|
-
await setupNllbTranslator().load();
|
|
3035
|
-
} catch {
|
|
3036
|
-
}
|
|
3037
|
-
}
|
|
3038
|
-
async function translateIncoming(message) {
|
|
3039
|
-
const lang = getTerminalLanguage().code;
|
|
3040
|
-
if (isEnglish2(lang) || !message.trim() || isCommand(message)) return message;
|
|
3041
|
-
await ensureReady();
|
|
3042
|
-
return translateForCoder(message, lang);
|
|
3043
|
-
}
|
|
3044
|
-
async function translateOutgoing(text) {
|
|
3045
|
-
const lang = getTerminalLanguage().code;
|
|
3046
|
-
if (isEnglish2(lang) || !text.trim()) return text;
|
|
3047
|
-
await ensureReady();
|
|
3048
|
-
return translateForUser(text, lang);
|
|
3049
|
-
}
|
|
3050
|
-
|
|
3051
|
-
// src/repl.ts
|
|
3052
|
-
function replHelp() {
|
|
3053
|
-
return `
|
|
3054
|
-
${accent("ORIRO terminal \u2014 help")}
|
|
3055
|
-
${dim("Just type to chat; ORIRO writes and runs code for you (keyless, free).")}
|
|
3056
|
-
|
|
3057
|
-
${accent("/help")} this help ${accent("/exit")} or ${accent("/quit")} leave ${dim("Ctrl-D / Ctrl-C also exit")}
|
|
3058
|
-
${dim("Run these OUTSIDE the chat (in your shell):")}
|
|
3059
|
-
${dim("oriro skills \xB7 routers \xB7 connectors \xB7 channels \xB7 scribe \xB7 language \xB7 avatar")}
|
|
3060
|
-
|
|
3061
|
-
`;
|
|
3062
|
-
}
|
|
3063
|
-
async function runRepl() {
|
|
3064
|
-
if (isFirstRun()) await runOnboarding();
|
|
3065
|
-
else stdout5.write(banner());
|
|
3066
|
-
const isEnglish3 = getTerminalLanguage().code.toLowerCase().startsWith("en");
|
|
3067
|
-
const { session } = await assembleOriroSession();
|
|
3068
|
-
const rl = createInterface4({ input: stdin4, output: stdout5 });
|
|
3069
|
-
let closing = false;
|
|
3070
|
-
const onSigint = () => {
|
|
3071
|
-
if (closing) return;
|
|
3072
|
-
closing = true;
|
|
3073
|
-
stdout5.write(dim("\nBye.\n"));
|
|
3074
|
-
try {
|
|
3075
|
-
rl.close();
|
|
3076
|
-
} catch {
|
|
3077
|
-
}
|
|
3078
|
-
try {
|
|
3079
|
-
session.dispose();
|
|
3080
|
-
} catch {
|
|
3081
|
-
}
|
|
3082
|
-
process.exit(0);
|
|
3083
|
-
};
|
|
3084
|
-
process.on("SIGINT", onSigint);
|
|
3085
|
-
try {
|
|
3086
|
-
for (; ; ) {
|
|
3087
|
-
let line;
|
|
3088
|
-
try {
|
|
3089
|
-
line = (await rl.question("\u203A ")).trim();
|
|
3090
|
-
} catch {
|
|
3091
|
-
break;
|
|
3092
|
-
}
|
|
3093
|
-
if (!line) continue;
|
|
3094
|
-
const slash = line.toLowerCase();
|
|
3095
|
-
if (slash === "/exit" || slash === "/quit") break;
|
|
3096
|
-
if (slash === "/help" || slash === "/?") {
|
|
3097
|
-
stdout5.write(replHelp());
|
|
3098
|
-
continue;
|
|
3099
|
-
}
|
|
3100
|
-
const english = await translateIncoming(line);
|
|
3101
|
-
noteUserInput(line);
|
|
3102
|
-
let out = "";
|
|
3103
|
-
const unsub = session.subscribe((e) => {
|
|
3104
|
-
if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
|
|
3105
|
-
const d = e.assistantMessageEvent.delta ?? "";
|
|
3106
|
-
out += d;
|
|
3107
|
-
if (isEnglish3) stdout5.write(d);
|
|
3108
|
-
}
|
|
3109
|
-
});
|
|
3110
|
-
try {
|
|
3111
|
-
await session.prompt(english);
|
|
3112
|
-
} finally {
|
|
3113
|
-
unsub();
|
|
3114
|
-
}
|
|
3115
|
-
if (isEnglish3) stdout5.write("\n\n");
|
|
3116
|
-
else stdout5.write(`${await translateOutgoing(out.trim())}
|
|
3117
|
-
|
|
3118
|
-
`);
|
|
3119
|
-
}
|
|
3120
|
-
} finally {
|
|
3121
|
-
process.removeListener("SIGINT", onSigint);
|
|
3122
|
-
if (!closing) {
|
|
3123
|
-
rl.close();
|
|
3124
|
-
session.dispose();
|
|
3125
|
-
stdout5.write(dim("\nBye.\n"));
|
|
3126
|
-
}
|
|
3127
|
-
}
|
|
3128
|
-
}
|
|
3129
|
-
|
|
3130
|
-
// src/routers/catalog.ts
|
|
3131
|
-
var C4 = (e) => ({
|
|
3132
|
-
api: "openai-completions",
|
|
3133
|
-
freeModels: [],
|
|
3134
|
-
tier: "free",
|
|
3135
|
-
kind: "chat",
|
|
3136
|
-
...e
|
|
3137
|
-
});
|
|
3138
|
-
var ROUTER_CATALOG = [
|
|
3139
|
-
// ── Keyless & live-verified (works now, zero keys, through the agent) ──
|
|
3140
|
-
C4({
|
|
3141
|
-
id: "pollinations",
|
|
3142
|
-
displayName: "Pollinations",
|
|
1348
|
+
displayName: "Pollinations",
|
|
3143
1349
|
baseUrl: "https://text.pollinations.ai/openai",
|
|
3144
1350
|
freeModels: ["openai", "mistral"],
|
|
3145
1351
|
obtainUrl: "https://pollinations.ai",
|
|
@@ -3410,66 +1616,2106 @@ var ROUTER_CATALOG = [
|
|
|
3410
1616
|
displayName: "Ollama (local)",
|
|
3411
1617
|
api: "ollama",
|
|
3412
1618
|
baseUrl: "http://localhost:11434/v1",
|
|
3413
|
-
freeModels: ["llama3.2"],
|
|
3414
|
-
keyless: true
|
|
3415
|
-
}),
|
|
3416
|
-
// ── Image / speech services (catalog completeness; not chat-routable by the Mux) ──
|
|
3417
|
-
C4({
|
|
3418
|
-
id: "stability",
|
|
3419
|
-
displayName: "Stability AI",
|
|
3420
|
-
baseUrl: "https://api.stability.ai/v2beta",
|
|
3421
|
-
freeModels: ["stable-image-core"],
|
|
3422
|
-
obtainUrl: "https://platform.stability.ai",
|
|
3423
|
-
kind: "image"
|
|
3424
|
-
}),
|
|
3425
|
-
C4({
|
|
3426
|
-
id: "fal",
|
|
3427
|
-
displayName: "fal.ai",
|
|
3428
|
-
baseUrl: "https://fal.run",
|
|
3429
|
-
freeModels: ["fal-ai/flux/schnell"],
|
|
3430
|
-
obtainUrl: "https://fal.ai",
|
|
3431
|
-
kind: "image"
|
|
3432
|
-
}),
|
|
3433
|
-
C4({
|
|
3434
|
-
id: "wavespeed",
|
|
3435
|
-
displayName: "WaveSpeedAI",
|
|
3436
|
-
baseUrl: "https://api.wavespeed.ai",
|
|
3437
|
-
freeModels: [],
|
|
3438
|
-
obtainUrl: "https://wavespeed.ai",
|
|
3439
|
-
kind: "image"
|
|
3440
|
-
}),
|
|
3441
|
-
C4({
|
|
3442
|
-
id: "ai-horde",
|
|
3443
|
-
displayName: "AI Horde",
|
|
3444
|
-
baseUrl: "https://aihorde.net/api/v2",
|
|
3445
|
-
freeModels: [],
|
|
3446
|
-
obtainUrl: "https://aihorde.net",
|
|
3447
|
-
keyless: true,
|
|
3448
|
-
kind: "image"
|
|
3449
|
-
}),
|
|
3450
|
-
C4({
|
|
3451
|
-
id: "assemblyai",
|
|
3452
|
-
displayName: "AssemblyAI",
|
|
3453
|
-
baseUrl: "https://api.assemblyai.com/v2",
|
|
3454
|
-
freeModels: [],
|
|
3455
|
-
obtainUrl: "https://assemblyai.com",
|
|
3456
|
-
kind: "speech"
|
|
3457
|
-
}),
|
|
3458
|
-
// ── Paid (requires payment/recharge — moved out of free per the CC rule) ──
|
|
3459
|
-
C4({
|
|
3460
|
-
id: "moonshot",
|
|
3461
|
-
displayName: "Moonshot (Direct)",
|
|
3462
|
-
baseUrl: "https://api.moonshot.ai/v1",
|
|
3463
|
-
freeModels: ["kimi-k2.6"],
|
|
3464
|
-
obtainUrl: "https://platform.moonshot.ai",
|
|
3465
|
-
tier: "paid"
|
|
3466
|
-
}),
|
|
3467
|
-
// ── ORIRO models — coming soon, greyed/"(free)", not selectable yet ──
|
|
3468
|
-
C4({ id: "oriro-gauss", displayName: "ORIRO-Gauss", baseUrl: "", comingSoon: true }),
|
|
3469
|
-
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
|
+
}
|
|
3470
2944
|
];
|
|
3471
|
-
|
|
3472
|
-
|
|
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
|
+
}
|
|
3473
3719
|
}
|
|
3474
3720
|
|
|
3475
3721
|
// src/commands/ui.ts
|
|
@@ -3534,6 +3780,117 @@ function registerRoutersCommand(program2) {
|
|
|
3534
3780
|
}
|
|
3535
3781
|
|
|
3536
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
|
+
}
|
|
3537
3894
|
function registerScribeCommand(program2) {
|
|
3538
3895
|
const scribe = program2.command("scribe").description("the consent-gated local work journal (off by default)");
|
|
3539
3896
|
scribe.command("on").description("enable the journal (recorded locally at ~/.oriro/scribe, never leaves your machine)").action(() => {
|
|
@@ -3548,11 +3905,94 @@ function registerScribeCommand(program2) {
|
|
|
3548
3905
|
scribe.command("status").description("show whether the journal is on or off").action(() => {
|
|
3549
3906
|
info(isScribeEnabled() ? "Scriber: ON" : "Scriber: OFF (default)");
|
|
3550
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
|
+
});
|
|
3551
3991
|
}
|
|
3552
3992
|
|
|
3553
3993
|
// src/connectors/connectors.ts
|
|
3554
|
-
import { readFileSync as
|
|
3555
|
-
import { join as
|
|
3994
|
+
import { readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
|
|
3995
|
+
import { join as join19 } from "path";
|
|
3556
3996
|
|
|
3557
3997
|
// src/connectors/catalog.ts
|
|
3558
3998
|
var CONNECTOR_CATALOG = [
|
|
@@ -4542,18 +4982,18 @@ function connectorBySlug(slug) {
|
|
|
4542
4982
|
|
|
4543
4983
|
// src/connectors/connectors.ts
|
|
4544
4984
|
function file2() {
|
|
4545
|
-
return
|
|
4985
|
+
return join19(oriroDir(), "connectors.json");
|
|
4546
4986
|
}
|
|
4547
4987
|
function readAdded() {
|
|
4548
4988
|
try {
|
|
4549
|
-
const v = JSON.parse(
|
|
4989
|
+
const v = JSON.parse(readFileSync19(file2(), "utf8"));
|
|
4550
4990
|
return Array.isArray(v) ? v : [];
|
|
4551
4991
|
} catch {
|
|
4552
4992
|
return [];
|
|
4553
4993
|
}
|
|
4554
4994
|
}
|
|
4555
4995
|
function writeAdded(slugs) {
|
|
4556
|
-
|
|
4996
|
+
writeFileSync14(join19(ensureOriroDir(), "connectors.json"), JSON.stringify([...new Set(slugs)], null, 2), "utf8");
|
|
4557
4997
|
}
|
|
4558
4998
|
function listConnectors(category) {
|
|
4559
4999
|
return category ? CONNECTOR_CATALOG.filter((c) => c.category === category) : CONNECTOR_CATALOG;
|
|
@@ -4620,14 +5060,14 @@ function registerConnectorsCommand(program2) {
|
|
|
4620
5060
|
}
|
|
4621
5061
|
|
|
4622
5062
|
// src/channels/config.ts
|
|
4623
|
-
import { readFileSync as
|
|
4624
|
-
import { join as
|
|
5063
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync15 } from "fs";
|
|
5064
|
+
import { join as join20 } from "path";
|
|
4625
5065
|
function file3() {
|
|
4626
|
-
return
|
|
5066
|
+
return join20(oriroDir(), "channels.json");
|
|
4627
5067
|
}
|
|
4628
5068
|
function readChannels() {
|
|
4629
5069
|
try {
|
|
4630
|
-
const v = JSON.parse(
|
|
5070
|
+
const v = JSON.parse(readFileSync20(file3(), "utf8"));
|
|
4631
5071
|
return Array.isArray(v) ? v : [];
|
|
4632
5072
|
} catch {
|
|
4633
5073
|
return [];
|
|
@@ -4636,10 +5076,10 @@ function readChannels() {
|
|
|
4636
5076
|
function saveChannel(cfg) {
|
|
4637
5077
|
const all = readChannels().filter((c) => c.kind !== cfg.kind);
|
|
4638
5078
|
all.push(cfg);
|
|
4639
|
-
|
|
5079
|
+
writeFileSync15(join20(ensureOriroDir(), "channels.json"), JSON.stringify(all, null, 2), "utf8");
|
|
4640
5080
|
}
|
|
4641
5081
|
function removeChannel(kind) {
|
|
4642
|
-
|
|
5082
|
+
writeFileSync15(join20(ensureOriroDir(), "channels.json"), JSON.stringify(readChannels().filter((c) => c.kind !== kind), null, 2), "utf8");
|
|
4643
5083
|
}
|
|
4644
5084
|
|
|
4645
5085
|
// src/channels/telegram.ts
|
|
@@ -4756,9 +5196,9 @@ async function startDiscord(token) {
|
|
|
4756
5196
|
}
|
|
4757
5197
|
|
|
4758
5198
|
// src/channels/whatsapp.ts
|
|
4759
|
-
import { join as
|
|
5199
|
+
import { join as join21 } from "path";
|
|
4760
5200
|
function whatsappAuthDir() {
|
|
4761
|
-
return
|
|
5201
|
+
return join21(oriroDir(), "whatsapp-auth");
|
|
4762
5202
|
}
|
|
4763
5203
|
async function startWhatsApp() {
|
|
4764
5204
|
let baileys;
|
|
@@ -4893,7 +5333,7 @@ function registerSkillsCommand(program2) {
|
|
|
4893
5333
|
}
|
|
4894
5334
|
|
|
4895
5335
|
// src/commands/language.ts
|
|
4896
|
-
import { stdin as
|
|
5336
|
+
import { stdin as stdin6 } from "process";
|
|
4897
5337
|
function resolveLanguage(input) {
|
|
4898
5338
|
return languageByCode(input) ?? LANGUAGES.find((l) => l.name.toLowerCase() === input.trim().toLowerCase());
|
|
4899
5339
|
}
|
|
@@ -4915,7 +5355,7 @@ function registerLanguageCommand(program2) {
|
|
|
4915
5355
|
ok(`${accent(lang.name)} is now your terminal language.`);
|
|
4916
5356
|
return;
|
|
4917
5357
|
}
|
|
4918
|
-
if (
|
|
5358
|
+
if (stdin6.isTTY) {
|
|
4919
5359
|
const lang = await selectLanguageInteractive();
|
|
4920
5360
|
setTerminalLanguage(lang);
|
|
4921
5361
|
ok(`${accent(lang.name)} is now your terminal language.`);
|
|
@@ -4928,7 +5368,7 @@ function registerLanguageCommand(program2) {
|
|
|
4928
5368
|
}
|
|
4929
5369
|
|
|
4930
5370
|
// src/commands/avatar.ts
|
|
4931
|
-
import { stdin as
|
|
5371
|
+
import { stdin as stdin7 } from "process";
|
|
4932
5372
|
function registerAvatarCommand(program2) {
|
|
4933
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) => {
|
|
4934
5374
|
if (opts.list) {
|
|
@@ -4946,7 +5386,7 @@ function registerAvatarCommand(program2) {
|
|
|
4946
5386
|
ok(`${accent(avatar.slug)} is now your terminal face.`);
|
|
4947
5387
|
return;
|
|
4948
5388
|
}
|
|
4949
|
-
if (
|
|
5389
|
+
if (stdin7.isTTY) {
|
|
4950
5390
|
const chosen = await selectAvatarInteractive();
|
|
4951
5391
|
if (!chosen) {
|
|
4952
5392
|
info("no change.");
|