@oriro/orirocli 0.3.4 → 0.3.5
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 +199 -28
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -4512,6 +4512,90 @@ ${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 registerPostureGate(pi) {
|
|
4574
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
4575
|
+
const d = decideTool({ toolName: event.toolName, guardianBlocked: false });
|
|
4576
|
+
if (d.decision === "block") {
|
|
4577
|
+
return {
|
|
4578
|
+
block: true,
|
|
4579
|
+
reason: `\u25A2 ${d.reason ?? "blocked by posture"} \u2014 present the plan as text; the user will /approve to execute`
|
|
4580
|
+
};
|
|
4581
|
+
}
|
|
4582
|
+
if (d.decision === "ask" && armed) {
|
|
4583
|
+
if (!ctx.hasUI) {
|
|
4584
|
+
return { block: true, reason: `posture '${getMode()}' requires approval and no UI is available` };
|
|
4585
|
+
}
|
|
4586
|
+
const choice = await ctx.ui.select(
|
|
4587
|
+
`\u25CF Posture '${getMode()}' \u2014 approve this action?
|
|
4588
|
+
Tool: ${event.toolName}
|
|
4589
|
+
|
|
4590
|
+
(Shift+Tab cycles postures; \u23F5\u23F5 Auto stops asking)`,
|
|
4591
|
+
["Allow once", "Deny"]
|
|
4592
|
+
);
|
|
4593
|
+
return choice === "Allow once" ? void 0 : { block: true, reason: "Denied by user (posture gate)" };
|
|
4594
|
+
}
|
|
4595
|
+
return void 0;
|
|
4596
|
+
});
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4515
4599
|
// src/head/pi-tool.ts
|
|
4516
4600
|
import { Type as Type2 } from "typebox";
|
|
4517
4601
|
|
|
@@ -5977,6 +6061,7 @@ async function assembleOriroSession(opts = {}) {
|
|
|
5977
6061
|
// bundled library + the user's own ~/.oriro/skills
|
|
5978
6062
|
extensionFactories: [
|
|
5979
6063
|
registerGuardian,
|
|
6064
|
+
registerPostureGate,
|
|
5980
6065
|
registerHead,
|
|
5981
6066
|
registerScribe,
|
|
5982
6067
|
registerOrchestrator,
|
|
@@ -6165,32 +6250,37 @@ async function translateOutgoing(text) {
|
|
|
6165
6250
|
// src/repl-ui/tui-repl.ts
|
|
6166
6251
|
import { ProcessTerminal, TUI, Editor, Text, Container } from "@earendil-works/pi-tui";
|
|
6167
6252
|
|
|
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
|
-
|
|
6253
|
+
// src/repl-ui/plan-mode.ts
|
|
6254
|
+
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.";
|
|
6255
|
+
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.";
|
|
6256
|
+
var prevMode = "manual";
|
|
6257
|
+
var ready = false;
|
|
6258
|
+
function enterPlan(from) {
|
|
6259
|
+
if (from !== "plan") prevMode = from;
|
|
6260
|
+
ready = false;
|
|
6261
|
+
}
|
|
6262
|
+
function notePlanOutput(output) {
|
|
6263
|
+
ready = output.trim().length > 0;
|
|
6264
|
+
return ready;
|
|
6265
|
+
}
|
|
6266
|
+
function approvePlan() {
|
|
6267
|
+
if (!ready) return { ok: false, reason: "no plan is waiting for approval \u2014 /plan <task> first" };
|
|
6268
|
+
ready = false;
|
|
6269
|
+
return { ok: true, restoreMode: prevMode, prompt: EXECUTE_PROMPT };
|
|
6270
|
+
}
|
|
6271
|
+
function rejectPlan() {
|
|
6272
|
+
const had = ready;
|
|
6273
|
+
ready = false;
|
|
6274
|
+
return had;
|
|
6275
|
+
}
|
|
6276
|
+
function parsePlanSlash(line) {
|
|
6277
|
+
const m = /^\/(plan|approve|reject)(?:\s+(\S[\s\S]*))?$/i.exec(line.trim());
|
|
6278
|
+
if (!m) return void 0;
|
|
6279
|
+
const cmd = m[1].toLowerCase();
|
|
6280
|
+
if (cmd === "plan") return m[2] ? { cmd: "plan", task: m[2].trim() } : { cmd: "plan" };
|
|
6281
|
+
if (cmd === "approve") return { cmd: "approve" };
|
|
6282
|
+
return { cmd: "reject" };
|
|
6192
6283
|
}
|
|
6193
|
-
var THINKING_PRIMER = "Think step by step and plan your approach before acting. Reason carefully and check your work.";
|
|
6194
6284
|
|
|
6195
6285
|
// src/repl-ui/verify-actions.ts
|
|
6196
6286
|
import { existsSync as existsSync15 } from "fs";
|
|
@@ -6656,6 +6746,7 @@ function footerText() {
|
|
|
6656
6746
|
return `${bar} ${think} ${dim("Shift+Tab posture \xB7 Alt+Shift+T thinking \xB7 /exit")}`;
|
|
6657
6747
|
}
|
|
6658
6748
|
async function runTuiRepl(session) {
|
|
6749
|
+
armPostureGate();
|
|
6659
6750
|
const isEnglish3 = getTerminalLanguage().code.toLowerCase().startsWith("en");
|
|
6660
6751
|
const term = new ProcessTerminal();
|
|
6661
6752
|
const tui = new TUI(term, true);
|
|
@@ -6675,7 +6766,8 @@ async function runTuiRepl(session) {
|
|
|
6675
6766
|
};
|
|
6676
6767
|
const removeListener = tui.addInputListener((data) => {
|
|
6677
6768
|
if (data === "\x1B[Z") {
|
|
6678
|
-
|
|
6769
|
+
const before = getMode();
|
|
6770
|
+
if (cycleMode() === "plan") enterPlan(before);
|
|
6679
6771
|
refreshFooter();
|
|
6680
6772
|
return { consume: true };
|
|
6681
6773
|
}
|
|
@@ -6718,6 +6810,7 @@ async function runTuiRepl(session) {
|
|
|
6718
6810
|
` ${accent("/routers")} pool add\xB7rotate ${accent("/model")} <id\u2026> switch ${accent("/usage")} health ${accent("/trace")} tool+router activity ${accent("/compact")} free context`,
|
|
6719
6811
|
` ${accent("/review")} artifacts ${accent("/save")} <n> [path] ${accent("/init")} AGENTS.md ${accent("/skills")} ${accent("/connectors")} ${accent("/voice")}`,
|
|
6720
6812
|
` ${accent("/sessions")} list saved ${accent("/undo")} rewind a turn ${dim("resume:")} ${accent("oriro -c")} / ${accent("oriro --resume <id>")}`,
|
|
6813
|
+
` ${accent("/plan")} <task> plan read-only ${accent("/approve")} execute it ${accent("/reject")} discard`,
|
|
6721
6814
|
` ${dim("Shift+Tab")} posture ${dim("Alt+Shift+T")} thinking ${accent("/help")} ${accent("/exit")}`
|
|
6722
6815
|
].join("\n");
|
|
6723
6816
|
chat.addChild(new Text(help, 0, 0));
|
|
@@ -6798,6 +6891,41 @@ async function runTuiRepl(session) {
|
|
|
6798
6891
|
})();
|
|
6799
6892
|
return;
|
|
6800
6893
|
}
|
|
6894
|
+
const plan = parsePlanSlash(text);
|
|
6895
|
+
let internalPrompt;
|
|
6896
|
+
let turnText = text;
|
|
6897
|
+
if (plan) {
|
|
6898
|
+
if (plan.cmd === "reject") {
|
|
6899
|
+
const had = rejectPlan();
|
|
6900
|
+
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));
|
|
6901
|
+
editor.setText("");
|
|
6902
|
+
tui.requestRender();
|
|
6903
|
+
return;
|
|
6904
|
+
}
|
|
6905
|
+
if (plan.cmd === "approve") {
|
|
6906
|
+
const r = approvePlan();
|
|
6907
|
+
if (!r.ok) {
|
|
6908
|
+
chat.addChild(new Text(dim(` \u25A2 ${r.reason}`), 0, 0));
|
|
6909
|
+
editor.setText("");
|
|
6910
|
+
tui.requestRender();
|
|
6911
|
+
return;
|
|
6912
|
+
}
|
|
6913
|
+
setMode(r.restoreMode);
|
|
6914
|
+
refreshFooter();
|
|
6915
|
+
internalPrompt = r.prompt;
|
|
6916
|
+
} else {
|
|
6917
|
+
enterPlan(getMode());
|
|
6918
|
+
setMode("plan");
|
|
6919
|
+
refreshFooter();
|
|
6920
|
+
if (!plan.task) {
|
|
6921
|
+
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));
|
|
6922
|
+
editor.setText("");
|
|
6923
|
+
tui.requestRender();
|
|
6924
|
+
return;
|
|
6925
|
+
}
|
|
6926
|
+
turnText = plan.task;
|
|
6927
|
+
}
|
|
6928
|
+
}
|
|
6801
6929
|
if (slash === "/voice") {
|
|
6802
6930
|
editor.setText("");
|
|
6803
6931
|
const status = new Text(dim(" \u{1F399} listening\u2026 (needs ffmpeg + the transformers voice peer)"), 0, 0);
|
|
@@ -6836,7 +6964,10 @@ async function runTuiRepl(session) {
|
|
|
6836
6964
|
busy = true;
|
|
6837
6965
|
bumpTurns();
|
|
6838
6966
|
void (async () => {
|
|
6839
|
-
let english = await translateIncoming(
|
|
6967
|
+
let english = internalPrompt ?? await translateIncoming(turnText);
|
|
6968
|
+
if (getMode() === "plan") english = `${PLAN_PRIMER}
|
|
6969
|
+
|
|
6970
|
+
${english}`;
|
|
6840
6971
|
if (getThinking()) english = `${THINKING_PRIMER}
|
|
6841
6972
|
|
|
6842
6973
|
${english}`;
|
|
@@ -6876,6 +7007,9 @@ ${english}`;
|
|
|
6876
7007
|
const hint = arts.length ? dim(`
|
|
6877
7008
|
\u2398 ${arts.length} artifact${arts.length === 1 ? "" : "s"} \u2014 /review to save`) : "";
|
|
6878
7009
|
streaming.setText((finalText || dim("(no response)")) + (warn ? dim(warn) : "") + hint);
|
|
7010
|
+
if (getMode() === "plan" && notePlanOutput(finalText)) {
|
|
7011
|
+
chat.addChild(new Text(dim(" \u25A2 plan ready \u2014 ") + accent("/approve") + dim(" to execute \xB7 ") + accent("/reject") + dim(" to discard"), 0, 0));
|
|
7012
|
+
}
|
|
6879
7013
|
tui.requestRender();
|
|
6880
7014
|
busy = false;
|
|
6881
7015
|
})();
|
|
@@ -6983,6 +7117,7 @@ function replHelp() {
|
|
|
6983
7117
|
${dim("Models & routers")} ${accent("/routers")} list\xB7add\xB7rotate the racing pool ${accent("/model")} <id\u2026> switch
|
|
6984
7118
|
${dim("This session")} ${accent("/usage")} pool health & turns ${accent("/trace")} activity ${accent("/compact")} free context ${accent("/undo")} rewind a turn
|
|
6985
7119
|
${dim("Continuity")} ${accent("/sessions")} list saved sessions ${dim("resume:")} ${accent("oriro -c")} ${dim("or")} ${accent("oriro --resume <id>")}
|
|
7120
|
+
${dim("Plan loop")} ${accent("/plan")} <task> read-only plan ${accent("/approve")} execute it ${accent("/reject")} discard
|
|
6986
7121
|
${dim("Artifacts")} ${accent("/review")} code/SVG from the last reply ${accent("/save")} <n> [path] write one
|
|
6987
7122
|
${dim("Project")} ${accent("/init")} write a starter AGENTS.md ORIRO reads each session
|
|
6988
7123
|
${dim("Capabilities")} ${accent("/skills")} ${accent("/connectors")} ${accent("/voice")} speak a turn
|
|
@@ -7082,8 +7217,40 @@ async function runReadlineRepl(session) {
|
|
|
7082
7217
|
stdout7.write(handleArtifactSlash(line).join("\n") + "\n");
|
|
7083
7218
|
continue;
|
|
7084
7219
|
}
|
|
7220
|
+
const plan = parsePlanSlash(line);
|
|
7221
|
+
let internalPrompt;
|
|
7222
|
+
let turnText = line;
|
|
7223
|
+
if (plan) {
|
|
7224
|
+
if (plan.cmd === "reject") {
|
|
7225
|
+
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")}
|
|
7226
|
+
`);
|
|
7227
|
+
continue;
|
|
7228
|
+
}
|
|
7229
|
+
if (plan.cmd === "approve") {
|
|
7230
|
+
const r = approvePlan();
|
|
7231
|
+
if (!r.ok) {
|
|
7232
|
+
stdout7.write(` ${dim(`\u25A2 ${r.reason}`)}
|
|
7233
|
+
`);
|
|
7234
|
+
continue;
|
|
7235
|
+
}
|
|
7236
|
+
setMode(r.restoreMode);
|
|
7237
|
+
internalPrompt = r.prompt;
|
|
7238
|
+
} else {
|
|
7239
|
+
enterPlan(getMode());
|
|
7240
|
+
setMode("plan");
|
|
7241
|
+
if (!plan.task) {
|
|
7242
|
+
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.")}
|
|
7243
|
+
`);
|
|
7244
|
+
continue;
|
|
7245
|
+
}
|
|
7246
|
+
turnText = plan.task;
|
|
7247
|
+
}
|
|
7248
|
+
}
|
|
7085
7249
|
bumpTurns();
|
|
7086
|
-
|
|
7250
|
+
let english = internalPrompt ?? await translateIncoming(turnText);
|
|
7251
|
+
if (getMode() === "plan") english = `${PLAN_PRIMER}
|
|
7252
|
+
|
|
7253
|
+
${english}`;
|
|
7087
7254
|
noteUserInput(line);
|
|
7088
7255
|
let out = "";
|
|
7089
7256
|
const unsub = session.subscribe(
|
|
@@ -7107,6 +7274,10 @@ async function runReadlineRepl(session) {
|
|
|
7107
7274
|
stdout7.write(`${shown}${phantomFileWarning(shown)}
|
|
7108
7275
|
${hint}
|
|
7109
7276
|
`);
|
|
7277
|
+
if (getMode() === "plan" && notePlanOutput(shown)) {
|
|
7278
|
+
stdout7.write(` ${dim("\u25A2 plan ready \u2014")} ${accent("/approve")} ${dim("to execute \xB7")} ${accent("/reject")} ${dim("to discard")}
|
|
7279
|
+
`);
|
|
7280
|
+
}
|
|
7110
7281
|
}
|
|
7111
7282
|
} finally {
|
|
7112
7283
|
process.removeListener("SIGINT", onSigint);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oriro/orirocli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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",
|
|
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"
|