@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.
Files changed (3) hide show
  1. package/README.md +16 -12
  2. package/dist/cli.js +93 -7
  3. 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 TIER | BYOK** | **Works in 100 Languages | Live Security Gaurdian-V3 (MCP Watch)**
5
+ # **FREE · KEYLESS · BYOK** | **Works in 100 Languages | Deterministic Security Guardian V3**
6
6
 
7
- A terminal coder that **sees the web**, **speaks and listens in 100 languages**, **guards itself**, and can wear a **floating avatar that talks back in its own voice** — all on‑device.
8
-
9
- A free, on-device-friendly terminal AI agent built on the Pi agent harness (used as a library).
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** — type in your language; the model reasons in English; replies come back translated (on-device NLLB).
15
- - **Guardian V3** — a security gate on every tool call, default-on, fail-closed.
16
- - **Head** — goes out, inspects a live site, and reverse-engineers it to code.
17
- - **Scriber** — a consent-gated, self-healing local work journal (never leaves your machine).
18
- - **323 skills**, **multi-agent orchestration** on the free pool, and **MCP connectors**.
19
- - **Channels** — drive ORIRO from Telegram/Discord/WhatsApp with your own bot.
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
- writeAdded(readAdded().filter((s) => s !== slug));
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
- ok(`removed ${accent(slug)}`);
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 = languageByCode(code);
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.2",
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": {