@oriro/orirocli 0.1.12 → 0.3.2
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/LICENSE +1 -1
- package/README.md +8 -2
- package/dist/cli.js +1553 -183
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1250,37 +1250,37 @@ function registerVoiceSynth(fn) {
|
|
|
1250
1250
|
function registerVoiceListen(fn) {
|
|
1251
1251
|
listener = fn;
|
|
1252
1252
|
}
|
|
1253
|
-
function audioPlayers(
|
|
1254
|
-
if (process.platform === "darwin") return [{ cmd: "afplay", args: [
|
|
1253
|
+
function audioPlayers(file6) {
|
|
1254
|
+
if (process.platform === "darwin") return [{ cmd: "afplay", args: [file6] }];
|
|
1255
1255
|
if (process.platform === "win32")
|
|
1256
1256
|
return [
|
|
1257
|
-
{ cmd: "powershell", args: ["-NoProfile", "-c", `(New-Object Media.SoundPlayer '${
|
|
1257
|
+
{ cmd: "powershell", args: ["-NoProfile", "-c", `(New-Object Media.SoundPlayer '${file6}').PlaySync()`] }
|
|
1258
1258
|
];
|
|
1259
1259
|
return [
|
|
1260
|
-
{ cmd: "aplay", args: ["-q",
|
|
1261
|
-
{ cmd: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet",
|
|
1262
|
-
{ cmd: "paplay", args: [
|
|
1260
|
+
{ cmd: "aplay", args: ["-q", file6] },
|
|
1261
|
+
{ cmd: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", file6] },
|
|
1262
|
+
{ cmd: "paplay", args: [file6] }
|
|
1263
1263
|
];
|
|
1264
1264
|
}
|
|
1265
1265
|
function playWav(wav) {
|
|
1266
|
-
const
|
|
1267
|
-
writeFileSync5(
|
|
1268
|
-
const players = audioPlayers(
|
|
1266
|
+
const file6 = join7(tmpdir(), `oriro-avatar-${process.pid}-${wav.length}.wav`);
|
|
1267
|
+
writeFileSync5(file6, wav);
|
|
1268
|
+
const players = audioPlayers(file6);
|
|
1269
1269
|
return new Promise((resolve3) => {
|
|
1270
1270
|
const tryPlayer = (i) => {
|
|
1271
1271
|
if (i >= players.length) {
|
|
1272
|
-
rmSync(
|
|
1272
|
+
rmSync(file6, { force: true });
|
|
1273
1273
|
return resolve3(false);
|
|
1274
1274
|
}
|
|
1275
1275
|
const p = players[i];
|
|
1276
1276
|
if (!p) {
|
|
1277
|
-
rmSync(
|
|
1277
|
+
rmSync(file6, { force: true });
|
|
1278
1278
|
return resolve3(false);
|
|
1279
1279
|
}
|
|
1280
1280
|
const child = spawn(p.cmd, p.args, { stdio: "ignore" });
|
|
1281
1281
|
child.on("error", () => tryPlayer(i + 1));
|
|
1282
1282
|
child.on("close", (code) => {
|
|
1283
|
-
rmSync(
|
|
1283
|
+
rmSync(file6, { force: true });
|
|
1284
1284
|
resolve3(code === 0);
|
|
1285
1285
|
});
|
|
1286
1286
|
};
|
|
@@ -1317,9 +1317,9 @@ import { existsSync, readFileSync as readFileSync6, rmSync as rmSync2 } from "fs
|
|
|
1317
1317
|
function tmpWav() {
|
|
1318
1318
|
return join8(tmpdir2(), `oriro-tts-${process.pid}-${Date.now()}-${Math.floor(performance.now())}.wav`);
|
|
1319
1319
|
}
|
|
1320
|
-
function readAndClean(
|
|
1321
|
-
const buf = readFileSync6(
|
|
1322
|
-
rmSync2(
|
|
1320
|
+
function readAndClean(file6) {
|
|
1321
|
+
const buf = readFileSync6(file6);
|
|
1322
|
+
rmSync2(file6, { force: true });
|
|
1323
1323
|
return new Uint8Array(buf);
|
|
1324
1324
|
}
|
|
1325
1325
|
function winSapi(text, lang) {
|
|
@@ -1755,13 +1755,6 @@ var ROUTER_CATALOG = [
|
|
|
1755
1755
|
freeModels: ["mistralai/Mistral-Small-Instruct"],
|
|
1756
1756
|
obtainUrl: "https://berget.ai"
|
|
1757
1757
|
}),
|
|
1758
|
-
C4({
|
|
1759
|
-
id: "huggingface",
|
|
1760
|
-
displayName: "Hugging Face",
|
|
1761
|
-
baseUrl: "https://router.huggingface.co/v1",
|
|
1762
|
-
freeModels: ["meta-llama/Llama-3.2-3B-Instruct"],
|
|
1763
|
-
obtainUrl: "https://huggingface.co/settings/tokens"
|
|
1764
|
-
}),
|
|
1765
1758
|
C4({
|
|
1766
1759
|
id: "replicate",
|
|
1767
1760
|
displayName: "Replicate",
|
|
@@ -1857,10 +1850,35 @@ var ROUTER_CATALOG = [
|
|
|
1857
1850
|
obtainUrl: "https://platform.moonshot.ai",
|
|
1858
1851
|
tier: "paid"
|
|
1859
1852
|
}),
|
|
1860
|
-
// ── ORIRO models —
|
|
1861
|
-
|
|
1862
|
-
|
|
1853
|
+
// ── ORIRO's OWN models — LIVE, keyless, first-class racers (2026-07-04) ──
|
|
1854
|
+
// Served through the same-origin oriro.ai worker proxy, which injects the serve key server-side
|
|
1855
|
+
// so the CLI stays keyless (no bearer ever touches the client) — the endpoints answer at
|
|
1856
|
+
// baseUrl + "/chat/completions" (race-{gauss,avila}.ts alias). ORIRO-Avila is V2.4 today
|
|
1857
|
+
// (AVILA_SERVE_URL set); ORIRO-Gauss races on the live serve and auto-upgrades to V2.4 the
|
|
1858
|
+
// moment GAUSS_SERVE_URL is flipped — no CLI change needed. Both are true GPU endpoints, so
|
|
1859
|
+
// they only race when the user opts them into the pool (`oriro routers add oriro-gauss`).
|
|
1860
|
+
C4({
|
|
1861
|
+
id: "oriro-gauss",
|
|
1862
|
+
displayName: "ORIRO-Gauss",
|
|
1863
|
+
baseUrl: "https://oriro.ai/api/race/gauss",
|
|
1864
|
+
freeModels: ["gauss"],
|
|
1865
|
+
obtainUrl: "https://oriro.ai",
|
|
1866
|
+
keyless: true,
|
|
1867
|
+
verified: true
|
|
1868
|
+
}),
|
|
1869
|
+
C4({
|
|
1870
|
+
id: "oriro-avila",
|
|
1871
|
+
displayName: "ORIRO-Avila",
|
|
1872
|
+
baseUrl: "https://oriro.ai/api/race/avila",
|
|
1873
|
+
freeModels: ["avila"],
|
|
1874
|
+
obtainUrl: "https://oriro.ai",
|
|
1875
|
+
keyless: true,
|
|
1876
|
+
verified: true
|
|
1877
|
+
})
|
|
1863
1878
|
];
|
|
1879
|
+
function selectableRouters() {
|
|
1880
|
+
return ROUTER_CATALOG.filter((r) => !r.comingSoon);
|
|
1881
|
+
}
|
|
1864
1882
|
function routerById(id) {
|
|
1865
1883
|
return ROUTER_CATALOG.find((r) => r.id === id);
|
|
1866
1884
|
}
|
|
@@ -3371,7 +3389,7 @@ import {
|
|
|
3371
3389
|
} from "@earendil-works/pi-coding-agent";
|
|
3372
3390
|
|
|
3373
3391
|
// src/routers/mux-provider.ts
|
|
3374
|
-
import { streamSimple as
|
|
3392
|
+
import { streamSimple as piStreamSimple2, createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
3375
3393
|
import { register as registerOpenAICompletions } from "@earendil-works/pi-ai/openai-completions";
|
|
3376
3394
|
|
|
3377
3395
|
// src/routers/mux.ts
|
|
@@ -3579,8 +3597,8 @@ function artifactsDir() {
|
|
|
3579
3597
|
// src/scribe/digest.ts
|
|
3580
3598
|
var DIGEST_CAP = 8192;
|
|
3581
3599
|
var TIMELINE_DAY_CAP = 400;
|
|
3582
|
-
function read(
|
|
3583
|
-
return existsSync8(
|
|
3600
|
+
function read(file6) {
|
|
3601
|
+
return existsSync8(file6) ? readFileSync12(file6, "utf8") : "";
|
|
3584
3602
|
}
|
|
3585
3603
|
function updateDigest(summary, context) {
|
|
3586
3604
|
mkdirSync10(scribeDir(), { recursive: true });
|
|
@@ -4118,7 +4136,54 @@ function attachScribe(session) {
|
|
|
4118
4136
|
});
|
|
4119
4137
|
}
|
|
4120
4138
|
|
|
4121
|
-
// src/
|
|
4139
|
+
// src/context/project-md.ts
|
|
4140
|
+
import { existsSync as existsSync13, readFileSync as readFileSync18, statSync as statSync2 } from "fs";
|
|
4141
|
+
import { join as join21, dirname as dirname3, parse } from "path";
|
|
4142
|
+
var NAMES = ["AGENTS.md", "CLAUDE.md", ".oriro/ORIRO.md"];
|
|
4143
|
+
var MAX_BYTES = 32 * 1024;
|
|
4144
|
+
var MAX_LEVELS = 24;
|
|
4145
|
+
function isRoot(dir) {
|
|
4146
|
+
return existsSync13(join21(dir, ".git")) || existsSync13(join21(dir, ".oriro"));
|
|
4147
|
+
}
|
|
4148
|
+
function discoverProjectInstructions(cwd) {
|
|
4149
|
+
const chain = [];
|
|
4150
|
+
let dir = cwd;
|
|
4151
|
+
const rootOfDrive = parse(cwd).root;
|
|
4152
|
+
for (let i = 0; i < MAX_LEVELS; i++) {
|
|
4153
|
+
for (const name of NAMES) {
|
|
4154
|
+
const p = join21(dir, name);
|
|
4155
|
+
try {
|
|
4156
|
+
if (existsSync13(p) && statSync2(p).isFile()) {
|
|
4157
|
+
let text = readFileSync18(p, "utf8");
|
|
4158
|
+
if (text.length > MAX_BYTES) text = text.slice(0, MAX_BYTES) + "\n\u2026(truncated)";
|
|
4159
|
+
text = text.trim();
|
|
4160
|
+
if (text) chain.push({ path: p, text });
|
|
4161
|
+
break;
|
|
4162
|
+
}
|
|
4163
|
+
} catch {
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
if (isRoot(dir)) break;
|
|
4167
|
+
const parent = dirname3(dir);
|
|
4168
|
+
if (parent === dir || dir === rootOfDrive) break;
|
|
4169
|
+
dir = parent;
|
|
4170
|
+
}
|
|
4171
|
+
return chain.reverse();
|
|
4172
|
+
}
|
|
4173
|
+
function buildProjectContext(cwd = process.cwd()) {
|
|
4174
|
+
let found;
|
|
4175
|
+
try {
|
|
4176
|
+
found = discoverProjectInstructions(cwd);
|
|
4177
|
+
} catch {
|
|
4178
|
+
return "";
|
|
4179
|
+
}
|
|
4180
|
+
if (!found.length) return "";
|
|
4181
|
+
const blocks = found.map((f) => `# Project instructions \u2014 ${f.path}
|
|
4182
|
+
${f.text}`);
|
|
4183
|
+
return "The user's project ships these instructions. Treat them as authoritative for work in this repository; when two files conflict, the one listed LAST (nearest the working directory) wins.\n\n" + blocks.join("\n\n");
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
// src/routers/mux-helpers.ts
|
|
4122
4187
|
var MUX_PROVIDER = "oriro-mux";
|
|
4123
4188
|
var MUX_MODEL = "oriro-free";
|
|
4124
4189
|
function errToCallError(msg) {
|
|
@@ -4138,6 +4203,145 @@ function buildErrorMessage(message) {
|
|
|
4138
4203
|
errorMessage: message
|
|
4139
4204
|
};
|
|
4140
4205
|
}
|
|
4206
|
+
|
|
4207
|
+
// src/routers/race.ts
|
|
4208
|
+
import { streamSimple as piStreamSimple } from "@earendil-works/pi-ai";
|
|
4209
|
+
|
|
4210
|
+
// src/routers/race-status.ts
|
|
4211
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
4212
|
+
var current = { phase: "idle", racers: [], winner: null };
|
|
4213
|
+
function emitRaceStatus(s) {
|
|
4214
|
+
current = s;
|
|
4215
|
+
for (const l of listeners) {
|
|
4216
|
+
try {
|
|
4217
|
+
l(s);
|
|
4218
|
+
} catch {
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
function onRaceStatus(l) {
|
|
4223
|
+
listeners.add(l);
|
|
4224
|
+
try {
|
|
4225
|
+
l(current);
|
|
4226
|
+
} catch {
|
|
4227
|
+
}
|
|
4228
|
+
return () => {
|
|
4229
|
+
listeners.delete(l);
|
|
4230
|
+
};
|
|
4231
|
+
}
|
|
4232
|
+
function getRaceStatus() {
|
|
4233
|
+
return current;
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
// src/routers/race.ts
|
|
4237
|
+
var DEFAULT_RACE_WIDTH = 3;
|
|
4238
|
+
var realStreamFactory = (router, context, options, signal) => piStreamSimple(routerModel(router), context, {
|
|
4239
|
+
...options ?? {},
|
|
4240
|
+
apiKey: router.apiKey,
|
|
4241
|
+
signal
|
|
4242
|
+
});
|
|
4243
|
+
async function raceMux(out, mux, byId, context, options, opts = {}) {
|
|
4244
|
+
const width = opts.width ?? DEFAULT_RACE_WIDTH;
|
|
4245
|
+
const streamFactory = opts.streamFactory ?? realStreamFactory;
|
|
4246
|
+
const push = (ev) => out.push(ev);
|
|
4247
|
+
const ranked = mux.ranked().filter((id) => byId.has(id));
|
|
4248
|
+
if (ranked.length === 0) {
|
|
4249
|
+
const msg = buildErrorMessage("All selected routers are unavailable. Add a BYOK key, select more free routers, or retry shortly.");
|
|
4250
|
+
push({ type: "error", reason: "error", error: msg });
|
|
4251
|
+
out.end(msg);
|
|
4252
|
+
emitRaceStatus({ phase: "failed", racers: [], winner: null });
|
|
4253
|
+
return;
|
|
4254
|
+
}
|
|
4255
|
+
const racers = ranked.slice(0, Math.max(1, Math.min(width, ranked.length)));
|
|
4256
|
+
emitRaceStatus({ phase: "racing", racers, winner: null });
|
|
4257
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
4258
|
+
for (const id of racers) controllers.set(id, new AbortController());
|
|
4259
|
+
const abortLosers = (keep) => {
|
|
4260
|
+
for (const [id, c] of controllers) if (id !== keep) {
|
|
4261
|
+
try {
|
|
4262
|
+
c.abort();
|
|
4263
|
+
} catch {
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
};
|
|
4267
|
+
let winner = null;
|
|
4268
|
+
let settled2 = false;
|
|
4269
|
+
let lastError;
|
|
4270
|
+
let remaining = racers.length;
|
|
4271
|
+
return await new Promise((resolve3) => {
|
|
4272
|
+
const failAll = () => {
|
|
4273
|
+
if (settled2) return;
|
|
4274
|
+
settled2 = true;
|
|
4275
|
+
const msg = lastError ?? buildErrorMessage("All racers failed this request.");
|
|
4276
|
+
push({ type: "error", reason: "error", error: msg });
|
|
4277
|
+
out.end(msg);
|
|
4278
|
+
emitRaceStatus({ phase: "failed", racers, winner: null });
|
|
4279
|
+
resolve3();
|
|
4280
|
+
};
|
|
4281
|
+
for (const id of racers) {
|
|
4282
|
+
const router = byId.get(id);
|
|
4283
|
+
const ctrl = controllers.get(id);
|
|
4284
|
+
const t0 = Date.now();
|
|
4285
|
+
void (async () => {
|
|
4286
|
+
let iAmWinner = false;
|
|
4287
|
+
let lastPartial;
|
|
4288
|
+
try {
|
|
4289
|
+
for await (const ev of streamFactory(router, context, options, ctrl.signal)) {
|
|
4290
|
+
if (settled2 && !iAmWinner) return;
|
|
4291
|
+
if (ev.type === "error") {
|
|
4292
|
+
mux.recordFailure(id, ev.error ? errToCallError(ev.error) : {});
|
|
4293
|
+
if (iAmWinner && !settled2) {
|
|
4294
|
+
settled2 = true;
|
|
4295
|
+
push(ev);
|
|
4296
|
+
out.end(ev.error);
|
|
4297
|
+
resolve3();
|
|
4298
|
+
return;
|
|
4299
|
+
}
|
|
4300
|
+
lastError = ev.error ?? lastError;
|
|
4301
|
+
return;
|
|
4302
|
+
}
|
|
4303
|
+
if (!iAmWinner) {
|
|
4304
|
+
if (winner !== null || settled2) return;
|
|
4305
|
+
winner = id;
|
|
4306
|
+
iAmWinner = true;
|
|
4307
|
+
mux.recordSuccess(id, Date.now() - t0);
|
|
4308
|
+
emitRaceStatus({ phase: "won", racers, winner: id });
|
|
4309
|
+
abortLosers(id);
|
|
4310
|
+
}
|
|
4311
|
+
if (ev.type === "done") {
|
|
4312
|
+
if (!settled2) {
|
|
4313
|
+
settled2 = true;
|
|
4314
|
+
const clean = sanitizeMessageToolCalls(scrubMessageIdentity(ev.message));
|
|
4315
|
+
push({ type: "done", reason: ev.reason, message: clean });
|
|
4316
|
+
out.end(clean);
|
|
4317
|
+
resolve3();
|
|
4318
|
+
}
|
|
4319
|
+
return;
|
|
4320
|
+
}
|
|
4321
|
+
lastPartial = ev.partial ?? lastPartial;
|
|
4322
|
+
push(sanitizeEventToolCalls(ev));
|
|
4323
|
+
}
|
|
4324
|
+
if (iAmWinner && !settled2) {
|
|
4325
|
+
settled2 = true;
|
|
4326
|
+
out.end(lastPartial ? sanitizeMessageToolCalls(scrubMessageIdentity(lastPartial)) : void 0);
|
|
4327
|
+
resolve3();
|
|
4328
|
+
} else if (!iAmWinner) {
|
|
4329
|
+
mux.recordFailure(id, {});
|
|
4330
|
+
}
|
|
4331
|
+
} catch (e) {
|
|
4332
|
+
if (e?.name === "AbortError") return;
|
|
4333
|
+
mux.recordFailure(id, e);
|
|
4334
|
+
if (!iAmWinner) lastError ??= buildErrorMessage(e instanceof Error ? e.message : String(e));
|
|
4335
|
+
} finally {
|
|
4336
|
+
remaining -= 1;
|
|
4337
|
+
if (remaining === 0 && !settled2) failAll();
|
|
4338
|
+
}
|
|
4339
|
+
})();
|
|
4340
|
+
}
|
|
4341
|
+
});
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
// src/routers/mux-provider.ts
|
|
4141
4345
|
async function driveMux(out, mux, byId, context, options) {
|
|
4142
4346
|
let lastError;
|
|
4143
4347
|
for (const id of mux.ranked()) {
|
|
@@ -4147,7 +4351,7 @@ async function driveMux(out, mux, byId, context, options) {
|
|
|
4147
4351
|
let committed = false;
|
|
4148
4352
|
let lastPartial;
|
|
4149
4353
|
try {
|
|
4150
|
-
const inner =
|
|
4354
|
+
const inner = piStreamSimple2(routerModel(router), context, {
|
|
4151
4355
|
...options ?? {},
|
|
4152
4356
|
apiKey: router.apiKey
|
|
4153
4357
|
});
|
|
@@ -4196,13 +4400,16 @@ async function driveMux(out, mux, byId, context, options) {
|
|
|
4196
4400
|
}
|
|
4197
4401
|
function registerOriroMux(registry, opts = {}) {
|
|
4198
4402
|
registerOpenAICompletions();
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4403
|
+
function resolveNow() {
|
|
4404
|
+
const pooled = resolvePool();
|
|
4405
|
+
const routers = opts.routers ?? (pooled.length > 0 ? pooled : KEYLESS_FLOOR);
|
|
4406
|
+
const byId = new Map(routers.map((r) => [r.id, r]));
|
|
4407
|
+
const mux = new RouterMux(routers.map((r) => r.id));
|
|
4408
|
+
try {
|
|
4409
|
+
mux.load(loadMuxState(oriroDir()));
|
|
4410
|
+
} catch {
|
|
4411
|
+
}
|
|
4412
|
+
return { routers, byId, mux };
|
|
4206
4413
|
}
|
|
4207
4414
|
registry.registerProvider(MUX_PROVIDER, {
|
|
4208
4415
|
name: "ORIRO Free (keyless Mux)",
|
|
@@ -4226,12 +4433,16 @@ function registerOriroMux(registry, opts = {}) {
|
|
|
4226
4433
|
],
|
|
4227
4434
|
streamSimple: (_model, context, options) => {
|
|
4228
4435
|
const out = createAssistantMessageEventStream();
|
|
4436
|
+
const { routers, byId, mux } = resolveNow();
|
|
4229
4437
|
const ctx = applyIdentity(context);
|
|
4438
|
+
const project = buildProjectContext();
|
|
4230
4439
|
const memory = buildScribeContext();
|
|
4231
|
-
const
|
|
4440
|
+
const extra = [project, memory].filter(Boolean).join("\n\n");
|
|
4441
|
+
const withMemory = extra ? { ...ctx, systemPrompt: `${ctx.systemPrompt}
|
|
4232
4442
|
|
|
4233
|
-
${
|
|
4234
|
-
|
|
4443
|
+
${extra}` } : ctx;
|
|
4444
|
+
const drive = routers.length > 1 ? raceMux(out, mux, byId, withMemory, options) : driveMux(out, mux, byId, withMemory, options);
|
|
4445
|
+
void drive.finally(() => {
|
|
4235
4446
|
try {
|
|
4236
4447
|
saveMuxState(oriroDir(), mux.snapshot());
|
|
4237
4448
|
} catch {
|
|
@@ -4554,7 +4765,7 @@ async function comparePages(opts) {
|
|
|
4554
4765
|
|
|
4555
4766
|
// src/head/run.ts
|
|
4556
4767
|
import { writeFile } from "fs/promises";
|
|
4557
|
-
import { join as
|
|
4768
|
+
import { join as join22 } from "path";
|
|
4558
4769
|
|
|
4559
4770
|
// src/head/inspection-html.ts
|
|
4560
4771
|
var PRIORITY_COLOR = {
|
|
@@ -5056,7 +5267,7 @@ async function runInspect(target, competitors, opts = {}) {
|
|
|
5056
5267
|
const report = await comparePages({ targetUrl: target, competitorUrls: competitors.length ? competitors : [target] });
|
|
5057
5268
|
const files = [];
|
|
5058
5269
|
if (opts.html) {
|
|
5059
|
-
const path =
|
|
5270
|
+
const path = join22(opts.outDir ?? process.cwd(), `oriro-head-${hostSlug(target)}-inspect.html`);
|
|
5060
5271
|
await writeFile(path, buildInspectionHtml(report), "utf8");
|
|
5061
5272
|
files.push(path);
|
|
5062
5273
|
}
|
|
@@ -5072,7 +5283,7 @@ function parseHeadTargets(text, selfOrigin) {
|
|
|
5072
5283
|
async function runUrlToCode(url, opts = {}) {
|
|
5073
5284
|
try {
|
|
5074
5285
|
const res = await urlToCode(url, headModels(), { goal: opts.goal, stack: opts.stack });
|
|
5075
|
-
const codePath =
|
|
5286
|
+
const codePath = join22(opts.outDir ?? process.cwd(), `oriro-head-${hostSlug(url)}${extForStack(opts.stack)}`);
|
|
5076
5287
|
await writeFile(codePath, res.code, "utf8");
|
|
5077
5288
|
return { summary: `Reverse-engineered ${url} into clean code (${res.code.length} chars) \u2192 ${codePath}`, files: [codePath] };
|
|
5078
5289
|
} catch (e) {
|
|
@@ -5082,7 +5293,7 @@ async function runUrlToCode(url, opts = {}) {
|
|
|
5082
5293
|
async function runUrlToSpec(url, opts = {}) {
|
|
5083
5294
|
try {
|
|
5084
5295
|
const res = await urlToSpec(url, headModels(), { goal: opts.goal });
|
|
5085
|
-
const specPath =
|
|
5296
|
+
const specPath = join22(opts.outDir ?? process.cwd(), `oriro-head-${hostSlug(url)}.spec.yaml`);
|
|
5086
5297
|
await writeFile(specPath, res.spec, "utf8");
|
|
5087
5298
|
return { summary: `Reverse-engineered ${url} into a YAML build spec \u2192 ${specPath}`, files: [specPath] };
|
|
5088
5299
|
} catch (e) {
|
|
@@ -5094,7 +5305,7 @@ async function runCapture(urls, opts = {}) {
|
|
|
5094
5305
|
const { captureScreens: captureScreens2, buildScreenshotFlowHtml: buildScreenshotFlowHtml2 } = await Promise.resolve().then(() => (init_screenshot_flow(), screenshot_flow_exports));
|
|
5095
5306
|
const caps = await captureScreens2(urls, { video: opts.video });
|
|
5096
5307
|
const html = buildScreenshotFlowHtml2([{ name: "Captured screens", captures: caps }]);
|
|
5097
|
-
const flowPath =
|
|
5308
|
+
const flowPath = join22(opts.outDir ?? process.cwd(), "oriro-head-flow.html");
|
|
5098
5309
|
await writeFile(flowPath, html, "utf8");
|
|
5099
5310
|
const ok2 = caps.filter((c) => c.ok).length;
|
|
5100
5311
|
return { summary: `Captured ${ok2}/${caps.length} full-page screenshots \u2192 ${flowPath}`, files: [flowPath] };
|
|
@@ -5115,7 +5326,7 @@ async function runVideoToCode(videoPath, opts = {}) {
|
|
|
5115
5326
|
{ videoPath, frames, mimeType: mime, goal: opts.goal, stack: opts.stack },
|
|
5116
5327
|
headVideoModels()
|
|
5117
5328
|
);
|
|
5118
|
-
const codePath =
|
|
5329
|
+
const codePath = join22(opts.outDir ?? process.cwd(), `oriro-head-video${extForStack(opts.stack)}`);
|
|
5119
5330
|
await writeFile(codePath, res.code, "utf8");
|
|
5120
5331
|
return { summary: `Watched ${videoPath} \u2192 built code (${res.code.length} chars) \u2192 ${codePath}
|
|
5121
5332
|
(experimental on the free floor \u2014 add a vision-capable router for pixel-faithful results.)`, files: [codePath] };
|
|
@@ -5297,29 +5508,29 @@ function registerOrchestrator(pi) {
|
|
|
5297
5508
|
import { Type as Type4 } from "typebox";
|
|
5298
5509
|
|
|
5299
5510
|
// src/agents/store.ts
|
|
5300
|
-
import { mkdirSync as mkdirSync15, readFileSync as
|
|
5301
|
-
import { join as
|
|
5511
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync19, writeFileSync as writeFileSync16, readdirSync as readdirSync2, rmSync as rmSync3, existsSync as existsSync14 } from "fs";
|
|
5512
|
+
import { join as join23 } from "path";
|
|
5302
5513
|
var SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
5303
5514
|
function isValidAgentName(name) {
|
|
5304
5515
|
return SLUG.test(name);
|
|
5305
5516
|
}
|
|
5306
5517
|
function agentsDir() {
|
|
5307
|
-
return
|
|
5518
|
+
return join23(oriroDir(), "agents");
|
|
5308
5519
|
}
|
|
5309
5520
|
function agentFile(name) {
|
|
5310
|
-
return
|
|
5521
|
+
return join23(agentsDir(), `${name}.json`);
|
|
5311
5522
|
}
|
|
5312
5523
|
function stateFile() {
|
|
5313
|
-
return
|
|
5524
|
+
return join23(agentsDir(), ".state.json");
|
|
5314
5525
|
}
|
|
5315
5526
|
function listAgents() {
|
|
5316
5527
|
const dir = agentsDir();
|
|
5317
|
-
if (!
|
|
5528
|
+
if (!existsSync14(dir)) return [];
|
|
5318
5529
|
const out = [];
|
|
5319
5530
|
for (const f of readdirSync2(dir)) {
|
|
5320
5531
|
if (!f.endsWith(".json") || f.startsWith(".")) continue;
|
|
5321
5532
|
try {
|
|
5322
|
-
const def = JSON.parse(
|
|
5533
|
+
const def = JSON.parse(readFileSync19(join23(dir, f), "utf8"));
|
|
5323
5534
|
if (def && typeof def.name === "string" && typeof def.task === "string") out.push(def);
|
|
5324
5535
|
} catch {
|
|
5325
5536
|
}
|
|
@@ -5328,7 +5539,7 @@ function listAgents() {
|
|
|
5328
5539
|
}
|
|
5329
5540
|
function loadAgent(name) {
|
|
5330
5541
|
try {
|
|
5331
|
-
return JSON.parse(
|
|
5542
|
+
return JSON.parse(readFileSync19(agentFile(name), "utf8"));
|
|
5332
5543
|
} catch {
|
|
5333
5544
|
return void 0;
|
|
5334
5545
|
}
|
|
@@ -5341,9 +5552,9 @@ function saveAgent(def) {
|
|
|
5341
5552
|
writeFileSync16(agentFile(def.name), JSON.stringify(def, null, 2), "utf8");
|
|
5342
5553
|
}
|
|
5343
5554
|
function removeAgent(name) {
|
|
5344
|
-
const
|
|
5345
|
-
if (!
|
|
5346
|
-
rmSync3(
|
|
5555
|
+
const file6 = agentFile(name);
|
|
5556
|
+
if (!existsSync14(file6)) return false;
|
|
5557
|
+
rmSync3(file6, { force: true });
|
|
5347
5558
|
const state = loadState();
|
|
5348
5559
|
if (state[name]) {
|
|
5349
5560
|
delete state[name];
|
|
@@ -5353,7 +5564,7 @@ function removeAgent(name) {
|
|
|
5353
5564
|
}
|
|
5354
5565
|
function loadState() {
|
|
5355
5566
|
try {
|
|
5356
|
-
return JSON.parse(
|
|
5567
|
+
return JSON.parse(readFileSync19(stateFile(), "utf8"));
|
|
5357
5568
|
} catch {
|
|
5358
5569
|
return {};
|
|
5359
5570
|
}
|
|
@@ -5479,6 +5690,218 @@ ${result.output.slice(0, 4e3)}` }],
|
|
|
5479
5690
|
});
|
|
5480
5691
|
}
|
|
5481
5692
|
|
|
5693
|
+
// src/connectors/mcp-client.ts
|
|
5694
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5695
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5696
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5697
|
+
var DISALLOWED_ENV = /* @__PURE__ */ new Set([
|
|
5698
|
+
"PATH",
|
|
5699
|
+
"LD_PRELOAD",
|
|
5700
|
+
"LD_LIBRARY_PATH",
|
|
5701
|
+
"DYLD_INSERT_LIBRARIES",
|
|
5702
|
+
"DYLD_LIBRARY_PATH",
|
|
5703
|
+
"NODE_OPTIONS",
|
|
5704
|
+
"PYTHONPATH",
|
|
5705
|
+
"PYTHONSTARTUP",
|
|
5706
|
+
"PERL5LIB",
|
|
5707
|
+
"RUBYOPT",
|
|
5708
|
+
"GEM_PATH",
|
|
5709
|
+
"APPINIT_DLLS",
|
|
5710
|
+
"COR_PROFILER",
|
|
5711
|
+
"BASH_ENV",
|
|
5712
|
+
"ENV",
|
|
5713
|
+
"IFS"
|
|
5714
|
+
]);
|
|
5715
|
+
var HANDSHAKE_TIMEOUT_MS = 8e3;
|
|
5716
|
+
var CALL_TIMEOUT_MS = 3e4;
|
|
5717
|
+
function sanitizeName(name) {
|
|
5718
|
+
return (name || "").toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) || "x";
|
|
5719
|
+
}
|
|
5720
|
+
function assertSafeUrl(raw, allowLocal = false) {
|
|
5721
|
+
const u = new URL(raw);
|
|
5722
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") throw new Error(`unsupported scheme: ${u.protocol}`);
|
|
5723
|
+
const host = u.hostname.toLowerCase();
|
|
5724
|
+
const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost");
|
|
5725
|
+
const isPrivate = /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || /^169\.254\./.test(host) || /^fe80:/i.test(host) || /^f[cd][0-9a-f]{2}:/i.test(host) || host === "169.254.169.254" || host === "metadata.google.internal";
|
|
5726
|
+
if ((isLoopback || isPrivate) && !allowLocal) {
|
|
5727
|
+
throw new Error(`blocked SSRF target ${host} (use --allow-local for loopback/LAN MCP servers)`);
|
|
5728
|
+
}
|
|
5729
|
+
if (u.protocol === "http:" && !isLoopback && !allowLocal) throw new Error(`refusing plaintext http to ${host} \u2014 use https`);
|
|
5730
|
+
return u;
|
|
5731
|
+
}
|
|
5732
|
+
function safeEnv(env) {
|
|
5733
|
+
const out = {};
|
|
5734
|
+
for (const [k, v] of Object.entries(env ?? {})) {
|
|
5735
|
+
if (DISALLOWED_ENV.has(k.toUpperCase())) continue;
|
|
5736
|
+
out[k] = v;
|
|
5737
|
+
}
|
|
5738
|
+
return out;
|
|
5739
|
+
}
|
|
5740
|
+
async function connectServer(name, config, opts = {}) {
|
|
5741
|
+
const client = new Client({ name: "oriro-cli", version: "0.1.0" }, { capabilities: {} });
|
|
5742
|
+
let transport;
|
|
5743
|
+
let stderr = "";
|
|
5744
|
+
if (config.type === "stdio") {
|
|
5745
|
+
const t = new StdioClientTransport({
|
|
5746
|
+
command: config.command,
|
|
5747
|
+
args: config.args ?? [],
|
|
5748
|
+
env: safeEnv(config.env),
|
|
5749
|
+
stderr: "pipe"
|
|
5750
|
+
});
|
|
5751
|
+
t.stderr?.on("data", (b) => {
|
|
5752
|
+
stderr += b.toString();
|
|
5753
|
+
});
|
|
5754
|
+
transport = t;
|
|
5755
|
+
} else {
|
|
5756
|
+
const url = assertSafeUrl(config.url, opts.allowLocal);
|
|
5757
|
+
transport = new StreamableHTTPClientTransport(url, {
|
|
5758
|
+
requestInit: { headers: { "User-Agent": "oriro-cli/0.1.0", ...config.headers ?? {} } }
|
|
5759
|
+
});
|
|
5760
|
+
}
|
|
5761
|
+
const timeoutMs = opts.timeoutMs ?? HANDSHAKE_TIMEOUT_MS;
|
|
5762
|
+
let timer;
|
|
5763
|
+
try {
|
|
5764
|
+
await Promise.race([
|
|
5765
|
+
client.connect(transport),
|
|
5766
|
+
new Promise((_, rej) => {
|
|
5767
|
+
timer = setTimeout(() => rej(new Error("handshake timed out")), timeoutMs);
|
|
5768
|
+
})
|
|
5769
|
+
]);
|
|
5770
|
+
} catch (e) {
|
|
5771
|
+
const detail = stderr.trim() ? `
|
|
5772
|
+
server stderr:
|
|
5773
|
+
${stderr.trim().slice(0, 800)}` : "";
|
|
5774
|
+
try {
|
|
5775
|
+
await transport.close();
|
|
5776
|
+
} catch {
|
|
5777
|
+
}
|
|
5778
|
+
throw new Error(`MCP connect failed (${name}): ${e instanceof Error ? e.message : String(e)}${detail}`);
|
|
5779
|
+
} finally {
|
|
5780
|
+
if (timer) clearTimeout(timer);
|
|
5781
|
+
}
|
|
5782
|
+
return {
|
|
5783
|
+
name,
|
|
5784
|
+
client,
|
|
5785
|
+
dispose: async () => {
|
|
5786
|
+
try {
|
|
5787
|
+
await client.close();
|
|
5788
|
+
} catch {
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
};
|
|
5792
|
+
}
|
|
5793
|
+
async function listAllTools(client) {
|
|
5794
|
+
const tools = [];
|
|
5795
|
+
let cursor;
|
|
5796
|
+
do {
|
|
5797
|
+
const res = await client.listTools(cursor ? { cursor } : void 0, { timeout: CALL_TIMEOUT_MS });
|
|
5798
|
+
for (const t of res.tools) tools.push({ name: t.name, description: t.description, inputSchema: t.inputSchema });
|
|
5799
|
+
cursor = res.nextCursor;
|
|
5800
|
+
} while (cursor);
|
|
5801
|
+
return tools;
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
// src/connectors/register.ts
|
|
5805
|
+
import { Type as Type5 } from "typebox";
|
|
5806
|
+
function registerToolList(pi, serverName, client, tools, seen = /* @__PURE__ */ new Set()) {
|
|
5807
|
+
const server = sanitizeName(serverName);
|
|
5808
|
+
const registered = [];
|
|
5809
|
+
for (const t of tools) {
|
|
5810
|
+
const publicName = `mcp__${server}__${sanitizeName(t.name)}`;
|
|
5811
|
+
if (seen.has(publicName)) continue;
|
|
5812
|
+
seen.add(publicName);
|
|
5813
|
+
const realName = t.name;
|
|
5814
|
+
pi.registerTool({
|
|
5815
|
+
name: publicName,
|
|
5816
|
+
label: `MCP: ${serverName}`,
|
|
5817
|
+
description: (t.description ?? `${t.name} (via ${serverName})`).slice(0, 1024),
|
|
5818
|
+
parameters: Type5.Object({}, { additionalProperties: true }),
|
|
5819
|
+
async execute(_id, params) {
|
|
5820
|
+
const details = { server: serverName, tool: realName };
|
|
5821
|
+
try {
|
|
5822
|
+
const res = await client.callTool(
|
|
5823
|
+
{ name: realName, arguments: params ?? {} },
|
|
5824
|
+
void 0,
|
|
5825
|
+
{ timeout: CALL_TIMEOUT_MS }
|
|
5826
|
+
);
|
|
5827
|
+
const text = (res.content ?? []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
|
|
5828
|
+
if (res.isError) {
|
|
5829
|
+
details.isError = true;
|
|
5830
|
+
return { content: [{ type: "text", text: `MCP tool error: ${text || "(no detail)"}` }], details };
|
|
5831
|
+
}
|
|
5832
|
+
return { content: [{ type: "text", text: text || "(no text content)" }], details };
|
|
5833
|
+
} catch (e) {
|
|
5834
|
+
details.isError = true;
|
|
5835
|
+
return { content: [{ type: "text", text: `MCP call failed: ${e instanceof Error ? e.message : String(e)}` }], details };
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
});
|
|
5839
|
+
registered.push(publicName);
|
|
5840
|
+
}
|
|
5841
|
+
return registered;
|
|
5842
|
+
}
|
|
5843
|
+
|
|
5844
|
+
// src/connectors/custom.ts
|
|
5845
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync17 } from "fs";
|
|
5846
|
+
import { join as join24 } from "path";
|
|
5847
|
+
function file3() {
|
|
5848
|
+
return join24(oriroDir(), "mcp-custom.json");
|
|
5849
|
+
}
|
|
5850
|
+
function readCustomServers() {
|
|
5851
|
+
try {
|
|
5852
|
+
const v = JSON.parse(readFileSync20(file3(), "utf8"));
|
|
5853
|
+
return Array.isArray(v) ? v : [];
|
|
5854
|
+
} catch {
|
|
5855
|
+
return [];
|
|
5856
|
+
}
|
|
5857
|
+
}
|
|
5858
|
+
function saveCustomServer(server) {
|
|
5859
|
+
const rest = readCustomServers().filter((s) => s.name.toLowerCase() !== server.name.toLowerCase());
|
|
5860
|
+
writeFileSync17(join24(ensureOriroDir(), "mcp-custom.json"), JSON.stringify([...rest, server], null, 2), "utf8");
|
|
5861
|
+
}
|
|
5862
|
+
function removeCustomServer(name) {
|
|
5863
|
+
const before = readCustomServers();
|
|
5864
|
+
const after = before.filter((s) => s.name.toLowerCase() !== name.toLowerCase());
|
|
5865
|
+
if (after.length === before.length) return false;
|
|
5866
|
+
writeFileSync17(join24(ensureOriroDir(), "mcp-custom.json"), JSON.stringify(after, null, 2), "utf8");
|
|
5867
|
+
return true;
|
|
5868
|
+
}
|
|
5869
|
+
function trustedServerNames() {
|
|
5870
|
+
return readCustomServers().filter((s) => s.trusted).map((s) => s.name);
|
|
5871
|
+
}
|
|
5872
|
+
function isServerTrusted(name) {
|
|
5873
|
+
return trustedServerNames().some((n) => n.toLowerCase() === name.toLowerCase());
|
|
5874
|
+
}
|
|
5875
|
+
|
|
5876
|
+
// src/connectors/session-connect.ts
|
|
5877
|
+
var CONNECT_TIMEOUT_MS = 8e3;
|
|
5878
|
+
async function prepareConnectors() {
|
|
5879
|
+
const targets = [];
|
|
5880
|
+
for (const c of addedConnectors()) {
|
|
5881
|
+
if (c.mcpUrl) targets.push({ name: c.slug, config: { type: "http", url: c.mcpUrl } });
|
|
5882
|
+
}
|
|
5883
|
+
for (const s of readCustomServers()) {
|
|
5884
|
+
if (s.trusted) targets.push({ name: s.name, config: s.config, allowLocal: true });
|
|
5885
|
+
}
|
|
5886
|
+
const out = [];
|
|
5887
|
+
for (const t of targets) {
|
|
5888
|
+
try {
|
|
5889
|
+
const conn = await connectServer(t.name, t.config, {
|
|
5890
|
+
timeoutMs: CONNECT_TIMEOUT_MS,
|
|
5891
|
+
...t.allowLocal ? { allowLocal: true } : {}
|
|
5892
|
+
});
|
|
5893
|
+
const tools = await listAllTools(conn.client);
|
|
5894
|
+
out.push({ name: t.name, client: conn.client, tools });
|
|
5895
|
+
} catch {
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
return out;
|
|
5899
|
+
}
|
|
5900
|
+
function registerPreparedConnectors(pi, prepared) {
|
|
5901
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5902
|
+
for (const p of prepared) registerToolList(pi, p.name, p.client, p.tools, seen);
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5482
5905
|
// src/onboarding/assemble.ts
|
|
5483
5906
|
async function assembleOriroSession(opts = {}) {
|
|
5484
5907
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -5487,13 +5910,22 @@ async function assembleOriroSession(opts = {}) {
|
|
|
5487
5910
|
const settingsManager = SettingsManager.create(cwd);
|
|
5488
5911
|
const model = registerOriroMux(modelRegistry, opts.routers ? { routers: opts.routers } : {});
|
|
5489
5912
|
if (!model) throw new Error("ORIRO keyless model unavailable");
|
|
5913
|
+
const preparedConnectors = await prepareConnectors();
|
|
5490
5914
|
const resourceLoader = new DefaultResourceLoader({
|
|
5491
5915
|
cwd,
|
|
5492
5916
|
agentDir: getAgentDir(),
|
|
5493
5917
|
settingsManager,
|
|
5494
5918
|
additionalSkillPaths: skillRoots(),
|
|
5495
5919
|
// bundled library + the user's own ~/.oriro/skills
|
|
5496
|
-
extensionFactories: [
|
|
5920
|
+
extensionFactories: [
|
|
5921
|
+
registerGuardian,
|
|
5922
|
+
registerHead,
|
|
5923
|
+
registerScribe,
|
|
5924
|
+
registerOrchestrator,
|
|
5925
|
+
registerAgentRunner,
|
|
5926
|
+
(pi) => registerPreparedConnectors(pi, preparedConnectors)
|
|
5927
|
+
// MCP connectors → agent tools
|
|
5928
|
+
]
|
|
5497
5929
|
});
|
|
5498
5930
|
await resourceLoader.reload();
|
|
5499
5931
|
const { session, extensionsResult } = await createAgentSession2({
|
|
@@ -5682,14 +6114,14 @@ var MODE_META = {
|
|
|
5682
6114
|
auto: { label: "Auto", indicator: "\u23F5\u23F5" },
|
|
5683
6115
|
plan: { label: "Plan", indicator: "\u25A2" }
|
|
5684
6116
|
};
|
|
5685
|
-
var
|
|
6117
|
+
var current2 = "manual";
|
|
5686
6118
|
function getMode() {
|
|
5687
|
-
return
|
|
6119
|
+
return current2;
|
|
5688
6120
|
}
|
|
5689
6121
|
function cycleMode() {
|
|
5690
|
-
const i = MODES.indexOf(
|
|
5691
|
-
|
|
5692
|
-
return
|
|
6122
|
+
const i = MODES.indexOf(current2);
|
|
6123
|
+
current2 = MODES[(i + 1) % MODES.length];
|
|
6124
|
+
return current2;
|
|
5693
6125
|
}
|
|
5694
6126
|
var thinking = false;
|
|
5695
6127
|
function getThinking() {
|
|
@@ -5702,7 +6134,7 @@ function toggleThinking() {
|
|
|
5702
6134
|
var THINKING_PRIMER = "Think step by step and plan your approach before acting. Reason carefully and check your work.";
|
|
5703
6135
|
|
|
5704
6136
|
// src/repl-ui/verify-actions.ts
|
|
5705
|
-
import { existsSync as
|
|
6137
|
+
import { existsSync as existsSync15 } from "fs";
|
|
5706
6138
|
import { isAbsolute, resolve } from "path";
|
|
5707
6139
|
var CLAIM = /\b(?:have|has)\s+been\s+created\b|\b(?:created|wrote|written|saved|generated)\b(?![ \t]*(?:by you|it yourself))/i;
|
|
5708
6140
|
var SUGGESTION = /\byou\s+(?:can|could|should|may)\s+(?:create|add|save|make|put)\b/i;
|
|
@@ -5715,7 +6147,7 @@ function phantomFileWarning(reply, cwd = process.cwd()) {
|
|
|
5715
6147
|
if (!p) continue;
|
|
5716
6148
|
if (/^https?:|node_modules|<[^>]+>|your-|example\./i.test(p)) continue;
|
|
5717
6149
|
const abs = isAbsolute(p) ? p : resolve(cwd, p.replace(/^[.][\\/]/, ""));
|
|
5718
|
-
if (!
|
|
6150
|
+
if (!existsSync15(abs)) missing.add(p);
|
|
5719
6151
|
}
|
|
5720
6152
|
if (missing.size === 0) return "";
|
|
5721
6153
|
if (SUGGESTION.test(reply) && !/\b(?:have|has)\s+been\s+created\b/i.test(reply)) return "";
|
|
@@ -5725,6 +6157,252 @@ function phantomFileWarning(reply, cwd = process.cwd()) {
|
|
|
5725
6157
|
\u26A0 ORIRO said it ${plural ? "created files" : "created a file"} (${list}), but ${plural ? "they're" : "it's"} not on disk \u2014 the free router may have described the write without actually running it. Retry, or add your own key with \`oriro routers\` for reliable coding.`;
|
|
5726
6158
|
}
|
|
5727
6159
|
|
|
6160
|
+
// src/repl-ui/slash-routers.ts
|
|
6161
|
+
function isRouterSlash(cmd) {
|
|
6162
|
+
return /^\/(routers?|model)(\s|$)/i.test(cmd.trim());
|
|
6163
|
+
}
|
|
6164
|
+
function poolLine() {
|
|
6165
|
+
const pool = resolvePool();
|
|
6166
|
+
return pool.length ? `${dim("racing now:")} ${accent(pool.map((p) => p.id).join(", "))}` : dim("racing now: (empty) \u2192 keyless floor");
|
|
6167
|
+
}
|
|
6168
|
+
function catalogLines(head) {
|
|
6169
|
+
const lines = [];
|
|
6170
|
+
lines.push(
|
|
6171
|
+
head === "/model" ? dim(" ORIRO models & free routers \u2014 they race, best answer wins:") : dim(" Router catalog \u2014 they race, best answer wins:")
|
|
6172
|
+
);
|
|
6173
|
+
for (const r of selectableRouters()) {
|
|
6174
|
+
const tier = r.keyless ? fgHex(PALETTE.success, "keyless") : dim(r.tier);
|
|
6175
|
+
lines.push(` ${accent(r.id.padEnd(20))} ${r.displayName.padEnd(22)} ${tier}`);
|
|
6176
|
+
}
|
|
6177
|
+
lines.push(` ${poolLine()}`);
|
|
6178
|
+
lines.push(dim(" add: /routers add <id> \xB7 rotate: /routers use <id> [<id>\u2026]"));
|
|
6179
|
+
return lines;
|
|
6180
|
+
}
|
|
6181
|
+
async function handleRouterSlash(raw) {
|
|
6182
|
+
const parts = raw.trim().split(/\s+/);
|
|
6183
|
+
const head = (parts[0] ?? "").toLowerCase();
|
|
6184
|
+
const sub = (parts[1] ?? "").toLowerCase();
|
|
6185
|
+
try {
|
|
6186
|
+
if (sub === "add") {
|
|
6187
|
+
const id = parts[2];
|
|
6188
|
+
if (!id) return [dim(" usage: /routers add <id> (e.g. /routers add oriro-gauss)")];
|
|
6189
|
+
const entry = routerById(id);
|
|
6190
|
+
if (!entry) return [dim(` unknown router '${id}' \u2014 try /routers list`)];
|
|
6191
|
+
const res = await addRouter(entry, {});
|
|
6192
|
+
if (res.ok) {
|
|
6193
|
+
return [
|
|
6194
|
+
` ${fgHex(PALETTE.success, "\u2713")} added ${accent(id)} (${res.validation.latencyMs}ms, model ${res.validation.model}) \u2192 ${fgHex(PALETTE.success, "now racing")}`,
|
|
6195
|
+
` ${poolLine()}`
|
|
6196
|
+
];
|
|
6197
|
+
}
|
|
6198
|
+
return [dim(` \u2717 could not add ${id}: ${res.validation.error ?? "validation failed"}`)];
|
|
6199
|
+
}
|
|
6200
|
+
const rotate = sub === "use" ? parts.slice(2) : head === "/model" && parts[1] && sub !== "list" ? parts.slice(1) : null;
|
|
6201
|
+
if (rotate) {
|
|
6202
|
+
if (!rotate.length) return [dim(" usage: /routers use <id> [<id>\u2026]")];
|
|
6203
|
+
const { applied, unknown } = useRouters(rotate);
|
|
6204
|
+
const out = [];
|
|
6205
|
+
if (applied.length) out.push(` ${fgHex(PALETTE.success, "\u2713")} now racing: ${accent(applied.join(", "))}`);
|
|
6206
|
+
if (unknown.length) out.push(dim(` not registered yet (add first): ${unknown.join(", ")}`));
|
|
6207
|
+
return out.length ? out : [dim(" nothing applied \u2014 add a router first: /routers add <id>")];
|
|
6208
|
+
}
|
|
6209
|
+
return catalogLines(head);
|
|
6210
|
+
} catch (e) {
|
|
6211
|
+
return [dim(` router command failed: ${e instanceof Error ? e.message : String(e)}`)];
|
|
6212
|
+
}
|
|
6213
|
+
}
|
|
6214
|
+
|
|
6215
|
+
// src/repl-ui/repl-state.ts
|
|
6216
|
+
var turns = 0;
|
|
6217
|
+
var trace = false;
|
|
6218
|
+
function bumpTurns() {
|
|
6219
|
+
turns += 1;
|
|
6220
|
+
}
|
|
6221
|
+
function getTurns() {
|
|
6222
|
+
return turns;
|
|
6223
|
+
}
|
|
6224
|
+
function getTrace() {
|
|
6225
|
+
return trace;
|
|
6226
|
+
}
|
|
6227
|
+
function toggleTrace() {
|
|
6228
|
+
trace = !trace;
|
|
6229
|
+
return trace;
|
|
6230
|
+
}
|
|
6231
|
+
|
|
6232
|
+
// src/repl-ui/slash-usage.ts
|
|
6233
|
+
function isUsageSlash(cmd) {
|
|
6234
|
+
return /^\/usage(\s|$)/i.test(cmd.trim());
|
|
6235
|
+
}
|
|
6236
|
+
function handleUsage() {
|
|
6237
|
+
const pool = resolvePool();
|
|
6238
|
+
const health = new Map(loadMuxState(oriroDir()).map((s) => [s.id, s]));
|
|
6239
|
+
const race = getRaceStatus();
|
|
6240
|
+
const lines = [];
|
|
6241
|
+
lines.push(dim(` turns this session: ${accent(String(getTurns()))} \xB7 thinking-trace: ${getTrace() ? fgHex(PALETTE.success, "on") : dim("off")}`));
|
|
6242
|
+
lines.push(dim(" racing pool (learned latency \xB7 health):"));
|
|
6243
|
+
if (!pool.length) {
|
|
6244
|
+
lines.push(dim(" (empty) \u2192 the keyless floor"));
|
|
6245
|
+
} else {
|
|
6246
|
+
const now = Date.now();
|
|
6247
|
+
for (const r of pool) {
|
|
6248
|
+
const s = health.get(r.id);
|
|
6249
|
+
const lat = s && Number.isFinite(s.latencyMs) ? `${Math.round(s.latencyMs)}ms` : "untried";
|
|
6250
|
+
const state = !s ? dim("new") : !s.healthy ? fgHex(PALETTE.error, "unhealthy") : s.cooldownUntil > now ? fgHex(PALETTE.error, "cooling") : fgHex(PALETTE.success, "healthy");
|
|
6251
|
+
lines.push(` ${accent(r.id.padEnd(20))} ${dim(lat.padEnd(9))} ${state}`);
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
if (race.winner && race.racers.length > 1) {
|
|
6255
|
+
lines.push(dim(` last race: ${race.racers.join(" \xB7 ")} \u2192 won: `) + accent(race.winner));
|
|
6256
|
+
}
|
|
6257
|
+
return lines;
|
|
6258
|
+
}
|
|
6259
|
+
|
|
6260
|
+
// src/repl-ui/slash-artifacts.ts
|
|
6261
|
+
import { existsSync as existsSync16, writeFileSync as writeFileSync18 } from "fs";
|
|
6262
|
+
|
|
6263
|
+
// src/repl-ui/artifacts.ts
|
|
6264
|
+
var LANG_EXT = {
|
|
6265
|
+
python: "py",
|
|
6266
|
+
py: "py",
|
|
6267
|
+
javascript: "js",
|
|
6268
|
+
js: "js",
|
|
6269
|
+
typescript: "ts",
|
|
6270
|
+
ts: "ts",
|
|
6271
|
+
tsx: "tsx",
|
|
6272
|
+
jsx: "jsx",
|
|
6273
|
+
html: "html",
|
|
6274
|
+
css: "css",
|
|
6275
|
+
json: "json",
|
|
6276
|
+
yaml: "yaml",
|
|
6277
|
+
yml: "yml",
|
|
6278
|
+
bash: "sh",
|
|
6279
|
+
sh: "sh",
|
|
6280
|
+
shell: "sh",
|
|
6281
|
+
sql: "sql",
|
|
6282
|
+
go: "go",
|
|
6283
|
+
rust: "rs",
|
|
6284
|
+
rs: "rs",
|
|
6285
|
+
java: "java",
|
|
6286
|
+
c: "c",
|
|
6287
|
+
cpp: "cpp",
|
|
6288
|
+
"c++": "cpp",
|
|
6289
|
+
ruby: "rb",
|
|
6290
|
+
rb: "rb",
|
|
6291
|
+
php: "php",
|
|
6292
|
+
markdown: "md",
|
|
6293
|
+
md: "md",
|
|
6294
|
+
svg: "svg"
|
|
6295
|
+
};
|
|
6296
|
+
function extFor(lang) {
|
|
6297
|
+
return LANG_EXT[lang.toLowerCase()] ?? "txt";
|
|
6298
|
+
}
|
|
6299
|
+
function extractArtifacts(text) {
|
|
6300
|
+
const out = [];
|
|
6301
|
+
if (!text) return out;
|
|
6302
|
+
const fence = /```([\w+#.-]*)\n([\s\S]*?)```/g;
|
|
6303
|
+
let m;
|
|
6304
|
+
while ((m = fence.exec(text)) !== null) {
|
|
6305
|
+
const lang = (m[1] ?? "").trim();
|
|
6306
|
+
const content = (m[2] ?? "").replace(/\n$/, "");
|
|
6307
|
+
if (!content.trim()) continue;
|
|
6308
|
+
const isSvg = lang.toLowerCase() === "svg" || /^\s*<svg[\s>]/.test(content);
|
|
6309
|
+
out.push({
|
|
6310
|
+
kind: isSvg ? "svg" : "code",
|
|
6311
|
+
lang: lang || (isSvg ? "svg" : ""),
|
|
6312
|
+
content,
|
|
6313
|
+
suggestedName: `artifact-${out.length + 1}.${isSvg ? "svg" : extFor(lang)}`
|
|
6314
|
+
});
|
|
6315
|
+
}
|
|
6316
|
+
const svg = /<svg[\s>][\s\S]*?<\/svg>/gi;
|
|
6317
|
+
while ((m = svg.exec(text)) !== null) {
|
|
6318
|
+
const content = m[0];
|
|
6319
|
+
if (out.some((a) => a.content.includes(content))) continue;
|
|
6320
|
+
out.push({ kind: "svg", lang: "svg", content, suggestedName: `artifact-${out.length + 1}.svg` });
|
|
6321
|
+
}
|
|
6322
|
+
return out;
|
|
6323
|
+
}
|
|
6324
|
+
var current3 = [];
|
|
6325
|
+
function setArtifacts(a) {
|
|
6326
|
+
current3 = a;
|
|
6327
|
+
}
|
|
6328
|
+
function getArtifacts() {
|
|
6329
|
+
return current3;
|
|
6330
|
+
}
|
|
6331
|
+
|
|
6332
|
+
// src/repl-ui/slash-artifacts.ts
|
|
6333
|
+
function isArtifactSlash(cmd) {
|
|
6334
|
+
return /^\/(review|artifacts?|save)(\s|$)/i.test(cmd.trim());
|
|
6335
|
+
}
|
|
6336
|
+
function handleArtifactSlash(raw) {
|
|
6337
|
+
const parts = raw.trim().split(/\s+/);
|
|
6338
|
+
const head = (parts[0] ?? "").toLowerCase();
|
|
6339
|
+
const arts = getArtifacts();
|
|
6340
|
+
if (head === "/save") {
|
|
6341
|
+
const idx = parseInt(parts[1] ?? "", 10);
|
|
6342
|
+
if (!Number.isInteger(idx) || idx < 1 || idx > arts.length) {
|
|
6343
|
+
return [dim(" usage: /save <n> [path] \u2014 run /review to see the artifacts")];
|
|
6344
|
+
}
|
|
6345
|
+
const art = arts[idx - 1];
|
|
6346
|
+
if (!art) return [dim(" no such artifact")];
|
|
6347
|
+
const dest = parts[2] || art.suggestedName;
|
|
6348
|
+
if (existsSync16(dest)) return [dim(` \u2717 ${dest} already exists \u2014 give a different path: /save ${idx} <path>`)];
|
|
6349
|
+
try {
|
|
6350
|
+
writeFileSync18(dest, art.content, "utf8");
|
|
6351
|
+
} catch (e) {
|
|
6352
|
+
return [dim(` \u2717 could not write ${dest}: ${e instanceof Error ? e.message : String(e)}`)];
|
|
6353
|
+
}
|
|
6354
|
+
return [` ${fgHex(PALETTE.success, "\u2713")} saved artifact ${accent(String(idx))} \u2192 ${accent(dest)} ${dim(`(${art.content.length} bytes)`)}`];
|
|
6355
|
+
}
|
|
6356
|
+
if (!arts.length) return [dim(" no artifacts in the last reply \u2014 ask for code or an SVG, then /review")];
|
|
6357
|
+
const lines = [dim(" Artifacts from the last reply \u2014 save one with /save <n> [path]:")];
|
|
6358
|
+
arts.forEach((a, i) => {
|
|
6359
|
+
const nlines = a.content.split("\n").length;
|
|
6360
|
+
const preview = (a.content.split("\n")[0] ?? "").slice(0, 48).replace(/\s+/g, " ");
|
|
6361
|
+
lines.push(` ${accent(String(i + 1))}. ${a.kind}${a.lang ? `/${a.lang}` : ""} \xB7 ${nlines} lines \xB7 \u2192 ${dim(a.suggestedName)} ${dim(preview)}`);
|
|
6362
|
+
});
|
|
6363
|
+
return lines;
|
|
6364
|
+
}
|
|
6365
|
+
|
|
6366
|
+
// src/repl-ui/slash-compact.ts
|
|
6367
|
+
function isCompactSlash(cmd) {
|
|
6368
|
+
return /^\/compact(\s|$)/i.test(cmd.trim());
|
|
6369
|
+
}
|
|
6370
|
+
function compactInstructions(cmd) {
|
|
6371
|
+
const rest = cmd.trim().replace(/^\/compact\s*/i, "").trim();
|
|
6372
|
+
return rest.length ? rest : void 0;
|
|
6373
|
+
}
|
|
6374
|
+
function formatCompactionResult(result) {
|
|
6375
|
+
const before = result.tokensBefore;
|
|
6376
|
+
const after = result.estimatedTokensAfter;
|
|
6377
|
+
const lines = [];
|
|
6378
|
+
if (typeof after === "number" && before > 0) {
|
|
6379
|
+
const freed = Math.max(0, before - after);
|
|
6380
|
+
const pct = Math.round(freed / before * 100);
|
|
6381
|
+
lines.push(
|
|
6382
|
+
` ${fgHex(PALETTE.success, "\u2713 compacted")} ${dim(`${before.toLocaleString()} \u2192 ${after.toLocaleString()} tokens`)} ${accent(`(${pct}% freed)`)}`
|
|
6383
|
+
);
|
|
6384
|
+
} else {
|
|
6385
|
+
lines.push(` ${fgHex(PALETTE.success, "\u2713 compacted")} ${dim(`${before.toLocaleString()} tokens summarized`)}`);
|
|
6386
|
+
}
|
|
6387
|
+
lines.push(dim(" history summarized; the summary is kept, raw turns dropped. Keep going."));
|
|
6388
|
+
return lines;
|
|
6389
|
+
}
|
|
6390
|
+
async function handleCompact(session, cmd) {
|
|
6391
|
+
if (session.isCompacting) {
|
|
6392
|
+
return [dim(" compaction already in progress \u2014 hold on\u2026")];
|
|
6393
|
+
}
|
|
6394
|
+
if (session.messages.length < 4) {
|
|
6395
|
+
return [dim(" not much to compact yet \u2014 keep chatting, then /compact frees context.")];
|
|
6396
|
+
}
|
|
6397
|
+
try {
|
|
6398
|
+
const result = await session.compact(compactInstructions(cmd));
|
|
6399
|
+
if (!result) return [dim(" nothing to compact right now.")];
|
|
6400
|
+
return formatCompactionResult(result);
|
|
6401
|
+
} catch (e) {
|
|
6402
|
+
return [` ${fgHex(PALETTE.error, "compaction failed")}: ${dim(e instanceof Error ? e.message : String(e))}`];
|
|
6403
|
+
}
|
|
6404
|
+
}
|
|
6405
|
+
|
|
5728
6406
|
// src/repl-ui/tui-repl.ts
|
|
5729
6407
|
var editorTheme = {
|
|
5730
6408
|
borderColor: (s) => dim(s),
|
|
@@ -5804,7 +6482,13 @@ async function runTuiRepl(session) {
|
|
|
5804
6482
|
const slash = text.toLowerCase();
|
|
5805
6483
|
if (slash === "/exit" || slash === "/quit") return cleanup();
|
|
5806
6484
|
if (slash === "/help" || slash === "/?") {
|
|
5807
|
-
|
|
6485
|
+
const help = [
|
|
6486
|
+
" Just type to chat \u2014 ORIRO writes and runs code for you (keyless, free).",
|
|
6487
|
+
` ${accent("/routers")} pool add\xB7rotate ${accent("/model")} <id\u2026> switch ${accent("/usage")} health ${accent("/trace")} tool+router activity ${accent("/compact")} free context`,
|
|
6488
|
+
` ${accent("/review")} artifacts from the last reply ${accent("/save")} <n> [path] ${accent("/skills")} ${accent("/connectors")} ${accent("/voice")}`,
|
|
6489
|
+
` ${dim("Shift+Tab")} posture ${dim("Alt+Shift+T")} thinking ${accent("/help")} ${accent("/exit")}`
|
|
6490
|
+
].join("\n");
|
|
6491
|
+
chat.addChild(new Text(help, 0, 0));
|
|
5808
6492
|
editor.setText("");
|
|
5809
6493
|
tui.requestRender();
|
|
5810
6494
|
return;
|
|
@@ -5821,6 +6505,49 @@ async function runTuiRepl(session) {
|
|
|
5821
6505
|
tui.requestRender();
|
|
5822
6506
|
return;
|
|
5823
6507
|
}
|
|
6508
|
+
if (isRouterSlash(slash)) {
|
|
6509
|
+
editor.setText("");
|
|
6510
|
+
const pending = new Text(dim(" \u2026"), 0, 0);
|
|
6511
|
+
chat.addChild(pending);
|
|
6512
|
+
tui.requestRender();
|
|
6513
|
+
void (async () => {
|
|
6514
|
+
const lines = await handleRouterSlash(text);
|
|
6515
|
+
pending.setText(lines.join("\n"));
|
|
6516
|
+
tui.requestRender();
|
|
6517
|
+
})();
|
|
6518
|
+
return;
|
|
6519
|
+
}
|
|
6520
|
+
if (isUsageSlash(slash)) {
|
|
6521
|
+
chat.addChild(new Text(handleUsage().join("\n"), 0, 0));
|
|
6522
|
+
editor.setText("");
|
|
6523
|
+
tui.requestRender();
|
|
6524
|
+
return;
|
|
6525
|
+
}
|
|
6526
|
+
if (slash === "/trace") {
|
|
6527
|
+
const on = toggleTrace();
|
|
6528
|
+
chat.addChild(new Text(dim(` trace ${on ? "ON \u2014 showing tool + router activity" : "off"}`), 0, 0));
|
|
6529
|
+
editor.setText("");
|
|
6530
|
+
tui.requestRender();
|
|
6531
|
+
return;
|
|
6532
|
+
}
|
|
6533
|
+
if (isArtifactSlash(slash)) {
|
|
6534
|
+
chat.addChild(new Text(handleArtifactSlash(text).join("\n"), 0, 0));
|
|
6535
|
+
editor.setText("");
|
|
6536
|
+
tui.requestRender();
|
|
6537
|
+
return;
|
|
6538
|
+
}
|
|
6539
|
+
if (isCompactSlash(slash)) {
|
|
6540
|
+
editor.setText("");
|
|
6541
|
+
const pending = new Text(dim(" compacting\u2026"), 0, 0);
|
|
6542
|
+
chat.addChild(pending);
|
|
6543
|
+
tui.requestRender();
|
|
6544
|
+
void (async () => {
|
|
6545
|
+
const lines = await handleCompact(session, text);
|
|
6546
|
+
pending.setText(lines.join("\n"));
|
|
6547
|
+
tui.requestRender();
|
|
6548
|
+
})();
|
|
6549
|
+
return;
|
|
6550
|
+
}
|
|
5824
6551
|
if (slash === "/voice") {
|
|
5825
6552
|
editor.setText("");
|
|
5826
6553
|
const status = new Text(dim(" \u{1F399} listening\u2026 (needs ffmpeg + the transformers voice peer)"), 0, 0);
|
|
@@ -5841,10 +6568,23 @@ async function runTuiRepl(session) {
|
|
|
5841
6568
|
editor.addToHistory(text);
|
|
5842
6569
|
editor.setText("");
|
|
5843
6570
|
chat.addChild(new Text(`${accent("\u203A")} ${text}`, 0, 1));
|
|
6571
|
+
const raceLine = new Text("", 0, 0);
|
|
6572
|
+
chat.addChild(raceLine);
|
|
5844
6573
|
const streaming = new Text(dim("\u2026"), 0, 0);
|
|
5845
6574
|
chat.addChild(streaming);
|
|
6575
|
+
const unsubRace = onRaceStatus((s) => {
|
|
6576
|
+
if (s.phase === "racing" && s.racers.length > 1) {
|
|
6577
|
+
raceLine.setText(dim(` \u23F1 racing: ${s.racers.join(" \xB7 ")}`));
|
|
6578
|
+
} else if (s.phase === "won" && s.winner && s.racers.length > 1) {
|
|
6579
|
+
raceLine.setText(dim(` \u23F1 ${s.racers.join(" \xB7 ")} \u2192 won: `) + accent(s.winner));
|
|
6580
|
+
} else {
|
|
6581
|
+
raceLine.setText("");
|
|
6582
|
+
}
|
|
6583
|
+
tui.requestRender();
|
|
6584
|
+
});
|
|
5846
6585
|
tui.requestRender();
|
|
5847
6586
|
busy = true;
|
|
6587
|
+
bumpTurns();
|
|
5848
6588
|
void (async () => {
|
|
5849
6589
|
let english = await translateIncoming(text);
|
|
5850
6590
|
if (getThinking()) english = `${THINKING_PRIMER}
|
|
@@ -5860,6 +6600,9 @@ ${english}`;
|
|
|
5860
6600
|
streaming.setText(out);
|
|
5861
6601
|
tui.requestRender();
|
|
5862
6602
|
}
|
|
6603
|
+
} else if (getTrace() && (e.type === "tool_start" || e.type === "tool_end" || e.type === "toolcall_start")) {
|
|
6604
|
+
chat.addChild(new Text(dim(` \u2699 ${e.type.replace("_", " ")}${e.toolName ? `: ${e.toolName}` : ""}`), 0, 0));
|
|
6605
|
+
tui.requestRender();
|
|
5863
6606
|
}
|
|
5864
6607
|
}
|
|
5865
6608
|
);
|
|
@@ -5870,13 +6613,19 @@ ${english}`;
|
|
|
5870
6613
|
tui.requestRender();
|
|
5871
6614
|
busy = false;
|
|
5872
6615
|
unsub();
|
|
6616
|
+
unsubRace();
|
|
5873
6617
|
return;
|
|
5874
6618
|
}
|
|
5875
6619
|
unsub();
|
|
6620
|
+
unsubRace();
|
|
5876
6621
|
const cleaned = scrubOutput(out);
|
|
5877
6622
|
const finalText = isEnglish3 ? cleaned.trim() : await translateOutgoing(cleaned.trim());
|
|
5878
6623
|
const warn = phantomFileWarning(finalText);
|
|
5879
|
-
|
|
6624
|
+
const arts = extractArtifacts(finalText);
|
|
6625
|
+
setArtifacts(arts);
|
|
6626
|
+
const hint = arts.length ? dim(`
|
|
6627
|
+
\u2398 ${arts.length} artifact${arts.length === 1 ? "" : "s"} \u2014 /review to save`) : "";
|
|
6628
|
+
streaming.setText((finalText || dim("(no response)")) + (warn ? dim(warn) : "") + hint);
|
|
5880
6629
|
tui.requestRender();
|
|
5881
6630
|
busy = false;
|
|
5882
6631
|
})();
|
|
@@ -5890,8 +6639,8 @@ ${english}`;
|
|
|
5890
6639
|
// src/voice/mic.ts
|
|
5891
6640
|
import { spawn as spawn3 } from "child_process";
|
|
5892
6641
|
import { tmpdir as tmpdir3 } from "os";
|
|
5893
|
-
import { join as
|
|
5894
|
-
import { existsSync as
|
|
6642
|
+
import { join as join25 } from "path";
|
|
6643
|
+
import { existsSync as existsSync17, statSync as statSync3 } from "fs";
|
|
5895
6644
|
function recorders(outFile, seconds) {
|
|
5896
6645
|
const dur = String(seconds);
|
|
5897
6646
|
if (process.platform === "darwin") {
|
|
@@ -5912,12 +6661,12 @@ function recorders(outFile, seconds) {
|
|
|
5912
6661
|
];
|
|
5913
6662
|
}
|
|
5914
6663
|
async function recordMic(seconds = 6) {
|
|
5915
|
-
const outFile =
|
|
6664
|
+
const outFile = join25(tmpdir3(), `oriro-voice-${process.pid}-${seconds}.wav`);
|
|
5916
6665
|
for (const r of recorders(outFile, seconds)) {
|
|
5917
6666
|
const okFile = await new Promise((resolve3) => {
|
|
5918
6667
|
const child = spawn3(r.cmd, r.args, { stdio: "ignore" });
|
|
5919
6668
|
child.on("error", () => resolve3(false));
|
|
5920
|
-
child.on("close", (code) => resolve3(code === 0 &&
|
|
6669
|
+
child.on("close", (code) => resolve3(code === 0 && existsSync17(outFile) && statSync3(outFile).size > 44));
|
|
5921
6670
|
});
|
|
5922
6671
|
if (okFile) return outFile;
|
|
5923
6672
|
}
|
|
@@ -5981,9 +6730,13 @@ function replHelp() {
|
|
|
5981
6730
|
${accent("ORIRO terminal \u2014 help")}
|
|
5982
6731
|
${dim("Just type to chat; ORIRO writes and runs code for you (keyless, free).")}
|
|
5983
6732
|
|
|
5984
|
-
${
|
|
5985
|
-
${dim("
|
|
5986
|
-
${dim("
|
|
6733
|
+
${dim("Models & routers")} ${accent("/routers")} list\xB7add\xB7rotate the racing pool ${accent("/model")} <id\u2026> switch
|
|
6734
|
+
${dim("This session")} ${accent("/usage")} pool health & turns ${accent("/trace")} show tool + router activity ${accent("/compact")} free context
|
|
6735
|
+
${dim("Artifacts")} ${accent("/review")} code/SVG from the last reply ${accent("/save")} <n> [path] write one
|
|
6736
|
+
${dim("Capabilities")} ${accent("/skills")} ${accent("/connectors")} ${accent("/voice")} speak a turn
|
|
6737
|
+
${dim("General")} ${accent("/help")} this ${accent("/exit")} / ${accent("/quit")} leave ${dim("(Ctrl-D / Ctrl-C also exit)")}
|
|
6738
|
+
|
|
6739
|
+
${dim("Full command list outside the chat:")} ${accent("oriro --help")}
|
|
5987
6740
|
|
|
5988
6741
|
`;
|
|
5989
6742
|
}
|
|
@@ -6042,6 +6795,28 @@ async function runReadlineRepl(session) {
|
|
|
6042
6795
|
`);
|
|
6043
6796
|
continue;
|
|
6044
6797
|
}
|
|
6798
|
+
if (isRouterSlash(slash)) {
|
|
6799
|
+
stdout7.write((await handleRouterSlash(line)).join("\n") + "\n");
|
|
6800
|
+
continue;
|
|
6801
|
+
}
|
|
6802
|
+
if (isUsageSlash(slash)) {
|
|
6803
|
+
stdout7.write(handleUsage().join("\n") + "\n");
|
|
6804
|
+
continue;
|
|
6805
|
+
}
|
|
6806
|
+
if (slash === "/trace") {
|
|
6807
|
+
stdout7.write(` ${dim(`trace ${toggleTrace() ? "ON" : "off"}`)}
|
|
6808
|
+
`);
|
|
6809
|
+
continue;
|
|
6810
|
+
}
|
|
6811
|
+
if (isCompactSlash(slash)) {
|
|
6812
|
+
stdout7.write((await handleCompact(session, line)).join("\n") + "\n");
|
|
6813
|
+
continue;
|
|
6814
|
+
}
|
|
6815
|
+
if (isArtifactSlash(slash)) {
|
|
6816
|
+
stdout7.write(handleArtifactSlash(line).join("\n") + "\n");
|
|
6817
|
+
continue;
|
|
6818
|
+
}
|
|
6819
|
+
bumpTurns();
|
|
6045
6820
|
const english = await translateIncoming(line);
|
|
6046
6821
|
noteUserInput(line);
|
|
6047
6822
|
let out = "";
|
|
@@ -6059,8 +6834,12 @@ async function runReadlineRepl(session) {
|
|
|
6059
6834
|
}
|
|
6060
6835
|
const cleaned = scrubOutput(out);
|
|
6061
6836
|
const shown = isEnglish3 ? cleaned.trim() : await translateOutgoing(cleaned.trim());
|
|
6837
|
+
const arts = extractArtifacts(shown);
|
|
6838
|
+
setArtifacts(arts);
|
|
6839
|
+
const hint = arts.length ? ` ${dim(`\u2398 ${arts.length} artifact${arts.length === 1 ? "" : "s"} \u2014 /review to save`)}
|
|
6840
|
+
` : "";
|
|
6062
6841
|
stdout7.write(`${shown}${phantomFileWarning(shown)}
|
|
6063
|
-
|
|
6842
|
+
${hint}
|
|
6064
6843
|
`);
|
|
6065
6844
|
}
|
|
6066
6845
|
} finally {
|
|
@@ -6073,7 +6852,54 @@ async function runReadlineRepl(session) {
|
|
|
6073
6852
|
}
|
|
6074
6853
|
}
|
|
6075
6854
|
|
|
6855
|
+
// src/headless.ts
|
|
6856
|
+
function isOutputFormatMode(s) {
|
|
6857
|
+
return s === "text" || s === "json" || s === "stream-json";
|
|
6858
|
+
}
|
|
6859
|
+
async function runHeadless(prompt, format) {
|
|
6860
|
+
if (!prompt.trim()) {
|
|
6861
|
+
process.stderr.write('error: empty prompt \u2014 pass text after -p, e.g. oriro -p "summarise this repo"\n');
|
|
6862
|
+
process.exitCode = 1;
|
|
6863
|
+
return;
|
|
6864
|
+
}
|
|
6865
|
+
const { session } = await assembleOriroSession({});
|
|
6866
|
+
let text = "";
|
|
6867
|
+
const unsub = session.subscribe(
|
|
6868
|
+
(e) => {
|
|
6869
|
+
if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
|
|
6870
|
+
const d = e.assistantMessageEvent.delta ?? "";
|
|
6871
|
+
text += d;
|
|
6872
|
+
if (format === "stream-json" && d) process.stdout.write(JSON.stringify({ type: "text_delta", delta: d }) + "\n");
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
);
|
|
6876
|
+
let error = "";
|
|
6877
|
+
try {
|
|
6878
|
+
await session.prompt(prompt);
|
|
6879
|
+
} catch (e) {
|
|
6880
|
+
error = e instanceof Error ? e.message : String(e);
|
|
6881
|
+
}
|
|
6882
|
+
unsub();
|
|
6883
|
+
const response = scrubOutput(text).trim();
|
|
6884
|
+
const ok2 = !error && response.length > 0;
|
|
6885
|
+
if (format === "json") {
|
|
6886
|
+
process.stdout.write(JSON.stringify({ ok: ok2, response, ...error ? { error } : {} }) + "\n");
|
|
6887
|
+
} else if (format === "stream-json") {
|
|
6888
|
+
process.stdout.write(JSON.stringify({ type: "done", ok: ok2, response, ...error ? { error } : {} }) + "\n");
|
|
6889
|
+
} else {
|
|
6890
|
+
process.stdout.write((response || (error ? `error: ${error}` : "(no response)")) + "\n");
|
|
6891
|
+
}
|
|
6892
|
+
process.exitCode = ok2 ? 0 : 1;
|
|
6893
|
+
try {
|
|
6894
|
+
session.dispose();
|
|
6895
|
+
} catch {
|
|
6896
|
+
}
|
|
6897
|
+
setTimeout(() => process.exit(ok2 ? 0 : 1), 400).unref();
|
|
6898
|
+
}
|
|
6899
|
+
|
|
6076
6900
|
// src/commands/ui.ts
|
|
6901
|
+
import { createInterface as createInterface7 } from "readline/promises";
|
|
6902
|
+
import { stdin as stdin7, stdout as stdout8 } from "process";
|
|
6077
6903
|
var ok = (s) => {
|
|
6078
6904
|
process.stdout.write(`${fgHex(PALETTE.success, "\u2713")} ${s}
|
|
6079
6905
|
`);
|
|
@@ -6098,11 +6924,159 @@ function die(msg) {
|
|
|
6098
6924
|
process.exitCode = 1;
|
|
6099
6925
|
throw new DieError(msg);
|
|
6100
6926
|
}
|
|
6927
|
+
async function confirmDestructive(what, opts = {}) {
|
|
6928
|
+
if (opts.force) return true;
|
|
6929
|
+
if (!stdin7.isTTY || !stdout8.isTTY) {
|
|
6930
|
+
die(`refusing to ${what} without confirmation \u2014 re-run with --force in a non-interactive shell`);
|
|
6931
|
+
}
|
|
6932
|
+
const rl = createInterface7({ input: stdin7, output: stdout8 });
|
|
6933
|
+
try {
|
|
6934
|
+
const ans = (await rl.question(`${fgHex(PALETTE.error, "?")} ${what} \u2014 this cannot be undone. Proceed? [y/N] `)).trim().toLowerCase();
|
|
6935
|
+
return ans === "y" || ans === "yes";
|
|
6936
|
+
} finally {
|
|
6937
|
+
rl.close();
|
|
6938
|
+
}
|
|
6939
|
+
}
|
|
6940
|
+
|
|
6941
|
+
// src/config/store.ts
|
|
6942
|
+
import { readFileSync as readFileSync21, writeFileSync as writeFileSync19, mkdirSync as mkdirSync16 } from "fs";
|
|
6943
|
+
import { join as join26 } from "path";
|
|
6944
|
+
var KEYS = {
|
|
6945
|
+
output: {
|
|
6946
|
+
desc: "default output format for list commands: text | json | csv",
|
|
6947
|
+
validate: (v) => ["text", "json", "csv"].includes(v) ? null : "must be text | json | csv"
|
|
6948
|
+
},
|
|
6949
|
+
lang: { desc: "preferred UI language code (e.g. en, hi, es) \u2014 overrides terminal detection" },
|
|
6950
|
+
thinking: {
|
|
6951
|
+
desc: "default REPL thinking mode: on | off",
|
|
6952
|
+
validate: (v) => ["on", "off"].includes(v) ? null : "must be on | off"
|
|
6953
|
+
}
|
|
6954
|
+
};
|
|
6955
|
+
function configKeys() {
|
|
6956
|
+
return Object.keys(KEYS).map((key) => ({ key, desc: KEYS[key].desc }));
|
|
6957
|
+
}
|
|
6958
|
+
function isConfigKey(k) {
|
|
6959
|
+
return k in KEYS;
|
|
6960
|
+
}
|
|
6961
|
+
function validateConfig(key, value) {
|
|
6962
|
+
return KEYS[key].validate?.(value) ?? null;
|
|
6963
|
+
}
|
|
6964
|
+
function file4() {
|
|
6965
|
+
return join26(oriroDir(), "config.json");
|
|
6966
|
+
}
|
|
6967
|
+
var cache = null;
|
|
6968
|
+
function readAll() {
|
|
6969
|
+
if (cache) return cache;
|
|
6970
|
+
try {
|
|
6971
|
+
const v = JSON.parse(readFileSync21(file4(), "utf8"));
|
|
6972
|
+
cache = v && typeof v === "object" ? v : {};
|
|
6973
|
+
} catch {
|
|
6974
|
+
cache = {};
|
|
6975
|
+
}
|
|
6976
|
+
return cache;
|
|
6977
|
+
}
|
|
6978
|
+
function configGet(key) {
|
|
6979
|
+
return readAll()[key];
|
|
6980
|
+
}
|
|
6981
|
+
function configAll() {
|
|
6982
|
+
return { ...readAll() };
|
|
6983
|
+
}
|
|
6984
|
+
function configSet(key, value) {
|
|
6985
|
+
const all = { ...readAll(), [key]: value };
|
|
6986
|
+
mkdirSync16(oriroDir(), { recursive: true });
|
|
6987
|
+
writeFileSync19(file4(), JSON.stringify(all, null, 2), "utf8");
|
|
6988
|
+
cache = all;
|
|
6989
|
+
}
|
|
6990
|
+
function configUnset(key) {
|
|
6991
|
+
const all = readAll();
|
|
6992
|
+
if (!(key in all)) return false;
|
|
6993
|
+
const rest = { ...all };
|
|
6994
|
+
delete rest[key];
|
|
6995
|
+
writeFileSync19(file4(), JSON.stringify(rest, null, 2), "utf8");
|
|
6996
|
+
cache = rest;
|
|
6997
|
+
return true;
|
|
6998
|
+
}
|
|
6999
|
+
|
|
7000
|
+
// src/commands/output.ts
|
|
7001
|
+
function parseFormat(o) {
|
|
7002
|
+
const f = (o ?? configGet("output") ?? "text").toLowerCase();
|
|
7003
|
+
if (f === "json" || f === "csv" || f === "text") return f;
|
|
7004
|
+
throw new Error(`invalid --output '${o}'. Use: text | json | csv`);
|
|
7005
|
+
}
|
|
7006
|
+
function applyQuery(rows, query) {
|
|
7007
|
+
if (!query) return rows;
|
|
7008
|
+
const [filterPart, selectField] = query.split(":", 2);
|
|
7009
|
+
let out = rows;
|
|
7010
|
+
const fp = filterPart ?? "";
|
|
7011
|
+
if (fp.includes("=")) {
|
|
7012
|
+
const [field2, value] = fp.split("=", 2);
|
|
7013
|
+
out = rows.filter((r) => String(r[field2] ?? "") === value);
|
|
7014
|
+
} else if (fp && !selectField) {
|
|
7015
|
+
return rows.map((r) => r[fp]);
|
|
7016
|
+
}
|
|
7017
|
+
if (selectField) return out.map((r) => r[selectField]);
|
|
7018
|
+
return out;
|
|
7019
|
+
}
|
|
7020
|
+
function csvCell(v) {
|
|
7021
|
+
const s = v === null || v === void 0 ? "" : String(v);
|
|
7022
|
+
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
7023
|
+
}
|
|
7024
|
+
function renderList2(rows, opts = {}) {
|
|
7025
|
+
const fmt = parseFormat(opts.output);
|
|
7026
|
+
const queried = applyQuery(rows, opts.query);
|
|
7027
|
+
if (fmt === "json") return JSON.stringify(queried, null, 2);
|
|
7028
|
+
if (!Array.isArray(queried) || queried.length === 0) return "";
|
|
7029
|
+
const first = queried[0];
|
|
7030
|
+
const scalar = typeof first !== "object" || first === null;
|
|
7031
|
+
if (scalar) return queried.map((v) => fmt === "csv" ? csvCell(v) : String(v)).join("\n");
|
|
7032
|
+
const objs = queried;
|
|
7033
|
+
const cols = opts.columns ?? [...new Set(objs.flatMap((r) => Object.keys(r)))];
|
|
7034
|
+
if (fmt === "csv") {
|
|
7035
|
+
return [cols.map(csvCell).join(","), ...objs.map((r) => cols.map((c) => csvCell(r[c])).join(","))].join("\n");
|
|
7036
|
+
}
|
|
7037
|
+
const widths = cols.map((c) => Math.max(c.length, ...objs.map((r) => String(r[c] ?? "").length)));
|
|
7038
|
+
const line = (cells) => cells.map((s, i) => s.padEnd(widths[i] ?? 0)).join(" ").trimEnd();
|
|
7039
|
+
return [line(cols), ...objs.map((r) => line(cols.map((c) => String(r[c] ?? ""))))].join("\n");
|
|
7040
|
+
}
|
|
7041
|
+
function isMachineOutput(opts) {
|
|
7042
|
+
return parseFormat(opts.output) !== "text";
|
|
7043
|
+
}
|
|
7044
|
+
function outputError(opts) {
|
|
7045
|
+
const f = (opts.output ?? configGet("output") ?? "text").toLowerCase();
|
|
7046
|
+
return f === "json" || f === "csv" || f === "text" ? null : `invalid --output '${opts.output}' \u2014 use text | json | csv`;
|
|
7047
|
+
}
|
|
6101
7048
|
|
|
6102
7049
|
// src/commands/routers.ts
|
|
6103
7050
|
function registerRoutersCommand(program2) {
|
|
6104
7051
|
const routers = program2.command("routers").description("manage the free-router pool the model runs on");
|
|
6105
|
-
routers.command("list").description("list the router catalog and the active pool").action(() => {
|
|
7052
|
+
routers.command("list").description("list the router catalog and the active pool").option("-o, --output <fmt>", "output format: text (default) | json | csv").option("-q, --query <expr>", "filter/select: 'field', 'field=value', or 'field=value:selectField'").action((opts) => {
|
|
7053
|
+
const oerr = outputError(opts);
|
|
7054
|
+
if (oerr) die(oerr);
|
|
7055
|
+
const pool = new Set(resolvePool().map((p) => p.id));
|
|
7056
|
+
if (isMachineOutput(opts) || opts.query) {
|
|
7057
|
+
const catalogRows = ROUTER_CATALOG.filter((r) => !r.comingSoon).map((r) => ({
|
|
7058
|
+
id: r.id,
|
|
7059
|
+
name: r.displayName,
|
|
7060
|
+
tier: r.keyless ? "keyless" : r.tier,
|
|
7061
|
+
keyless: Boolean(r.keyless),
|
|
7062
|
+
source: "catalog",
|
|
7063
|
+
active: pool.has(r.id)
|
|
7064
|
+
}));
|
|
7065
|
+
const customRows = registeredRouters().filter((r) => !ROUTER_CATALOG.some((c) => c.id === r.id)).map((r) => ({
|
|
7066
|
+
id: r.id,
|
|
7067
|
+
name: r.name,
|
|
7068
|
+
tier: r.apiKey && r.apiKey !== KEYLESS_SENTINEL ? "byok" : "keyless",
|
|
7069
|
+
keyless: !r.apiKey || r.apiKey === KEYLESS_SENTINEL,
|
|
7070
|
+
source: "custom",
|
|
7071
|
+
active: pool.has(r.id)
|
|
7072
|
+
}));
|
|
7073
|
+
process.stdout.write(renderList2([...catalogRows, ...customRows], {
|
|
7074
|
+
output: opts.output,
|
|
7075
|
+
query: opts.query,
|
|
7076
|
+
columns: ["id", "name", "tier", "keyless", "active", "source"]
|
|
7077
|
+
}) + "\n");
|
|
7078
|
+
return;
|
|
7079
|
+
}
|
|
6106
7080
|
heading("Routers");
|
|
6107
7081
|
for (const r of ROUTER_CATALOG) {
|
|
6108
7082
|
if (r.comingSoon) {
|
|
@@ -6125,8 +7099,7 @@ function registerRoutersCommand(program2) {
|
|
|
6125
7099
|
`);
|
|
6126
7100
|
}
|
|
6127
7101
|
}
|
|
6128
|
-
|
|
6129
|
-
info(pool.length ? `active pool: ${pool.map((p) => p.id).join(", ")}` : "active pool: empty \u2192 using the keyless floor");
|
|
7102
|
+
info(pool.size ? `active pool: ${[...pool].join(", ")}` : "active pool: empty \u2192 using the keyless floor");
|
|
6130
7103
|
});
|
|
6131
7104
|
routers.command("add <name>").description("live-validate a router and add it to the pool \u2014 a catalog name, OR any custom endpoint via --url").option("-k, --key <key>", "API key (BYOK) \u2014 omit for a keyless free router").option("-m, --model <id>", "model id to run (REQUIRED for a custom --url router)").option("--url <baseUrl>", "add ANY custom free/BYOK router by its OpenAI-compatible base URL (the part BEFORE /chat/completions)").option("--api <api>", "custom router API: 'openai' (default) or 'google'", "openai").action(async (name, opts) => {
|
|
6132
7105
|
let entry;
|
|
@@ -6162,10 +7135,10 @@ function registerRoutersCommand(program2) {
|
|
|
6162
7135
|
}
|
|
6163
7136
|
|
|
6164
7137
|
// src/commands/scribe.ts
|
|
6165
|
-
import { readFileSync as
|
|
7138
|
+
import { readFileSync as readFileSync23 } from "fs";
|
|
6166
7139
|
|
|
6167
7140
|
// src/scribe/transcript.ts
|
|
6168
|
-
import { existsSync as
|
|
7141
|
+
import { existsSync as existsSync19, readFileSync as readFileSync22 } from "fs";
|
|
6169
7142
|
function parseHookStdin(raw) {
|
|
6170
7143
|
try {
|
|
6171
7144
|
const j = JSON.parse(raw);
|
|
@@ -6198,8 +7171,8 @@ function isHumanUser(e) {
|
|
|
6198
7171
|
}
|
|
6199
7172
|
var FILE_KEYS = ["file_path", "path", "notebook_path", "filePath"];
|
|
6200
7173
|
function lastTurnFromTranscript(path) {
|
|
6201
|
-
if (!
|
|
6202
|
-
const raw =
|
|
7174
|
+
if (!existsSync19(path)) return null;
|
|
7175
|
+
const raw = readFileSync22(path, "utf8");
|
|
6203
7176
|
const entries = [];
|
|
6204
7177
|
for (const line of raw.split("\n")) {
|
|
6205
7178
|
if (!line.trim()) continue;
|
|
@@ -6260,7 +7233,7 @@ function lastTurnFromTranscript(path) {
|
|
|
6260
7233
|
// src/commands/scribe.ts
|
|
6261
7234
|
function readStdin() {
|
|
6262
7235
|
try {
|
|
6263
|
-
return
|
|
7236
|
+
return readFileSync23(0, "utf8");
|
|
6264
7237
|
} catch {
|
|
6265
7238
|
return "";
|
|
6266
7239
|
}
|
|
@@ -6373,40 +7346,8 @@ function registerScribeCommand(program2) {
|
|
|
6373
7346
|
}
|
|
6374
7347
|
|
|
6375
7348
|
// src/commands/connectors.ts
|
|
6376
|
-
import { createInterface as
|
|
6377
|
-
import { stdin as
|
|
6378
|
-
|
|
6379
|
-
// src/connectors/custom.ts
|
|
6380
|
-
import { readFileSync as readFileSync21, writeFileSync as writeFileSync17 } from "fs";
|
|
6381
|
-
import { join as join24 } from "path";
|
|
6382
|
-
function file3() {
|
|
6383
|
-
return join24(oriroDir(), "mcp-custom.json");
|
|
6384
|
-
}
|
|
6385
|
-
function readCustomServers() {
|
|
6386
|
-
try {
|
|
6387
|
-
const v = JSON.parse(readFileSync21(file3(), "utf8"));
|
|
6388
|
-
return Array.isArray(v) ? v : [];
|
|
6389
|
-
} catch {
|
|
6390
|
-
return [];
|
|
6391
|
-
}
|
|
6392
|
-
}
|
|
6393
|
-
function saveCustomServer(server) {
|
|
6394
|
-
const rest = readCustomServers().filter((s) => s.name.toLowerCase() !== server.name.toLowerCase());
|
|
6395
|
-
writeFileSync17(join24(ensureOriroDir(), "mcp-custom.json"), JSON.stringify([...rest, server], null, 2), "utf8");
|
|
6396
|
-
}
|
|
6397
|
-
function removeCustomServer(name) {
|
|
6398
|
-
const before = readCustomServers();
|
|
6399
|
-
const after = before.filter((s) => s.name.toLowerCase() !== name.toLowerCase());
|
|
6400
|
-
if (after.length === before.length) return false;
|
|
6401
|
-
writeFileSync17(join24(ensureOriroDir(), "mcp-custom.json"), JSON.stringify(after, null, 2), "utf8");
|
|
6402
|
-
return true;
|
|
6403
|
-
}
|
|
6404
|
-
function trustedServerNames() {
|
|
6405
|
-
return readCustomServers().filter((s) => s.trusted).map((s) => s.name);
|
|
6406
|
-
}
|
|
6407
|
-
function isServerTrusted(name) {
|
|
6408
|
-
return trustedServerNames().some((n) => n.toLowerCase() === name.toLowerCase());
|
|
6409
|
-
}
|
|
7349
|
+
import { createInterface as createInterface8 } from "readline/promises";
|
|
7350
|
+
import { stdin as stdin8, stdout as stdout9 } from "process";
|
|
6410
7351
|
|
|
6411
7352
|
// src/connectors/setup.ts
|
|
6412
7353
|
function buildServerConfig(i) {
|
|
@@ -6437,39 +7378,41 @@ function parsePairs(s) {
|
|
|
6437
7378
|
return out;
|
|
6438
7379
|
}
|
|
6439
7380
|
|
|
6440
|
-
// src/connectors/mcp-client.ts
|
|
6441
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6442
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6443
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6444
|
-
function assertSafeUrl(raw, allowLocal = false) {
|
|
6445
|
-
const u = new URL(raw);
|
|
6446
|
-
if (u.protocol !== "https:" && u.protocol !== "http:") throw new Error(`unsupported scheme: ${u.protocol}`);
|
|
6447
|
-
const host = u.hostname.toLowerCase();
|
|
6448
|
-
const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost");
|
|
6449
|
-
const isPrivate = /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || /^169\.254\./.test(host) || /^fe80:/i.test(host) || /^f[cd][0-9a-f]{2}:/i.test(host) || host === "169.254.169.254" || host === "metadata.google.internal";
|
|
6450
|
-
if ((isLoopback || isPrivate) && !allowLocal) {
|
|
6451
|
-
throw new Error(`blocked SSRF target ${host} (use --allow-local for loopback/LAN MCP servers)`);
|
|
6452
|
-
}
|
|
6453
|
-
if (u.protocol === "http:" && !isLoopback && !allowLocal) throw new Error(`refusing plaintext http to ${host} \u2014 use https`);
|
|
6454
|
-
return u;
|
|
6455
|
-
}
|
|
6456
|
-
|
|
6457
7381
|
// src/commands/connectors.ts
|
|
6458
7382
|
function registerConnectorsCommand(program2) {
|
|
6459
7383
|
const connectors = program2.command("connectors").description("MCP connectors \u2014 add external tools/services (inert until used)");
|
|
6460
|
-
connectors.command("list [category]").description("list the connector catalog (optionally filtered by category)").action((category) => {
|
|
7384
|
+
connectors.command("list [category]").description("list the connector catalog (optionally filtered by category)").option("-o, --output <fmt>", "output format: text (default) | json | csv").option("-q, --query <expr>", "filter/select: 'field', 'field=value', or 'field=value:selectField'").action((category, opts) => {
|
|
7385
|
+
const oerr = outputError(opts);
|
|
7386
|
+
if (oerr) die(oerr);
|
|
6461
7387
|
if (category && !connectorCategories().includes(category)) {
|
|
6462
7388
|
die(`unknown category '${category}' \u2014 categories: ${connectorCategories().join(", ")}`);
|
|
6463
7389
|
}
|
|
6464
7390
|
const entries = listConnectors(category);
|
|
6465
7391
|
const added = new Set(addedConnectors().map((c) => c.slug));
|
|
7392
|
+
if (isMachineOutput(opts) || opts.query) {
|
|
7393
|
+
const rows = entries.map((c) => ({
|
|
7394
|
+
slug: c.slug,
|
|
7395
|
+
name: c.name,
|
|
7396
|
+
category: c.category,
|
|
7397
|
+
addable: Boolean(c.mcpUrl),
|
|
7398
|
+
added: added.has(c.slug)
|
|
7399
|
+
}));
|
|
7400
|
+
process.stdout.write(renderList2(rows, {
|
|
7401
|
+
output: opts.output,
|
|
7402
|
+
query: opts.query,
|
|
7403
|
+
columns: ["slug", "name", "category", "addable", "added"]
|
|
7404
|
+
}) + "\n");
|
|
7405
|
+
return;
|
|
7406
|
+
}
|
|
6466
7407
|
heading(category ? `Connectors \xB7 ${category}` : "Connectors");
|
|
6467
7408
|
let addable = 0;
|
|
7409
|
+
const NAME_W = 34;
|
|
6468
7410
|
for (const c of entries) {
|
|
6469
7411
|
const canAdd = !!c.mcpUrl;
|
|
6470
7412
|
if (canAdd) addable++;
|
|
6471
7413
|
const mark = !canAdd ? dim("\xB7") : added.has(c.slug) ? accent("\u25CF") : dim("\u25CB");
|
|
6472
|
-
const
|
|
7414
|
+
const label = (canAdd ? c.name : `${c.name} (coming soon)`).padEnd(NAME_W);
|
|
7415
|
+
const name = canAdd ? label : dim(label);
|
|
6473
7416
|
process.stdout.write(` ${mark} ${(canAdd ? accent : dim)(c.slug.padEnd(20))} ${name} ${dim(c.category)}
|
|
6474
7417
|
`);
|
|
6475
7418
|
}
|
|
@@ -6484,12 +7427,20 @@ function registerConnectorsCommand(program2) {
|
|
|
6484
7427
|
if (!res.ok) die(res.error ?? `could not add '${slug}'`);
|
|
6485
7428
|
ok(`added ${accent(slug)} \u2014 recorded locally`);
|
|
6486
7429
|
});
|
|
6487
|
-
connectors.command("remove <slug>").description("remove a connector").action((slug) => {
|
|
7430
|
+
connectors.command("remove <slug>").description("remove a connector").option("-f, --force", "skip the confirmation prompt").action(async (slug, opts) => {
|
|
7431
|
+
if (!isConnectorAdded(slug)) {
|
|
7432
|
+
info(`'${slug}' is not in your added list \u2014 nothing to remove`);
|
|
7433
|
+
return;
|
|
7434
|
+
}
|
|
7435
|
+
if (!await confirmDestructive(`remove connector '${slug}'`, opts)) {
|
|
7436
|
+
info("cancelled");
|
|
7437
|
+
return;
|
|
7438
|
+
}
|
|
6488
7439
|
if (removeConnector(slug)) ok(`removed ${accent(slug)}`);
|
|
6489
7440
|
else info(`'${slug}' is not in your added list \u2014 nothing to remove`);
|
|
6490
7441
|
});
|
|
6491
7442
|
connectors.command("setup").description("guided setup of a CUSTOM MCP server \u2014 Guardian-vetted, no JSON").option("--name <name>", "a short name for the server").option("--command <cmd>", "stdio launch command, e.g. 'npx -y @scope/mcp'").option("--args <args>", "space-separated args for --command").option("--env <pairs>", "comma-separated KEY=VAL env vars").option("--url <url>", "http(s) MCP endpoint (instead of --command)").option("--header <pairs>", "comma-separated KEY=VAL headers (with --url)").option("--allow-local", "permit loopback/LAN URL targets").option("-y, --yes", "trust and save when Guardian says 'ask'").action(async (opts) => {
|
|
6492
|
-
const interactive = !!
|
|
7443
|
+
const interactive = !!stdin8.isTTY && !!stdout9.isTTY;
|
|
6493
7444
|
let { name, command, url } = opts;
|
|
6494
7445
|
let argsStr = opts.args;
|
|
6495
7446
|
let envStr = opts.env;
|
|
@@ -6508,7 +7459,7 @@ function registerConnectorsCommand(program2) {
|
|
|
6508
7459
|
);
|
|
6509
7460
|
return;
|
|
6510
7461
|
}
|
|
6511
|
-
const rl =
|
|
7462
|
+
const rl = createInterface8({ input: stdin8, output: stdout9 });
|
|
6512
7463
|
try {
|
|
6513
7464
|
name = name || (await rl.question("Server name: ")).trim();
|
|
6514
7465
|
if (!command && !url) {
|
|
@@ -6550,7 +7501,7 @@ function registerConnectorsCommand(program2) {
|
|
|
6550
7501
|
if (opts.yes) {
|
|
6551
7502
|
trusted = true;
|
|
6552
7503
|
} else if (interactive) {
|
|
6553
|
-
const rl =
|
|
7504
|
+
const rl = createInterface8({ input: stdin8, output: stdout9 });
|
|
6554
7505
|
try {
|
|
6555
7506
|
const ans = (await rl.question(`Trust and save "${name}"? [y/N] `)).trim().toLowerCase();
|
|
6556
7507
|
trusted = ans === "y" || ans === "yes";
|
|
@@ -6592,14 +7543,14 @@ function registerConnectorsCommand(program2) {
|
|
|
6592
7543
|
}
|
|
6593
7544
|
|
|
6594
7545
|
// src/channels/config.ts
|
|
6595
|
-
import { readFileSync as
|
|
6596
|
-
import { join as
|
|
6597
|
-
function
|
|
6598
|
-
return
|
|
7546
|
+
import { readFileSync as readFileSync24, writeFileSync as writeFileSync20 } from "fs";
|
|
7547
|
+
import { join as join27 } from "path";
|
|
7548
|
+
function file5() {
|
|
7549
|
+
return join27(oriroDir(), "channels.json");
|
|
6599
7550
|
}
|
|
6600
7551
|
function readChannels() {
|
|
6601
7552
|
try {
|
|
6602
|
-
const v = JSON.parse(
|
|
7553
|
+
const v = JSON.parse(readFileSync24(file5(), "utf8"));
|
|
6603
7554
|
return Array.isArray(v) ? v : [];
|
|
6604
7555
|
} catch {
|
|
6605
7556
|
return [];
|
|
@@ -6608,10 +7559,10 @@ function readChannels() {
|
|
|
6608
7559
|
function saveChannel(cfg) {
|
|
6609
7560
|
const all = readChannels().filter((c) => c.kind !== cfg.kind);
|
|
6610
7561
|
all.push(cfg);
|
|
6611
|
-
|
|
7562
|
+
writeFileSync20(join27(ensureOriroDir(), "channels.json"), JSON.stringify(all, null, 2), "utf8");
|
|
6612
7563
|
}
|
|
6613
7564
|
function removeChannel(kind) {
|
|
6614
|
-
|
|
7565
|
+
writeFileSync20(join27(ensureOriroDir(), "channels.json"), JSON.stringify(readChannels().filter((c) => c.kind !== kind), null, 2), "utf8");
|
|
6615
7566
|
}
|
|
6616
7567
|
|
|
6617
7568
|
// src/channels/telegram.ts
|
|
@@ -6728,9 +7679,9 @@ async function startDiscord(token) {
|
|
|
6728
7679
|
}
|
|
6729
7680
|
|
|
6730
7681
|
// src/channels/whatsapp.ts
|
|
6731
|
-
import { join as
|
|
7682
|
+
import { join as join28 } from "path";
|
|
6732
7683
|
function whatsappAuthDir() {
|
|
6733
|
-
return
|
|
7684
|
+
return join28(oriroDir(), "whatsapp-auth");
|
|
6734
7685
|
}
|
|
6735
7686
|
async function startWhatsApp() {
|
|
6736
7687
|
let baileys;
|
|
@@ -6848,12 +7799,26 @@ function registerChannelsCommand(program2) {
|
|
|
6848
7799
|
}
|
|
6849
7800
|
|
|
6850
7801
|
// src/commands/skills.ts
|
|
6851
|
-
import { existsSync as
|
|
6852
|
-
import { resolve as resolve2, join as
|
|
7802
|
+
import { existsSync as existsSync20, statSync as statSync4, mkdirSync as mkdirSync17, cpSync, rmSync as rmSync4 } from "fs";
|
|
7803
|
+
import { resolve as resolve2, join as join29, basename, dirname as dirname4 } from "path";
|
|
6853
7804
|
function registerSkillsCommand(program2) {
|
|
6854
7805
|
const skills = program2.command("skills").description("the ORIRO skill library \u2014 bundled + your own");
|
|
6855
|
-
skills.command("list").description("show CORE / TAIL skill counts (use --all to list names)").option("-a, --all", "list every skill name").action(async (opts) => {
|
|
7806
|
+
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) => {
|
|
7807
|
+
const oerr = outputError(opts);
|
|
7808
|
+
if (oerr) die(oerr);
|
|
6856
7809
|
const s = await loadOriroSkills();
|
|
7810
|
+
if (isMachineOutput(opts) || opts.query) {
|
|
7811
|
+
const rows = s.all.map((sk) => ({
|
|
7812
|
+
name: sk.name,
|
|
7813
|
+
tier: sk.disableModelInvocation ? "TAIL" : "CORE"
|
|
7814
|
+
}));
|
|
7815
|
+
process.stdout.write(renderList2(rows, {
|
|
7816
|
+
output: opts.output,
|
|
7817
|
+
query: opts.query,
|
|
7818
|
+
columns: ["name", "tier"]
|
|
7819
|
+
}) + "\n");
|
|
7820
|
+
return;
|
|
7821
|
+
}
|
|
6857
7822
|
heading("Skills");
|
|
6858
7823
|
info(`${accent(String(s.all.length))} loaded \xB7 ${accent(String(s.core.length))} CORE (model-visible) \xB7 ${accent(String(s.tail.length))} TAIL (/name-only)`);
|
|
6859
7824
|
if (opts.all) {
|
|
@@ -6867,38 +7832,42 @@ function registerSkillsCommand(program2) {
|
|
|
6867
7832
|
});
|
|
6868
7833
|
skills.command("add <path>").description("add your own skill \u2014 a folder containing SKILL.md, or a SKILL.md file").action((p) => {
|
|
6869
7834
|
const src = resolve2(p);
|
|
6870
|
-
if (!
|
|
7835
|
+
if (!existsSync20(src)) die(`not found: ${src}`);
|
|
6871
7836
|
const dest = userSkillsDir();
|
|
6872
|
-
|
|
6873
|
-
const st =
|
|
7837
|
+
mkdirSync17(dest, { recursive: true });
|
|
7838
|
+
const st = statSync4(src);
|
|
6874
7839
|
if (st.isDirectory()) {
|
|
6875
|
-
if (!
|
|
7840
|
+
if (!existsSync20(join29(src, "SKILL.md"))) die(`no SKILL.md in ${src} \u2014 a skill folder must contain SKILL.md`);
|
|
6876
7841
|
const name = basename(src);
|
|
6877
|
-
cpSync(src,
|
|
6878
|
-
ok(`added skill ${accent(name)} \u2192 ${
|
|
7842
|
+
cpSync(src, join29(dest, name), { recursive: true });
|
|
7843
|
+
ok(`added skill ${accent(name)} \u2192 ${join29(dest, name)}`);
|
|
6879
7844
|
} else if (basename(src).toLowerCase() === "skill.md") {
|
|
6880
|
-
const name = basename(
|
|
6881
|
-
|
|
6882
|
-
cpSync(src,
|
|
6883
|
-
ok(`added skill ${accent(name)} \u2192 ${
|
|
7845
|
+
const name = basename(dirname4(src)) || "custom-skill";
|
|
7846
|
+
mkdirSync17(join29(dest, name), { recursive: true });
|
|
7847
|
+
cpSync(src, join29(dest, name, "SKILL.md"));
|
|
7848
|
+
ok(`added skill ${accent(name)} \u2192 ${join29(dest, name)}`);
|
|
6884
7849
|
} else {
|
|
6885
7850
|
die("expected a folder containing SKILL.md, or a SKILL.md file");
|
|
6886
7851
|
}
|
|
6887
7852
|
info("It loads on next launch \u2014 and is available in chat via /skill.");
|
|
6888
7853
|
});
|
|
6889
|
-
skills.command("remove <name>").description("remove a skill you added").action((name) => {
|
|
6890
|
-
const target =
|
|
6891
|
-
if (!
|
|
7854
|
+
skills.command("remove <name>").description("remove a skill you added").option("-f, --force", "skip the confirmation prompt").action(async (name, opts) => {
|
|
7855
|
+
const target = join29(userSkillsDir(), name);
|
|
7856
|
+
if (!existsSync20(target)) {
|
|
6892
7857
|
info(`'${name}' is not a user-added skill \u2014 nothing to remove`);
|
|
6893
7858
|
return;
|
|
6894
7859
|
}
|
|
7860
|
+
if (!await confirmDestructive(`remove skill '${name}'`, opts)) {
|
|
7861
|
+
info("cancelled");
|
|
7862
|
+
return;
|
|
7863
|
+
}
|
|
6895
7864
|
rmSync4(target, { recursive: true, force: true });
|
|
6896
7865
|
ok(`removed ${accent(name)}`);
|
|
6897
7866
|
});
|
|
6898
7867
|
}
|
|
6899
7868
|
|
|
6900
7869
|
// src/commands/language.ts
|
|
6901
|
-
import { stdin as
|
|
7870
|
+
import { stdin as stdin9 } from "process";
|
|
6902
7871
|
function resolveLanguage(input) {
|
|
6903
7872
|
return languageByCode(input) ?? LANGUAGES.find((l) => l.name.toLowerCase() === input.trim().toLowerCase());
|
|
6904
7873
|
}
|
|
@@ -6920,7 +7889,7 @@ function registerLanguageCommand(program2) {
|
|
|
6920
7889
|
ok(`${accent(lang.name)} is now your terminal language.`);
|
|
6921
7890
|
return;
|
|
6922
7891
|
}
|
|
6923
|
-
if (
|
|
7892
|
+
if (stdin9.isTTY) {
|
|
6924
7893
|
const lang = await selectLanguageInteractive();
|
|
6925
7894
|
setTerminalLanguage(lang);
|
|
6926
7895
|
ok(`${accent(lang.name)} is now your terminal language.`);
|
|
@@ -6933,7 +7902,7 @@ function registerLanguageCommand(program2) {
|
|
|
6933
7902
|
}
|
|
6934
7903
|
|
|
6935
7904
|
// src/commands/avatar.ts
|
|
6936
|
-
import { stdin as
|
|
7905
|
+
import { stdin as stdin10 } from "process";
|
|
6937
7906
|
function registerAvatarCommand(program2) {
|
|
6938
7907
|
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) => {
|
|
6939
7908
|
if (opts.list) {
|
|
@@ -6951,7 +7920,7 @@ function registerAvatarCommand(program2) {
|
|
|
6951
7920
|
ok(`${accent(avatar.slug)} is now your terminal face.`);
|
|
6952
7921
|
return;
|
|
6953
7922
|
}
|
|
6954
|
-
if (
|
|
7923
|
+
if (stdin10.isTTY) {
|
|
6955
7924
|
const chosen = await selectAvatarInteractive();
|
|
6956
7925
|
if (!chosen) {
|
|
6957
7926
|
info("no change.");
|
|
@@ -7029,12 +7998,12 @@ function registerHeadCommand(program2) {
|
|
|
7029
7998
|
}
|
|
7030
7999
|
|
|
7031
8000
|
// src/commands/voice.ts
|
|
7032
|
-
import { stdin as
|
|
8001
|
+
import { stdin as stdin11, stdout as stdout10 } from "process";
|
|
7033
8002
|
function registerVoiceCommand(program2) {
|
|
7034
|
-
program2.command("voice").description("speech-to-text \u2014 transcribe an audio file or the mic (on-device Whisper, experimental)").argument("[file]", "audio file to transcribe (omit to record from the mic on a real terminal)").option("--translate", "translate speech to English (Whisper translate task)").option("--seconds <n>", "mic recording length in seconds", "6").action(async (
|
|
7035
|
-
const interactive = !!
|
|
8003
|
+
program2.command("voice").description("speech-to-text \u2014 transcribe an audio file or the mic (on-device Whisper, experimental)").argument("[file]", "audio file to transcribe (omit to record from the mic on a real terminal)").option("--translate", "translate speech to English (Whisper translate task)").option("--seconds <n>", "mic recording length in seconds", "6").action(async (file6, opts) => {
|
|
8004
|
+
const interactive = !!stdin11.isTTY && !!stdout10.isTTY;
|
|
7036
8005
|
heading("ORIRO voice \u{1F399}");
|
|
7037
|
-
let audio =
|
|
8006
|
+
let audio = file6;
|
|
7038
8007
|
if (!audio) {
|
|
7039
8008
|
if (!interactive) {
|
|
7040
8009
|
info("On-device speech-to-text (experimental \u2014 needs ffmpeg + the transformers voice peer).");
|
|
@@ -7067,7 +8036,7 @@ function registerVoiceCommand(program2) {
|
|
|
7067
8036
|
}
|
|
7068
8037
|
|
|
7069
8038
|
// src/agents/catalog.ts
|
|
7070
|
-
import { readFileSync as
|
|
8039
|
+
import { readFileSync as readFileSync25 } from "fs";
|
|
7071
8040
|
function parseAgentDef(raw, now) {
|
|
7072
8041
|
if (!raw || typeof raw !== "object") return { ok: false, error: "not a JSON object" };
|
|
7073
8042
|
const o = raw;
|
|
@@ -7094,7 +8063,7 @@ async function fetchAgentSource(pathOrUrl) {
|
|
|
7094
8063
|
if (!res.ok) throw new Error(`fetch failed: HTTP ${res.status}`);
|
|
7095
8064
|
return await res.json();
|
|
7096
8065
|
}
|
|
7097
|
-
return JSON.parse(
|
|
8066
|
+
return JSON.parse(readFileSync25(pathOrUrl, "utf8"));
|
|
7098
8067
|
}
|
|
7099
8068
|
async function addAgentFromSource(pathOrUrl, now) {
|
|
7100
8069
|
let raw;
|
|
@@ -7110,6 +8079,67 @@ async function addAgentFromSource(pathOrUrl, now) {
|
|
|
7110
8079
|
return { ok: true, name: parsed.def.name, overwrote };
|
|
7111
8080
|
}
|
|
7112
8081
|
|
|
8082
|
+
// src/commands/schedule.ts
|
|
8083
|
+
import { spawnSync } from "child_process";
|
|
8084
|
+
import { platform } from "process";
|
|
8085
|
+
var TASK_NAME = "ORIRO_Agents_Tick";
|
|
8086
|
+
function intervalMinutes(spec) {
|
|
8087
|
+
const m = /^(\d+)(m|h)$/.exec(spec.trim());
|
|
8088
|
+
if (!m) return null;
|
|
8089
|
+
const n = parseInt(m[1], 10);
|
|
8090
|
+
if (n <= 0) return null;
|
|
8091
|
+
return m[2] === "h" ? n * 60 : n;
|
|
8092
|
+
}
|
|
8093
|
+
function tickInvocation() {
|
|
8094
|
+
return { node: process.execPath, bin: process.argv[1] ?? "oriro" };
|
|
8095
|
+
}
|
|
8096
|
+
function buildCron(mins, remove) {
|
|
8097
|
+
const { node, bin } = tickInvocation();
|
|
8098
|
+
if (platform === "win32") {
|
|
8099
|
+
if (remove) return { cmd: `schtasks /Delete /TN ${TASK_NAME} /F`, note: "Windows Task Scheduler" };
|
|
8100
|
+
const sc = mins % 60 === 0 ? `/SC HOURLY /MO ${mins / 60}` : `/SC MINUTE /MO ${mins}`;
|
|
8101
|
+
return {
|
|
8102
|
+
cmd: `schtasks /Create /TN ${TASK_NAME} /TR "\\"${node}\\" \\"${bin}\\" agents tick" ${sc} /F`,
|
|
8103
|
+
note: "Windows Task Scheduler"
|
|
8104
|
+
};
|
|
8105
|
+
}
|
|
8106
|
+
const line = `*/${mins} * * * * "${node}" "${bin}" agents tick # ${TASK_NAME}`;
|
|
8107
|
+
if (remove) {
|
|
8108
|
+
return { cmd: `crontab -l 2>/dev/null | grep -v '# ${TASK_NAME}' | crontab -`, note: "crontab" };
|
|
8109
|
+
}
|
|
8110
|
+
return {
|
|
8111
|
+
cmd: `( crontab -l 2>/dev/null | grep -v '# ${TASK_NAME}'; echo '${line}' ) | crontab -`,
|
|
8112
|
+
note: "crontab"
|
|
8113
|
+
};
|
|
8114
|
+
}
|
|
8115
|
+
function runShell(cmd) {
|
|
8116
|
+
const r = platform === "win32" ? spawnSync("cmd", ["/c", cmd], { encoding: "utf8" }) : spawnSync("sh", ["-c", cmd], { encoding: "utf8" });
|
|
8117
|
+
if (r.status !== 0) {
|
|
8118
|
+
info(dim((r.stderr || r.stdout || "").trim().slice(0, 300)));
|
|
8119
|
+
return false;
|
|
8120
|
+
}
|
|
8121
|
+
return true;
|
|
8122
|
+
}
|
|
8123
|
+
function registerAgentsCron(agents) {
|
|
8124
|
+
agents.command("cron").description("install an OS scheduler that runs `agents tick` on an interval (fires scheduled agents)").option("--every <spec>", "interval: Nm | Nh", "5m").option("--remove", "remove the scheduler entry instead of installing it").option("--apply", "actually apply the change (default: just print the command to run)").action((opts) => {
|
|
8125
|
+
const mins = intervalMinutes(opts.every);
|
|
8126
|
+
if (!opts.remove && mins === null) die(`invalid --every '${opts.every}' \u2014 use Nm or Nh (e.g. 5m, 2h)`);
|
|
8127
|
+
const { cmd, note } = buildCron(mins ?? 5, Boolean(opts.remove));
|
|
8128
|
+
heading(opts.remove ? "Remove scheduled agents" : "Schedule agents");
|
|
8129
|
+
info(`${note}: runs ${accent("oriro agents tick")} ${opts.remove ? "" : `every ${accent(opts.every)}`}`);
|
|
8130
|
+
if (!opts.apply) {
|
|
8131
|
+
process.stdout.write(`
|
|
8132
|
+
${cmd}
|
|
8133
|
+
|
|
8134
|
+
`);
|
|
8135
|
+
info(dim("printed only \u2014 re-run with --apply to make this change, or run the command yourself"));
|
|
8136
|
+
return;
|
|
8137
|
+
}
|
|
8138
|
+
if (runShell(cmd)) ok(opts.remove ? "scheduler entry removed" : `scheduled \u2014 agents tick will run every ${opts.every}`);
|
|
8139
|
+
else die("could not apply the schedule (see the message above) \u2014 you can run the printed command manually");
|
|
8140
|
+
});
|
|
8141
|
+
}
|
|
8142
|
+
|
|
7113
8143
|
// src/commands/agents.ts
|
|
7114
8144
|
function nowIso() {
|
|
7115
8145
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -7143,14 +8173,32 @@ function registerAgentsCommand(program2) {
|
|
|
7143
8173
|
info(`make one: ${accent('oriro agents make <name> --task "\u2026" [--router <id>] [--schedule 1h]')}`);
|
|
7144
8174
|
info(`then: ${accent("oriro agents run <name>")} ${dim("\xB7 or")} ${accent("oriro agents tick")} ${dim("for scheduled ones")}`);
|
|
7145
8175
|
});
|
|
7146
|
-
agents.command("list").description("list your saved agents").action(() => {
|
|
8176
|
+
agents.command("list").description("list your saved agents").option("-o, --output <fmt>", "output format: text (default) | json | csv").option("-q, --query <expr>", "filter/select: 'field', 'field=value', or 'field=value:selectField'").action((opts) => {
|
|
8177
|
+
const oerr = outputError(opts);
|
|
8178
|
+
if (oerr) die(oerr);
|
|
7147
8179
|
const all = listAgents();
|
|
8180
|
+
const state = loadState();
|
|
8181
|
+
if (isMachineOutput(opts) || opts.query) {
|
|
8182
|
+
const rows = all.map((a) => ({
|
|
8183
|
+
name: a.name,
|
|
8184
|
+
brain: a.router ?? "pool",
|
|
8185
|
+
schedule: a.schedule ?? "manual",
|
|
8186
|
+
description: a.description ?? "",
|
|
8187
|
+
lastRun: state[a.name]?.lastRunAt ? new Date(state[a.name].lastRunAt).toISOString() : "",
|
|
8188
|
+
lastOk: state[a.name]?.lastOk ?? null
|
|
8189
|
+
}));
|
|
8190
|
+
process.stdout.write(renderList2(rows, {
|
|
8191
|
+
output: opts.output,
|
|
8192
|
+
query: opts.query,
|
|
8193
|
+
columns: ["name", "brain", "schedule", "lastRun", "lastOk"]
|
|
8194
|
+
}) + "\n");
|
|
8195
|
+
return;
|
|
8196
|
+
}
|
|
7148
8197
|
heading("Agents");
|
|
7149
8198
|
if (!all.length) {
|
|
7150
8199
|
info(`no agents yet \u2014 make one: ${accent('oriro agents make my-agent --task "\u2026"')}`);
|
|
7151
8200
|
return;
|
|
7152
8201
|
}
|
|
7153
|
-
const state = loadState();
|
|
7154
8202
|
for (const a of all) {
|
|
7155
8203
|
printAgent(a);
|
|
7156
8204
|
const last = state[a.name]?.lastRunAt;
|
|
@@ -7200,13 +8248,22 @@ function registerAgentsCommand(program2) {
|
|
|
7200
8248
|
ok(`${res.overwrote ? "updated" : "added"} agent ${accent(res.name ?? "")} ${dim("\u2192 ~/.oriro/agents")}`);
|
|
7201
8249
|
info(`run it: ${accent(`oriro agents run ${res.name}`)}`);
|
|
7202
8250
|
});
|
|
7203
|
-
agents.command("remove <name>").description("delete an agent").action((name) => {
|
|
8251
|
+
agents.command("remove <name>").description("delete an agent").option("-f, --force", "skip the confirmation prompt").action(async (name, opts) => {
|
|
8252
|
+
if (!loadAgent(name)) {
|
|
8253
|
+
info(`'${name}' is not a saved agent \u2014 nothing to remove`);
|
|
8254
|
+
return;
|
|
8255
|
+
}
|
|
8256
|
+
if (!await confirmDestructive(`remove agent '${name}'`, opts)) {
|
|
8257
|
+
info("cancelled");
|
|
8258
|
+
return;
|
|
8259
|
+
}
|
|
7204
8260
|
if (!removeAgent(name)) {
|
|
7205
8261
|
info(`'${name}' is not a saved agent \u2014 nothing to remove`);
|
|
7206
8262
|
return;
|
|
7207
8263
|
}
|
|
7208
8264
|
ok(`removed ${accent(name)}`);
|
|
7209
8265
|
});
|
|
8266
|
+
registerAgentsCron(agents);
|
|
7210
8267
|
agents.command("tick").description("run every DUE scheduled agent once, then exit (wire to OS cron / Task Scheduler)").action(async () => {
|
|
7211
8268
|
const state = loadState();
|
|
7212
8269
|
const now = Date.now();
|
|
@@ -7241,19 +8298,327 @@ function registerAgentsCommand(program2) {
|
|
|
7241
8298
|
});
|
|
7242
8299
|
}
|
|
7243
8300
|
|
|
8301
|
+
// src/commands/completion.ts
|
|
8302
|
+
function extractTree(program2) {
|
|
8303
|
+
const nodes = [];
|
|
8304
|
+
for (const c of program2.commands) {
|
|
8305
|
+
const name = c.name();
|
|
8306
|
+
if (name === "completion") continue;
|
|
8307
|
+
nodes.push({
|
|
8308
|
+
name,
|
|
8309
|
+
subs: c.commands.map((s) => s.name()),
|
|
8310
|
+
opts: c.options.map((o) => o.long).filter((l) => Boolean(l))
|
|
8311
|
+
});
|
|
8312
|
+
}
|
|
8313
|
+
return nodes;
|
|
8314
|
+
}
|
|
8315
|
+
var SHELLS = ["bash", "zsh", "fish", "pwsh"];
|
|
8316
|
+
function topNames(tree) {
|
|
8317
|
+
return [...tree.map((n) => n.name), "completion", "help"].join(" ");
|
|
8318
|
+
}
|
|
8319
|
+
function genBash(tree) {
|
|
8320
|
+
const cases = tree.map((n) => ` ${n.name}) COMPREPLY=( $(compgen -W "${n.subs.join(" ")} ${n.opts.join(" ")}" -- "$cur") );;`).join("\n");
|
|
8321
|
+
return `# ORIRO bash completion. Install: oriro completion bash > /etc/bash_completion.d/oriro
|
|
8322
|
+
# or (per-user): oriro completion bash >> ~/.bashrc
|
|
8323
|
+
_oriro_complete() {
|
|
8324
|
+
local cur prev cword
|
|
8325
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
8326
|
+
cword=$COMP_CWORD
|
|
8327
|
+
if [ "$cword" -eq 1 ]; then
|
|
8328
|
+
COMPREPLY=( $(compgen -W "${topNames(tree)}" -- "$cur") )
|
|
8329
|
+
return
|
|
8330
|
+
fi
|
|
8331
|
+
case "\${COMP_WORDS[1]}" in
|
|
8332
|
+
${cases}
|
|
8333
|
+
*) COMPREPLY=();;
|
|
8334
|
+
esac
|
|
8335
|
+
}
|
|
8336
|
+
complete -F _oriro_complete oriro
|
|
8337
|
+
`;
|
|
8338
|
+
}
|
|
8339
|
+
function genZsh(tree) {
|
|
8340
|
+
const cases = tree.map((n) => ` ${n.name}) compadd ${n.subs.join(" ")} ${n.opts.join(" ")} ;;`).join("\n");
|
|
8341
|
+
return `#compdef oriro
|
|
8342
|
+
# ORIRO zsh completion. Install: oriro completion zsh > "\${fpath[1]}/_oriro" (then restart the shell)
|
|
8343
|
+
_oriro() {
|
|
8344
|
+
local -a words; words=("\${(@)words}")
|
|
8345
|
+
if (( CURRENT == 2 )); then
|
|
8346
|
+
compadd ${topNames(tree)}
|
|
8347
|
+
return
|
|
8348
|
+
fi
|
|
8349
|
+
case "\${words[2]}" in
|
|
8350
|
+
${cases}
|
|
8351
|
+
esac
|
|
8352
|
+
}
|
|
8353
|
+
_oriro "$@"
|
|
8354
|
+
`;
|
|
8355
|
+
}
|
|
8356
|
+
function genFish(tree) {
|
|
8357
|
+
const lines = [
|
|
8358
|
+
"# ORIRO fish completion. Install: oriro completion fish > ~/.config/fish/completions/oriro.fish",
|
|
8359
|
+
`complete -c oriro -f -n __fish_use_subcommand -a "${topNames(tree)}"`
|
|
8360
|
+
];
|
|
8361
|
+
for (const n of tree) {
|
|
8362
|
+
if (n.subs.length) {
|
|
8363
|
+
lines.push(`complete -c oriro -f -n "__fish_seen_subcommand_from ${n.name}" -a "${n.subs.join(" ")}"`);
|
|
8364
|
+
}
|
|
8365
|
+
}
|
|
8366
|
+
return lines.join("\n") + "\n";
|
|
8367
|
+
}
|
|
8368
|
+
function genPwsh(tree) {
|
|
8369
|
+
const cases = tree.map((n) => ` '${n.name}' { @(${[...n.subs, ...n.opts].map((s) => `'${s}'`).join(", ")}) }`).join("\n");
|
|
8370
|
+
const top = [...tree.map((n) => n.name), "completion", "help"].map((s) => `'${s}'`).join(", ");
|
|
8371
|
+
return `# ORIRO PowerShell completion. Install: oriro completion pwsh >> $PROFILE (then restart pwsh)
|
|
8372
|
+
Register-ArgumentCompleter -Native -CommandName oriro -ScriptBlock {
|
|
8373
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
8374
|
+
$tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
|
|
8375
|
+
$candidates = if ($tokens.Count -le 2) {
|
|
8376
|
+
@(${top})
|
|
8377
|
+
} else {
|
|
8378
|
+
switch ($tokens[1]) {
|
|
8379
|
+
${cases}
|
|
8380
|
+
default { @() }
|
|
8381
|
+
}
|
|
8382
|
+
}
|
|
8383
|
+
$candidates | Where-Object { $_ -like "$wordToComplete*" } |
|
|
8384
|
+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
|
|
8385
|
+
}
|
|
8386
|
+
`;
|
|
8387
|
+
}
|
|
8388
|
+
var GENERATORS = {
|
|
8389
|
+
bash: genBash,
|
|
8390
|
+
zsh: genZsh,
|
|
8391
|
+
fish: genFish,
|
|
8392
|
+
pwsh: genPwsh
|
|
8393
|
+
};
|
|
8394
|
+
function registerCompletionCommand(program2) {
|
|
8395
|
+
program2.command("completion <shell>").description("print a shell tab-completion script (bash | zsh | fish | pwsh)").action((shell) => {
|
|
8396
|
+
const s = shell.toLowerCase();
|
|
8397
|
+
if (!SHELLS.includes(s)) {
|
|
8398
|
+
die(`unsupported shell '${shell}'. Use one of: ${SHELLS.join(", ")}`);
|
|
8399
|
+
return;
|
|
8400
|
+
}
|
|
8401
|
+
process.stdout.write(GENERATORS[s](extractTree(program2)));
|
|
8402
|
+
});
|
|
8403
|
+
}
|
|
8404
|
+
|
|
8405
|
+
// src/commands/config.ts
|
|
8406
|
+
function registerConfigCommand(program2) {
|
|
8407
|
+
const config = program2.command("config").description("your durable CLI settings (defaults in ~/.oriro/config.json)");
|
|
8408
|
+
config.command("list").description("show every setting, its value, and what it does").action(() => {
|
|
8409
|
+
const all = configAll();
|
|
8410
|
+
heading("Config");
|
|
8411
|
+
for (const { key, desc } of configKeys()) {
|
|
8412
|
+
const val = all[key];
|
|
8413
|
+
process.stdout.write(` ${accent(key.padEnd(10))} ${val !== void 0 ? accent(val) : dim("(default)")} ${dim(desc)}
|
|
8414
|
+
`);
|
|
8415
|
+
}
|
|
8416
|
+
info(`set: ${accent("oriro config set <key> <value>")} \xB7 clear: ${accent("oriro config unset <key>")}`);
|
|
8417
|
+
});
|
|
8418
|
+
config.command("get <key>").description("print one setting's value").action((key) => {
|
|
8419
|
+
if (!isConfigKey(key)) die(`unknown key '${key}' \u2014 run \`oriro config list\``);
|
|
8420
|
+
const val = configGet(key);
|
|
8421
|
+
if (val === void 0) {
|
|
8422
|
+
info(`${key} is unset (using the built-in default)`);
|
|
8423
|
+
return;
|
|
8424
|
+
}
|
|
8425
|
+
process.stdout.write(`${val}
|
|
8426
|
+
`);
|
|
8427
|
+
});
|
|
8428
|
+
config.command("set <key> <value>").description("set a setting (validated)").action((key, value) => {
|
|
8429
|
+
if (!isConfigKey(key)) die(`unknown key '${key}' \u2014 run \`oriro config list\``);
|
|
8430
|
+
const err = validateConfig(key, value);
|
|
8431
|
+
if (err) die(`invalid value for '${key}': ${err}`);
|
|
8432
|
+
configSet(key, value);
|
|
8433
|
+
ok(`${accent(key)} = ${accent(value)}`);
|
|
8434
|
+
});
|
|
8435
|
+
config.command("unset <key>").description("clear a setting back to its built-in default").action((key) => {
|
|
8436
|
+
if (!isConfigKey(key)) die(`unknown key '${key}' \u2014 run \`oriro config list\``);
|
|
8437
|
+
if (configUnset(key)) ok(`cleared ${accent(key)}`);
|
|
8438
|
+
else info(`${key} was already at its default`);
|
|
8439
|
+
});
|
|
8440
|
+
}
|
|
8441
|
+
|
|
8442
|
+
// src/commands/setup.ts
|
|
8443
|
+
import { rmSync as rmSync5 } from "fs";
|
|
8444
|
+
import { join as join30 } from "path";
|
|
8445
|
+
import { stdin as stdin12, stdout as stdout11 } from "process";
|
|
8446
|
+
var MARKERS = [
|
|
8447
|
+
"language.json",
|
|
8448
|
+
"avatar.json",
|
|
8449
|
+
"skills-onboarded.json",
|
|
8450
|
+
"connectors-onboarded.json",
|
|
8451
|
+
"models-onboarded.json",
|
|
8452
|
+
join30("routers", "onboarded.json")
|
|
8453
|
+
];
|
|
8454
|
+
function registerSetupCommand(program2) {
|
|
8455
|
+
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) => {
|
|
8456
|
+
if (opts.reset) {
|
|
8457
|
+
for (const m of MARKERS) {
|
|
8458
|
+
try {
|
|
8459
|
+
rmSync5(join30(oriroDir(), m), { force: true });
|
|
8460
|
+
} catch {
|
|
8461
|
+
}
|
|
8462
|
+
}
|
|
8463
|
+
ok("reset \u2014 every step will be asked again");
|
|
8464
|
+
}
|
|
8465
|
+
if (!stdin12.isTTY || !stdout11.isTTY) {
|
|
8466
|
+
heading("ORIRO setup");
|
|
8467
|
+
info(`ORIRO is ${accent("keyless")} \u2014 no login, no API keys. Run ${accent("oriro setup")} in a real terminal for the guided wizard.`);
|
|
8468
|
+
info(dim("or configure directly: oriro language <code> \xB7 oriro routers add <id> \xB7 oriro connectors add <slug> \xB7 oriro config set <k> <v>"));
|
|
8469
|
+
return;
|
|
8470
|
+
}
|
|
8471
|
+
await runOnboarding();
|
|
8472
|
+
});
|
|
8473
|
+
}
|
|
8474
|
+
|
|
8475
|
+
// src/commands/import.ts
|
|
8476
|
+
import { existsSync as existsSync21, readFileSync as readFileSync26, readdirSync as readdirSync3, statSync as statSync5, cpSync as cpSync2, mkdirSync as mkdirSync18 } from "fs";
|
|
8477
|
+
import { join as join31, basename as basename2 } from "path";
|
|
8478
|
+
function registerImportCommand(program2) {
|
|
8479
|
+
const imp = program2.command("import").description("migrate from another CLI (MCP servers, skills)");
|
|
8480
|
+
imp.command("mcp <file>").description("import MCP servers from a Claude-compatible mcp.json (Guardian-vetted)").action((file6) => {
|
|
8481
|
+
if (!existsSync21(file6)) die(`no such file: ${file6}`);
|
|
8482
|
+
let servers;
|
|
8483
|
+
try {
|
|
8484
|
+
const j = JSON.parse(readFileSync26(file6, "utf8"));
|
|
8485
|
+
servers = j.mcpServers ?? j.servers ?? {};
|
|
8486
|
+
} catch (e) {
|
|
8487
|
+
die(`could not parse ${file6}: ${e instanceof Error ? e.message : String(e)}`);
|
|
8488
|
+
return;
|
|
8489
|
+
}
|
|
8490
|
+
const names = Object.keys(servers);
|
|
8491
|
+
if (!names.length) die(`no "mcpServers" found in ${file6}`);
|
|
8492
|
+
heading(`Import MCP \xB7 ${names.length} server${names.length === 1 ? "" : "s"}`);
|
|
8493
|
+
let imported = 0, blocked2 = 0;
|
|
8494
|
+
for (const name of names) {
|
|
8495
|
+
const s = servers[name];
|
|
8496
|
+
const input = {
|
|
8497
|
+
name,
|
|
8498
|
+
...s.command ? { command: s.command } : {},
|
|
8499
|
+
...s.args ? { args: s.args } : {},
|
|
8500
|
+
...s.env ? { env: s.env } : {},
|
|
8501
|
+
...s.url ? { url: s.url } : {},
|
|
8502
|
+
...s.headers ? { headers: s.headers } : {}
|
|
8503
|
+
};
|
|
8504
|
+
if (s.url) {
|
|
8505
|
+
try {
|
|
8506
|
+
assertSafeUrl(s.url);
|
|
8507
|
+
} catch (e) {
|
|
8508
|
+
process.stdout.write(` ${fgHex(PALETTE.error, "\u2717")} ${name} ${dim(`blocked: ${e instanceof Error ? e.message : String(e)}`)}
|
|
8509
|
+
`);
|
|
8510
|
+
blocked2++;
|
|
8511
|
+
continue;
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
8514
|
+
const outcome = vetServer(input);
|
|
8515
|
+
if (outcome.decision === "block") {
|
|
8516
|
+
process.stdout.write(` ${fgHex(PALETTE.error, "\u2717")} ${name} ${dim(`blocked: ${outcome.reason}`)}
|
|
8517
|
+
`);
|
|
8518
|
+
blocked2++;
|
|
8519
|
+
continue;
|
|
8520
|
+
}
|
|
8521
|
+
saveCustomServer({ name, config: buildServerConfig(input), trusted: outcome.decision === "allow" });
|
|
8522
|
+
const mark = outcome.decision === "allow" ? fgHex(PALETTE.success, "\u2713 trusted") : dim("\u25CB needs trust");
|
|
8523
|
+
process.stdout.write(` ${mark} ${accent(name)}
|
|
8524
|
+
`);
|
|
8525
|
+
imported++;
|
|
8526
|
+
}
|
|
8527
|
+
info(`${imported} imported \xB7 ${blocked2} blocked${imported ? ` \u2014 they connect in-session; see \`oriro connectors custom\`` : ""}`);
|
|
8528
|
+
});
|
|
8529
|
+
imp.command("skills <dir>").description("import SKILL.md skill folders from another CLI's skills directory").action((dir) => {
|
|
8530
|
+
if (!existsSync21(dir) || !statSync5(dir).isDirectory()) die(`no such directory: ${dir}`);
|
|
8531
|
+
const dest = userSkillsDir();
|
|
8532
|
+
mkdirSync18(dest, { recursive: true });
|
|
8533
|
+
heading("Import skills");
|
|
8534
|
+
const sources = existsSync21(join31(dir, "SKILL.md")) ? [dir] : readdirSync3(dir).map((e) => join31(dir, e)).filter((p) => statSync5(p).isDirectory() && existsSync21(join31(p, "SKILL.md")));
|
|
8535
|
+
let n = 0;
|
|
8536
|
+
for (const src of sources) {
|
|
8537
|
+
cpSync2(src, join31(dest, basename2(src)), { recursive: true });
|
|
8538
|
+
process.stdout.write(` ${fgHex(PALETTE.success, "\u2713")} ${accent(basename2(src))}
|
|
8539
|
+
`);
|
|
8540
|
+
n++;
|
|
8541
|
+
}
|
|
8542
|
+
if (n === 0) info(dim(`no SKILL.md skill folder found at or inside ${dir}`));
|
|
8543
|
+
else ok(`imported ${n} skill${n === 1 ? "" : "s"} \u2192 ${dim(dest)}`);
|
|
8544
|
+
});
|
|
8545
|
+
}
|
|
8546
|
+
|
|
8547
|
+
// src/commands/help-on-error.ts
|
|
8548
|
+
function lev(a, b) {
|
|
8549
|
+
const m = a.length, n = b.length;
|
|
8550
|
+
if (!m) return n;
|
|
8551
|
+
if (!n) return m;
|
|
8552
|
+
let prev = Array.from({ length: n + 1 }, (_, i) => i);
|
|
8553
|
+
let curr = new Array(n + 1);
|
|
8554
|
+
for (let i = 1; i <= m; i++) {
|
|
8555
|
+
curr[0] = i;
|
|
8556
|
+
for (let j = 1; j <= n; j++) {
|
|
8557
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
8558
|
+
curr[j] = Math.min((curr[j - 1] ?? 0) + 1, (prev[j] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
8559
|
+
}
|
|
8560
|
+
[prev, curr] = [curr, prev];
|
|
8561
|
+
}
|
|
8562
|
+
return prev[n] ?? n;
|
|
8563
|
+
}
|
|
8564
|
+
function didYouMean(input, candidates) {
|
|
8565
|
+
let best;
|
|
8566
|
+
let bestD = Infinity;
|
|
8567
|
+
for (const c of candidates) {
|
|
8568
|
+
const d = lev(input.toLowerCase(), c.toLowerCase());
|
|
8569
|
+
if (d < bestD) {
|
|
8570
|
+
bestD = d;
|
|
8571
|
+
best = c;
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8574
|
+
return best !== void 0 && bestD <= Math.max(2, Math.floor(input.length * 0.4)) ? best : void 0;
|
|
8575
|
+
}
|
|
8576
|
+
function fullPath(cmd) {
|
|
8577
|
+
const parts = [];
|
|
8578
|
+
let c = cmd;
|
|
8579
|
+
while (c && c.name() !== "oriro") {
|
|
8580
|
+
parts.unshift(c.name());
|
|
8581
|
+
c = c.parent;
|
|
8582
|
+
}
|
|
8583
|
+
return parts.length ? `oriro ${parts.join(" ")}` : "oriro <command>";
|
|
8584
|
+
}
|
|
8585
|
+
function enableHelpOnError(program2) {
|
|
8586
|
+
const apply = (cmd) => {
|
|
8587
|
+
cmd.showHelpAfterError(`
|
|
8588
|
+
(run: ${fullPath(cmd)} --help for usage)`);
|
|
8589
|
+
cmd.showSuggestionAfterError(true);
|
|
8590
|
+
for (const sub of cmd.commands) apply(sub);
|
|
8591
|
+
};
|
|
8592
|
+
apply(program2);
|
|
8593
|
+
}
|
|
8594
|
+
|
|
7244
8595
|
// src/cli.ts
|
|
7245
8596
|
var version = createRequire(import.meta.url)("../package.json").version;
|
|
7246
8597
|
var program = new Command();
|
|
7247
|
-
program.name("oriro").description("ORIRO \u2014 a free, on-device-friendly terminal AI agent.").version(version, "-v, --version").action(async (
|
|
8598
|
+
program.name("oriro").description("ORIRO \u2014 a free, on-device-friendly terminal AI agent.").version(version, "-v, --version").option("-p, --print <prompt>", "headless one-shot: run a single prompt, print the answer, exit (CI-friendly)").option("--output-format <fmt>", "with --print: text | json | stream-json", "text").action(async (options, command) => {
|
|
8599
|
+
if (options.print !== void 0) {
|
|
8600
|
+
const fmt = options.outputFormat ?? "text";
|
|
8601
|
+
if (!isOutputFormatMode(fmt)) {
|
|
8602
|
+
process.stderr.write(`error: --output-format must be text | json | stream-json
|
|
8603
|
+
`);
|
|
8604
|
+
process.exitCode = 1;
|
|
8605
|
+
return;
|
|
8606
|
+
}
|
|
8607
|
+
await runHeadless(options.print, fmt);
|
|
8608
|
+
return;
|
|
8609
|
+
}
|
|
7248
8610
|
if (command.args.length > 0) {
|
|
7249
|
-
const arg = command.args[0];
|
|
8611
|
+
const arg = command.args[0] ?? "";
|
|
7250
8612
|
if (arg === "help") {
|
|
7251
8613
|
command.outputHelp();
|
|
7252
8614
|
return;
|
|
7253
8615
|
}
|
|
7254
|
-
|
|
7255
|
-
|
|
8616
|
+
const names = command.commands.map((c) => c.name());
|
|
8617
|
+
const guess = didYouMean(arg, names);
|
|
8618
|
+
process.stderr.write(`error: unknown command '${arg}'${guess ? ` \u2014 did you mean '${guess}'?` : ""}
|
|
8619
|
+
|
|
7256
8620
|
`);
|
|
8621
|
+
command.outputHelp();
|
|
7257
8622
|
process.exitCode = 1;
|
|
7258
8623
|
return;
|
|
7259
8624
|
}
|
|
@@ -7269,6 +8634,11 @@ registerAvatarCommand(program);
|
|
|
7269
8634
|
registerHeadCommand(program);
|
|
7270
8635
|
registerVoiceCommand(program);
|
|
7271
8636
|
registerAgentsCommand(program);
|
|
8637
|
+
registerConfigCommand(program);
|
|
8638
|
+
registerSetupCommand(program);
|
|
8639
|
+
registerImportCommand(program);
|
|
8640
|
+
registerCompletionCommand(program);
|
|
8641
|
+
enableHelpOnError(program);
|
|
7272
8642
|
program.parseAsync().catch((e) => {
|
|
7273
8643
|
if (e instanceof DieError) return;
|
|
7274
8644
|
process.stderr.write(`
|