@oriro/orirocli 0.3.4 → 0.3.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/dist/cli.js +379 -56
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -4512,6 +4512,95 @@ ${extra}` } : ctx;
|
|
|
4512
4512
|
return registry.find(MUX_PROVIDER, MUX_MODEL);
|
|
4513
4513
|
}
|
|
4514
4514
|
|
|
4515
|
+
// src/repl-ui/permission.ts
|
|
4516
|
+
var MODES = ["manual", "accept_edits", "auto", "plan"];
|
|
4517
|
+
var MODE_META = {
|
|
4518
|
+
manual: { label: "Manual", indicator: "\u25CF" },
|
|
4519
|
+
accept_edits: { label: "Accept Edits", indicator: "\u270E" },
|
|
4520
|
+
auto: { label: "Auto", indicator: "\u23F5\u23F5" },
|
|
4521
|
+
plan: { label: "Plan", indicator: "\u25A2" }
|
|
4522
|
+
};
|
|
4523
|
+
var current2 = "manual";
|
|
4524
|
+
function getMode() {
|
|
4525
|
+
return current2;
|
|
4526
|
+
}
|
|
4527
|
+
function setMode(m) {
|
|
4528
|
+
current2 = m;
|
|
4529
|
+
}
|
|
4530
|
+
function cycleMode() {
|
|
4531
|
+
const i = MODES.indexOf(current2);
|
|
4532
|
+
current2 = MODES[(i + 1) % MODES.length];
|
|
4533
|
+
return current2;
|
|
4534
|
+
}
|
|
4535
|
+
var thinking = false;
|
|
4536
|
+
function getThinking() {
|
|
4537
|
+
return thinking;
|
|
4538
|
+
}
|
|
4539
|
+
function toggleThinking() {
|
|
4540
|
+
thinking = !thinking;
|
|
4541
|
+
return thinking;
|
|
4542
|
+
}
|
|
4543
|
+
var THINKING_PRIMER = "Think step by step and plan your approach before acting. Reason carefully and check your work.";
|
|
4544
|
+
function classifyTool(toolName) {
|
|
4545
|
+
const n = toolName.toLowerCase();
|
|
4546
|
+
if (/(^|_)(read|ls|grep|find|glob|inspect|view|cat|list)/.test(n)) return "read";
|
|
4547
|
+
if (/(^|_)(edit|write|apply|patch|create|update|str_replace|multiedit)/.test(n)) return "edit";
|
|
4548
|
+
if (/(^|_)(bash|shell|exec|run|terminal|command|sh)/.test(n)) return "exec";
|
|
4549
|
+
return "other";
|
|
4550
|
+
}
|
|
4551
|
+
function decideTool(opts) {
|
|
4552
|
+
const mode = opts.mode ?? current2;
|
|
4553
|
+
if (opts.guardianBlocked) return { decision: "block", reason: "ORIRO Guardian" };
|
|
4554
|
+
const kind = classifyTool(opts.toolName);
|
|
4555
|
+
if (mode === "plan") {
|
|
4556
|
+
return kind === "read" ? { decision: "allow" } : { decision: "block", reason: "Plan mode is read-only" };
|
|
4557
|
+
}
|
|
4558
|
+
if (mode === "manual") {
|
|
4559
|
+
return kind === "read" ? { decision: "allow" } : { decision: "ask" };
|
|
4560
|
+
}
|
|
4561
|
+
if (mode === "accept_edits") {
|
|
4562
|
+
if (kind === "read" || kind === "edit") return { decision: "allow" };
|
|
4563
|
+
return { decision: "ask" };
|
|
4564
|
+
}
|
|
4565
|
+
return { decision: "allow" };
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
// src/repl-ui/posture-gate.ts
|
|
4569
|
+
var armed = false;
|
|
4570
|
+
function armPostureGate() {
|
|
4571
|
+
armed = true;
|
|
4572
|
+
}
|
|
4573
|
+
function bypassPosture(depthEnv) {
|
|
4574
|
+
const d = Number(depthEnv);
|
|
4575
|
+
return Number.isFinite(d) && d > 0;
|
|
4576
|
+
}
|
|
4577
|
+
function registerPostureGate(pi) {
|
|
4578
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
4579
|
+
if (bypassPosture(process.env.ORIRO_AGENT_DEPTH)) return void 0;
|
|
4580
|
+
const d = decideTool({ toolName: event.toolName, guardianBlocked: false });
|
|
4581
|
+
if (d.decision === "block") {
|
|
4582
|
+
return {
|
|
4583
|
+
block: true,
|
|
4584
|
+
reason: `\u25A2 ${d.reason ?? "blocked by posture"} \u2014 present the plan as text; the user will /approve to execute`
|
|
4585
|
+
};
|
|
4586
|
+
}
|
|
4587
|
+
if (d.decision === "ask" && armed) {
|
|
4588
|
+
if (!ctx.hasUI) {
|
|
4589
|
+
return { block: true, reason: `posture '${getMode()}' requires approval and no UI is available` };
|
|
4590
|
+
}
|
|
4591
|
+
const choice = await ctx.ui.select(
|
|
4592
|
+
`\u25CF Posture '${getMode()}' \u2014 approve this action?
|
|
4593
|
+
Tool: ${event.toolName}
|
|
4594
|
+
|
|
4595
|
+
(Shift+Tab cycles postures; \u23F5\u23F5 Auto stops asking)`,
|
|
4596
|
+
["Allow once", "Deny"]
|
|
4597
|
+
);
|
|
4598
|
+
return choice === "Allow once" ? void 0 : { block: true, reason: "Denied by user (posture gate)" };
|
|
4599
|
+
}
|
|
4600
|
+
return void 0;
|
|
4601
|
+
});
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4515
4604
|
// src/head/pi-tool.ts
|
|
4516
4605
|
import { Type as Type2 } from "typebox";
|
|
4517
4606
|
|
|
@@ -5977,6 +6066,7 @@ async function assembleOriroSession(opts = {}) {
|
|
|
5977
6066
|
// bundled library + the user's own ~/.oriro/skills
|
|
5978
6067
|
extensionFactories: [
|
|
5979
6068
|
registerGuardian,
|
|
6069
|
+
registerPostureGate,
|
|
5980
6070
|
registerHead,
|
|
5981
6071
|
registerScribe,
|
|
5982
6072
|
registerOrchestrator,
|
|
@@ -6165,32 +6255,37 @@ async function translateOutgoing(text) {
|
|
|
6165
6255
|
// src/repl-ui/tui-repl.ts
|
|
6166
6256
|
import { ProcessTerminal, TUI, Editor, Text, Container } from "@earendil-works/pi-tui";
|
|
6167
6257
|
|
|
6168
|
-
// src/repl-ui/
|
|
6169
|
-
var
|
|
6170
|
-
var
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
function
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
}
|
|
6185
|
-
|
|
6186
|
-
function
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6258
|
+
// src/repl-ui/plan-mode.ts
|
|
6259
|
+
var PLAN_PRIMER = "PLAN MODE \u2014 read-only. Produce a concrete implementation plan for the request below: numbered steps, the exact files to change and how, the commands to run, and the risks. Do NOT make any changes \u2014 no edits, no writes, no commands (write/exec tools are blocked in this mode). Finish with a short 'Verify' list of what will prove the work is correct after execution.";
|
|
6260
|
+
var EXECUTE_PROMPT = "APPROVED: the plan you presented above has been approved by the user. Execute it now, step by step, exactly as written \u2014 implement, run, and verify each step. Do not re-plan and do not ask for approval again; Guardian still protects against dangerous actions.";
|
|
6261
|
+
var prevMode = "manual";
|
|
6262
|
+
var ready = false;
|
|
6263
|
+
function enterPlan(from) {
|
|
6264
|
+
if (from !== "plan") prevMode = from;
|
|
6265
|
+
ready = false;
|
|
6266
|
+
}
|
|
6267
|
+
function notePlanOutput(output) {
|
|
6268
|
+
ready = output.trim().length > 0;
|
|
6269
|
+
return ready;
|
|
6270
|
+
}
|
|
6271
|
+
function approvePlan() {
|
|
6272
|
+
if (!ready) return { ok: false, reason: "no plan is waiting for approval \u2014 /plan <task> first" };
|
|
6273
|
+
ready = false;
|
|
6274
|
+
return { ok: true, restoreMode: prevMode, prompt: EXECUTE_PROMPT };
|
|
6275
|
+
}
|
|
6276
|
+
function rejectPlan() {
|
|
6277
|
+
const had = ready;
|
|
6278
|
+
ready = false;
|
|
6279
|
+
return had;
|
|
6280
|
+
}
|
|
6281
|
+
function parsePlanSlash(line) {
|
|
6282
|
+
const m = /^\/(plan|approve|reject)(?:\s+(\S[\s\S]*))?$/i.exec(line.trim());
|
|
6283
|
+
if (!m) return void 0;
|
|
6284
|
+
const cmd = m[1].toLowerCase();
|
|
6285
|
+
if (cmd === "plan") return m[2] ? { cmd: "plan", task: m[2].trim() } : { cmd: "plan" };
|
|
6286
|
+
if (cmd === "approve") return { cmd: "approve" };
|
|
6287
|
+
return { cmd: "reject" };
|
|
6192
6288
|
}
|
|
6193
|
-
var THINKING_PRIMER = "Think step by step and plan your approach before acting. Reason carefully and check your work.";
|
|
6194
6289
|
|
|
6195
6290
|
// src/repl-ui/verify-actions.ts
|
|
6196
6291
|
import { existsSync as existsSync15 } from "fs";
|
|
@@ -6634,6 +6729,136 @@ async function handleUndo(session) {
|
|
|
6634
6729
|
}
|
|
6635
6730
|
}
|
|
6636
6731
|
|
|
6732
|
+
// src/agents/worktree.ts
|
|
6733
|
+
import { execFile } from "child_process";
|
|
6734
|
+
import { promisify } from "util";
|
|
6735
|
+
import { join as join27, basename as basename2 } from "path";
|
|
6736
|
+
var run = promisify(execFile);
|
|
6737
|
+
var MAX_FAN = 4;
|
|
6738
|
+
function parseAgentsSlash(line) {
|
|
6739
|
+
const m = /^\/agents(?:\s+(\S[\s\S]*))?$/i.exec(line.trim());
|
|
6740
|
+
if (!m) return void 0;
|
|
6741
|
+
const rest = m[1]?.trim();
|
|
6742
|
+
if (!rest) return { cmd: "help" };
|
|
6743
|
+
const nx = /^(\d+)x\s+(\S[\s\S]*)$/i.exec(rest);
|
|
6744
|
+
if (nx) {
|
|
6745
|
+
const n = Math.min(Math.max(Number(nx[1]), 1), MAX_FAN);
|
|
6746
|
+
return { cmd: "fan", tasks: Array.from({ length: n }, () => nx[2].trim()) };
|
|
6747
|
+
}
|
|
6748
|
+
const tasks = rest.split("|").map((s) => s.trim()).filter(Boolean).slice(0, MAX_FAN);
|
|
6749
|
+
return tasks.length ? { cmd: "fan", tasks } : { cmd: "help" };
|
|
6750
|
+
}
|
|
6751
|
+
function fanStamp(now) {
|
|
6752
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
6753
|
+
return `${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|
|
6754
|
+
}
|
|
6755
|
+
function fanBranch(stamp, i) {
|
|
6756
|
+
return `oriro/agents/${stamp}-a${i + 1}`;
|
|
6757
|
+
}
|
|
6758
|
+
function fanDir(repoRoot, stamp, i) {
|
|
6759
|
+
return join27(oriroDir(), "worktrees", `${basename2(repoRoot)}-${stamp}-a${i + 1}`);
|
|
6760
|
+
}
|
|
6761
|
+
async function git(cwd, ...args) {
|
|
6762
|
+
try {
|
|
6763
|
+
const { stdout: stdout12 } = await run("git", ["-C", cwd, ...args], { windowsHide: true });
|
|
6764
|
+
return { ok: true, out: stdout12.trim() };
|
|
6765
|
+
} catch (e) {
|
|
6766
|
+
return { ok: false, out: e instanceof Error ? e.message : String(e) };
|
|
6767
|
+
}
|
|
6768
|
+
}
|
|
6769
|
+
async function gitRoot(cwd) {
|
|
6770
|
+
const r = await git(cwd, "rev-parse", "--show-toplevel");
|
|
6771
|
+
return r.ok && r.out ? r.out : void 0;
|
|
6772
|
+
}
|
|
6773
|
+
async function addWorktree(root, dir, branch) {
|
|
6774
|
+
const r = await git(root, "worktree", "add", "-b", branch, dir);
|
|
6775
|
+
return r.ok ? void 0 : r.out;
|
|
6776
|
+
}
|
|
6777
|
+
async function changedFiles(dir) {
|
|
6778
|
+
const r = await git(dir, "status", "--short");
|
|
6779
|
+
return r.ok && r.out ? r.out.split("\n").map((s) => s.trim()).filter(Boolean) : [];
|
|
6780
|
+
}
|
|
6781
|
+
async function removeWorktree(root, dir, branch, force = false) {
|
|
6782
|
+
await git(root, "worktree", "remove", ...force ? ["--force"] : [], dir);
|
|
6783
|
+
if (branch) await git(root, "branch", "-D", branch);
|
|
6784
|
+
}
|
|
6785
|
+
var SNIPPET = 400;
|
|
6786
|
+
function formatFanReport(reports) {
|
|
6787
|
+
const lines = [];
|
|
6788
|
+
for (const r of reports) {
|
|
6789
|
+
lines.push(` \u2692 ${r.role} ${r.ok ? "\u2713" : "\u2717"} \u2014 ${r.task.length > 70 ? `${r.task.slice(0, 70)}\u2026` : r.task}`);
|
|
6790
|
+
const snip = r.output.length > SNIPPET ? `${r.output.slice(0, SNIPPET)}\u2026` : r.output;
|
|
6791
|
+
if (snip) lines.push(...snip.split("\n").map((l) => ` ${l}`));
|
|
6792
|
+
if (r.dir && r.branch && r.changes?.length) {
|
|
6793
|
+
lines.push(` \u270E ${r.changes.length} file${r.changes.length === 1 ? "" : "s"} changed on ${r.branch}`);
|
|
6794
|
+
lines.push(` review: cd "${r.dir}" \xB7 keep: commit there, then \`git merge ${r.branch}\` here`);
|
|
6795
|
+
} else if (r.changes && r.changes.length === 0) {
|
|
6796
|
+
lines.push(" (no file changes \u2014 worktree cleaned up)");
|
|
6797
|
+
}
|
|
6798
|
+
}
|
|
6799
|
+
const kept = reports.filter((r) => r.dir).length;
|
|
6800
|
+
lines.push(` \u2692 fan-out done: ${reports.filter((r) => r.ok).length}/${reports.length} ok${kept ? ` \xB7 ${kept} worktree${kept === 1 ? "" : "s"} kept for review` : ""}`);
|
|
6801
|
+
return lines;
|
|
6802
|
+
}
|
|
6803
|
+
|
|
6804
|
+
// src/agents/fanout.ts
|
|
6805
|
+
var CONCURRENCY = 2;
|
|
6806
|
+
function defFor(role, task) {
|
|
6807
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6808
|
+
return { name: `fan-${role}`, task, createdAt: now, updatedAt: now };
|
|
6809
|
+
}
|
|
6810
|
+
async function runFanout(tasks, cwd) {
|
|
6811
|
+
const capped = tasks.slice(0, MAX_FAN);
|
|
6812
|
+
const root = await gitRoot(cwd);
|
|
6813
|
+
const stamp = fanStamp(/* @__PURE__ */ new Date());
|
|
6814
|
+
const prevDepth = process.env.ORIRO_AGENT_DEPTH;
|
|
6815
|
+
process.env.ORIRO_AGENT_DEPTH = String((Number(prevDepth) || 0) + 1);
|
|
6816
|
+
let reports;
|
|
6817
|
+
try {
|
|
6818
|
+
reports = await runPool(capped.map((task, i) => ({ task, i })), CONCURRENCY, async ({ task, i }) => {
|
|
6819
|
+
const role = `a${i + 1}`;
|
|
6820
|
+
if (!root) {
|
|
6821
|
+
const r2 = await runAgent2(defFor(role, task), { cwd });
|
|
6822
|
+
return { role, task, ok: r2.ok, output: r2.output };
|
|
6823
|
+
}
|
|
6824
|
+
const dir = fanDir(root, stamp, i);
|
|
6825
|
+
const branch = fanBranch(stamp, i);
|
|
6826
|
+
const err = await addWorktree(root, dir, branch);
|
|
6827
|
+
if (err) return { role, task, ok: false, output: `could not create worktree: ${err}` };
|
|
6828
|
+
const r = await runAgent2(defFor(role, task), { cwd: dir });
|
|
6829
|
+
const changes = await changedFiles(dir);
|
|
6830
|
+
if (changes.length === 0) {
|
|
6831
|
+
await removeWorktree(root, dir, branch);
|
|
6832
|
+
return { role, task, ok: r.ok, output: r.output, changes: [] };
|
|
6833
|
+
}
|
|
6834
|
+
return { role, task, ok: r.ok, output: r.output, dir, branch, changes };
|
|
6835
|
+
});
|
|
6836
|
+
} finally {
|
|
6837
|
+
if (prevDepth === void 0) delete process.env.ORIRO_AGENT_DEPTH;
|
|
6838
|
+
else process.env.ORIRO_AGENT_DEPTH = prevDepth;
|
|
6839
|
+
}
|
|
6840
|
+
const lines = formatFanReport(reports);
|
|
6841
|
+
if (!root) lines.unshift(" \u2692 not a git repo \u2014 agents ran in the SAME directory (no worktree isolation)");
|
|
6842
|
+
return lines;
|
|
6843
|
+
}
|
|
6844
|
+
|
|
6845
|
+
// src/repl-ui/slash-agents.ts
|
|
6846
|
+
function isAgentsSlash(slash) {
|
|
6847
|
+
return parseAgentsSlash(slash) !== void 0;
|
|
6848
|
+
}
|
|
6849
|
+
async function handleAgents(line) {
|
|
6850
|
+
const p = parseAgentsSlash(line);
|
|
6851
|
+
if (!p || p.cmd === "help") {
|
|
6852
|
+
return [
|
|
6853
|
+
` ${accent("/agents")} ${dim("\u2014 parallel sub-agents in isolated git worktrees (results merged here)")}`,
|
|
6854
|
+
` ${accent("/agents 3x <task>")} ${dim("three agents race the same task")}`,
|
|
6855
|
+
` ${accent("/agents <task A> | <task B>")} ${dim(`different tasks in parallel (max ${MAX_FAN}; '|' separates tasks)`)}`,
|
|
6856
|
+
` ${dim("each agent gets its own worktree + branch; clean ones are removed, changed ones kept for review")}`
|
|
6857
|
+
];
|
|
6858
|
+
}
|
|
6859
|
+
return runFanout(p.tasks, process.cwd());
|
|
6860
|
+
}
|
|
6861
|
+
|
|
6637
6862
|
// src/repl-ui/tui-repl.ts
|
|
6638
6863
|
var editorTheme = {
|
|
6639
6864
|
borderColor: (s) => dim(s),
|
|
@@ -6656,6 +6881,7 @@ function footerText() {
|
|
|
6656
6881
|
return `${bar} ${think} ${dim("Shift+Tab posture \xB7 Alt+Shift+T thinking \xB7 /exit")}`;
|
|
6657
6882
|
}
|
|
6658
6883
|
async function runTuiRepl(session) {
|
|
6884
|
+
armPostureGate();
|
|
6659
6885
|
const isEnglish3 = getTerminalLanguage().code.toLowerCase().startsWith("en");
|
|
6660
6886
|
const term = new ProcessTerminal();
|
|
6661
6887
|
const tui = new TUI(term, true);
|
|
@@ -6675,7 +6901,8 @@ async function runTuiRepl(session) {
|
|
|
6675
6901
|
};
|
|
6676
6902
|
const removeListener = tui.addInputListener((data) => {
|
|
6677
6903
|
if (data === "\x1B[Z") {
|
|
6678
|
-
|
|
6904
|
+
const before = getMode();
|
|
6905
|
+
if (cycleMode() === "plan") enterPlan(before);
|
|
6679
6906
|
refreshFooter();
|
|
6680
6907
|
return { consume: true };
|
|
6681
6908
|
}
|
|
@@ -6718,6 +6945,7 @@ async function runTuiRepl(session) {
|
|
|
6718
6945
|
` ${accent("/routers")} pool add\xB7rotate ${accent("/model")} <id\u2026> switch ${accent("/usage")} health ${accent("/trace")} tool+router activity ${accent("/compact")} free context`,
|
|
6719
6946
|
` ${accent("/review")} artifacts ${accent("/save")} <n> [path] ${accent("/init")} AGENTS.md ${accent("/skills")} ${accent("/connectors")} ${accent("/voice")}`,
|
|
6720
6947
|
` ${accent("/sessions")} list saved ${accent("/undo")} rewind a turn ${dim("resume:")} ${accent("oriro -c")} / ${accent("oriro --resume <id>")}`,
|
|
6948
|
+
` ${accent("/plan")} <task> plan read-only ${accent("/approve")} execute it ${accent("/reject")} discard ${accent("/agents")} parallel worktree fan-out`,
|
|
6721
6949
|
` ${dim("Shift+Tab")} posture ${dim("Alt+Shift+T")} thinking ${accent("/help")} ${accent("/exit")}`
|
|
6722
6950
|
].join("\n");
|
|
6723
6951
|
chat.addChild(new Text(help, 0, 0));
|
|
@@ -6798,6 +7026,53 @@ async function runTuiRepl(session) {
|
|
|
6798
7026
|
})();
|
|
6799
7027
|
return;
|
|
6800
7028
|
}
|
|
7029
|
+
if (isAgentsSlash(slash)) {
|
|
7030
|
+
editor.setText("");
|
|
7031
|
+
const pending = new Text(dim(" \u2692 deploying agents\u2026"), 0, 0);
|
|
7032
|
+
chat.addChild(pending);
|
|
7033
|
+
tui.requestRender();
|
|
7034
|
+
void (async () => {
|
|
7035
|
+
const lines = await handleAgents(text);
|
|
7036
|
+
pending.setText(lines.join("\n"));
|
|
7037
|
+
tui.requestRender();
|
|
7038
|
+
})();
|
|
7039
|
+
return;
|
|
7040
|
+
}
|
|
7041
|
+
const plan = parsePlanSlash(text);
|
|
7042
|
+
let internalPrompt;
|
|
7043
|
+
let turnText = text;
|
|
7044
|
+
if (plan) {
|
|
7045
|
+
if (plan.cmd === "reject") {
|
|
7046
|
+
const had = rejectPlan();
|
|
7047
|
+
chat.addChild(new Text(dim(had ? " \u25A2 plan discarded \u2014 refine the request (still in Plan) or Shift+Tab out" : " \u25A2 nothing to reject \u2014 no plan is waiting"), 0, 0));
|
|
7048
|
+
editor.setText("");
|
|
7049
|
+
tui.requestRender();
|
|
7050
|
+
return;
|
|
7051
|
+
}
|
|
7052
|
+
if (plan.cmd === "approve") {
|
|
7053
|
+
const r = approvePlan();
|
|
7054
|
+
if (!r.ok) {
|
|
7055
|
+
chat.addChild(new Text(dim(` \u25A2 ${r.reason}`), 0, 0));
|
|
7056
|
+
editor.setText("");
|
|
7057
|
+
tui.requestRender();
|
|
7058
|
+
return;
|
|
7059
|
+
}
|
|
7060
|
+
setMode(r.restoreMode);
|
|
7061
|
+
refreshFooter();
|
|
7062
|
+
internalPrompt = r.prompt;
|
|
7063
|
+
} else {
|
|
7064
|
+
enterPlan(getMode());
|
|
7065
|
+
setMode("plan");
|
|
7066
|
+
refreshFooter();
|
|
7067
|
+
if (!plan.task) {
|
|
7068
|
+
chat.addChild(new Text(dim(" \u25A2 Plan mode \u2014 describe the task and I'll plan it (read-only). Then ") + accent("/approve") + dim(" to execute \xB7 ") + accent("/reject") + dim(" to discard."), 0, 0));
|
|
7069
|
+
editor.setText("");
|
|
7070
|
+
tui.requestRender();
|
|
7071
|
+
return;
|
|
7072
|
+
}
|
|
7073
|
+
turnText = plan.task;
|
|
7074
|
+
}
|
|
7075
|
+
}
|
|
6801
7076
|
if (slash === "/voice") {
|
|
6802
7077
|
editor.setText("");
|
|
6803
7078
|
const status = new Text(dim(" \u{1F399} listening\u2026 (needs ffmpeg + the transformers voice peer)"), 0, 0);
|
|
@@ -6836,7 +7111,10 @@ async function runTuiRepl(session) {
|
|
|
6836
7111
|
busy = true;
|
|
6837
7112
|
bumpTurns();
|
|
6838
7113
|
void (async () => {
|
|
6839
|
-
let english = await translateIncoming(
|
|
7114
|
+
let english = internalPrompt ?? await translateIncoming(turnText);
|
|
7115
|
+
if (getMode() === "plan") english = `${PLAN_PRIMER}
|
|
7116
|
+
|
|
7117
|
+
${english}`;
|
|
6840
7118
|
if (getThinking()) english = `${THINKING_PRIMER}
|
|
6841
7119
|
|
|
6842
7120
|
${english}`;
|
|
@@ -6876,6 +7154,9 @@ ${english}`;
|
|
|
6876
7154
|
const hint = arts.length ? dim(`
|
|
6877
7155
|
\u2398 ${arts.length} artifact${arts.length === 1 ? "" : "s"} \u2014 /review to save`) : "";
|
|
6878
7156
|
streaming.setText((finalText || dim("(no response)")) + (warn ? dim(warn) : "") + hint);
|
|
7157
|
+
if (getMode() === "plan" && notePlanOutput(finalText)) {
|
|
7158
|
+
chat.addChild(new Text(dim(" \u25A2 plan ready \u2014 ") + accent("/approve") + dim(" to execute \xB7 ") + accent("/reject") + dim(" to discard"), 0, 0));
|
|
7159
|
+
}
|
|
6879
7160
|
tui.requestRender();
|
|
6880
7161
|
busy = false;
|
|
6881
7162
|
})();
|
|
@@ -6889,7 +7170,7 @@ ${english}`;
|
|
|
6889
7170
|
// src/voice/mic.ts
|
|
6890
7171
|
import { spawn as spawn3 } from "child_process";
|
|
6891
7172
|
import { tmpdir as tmpdir3 } from "os";
|
|
6892
|
-
import { join as
|
|
7173
|
+
import { join as join28 } from "path";
|
|
6893
7174
|
import { existsSync as existsSync18, statSync as statSync4 } from "fs";
|
|
6894
7175
|
function recorders(outFile, seconds) {
|
|
6895
7176
|
const dur = String(seconds);
|
|
@@ -6911,7 +7192,7 @@ function recorders(outFile, seconds) {
|
|
|
6911
7192
|
];
|
|
6912
7193
|
}
|
|
6913
7194
|
async function recordMic(seconds = 6) {
|
|
6914
|
-
const outFile =
|
|
7195
|
+
const outFile = join28(tmpdir3(), `oriro-voice-${process.pid}-${seconds}.wav`);
|
|
6915
7196
|
for (const r of recorders(outFile, seconds)) {
|
|
6916
7197
|
const okFile = await new Promise((resolve3) => {
|
|
6917
7198
|
const child = spawn3(r.cmd, r.args, { stdio: "ignore" });
|
|
@@ -6983,6 +7264,8 @@ function replHelp() {
|
|
|
6983
7264
|
${dim("Models & routers")} ${accent("/routers")} list\xB7add\xB7rotate the racing pool ${accent("/model")} <id\u2026> switch
|
|
6984
7265
|
${dim("This session")} ${accent("/usage")} pool health & turns ${accent("/trace")} activity ${accent("/compact")} free context ${accent("/undo")} rewind a turn
|
|
6985
7266
|
${dim("Continuity")} ${accent("/sessions")} list saved sessions ${dim("resume:")} ${accent("oriro -c")} ${dim("or")} ${accent("oriro --resume <id>")}
|
|
7267
|
+
${dim("Plan loop")} ${accent("/plan")} <task> read-only plan ${accent("/approve")} execute it ${accent("/reject")} discard
|
|
7268
|
+
${dim("Fan-out")} ${accent("/agents")} <A> | <B> parallel sub-agents in isolated git worktrees
|
|
6986
7269
|
${dim("Artifacts")} ${accent("/review")} code/SVG from the last reply ${accent("/save")} <n> [path] write one
|
|
6987
7270
|
${dim("Project")} ${accent("/init")} write a starter AGENTS.md ORIRO reads each session
|
|
6988
7271
|
${dim("Capabilities")} ${accent("/skills")} ${accent("/connectors")} ${accent("/voice")} speak a turn
|
|
@@ -7082,8 +7365,44 @@ async function runReadlineRepl(session) {
|
|
|
7082
7365
|
stdout7.write(handleArtifactSlash(line).join("\n") + "\n");
|
|
7083
7366
|
continue;
|
|
7084
7367
|
}
|
|
7368
|
+
if (isAgentsSlash(slash)) {
|
|
7369
|
+
stdout7.write((await handleAgents(line)).join("\n") + "\n");
|
|
7370
|
+
continue;
|
|
7371
|
+
}
|
|
7372
|
+
const plan = parsePlanSlash(line);
|
|
7373
|
+
let internalPrompt;
|
|
7374
|
+
let turnText = line;
|
|
7375
|
+
if (plan) {
|
|
7376
|
+
if (plan.cmd === "reject") {
|
|
7377
|
+
stdout7.write(` ${dim(rejectPlan() ? "\u25A2 plan discarded \u2014 refine the request (still in Plan) or /approve a new plan later" : "\u25A2 nothing to reject \u2014 no plan is waiting")}
|
|
7378
|
+
`);
|
|
7379
|
+
continue;
|
|
7380
|
+
}
|
|
7381
|
+
if (plan.cmd === "approve") {
|
|
7382
|
+
const r = approvePlan();
|
|
7383
|
+
if (!r.ok) {
|
|
7384
|
+
stdout7.write(` ${dim(`\u25A2 ${r.reason}`)}
|
|
7385
|
+
`);
|
|
7386
|
+
continue;
|
|
7387
|
+
}
|
|
7388
|
+
setMode(r.restoreMode);
|
|
7389
|
+
internalPrompt = r.prompt;
|
|
7390
|
+
} else {
|
|
7391
|
+
enterPlan(getMode());
|
|
7392
|
+
setMode("plan");
|
|
7393
|
+
if (!plan.task) {
|
|
7394
|
+
stdout7.write(` ${dim("\u25A2 Plan mode \u2014 describe the task and I'll plan it (read-only). Then")} ${accent("/approve")} ${dim("to execute \xB7")} ${accent("/reject")} ${dim("to discard.")}
|
|
7395
|
+
`);
|
|
7396
|
+
continue;
|
|
7397
|
+
}
|
|
7398
|
+
turnText = plan.task;
|
|
7399
|
+
}
|
|
7400
|
+
}
|
|
7085
7401
|
bumpTurns();
|
|
7086
|
-
|
|
7402
|
+
let english = internalPrompt ?? await translateIncoming(turnText);
|
|
7403
|
+
if (getMode() === "plan") english = `${PLAN_PRIMER}
|
|
7404
|
+
|
|
7405
|
+
${english}`;
|
|
7087
7406
|
noteUserInput(line);
|
|
7088
7407
|
let out = "";
|
|
7089
7408
|
const unsub = session.subscribe(
|
|
@@ -7107,6 +7426,10 @@ async function runReadlineRepl(session) {
|
|
|
7107
7426
|
stdout7.write(`${shown}${phantomFileWarning(shown)}
|
|
7108
7427
|
${hint}
|
|
7109
7428
|
`);
|
|
7429
|
+
if (getMode() === "plan" && notePlanOutput(shown)) {
|
|
7430
|
+
stdout7.write(` ${dim("\u25A2 plan ready \u2014")} ${accent("/approve")} ${dim("to execute \xB7")} ${accent("/reject")} ${dim("to discard")}
|
|
7431
|
+
`);
|
|
7432
|
+
}
|
|
7110
7433
|
}
|
|
7111
7434
|
} finally {
|
|
7112
7435
|
process.removeListener("SIGINT", onSigint);
|
|
@@ -7206,7 +7529,7 @@ async function confirmDestructive(what, opts = {}) {
|
|
|
7206
7529
|
|
|
7207
7530
|
// src/config/store.ts
|
|
7208
7531
|
import { readFileSync as readFileSync22, writeFileSync as writeFileSync20, mkdirSync as mkdirSync16 } from "fs";
|
|
7209
|
-
import { join as
|
|
7532
|
+
import { join as join29 } from "path";
|
|
7210
7533
|
var KEYS = {
|
|
7211
7534
|
output: {
|
|
7212
7535
|
desc: "default output format for list commands: text | json | csv",
|
|
@@ -7228,7 +7551,7 @@ function validateConfig(key, value) {
|
|
|
7228
7551
|
return KEYS[key].validate?.(value) ?? null;
|
|
7229
7552
|
}
|
|
7230
7553
|
function file4() {
|
|
7231
|
-
return
|
|
7554
|
+
return join29(oriroDir(), "config.json");
|
|
7232
7555
|
}
|
|
7233
7556
|
var cache = null;
|
|
7234
7557
|
function readAll() {
|
|
@@ -7831,9 +8154,9 @@ function registerConnectorsCommand(program2) {
|
|
|
7831
8154
|
|
|
7832
8155
|
// src/channels/config.ts
|
|
7833
8156
|
import { readFileSync as readFileSync25, writeFileSync as writeFileSync21 } from "fs";
|
|
7834
|
-
import { join as
|
|
8157
|
+
import { join as join30 } from "path";
|
|
7835
8158
|
function file5() {
|
|
7836
|
-
return
|
|
8159
|
+
return join30(oriroDir(), "channels.json");
|
|
7837
8160
|
}
|
|
7838
8161
|
function readChannels() {
|
|
7839
8162
|
try {
|
|
@@ -7846,10 +8169,10 @@ function readChannels() {
|
|
|
7846
8169
|
function saveChannel(cfg) {
|
|
7847
8170
|
const all = readChannels().filter((c) => c.kind !== cfg.kind);
|
|
7848
8171
|
all.push(cfg);
|
|
7849
|
-
writeFileSync21(
|
|
8172
|
+
writeFileSync21(join30(ensureOriroDir(), "channels.json"), JSON.stringify(all, null, 2), "utf8");
|
|
7850
8173
|
}
|
|
7851
8174
|
function removeChannel(kind) {
|
|
7852
|
-
writeFileSync21(
|
|
8175
|
+
writeFileSync21(join30(ensureOriroDir(), "channels.json"), JSON.stringify(readChannels().filter((c) => c.kind !== kind), null, 2), "utf8");
|
|
7853
8176
|
}
|
|
7854
8177
|
|
|
7855
8178
|
// src/channels/telegram.ts
|
|
@@ -7966,9 +8289,9 @@ async function startDiscord(token) {
|
|
|
7966
8289
|
}
|
|
7967
8290
|
|
|
7968
8291
|
// src/channels/whatsapp.ts
|
|
7969
|
-
import { join as
|
|
8292
|
+
import { join as join31 } from "path";
|
|
7970
8293
|
function whatsappAuthDir() {
|
|
7971
|
-
return
|
|
8294
|
+
return join31(oriroDir(), "whatsapp-auth");
|
|
7972
8295
|
}
|
|
7973
8296
|
async function startWhatsApp() {
|
|
7974
8297
|
let baileys;
|
|
@@ -8087,7 +8410,7 @@ function registerChannelsCommand(program2) {
|
|
|
8087
8410
|
|
|
8088
8411
|
// src/commands/skills.ts
|
|
8089
8412
|
import { existsSync as existsSync21, statSync as statSync5, mkdirSync as mkdirSync17, cpSync, rmSync as rmSync4 } from "fs";
|
|
8090
|
-
import { resolve as resolve2, join as
|
|
8413
|
+
import { resolve as resolve2, join as join32, basename as basename3, dirname as dirname4 } from "path";
|
|
8091
8414
|
function registerSkillsCommand(program2) {
|
|
8092
8415
|
const skills = program2.command("skills").description("the ORIRO skill library \u2014 bundled + your own");
|
|
8093
8416
|
skills.command("list").description("show CORE / TAIL skill counts (use --all to list names)").option("-a, --all", "list every skill name").option("-o, --output <fmt>", "output format: text (default) | json | csv").option("-q, --query <expr>", "filter/select: 'field', 'field=value', or 'field=value:selectField'").action(async (opts) => {
|
|
@@ -8124,22 +8447,22 @@ function registerSkillsCommand(program2) {
|
|
|
8124
8447
|
mkdirSync17(dest, { recursive: true });
|
|
8125
8448
|
const st = statSync5(src);
|
|
8126
8449
|
if (st.isDirectory()) {
|
|
8127
|
-
if (!existsSync21(
|
|
8128
|
-
const name =
|
|
8129
|
-
cpSync(src,
|
|
8130
|
-
ok(`added skill ${accent(name)} \u2192 ${
|
|
8131
|
-
} else if (
|
|
8132
|
-
const name =
|
|
8133
|
-
mkdirSync17(
|
|
8134
|
-
cpSync(src,
|
|
8135
|
-
ok(`added skill ${accent(name)} \u2192 ${
|
|
8450
|
+
if (!existsSync21(join32(src, "SKILL.md"))) die(`no SKILL.md in ${src} \u2014 a skill folder must contain SKILL.md`);
|
|
8451
|
+
const name = basename3(src);
|
|
8452
|
+
cpSync(src, join32(dest, name), { recursive: true });
|
|
8453
|
+
ok(`added skill ${accent(name)} \u2192 ${join32(dest, name)}`);
|
|
8454
|
+
} else if (basename3(src).toLowerCase() === "skill.md") {
|
|
8455
|
+
const name = basename3(dirname4(src)) || "custom-skill";
|
|
8456
|
+
mkdirSync17(join32(dest, name), { recursive: true });
|
|
8457
|
+
cpSync(src, join32(dest, name, "SKILL.md"));
|
|
8458
|
+
ok(`added skill ${accent(name)} \u2192 ${join32(dest, name)}`);
|
|
8136
8459
|
} else {
|
|
8137
8460
|
die("expected a folder containing SKILL.md, or a SKILL.md file");
|
|
8138
8461
|
}
|
|
8139
8462
|
info("It loads on next launch \u2014 and is available in chat via /skill.");
|
|
8140
8463
|
});
|
|
8141
8464
|
skills.command("remove <name>").description("remove a skill you added").option("-f, --force", "skip the confirmation prompt").action(async (name, opts) => {
|
|
8142
|
-
const target =
|
|
8465
|
+
const target = join32(userSkillsDir(), name);
|
|
8143
8466
|
if (!existsSync21(target)) {
|
|
8144
8467
|
info(`'${name}' is not a user-added skill \u2014 nothing to remove`);
|
|
8145
8468
|
return;
|
|
@@ -8728,7 +9051,7 @@ function registerConfigCommand(program2) {
|
|
|
8728
9051
|
|
|
8729
9052
|
// src/commands/setup.ts
|
|
8730
9053
|
import { rmSync as rmSync5 } from "fs";
|
|
8731
|
-
import { join as
|
|
9054
|
+
import { join as join33 } from "path";
|
|
8732
9055
|
import { stdin as stdin12, stdout as stdout11 } from "process";
|
|
8733
9056
|
var MARKERS = [
|
|
8734
9057
|
"language.json",
|
|
@@ -8736,14 +9059,14 @@ var MARKERS = [
|
|
|
8736
9059
|
"skills-onboarded.json",
|
|
8737
9060
|
"connectors-onboarded.json",
|
|
8738
9061
|
"models-onboarded.json",
|
|
8739
|
-
|
|
9062
|
+
join33("routers", "onboarded.json")
|
|
8740
9063
|
];
|
|
8741
9064
|
function registerSetupCommand(program2) {
|
|
8742
9065
|
program2.command("setup").description("run the guided setup wizard (language \xB7 routers \xB7 connectors \xB7 skills \xB7 avatar)").option("--reset", "clear your settled choices and re-ask every step").action(async (opts) => {
|
|
8743
9066
|
if (opts.reset) {
|
|
8744
9067
|
for (const m of MARKERS) {
|
|
8745
9068
|
try {
|
|
8746
|
-
rmSync5(
|
|
9069
|
+
rmSync5(join33(oriroDir(), m), { force: true });
|
|
8747
9070
|
} catch {
|
|
8748
9071
|
}
|
|
8749
9072
|
}
|
|
@@ -8761,7 +9084,7 @@ function registerSetupCommand(program2) {
|
|
|
8761
9084
|
|
|
8762
9085
|
// src/commands/import.ts
|
|
8763
9086
|
import { existsSync as existsSync22, readFileSync as readFileSync27, readdirSync as readdirSync4, statSync as statSync6, cpSync as cpSync2, mkdirSync as mkdirSync18 } from "fs";
|
|
8764
|
-
import { join as
|
|
9087
|
+
import { join as join34, basename as basename4 } from "path";
|
|
8765
9088
|
function registerImportCommand(program2) {
|
|
8766
9089
|
const imp = program2.command("import").description("migrate from another CLI (MCP servers, skills)");
|
|
8767
9090
|
imp.command("mcp <file>").description("import MCP servers from a Claude-compatible mcp.json (Guardian-vetted)").action((file6) => {
|
|
@@ -8818,11 +9141,11 @@ function registerImportCommand(program2) {
|
|
|
8818
9141
|
const dest = userSkillsDir();
|
|
8819
9142
|
mkdirSync18(dest, { recursive: true });
|
|
8820
9143
|
heading("Import skills");
|
|
8821
|
-
const sources = existsSync22(
|
|
9144
|
+
const sources = existsSync22(join34(dir, "SKILL.md")) ? [dir] : readdirSync4(dir).map((e) => join34(dir, e)).filter((p) => statSync6(p).isDirectory() && existsSync22(join34(p, "SKILL.md")));
|
|
8822
9145
|
let n = 0;
|
|
8823
9146
|
for (const src of sources) {
|
|
8824
|
-
cpSync2(src,
|
|
8825
|
-
process.stdout.write(` ${fgHex(PALETTE.success, "\u2713")} ${accent(
|
|
9147
|
+
cpSync2(src, join34(dest, basename4(src)), { recursive: true });
|
|
9148
|
+
process.stdout.write(` ${fgHex(PALETTE.success, "\u2713")} ${accent(basename4(src))}
|
|
8826
9149
|
`);
|
|
8827
9150
|
n++;
|
|
8828
9151
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oriro/orirocli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"dev": "tsx src/cli.ts",
|
|
24
24
|
"build": "tsup",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
|
-
"test:unit": "tsx scripts/test-tool-sanitize.ts && tsx scripts/test-guardian.ts && tsx scripts/test-scribe.ts && tsx scripts/test-race.ts && tsx scripts/test-weights.ts && tsx scripts/test-output.ts && tsx scripts/test-connectors.ts && tsx scripts/test-artifacts.ts && tsx scripts/test-project-md.ts && tsx scripts/test-compact.ts && tsx scripts/test-init.ts && tsx scripts/test-sessions.ts",
|
|
26
|
+
"test:unit": "tsx scripts/test-tool-sanitize.ts && tsx scripts/test-guardian.ts && tsx scripts/test-scribe.ts && tsx scripts/test-race.ts && tsx scripts/test-weights.ts && tsx scripts/test-output.ts && tsx scripts/test-connectors.ts && tsx scripts/test-artifacts.ts && tsx scripts/test-project-md.ts && tsx scripts/test-compact.ts && tsx scripts/test-init.ts && tsx scripts/test-sessions.ts && tsx scripts/test-permission.ts && tsx scripts/test-plan-mode.ts && tsx scripts/test-agents-fanout.ts",
|
|
27
27
|
"smoke": "npm run build && node scripts/smoke.mjs",
|
|
28
28
|
"prepublishOnly": "npm run build && npm run test:unit && node scripts/smoke.mjs && node scripts/prepublish-check.mjs",
|
|
29
29
|
"start": "node dist/cli.js"
|