@oriro/orirocli 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -12
- package/dist/cli.js +93 -7
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
# ORIRO‑Terminal - **“Head | Memory | Eyeball for AI”**
|
|
4
4
|
|
|
5
|
-
# **FREE
|
|
5
|
+
# **FREE · KEYLESS · BYOK** | **Works in 100 Languages | Deterministic Security Guardian V3**
|
|
6
6
|
|
|
7
|
-
A
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
A free, keyless terminal AI coder — built on the Pi agent harness (used as a library).
|
|
8
|
+
It **writes and runs code** from a pool of free routers, works in **your language**, **guards every action**,
|
|
9
|
+
can **inspect a live site's structure**, and greets you with an **avatar in its own on-device voice**.
|
|
10
10
|
Your language, your machine, no paid keys required.
|
|
11
11
|
|
|
12
|
-
## What's inside
|
|
13
|
-
- **Keyless free-router Mux** — best-router selection + invisible failover across free providers, with an on-device floor. Never a paid key.
|
|
14
|
-
- **100 languages** —
|
|
15
|
-
- **Guardian V3** — a security gate on every tool call
|
|
16
|
-
- **Head** —
|
|
17
|
-
- **Scriber** — a consent-gated
|
|
18
|
-
- **323 skills
|
|
19
|
-
- **Channels** —
|
|
12
|
+
## What's inside (this release)
|
|
13
|
+
- **Keyless free-router Mux** — best-router selection + invisible failover across free providers, with an on-device floor. **Never a paid key.** BYOK optional (live-validated).
|
|
14
|
+
- **100 languages** — pick yours at first run; the model works in English. On-device NLLB translation is an optional add-on (without it, your text passes through as-is).
|
|
15
|
+
- **Guardian V3 (Lite)** — a **deterministic** security gate on every tool call (default-on, fail-closed): blocks `curl|sh` remote-exec, destructive wipes, reverse shells, and env/secret exfil. No weights, no tokenizer, no download.
|
|
16
|
+
- **Head** — fetches a live site, detects its **sections/structure**, and reports the gaps to build from (the coder writes the code from that report).
|
|
17
|
+
- **Scriber (memory)** — a consent-gated local work journal, **off by default**; turns are recalled across sessions and never leave your machine.
|
|
18
|
+
- **323 skills** (CORE/TAIL tiered) + **multi-agent orchestration** on the free pool.
|
|
19
|
+
- **MCP connector catalog** (59) and **Channels** — run ORIRO from Telegram/Discord/WhatsApp with **your own** bot.
|
|
20
|
+
- **Avatar** — pick a face at onboarding; it greets you aloud in its paired on-device voice.
|
|
21
|
+
|
|
22
|
+
## On the roadmap (not in this release)
|
|
23
|
+
Full-page **screenshot → code** Head (Playwright), the **two-way voice loop** (speak + listen/STT), in-REPL **permission modes**, and **`oriro mcp`** guided setup. Today the Head is fetch/structure-based and voice is the avatar's spoken greeting.
|
|
20
24
|
|
|
21
25
|
## Install
|
|
22
26
|
|
package/dist/cli.js
CHANGED
|
@@ -475,6 +475,10 @@ var REVERSE_SHELL = [
|
|
|
475
475
|
];
|
|
476
476
|
var SECRET_PATHS = /(\.ssh\/id_|\.ssh\/.*_rsa|\.aws\/credentials|\.oriro\/credentials|\.config\/gcloud|\.env(\.|\b)|\.netrc|id_ed25519|\.kube\/config|wallet\.dat|\.gnupg\/)/i;
|
|
477
477
|
var NET_SINK = /\b(curl|wget|nc|ncat|socat|scp|rsync|ftp|tftp|invoke-webrequest|invoke-restmethod)\b/i;
|
|
478
|
+
var ENV_EXFIL = [
|
|
479
|
+
/\$\(\s*(printenv|env)\b/i,
|
|
480
|
+
/\$\(\s*cat\b[^)]*(\.ssh|\.aws|\.env|\.netrc|credential|secret|token|id_rsa|id_ed25519)/i
|
|
481
|
+
];
|
|
478
482
|
var PERSISTENCE = [
|
|
479
483
|
/\bcrontab\b\s+(-|\S+)/i,
|
|
480
484
|
// crontab install
|
|
@@ -542,6 +546,14 @@ var DEFAULT_RULES = [
|
|
|
542
546
|
return null;
|
|
543
547
|
}
|
|
544
548
|
},
|
|
549
|
+
{
|
|
550
|
+
id: "env-exfiltration",
|
|
551
|
+
description: "Block dumping env vars / secret files into a network request (curl \u2026$(printenv SECRET)).",
|
|
552
|
+
match: (c) => {
|
|
553
|
+
const cmd = norm(cmdOf(c));
|
|
554
|
+
return cmd && NET_SINK.test(cmd) && anyMatch(ENV_EXFIL, cmd) ? block("env-exfiltration", "Sending environment variables / secret files off the machine") : null;
|
|
555
|
+
}
|
|
556
|
+
},
|
|
545
557
|
{
|
|
546
558
|
id: "persistence",
|
|
547
559
|
description: "Flag cron/rc/startup/service edits used for Trojan persistence.",
|
|
@@ -1527,6 +1539,48 @@ function scrubMessageIdentity(msg) {
|
|
|
1527
1539
|
};
|
|
1528
1540
|
}
|
|
1529
1541
|
|
|
1542
|
+
// src/routers/tool-sanitize.ts
|
|
1543
|
+
var CONTROL_TOKEN = /<\|[^|]*\|>/g;
|
|
1544
|
+
var RECIPIENT_PREFIX = /^(?:to=)?(?:functions?|tools?|recipient)[.=]/i;
|
|
1545
|
+
var RECIPIENT = /(?:to=)?(?:functions?|tools?|recipient)[.=]([A-Za-z0-9_.:-]+)/i;
|
|
1546
|
+
var CLEAN_NAME = /^[A-Za-z0-9_.:-]+$/;
|
|
1547
|
+
function sanitizeToolName(raw) {
|
|
1548
|
+
if (!raw) return raw;
|
|
1549
|
+
if (!raw.includes("<|") && !RECIPIENT_PREFIX.test(raw)) return raw;
|
|
1550
|
+
const base = (raw.split("<|")[0] ?? "").replace(RECIPIENT_PREFIX, "").trim();
|
|
1551
|
+
if (base && CLEAN_NAME.test(base)) return base;
|
|
1552
|
+
const recip = raw.match(RECIPIENT);
|
|
1553
|
+
if (recip?.[1]) return recip[1];
|
|
1554
|
+
const m = raw.replace(CONTROL_TOKEN, " ").match(/[A-Za-z_][A-Za-z0-9_.:-]*/);
|
|
1555
|
+
return m ? m[0] : raw;
|
|
1556
|
+
}
|
|
1557
|
+
function sanitizeMessageToolCalls(msg) {
|
|
1558
|
+
let changed = false;
|
|
1559
|
+
const content = msg.content.map((c) => {
|
|
1560
|
+
if (c.type === "toolCall") {
|
|
1561
|
+
const name = sanitizeToolName(c.name);
|
|
1562
|
+
if (name !== c.name) {
|
|
1563
|
+
changed = true;
|
|
1564
|
+
return { ...c, name };
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return c;
|
|
1568
|
+
});
|
|
1569
|
+
return changed ? { ...msg, content } : msg;
|
|
1570
|
+
}
|
|
1571
|
+
function sanitizeEventToolCalls(ev) {
|
|
1572
|
+
let next = ev;
|
|
1573
|
+
if ("partial" in next && next.partial) {
|
|
1574
|
+
const partial = sanitizeMessageToolCalls(next.partial);
|
|
1575
|
+
if (partial !== next.partial) next = { ...next, partial };
|
|
1576
|
+
}
|
|
1577
|
+
if (next.type === "toolcall_end" && next.toolCall) {
|
|
1578
|
+
const name = sanitizeToolName(next.toolCall.name);
|
|
1579
|
+
if (name !== next.toolCall.name) next = { ...next, toolCall: { ...next.toolCall, name } };
|
|
1580
|
+
}
|
|
1581
|
+
return next;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1530
1584
|
// src/routers/mux-provider.ts
|
|
1531
1585
|
var MUX_PROVIDER = "oriro-mux";
|
|
1532
1586
|
var MUX_MODEL = "oriro-free";
|
|
@@ -1576,17 +1630,17 @@ async function driveMux(out, mux, byId, context, options) {
|
|
|
1576
1630
|
committed = true;
|
|
1577
1631
|
if (ev.type === "done") {
|
|
1578
1632
|
mux.recordSuccess(id, Date.now() - t0);
|
|
1579
|
-
const clean = scrubMessageIdentity(ev.message);
|
|
1633
|
+
const clean = sanitizeMessageToolCalls(scrubMessageIdentity(ev.message));
|
|
1580
1634
|
out.push({ type: "done", reason: ev.reason, message: clean });
|
|
1581
1635
|
out.end(clean);
|
|
1582
1636
|
return;
|
|
1583
1637
|
}
|
|
1584
1638
|
lastPartial = ev.partial;
|
|
1585
|
-
out.push(ev);
|
|
1639
|
+
out.push(sanitizeEventToolCalls(ev));
|
|
1586
1640
|
}
|
|
1587
1641
|
if (failedBeforeContent) continue;
|
|
1588
1642
|
mux.recordSuccess(id, Date.now() - t0);
|
|
1589
|
-
out.end(lastPartial ? scrubMessageIdentity(lastPartial) : void 0);
|
|
1643
|
+
out.end(lastPartial ? sanitizeMessageToolCalls(scrubMessageIdentity(lastPartial)) : void 0);
|
|
1590
1644
|
return;
|
|
1591
1645
|
} catch (e) {
|
|
1592
1646
|
mux.recordFailure(id, e);
|
|
@@ -2771,6 +2825,17 @@ function setupNllbTranslator(opts) {
|
|
|
2771
2825
|
}
|
|
2772
2826
|
|
|
2773
2827
|
// src/repl.ts
|
|
2828
|
+
function replHelp() {
|
|
2829
|
+
return `
|
|
2830
|
+
${accent("ORIRO terminal \u2014 help")}
|
|
2831
|
+
${dim("Just type to chat; ORIRO writes and runs code for you (keyless, free).")}
|
|
2832
|
+
|
|
2833
|
+
${accent("/help")} this help ${accent("/exit")} or ${accent("/quit")} leave ${dim("Ctrl-D / Ctrl-C also exit")}
|
|
2834
|
+
${dim("Run these OUTSIDE the chat (in your shell):")}
|
|
2835
|
+
${dim("oriro skills \xB7 routers \xB7 connectors \xB7 channels \xB7 scribe \xB7 language \xB7 avatar")}
|
|
2836
|
+
|
|
2837
|
+
`;
|
|
2838
|
+
}
|
|
2774
2839
|
async function runRepl() {
|
|
2775
2840
|
if (isFirstRun()) await runOnboarding();
|
|
2776
2841
|
else stdout4.write(banner());
|
|
@@ -2789,6 +2854,10 @@ async function runRepl() {
|
|
|
2789
2854
|
}
|
|
2790
2855
|
if (!line) continue;
|
|
2791
2856
|
if (line === "/exit" || line === "/quit") break;
|
|
2857
|
+
if (line === "/help" || line === "/?") {
|
|
2858
|
+
stdout4.write(replHelp());
|
|
2859
|
+
continue;
|
|
2860
|
+
}
|
|
2792
2861
|
const english = await translateForCoder(line, lang);
|
|
2793
2862
|
let out = "";
|
|
2794
2863
|
const unsub = session.subscribe((e) => {
|
|
@@ -4242,6 +4311,9 @@ function writeAdded(slugs) {
|
|
|
4242
4311
|
function listConnectors(category) {
|
|
4243
4312
|
return category ? CONNECTOR_CATALOG.filter((c) => c.category === category) : CONNECTOR_CATALOG;
|
|
4244
4313
|
}
|
|
4314
|
+
function connectorCategories() {
|
|
4315
|
+
return [...new Set(CONNECTOR_CATALOG.map((c) => c.category))].sort();
|
|
4316
|
+
}
|
|
4245
4317
|
function addConnector(slug) {
|
|
4246
4318
|
const entry = connectorBySlug(slug);
|
|
4247
4319
|
if (!entry) return { ok: false, error: `unknown connector '${slug}' \u2014 run \`oriro connectors list\`` };
|
|
@@ -4255,13 +4327,20 @@ function addedConnectors() {
|
|
|
4255
4327
|
return CONNECTOR_CATALOG.filter((c) => added.has(c.slug));
|
|
4256
4328
|
}
|
|
4257
4329
|
function removeConnector(slug) {
|
|
4258
|
-
|
|
4330
|
+
const before = readAdded();
|
|
4331
|
+
if (!before.includes(slug)) return false;
|
|
4332
|
+
writeAdded(before.filter((s) => s !== slug));
|
|
4333
|
+
return true;
|
|
4259
4334
|
}
|
|
4260
4335
|
|
|
4261
4336
|
// src/commands/connectors.ts
|
|
4262
4337
|
function registerConnectorsCommand(program2) {
|
|
4263
4338
|
const connectors = program2.command("connectors").description("MCP connectors \u2014 add external tools/services (inert until used)");
|
|
4264
4339
|
connectors.command("list [category]").description("list the connector catalog (optionally filtered by category)").action((category) => {
|
|
4340
|
+
if (category && !connectorCategories().includes(category)) {
|
|
4341
|
+
info(`unknown category '${category}' \u2014 categories: ${connectorCategories().join(", ")}`);
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4265
4344
|
const entries = listConnectors(category);
|
|
4266
4345
|
const added = new Set(addedConnectors().map((c) => c.slug));
|
|
4267
4346
|
heading(category ? `Connectors \xB7 ${category}` : "Connectors");
|
|
@@ -4278,8 +4357,8 @@ function registerConnectorsCommand(program2) {
|
|
|
4278
4357
|
ok(`added ${accent(slug)} \u2014 inert until a session uses it`);
|
|
4279
4358
|
});
|
|
4280
4359
|
connectors.command("remove <slug>").description("remove a connector").action((slug) => {
|
|
4281
|
-
removeConnector(slug);
|
|
4282
|
-
|
|
4360
|
+
if (removeConnector(slug)) ok(`removed ${accent(slug)}`);
|
|
4361
|
+
else info(`'${slug}' is not in your added list \u2014 nothing to remove`);
|
|
4283
4362
|
});
|
|
4284
4363
|
}
|
|
4285
4364
|
|
|
@@ -4529,6 +4608,10 @@ function registerChannelsCommand(program2) {
|
|
|
4529
4608
|
});
|
|
4530
4609
|
channels.command("remove <kind>").description("remove a configured channel").action((kind) => {
|
|
4531
4610
|
if (!isKind(kind)) die(`unknown channel '${kind}' \u2014 one of: ${KINDS.join(", ")}`);
|
|
4611
|
+
if (!readChannels().some((c) => c.kind === kind)) {
|
|
4612
|
+
info(`no ${kind} channel configured \u2014 nothing to remove`);
|
|
4613
|
+
return;
|
|
4614
|
+
}
|
|
4532
4615
|
removeChannel(kind);
|
|
4533
4616
|
ok(`removed ${accent(kind)}`);
|
|
4534
4617
|
});
|
|
@@ -4553,6 +4636,9 @@ function registerSkillsCommand(program2) {
|
|
|
4553
4636
|
|
|
4554
4637
|
// src/commands/language.ts
|
|
4555
4638
|
import { stdin as stdin5 } from "process";
|
|
4639
|
+
function resolveLanguage(input) {
|
|
4640
|
+
return languageByCode(input) ?? LANGUAGES.find((l) => l.name.toLowerCase() === input.trim().toLowerCase());
|
|
4641
|
+
}
|
|
4556
4642
|
function registerLanguageCommand(program2) {
|
|
4557
4643
|
program2.command("language").description("show or change your terminal language").argument("[code]", "switch directly to this language (ISO code or name, e.g. es)").option("-a, --all", "list every available language").action(async (code, opts) => {
|
|
4558
4644
|
if (opts.all) {
|
|
@@ -4565,7 +4651,7 @@ function registerLanguageCommand(program2) {
|
|
|
4565
4651
|
return;
|
|
4566
4652
|
}
|
|
4567
4653
|
if (code) {
|
|
4568
|
-
const lang =
|
|
4654
|
+
const lang = resolveLanguage(code);
|
|
4569
4655
|
if (!lang) die(`unknown language '${code}' \u2014 run \`oriro language --all\` to see the list`);
|
|
4570
4656
|
setTerminalLanguage(lang);
|
|
4571
4657
|
ok(`${accent(lang.name)} is now your terminal language.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oriro/orirocli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "ORIRO — a free, on-device-friendly terminal AI agent. Built on the Pi agent harness (used as a library).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
"dev": "tsx src/cli.ts",
|
|
23
23
|
"build": "tsup",
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test:unit": "tsx scripts/test-tool-sanitize.ts && tsx scripts/test-guardian.ts",
|
|
25
26
|
"smoke": "npm run build && node scripts/smoke.mjs",
|
|
26
|
-
"prepublishOnly": "npm run build && node scripts/smoke.mjs && node scripts/prepublish-check.mjs",
|
|
27
|
+
"prepublishOnly": "npm run build && npm run test:unit && node scripts/smoke.mjs && node scripts/prepublish-check.mjs",
|
|
27
28
|
"start": "node dist/cli.js"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|