@corenel/cli 0.3.1 → 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.
Files changed (3) hide show
  1. package/README.md +28 -2
  2. package/dist/cli.js +177 -90
  3. package/package.json +15 -15
package/README.md CHANGED
@@ -7,7 +7,33 @@ transport). Early/beta: the headless proof the kernel runs outside the browser.
7
7
  npx @corenel/cli
8
8
  ```
9
9
 
10
- Requires Node 18+. Bring your provider key via the environment. More verbs
11
- (`run` / `ask` / `chat` / `login`, and `start --sidecar`) are in progress.
10
+ Requires Node 18+. Bring your provider key via the environment.
11
+
12
+ Running `corenel` with no command opens an interactive agent in the current
13
+ directory. On a terminal it opens the full terminal face; when output is piped,
14
+ under `TERM=dumb`, in CI, or with `--no-tui`, it falls back to a plain
15
+ line-by-line loop. `--tui` demands the full face and fails rather than falling
16
+ back. `--resume` reopens the last conversation.
17
+
18
+ Assistant replies render as markdown. More verbs (`run` / `ask` / `chat` /
19
+ `login`, and `start --sidecar`) are in progress.
20
+
21
+ ## Releasing
22
+
23
+ `@corenel/term` (the terminal face) is a real runtime dependency, not bundled
24
+ into `dist/cli.js` -- see the note at the top of `build.mjs`. It therefore has
25
+ to be published FIRST; publishing `@corenel/cli` against an unpublished
26
+ `@corenel/term` yields a package that cannot install at all, so `npx
27
+ @corenel/cli` fails before any verb runs.
28
+
29
+ pnpm --filter @corenel/term build && pnpm --filter @corenel/term publish
30
+ pnpm --filter @corenel/cli build && pnpm --filter @corenel/cli publish
31
+
32
+ Publish with **pnpm, never npm**: this CLI depends on `@corenel/term` as
33
+ `workspace:*`, and pnpm rewrites the `workspace:` protocol to a real version at
34
+ pack/publish time while npm ships it verbatim -- an `npm publish` from this
35
+ workspace produces a manifest whose install fails with
36
+ `EUNSUPPORTEDPROTOCOL / Unsupported URL Type "workspace:"`, which is the very
37
+ breakage this section exists to prevent.
12
38
 
13
39
  Licensed under Elastic-2.0.
package/dist/cli.js CHANGED
@@ -20356,6 +20356,7 @@ function nodeToolCtx(signal, opts = {}) {
20356
20356
  kv: inMemoryKv(),
20357
20357
  search: nullSearch,
20358
20358
  confirm: async () => true,
20359
+ ...opts.ask ? { ask: opts.ask } : {},
20359
20360
  ...opts.files ? { files: opts.files, fileMeta: { kind: opts.files.kind, label: opts.files.label, isLocalDisk: opts.files.isLocalDisk, writable: opts.files.writable } } : {}
20360
20361
  };
20361
20362
  }
@@ -20936,11 +20937,11 @@ async function runExtension(ext, rest, look = {}) {
20936
20937
  // src/commands.ts
20937
20938
  var COMMANDS = [
20938
20939
  { name: "", args: "", summary: "with no command: an interactive agent in this directory" },
20939
- { name: "run", args: '"<prompt>"', summary: "one-shot turn, then exit" },
20940
+ { name: "run", args: '"<prompt>" [--session ID]', summary: "one-shot turn, then exit (--session runs it inside a sandbox)" },
20940
20941
  { name: "run-agent", args: '<name> --input "<text>"', summary: "run a crew agent; events as JSON Lines on stdout" },
20941
20942
  { name: "sidecar", args: "[start] [flags]", summary: "run the sidecar on this machine" },
20942
20943
  { name: "acp", args: "<agent> [flags]", summary: "proxy an external coding agent under Corenel policy" },
20943
- { name: "sessions", args: "[list|show <id>]", summary: "list and inspect local sandbox sessions" },
20944
+ { name: "sessions", args: "[list|show|create|run]", summary: "create, list and run in local sandbox sessions" },
20944
20945
  { name: "login", args: "[--base URL]", summary: "sign in through your browser (device flow)" },
20945
20946
  { name: "auth", summary: "show whether you are signed in, and as what" },
20946
20947
  { name: "logout", summary: "forget the stored token" },
@@ -20972,10 +20973,18 @@ function helpText(version2) {
20972
20973
  " --model <id> model for this turn (default gpt-4o-mini)",
20973
20974
  " --base <url> API base (default https://api.corenel.ai/api, or CORENEL_API_BASE)",
20974
20975
  " --allow-shell run commands without asking each time",
20976
+ " --no-tui plain line-by-line mode (automatic when output is piped)",
20977
+ " --tui force the full terminal face, and fail if it cannot start",
20978
+ " --resume [id] reopen the last conversation, or the one named",
20975
20979
  " --debug trace every agent step (also CORENEL_DEBUG=1)",
20976
20980
  " --version, -v print the version",
20977
20981
  " --help, -h print this",
20978
20982
  "",
20983
+ "--resume reopens a CONVERSATION (this face's own chat log, saved under",
20984
+ ".corenel/sessions/) -- not the same thing as `run --session ID` or the",
20985
+ "`sessions` command below, which forward into a sandbox on the sidecar.",
20986
+ "Same word, two different nouns.",
20987
+ "",
20979
20988
  "sidecar, acp and sessions are provided by @corenel/sidecar, installed separately.",
20980
20989
  "Each takes its own flags: `corenel sidecar --help`.",
20981
20990
  ""
@@ -21140,35 +21149,38 @@ ${failed.length} problem${failed.length > 1 ? "s" : ""} above.
21140
21149
 
21141
21150
  // src/repl.ts
21142
21151
  import { createInterface } from "node:readline";
21152
+ import { TermSession } from "@corenel/term/session";
21153
+ import { describeToolCall } from "@corenel/term/describe";
21143
21154
  function isQuit(line) {
21144
21155
  const t = line.trim().toLowerCase();
21145
21156
  return t === "/exit" || t === "/quit" || t === "exit" || t === "quit";
21146
21157
  }
21147
- function isClear(line) {
21148
- return line.trim().toLowerCase() === "/clear";
21149
- }
21150
- function isToolsCmd(line) {
21151
- const t = line.trim().toLowerCase();
21152
- return t === "/tools" || t === "/help";
21158
+ function nextShownCursor(itemsLength, shown) {
21159
+ return itemsLength < shown ? 0 : shown;
21153
21160
  }
21154
- function formatTools2(tools) {
21155
- const w = Math.max(...tools.map((t) => t.name.length));
21156
- const line = (t) => {
21157
- const what = t.means ?? t.description.split(". ")[0];
21158
- return ` ${t.name.padEnd(w)} ${t.mutates ? "*" : " "} ${what}`;
21159
- };
21160
- return [
21161
- `${tools.length} tools available (* = can change things, and will ask first):`,
21162
- ...[...tools].sort((a, b) => a.name.localeCompare(b.name)).map(line)
21163
- ].join("\n");
21161
+ function formatQuestion(q) {
21162
+ const lines = [` ${q.header} ${q.question}`];
21163
+ q.options.forEach((o, i) => {
21164
+ lines.push(` ${i + 1}) ${o.label}`);
21165
+ });
21166
+ return `${lines.join("\n")}
21167
+ `;
21164
21168
  }
21165
- function describeToolCall(name, args) {
21166
- const a = args ?? {};
21167
- const subject = a.path ?? a.file ?? a.command ?? a.cmd ?? a.query ?? a.pattern ?? a.url;
21168
- const text = typeof subject === "string" ? subject : "";
21169
- const trimmed = text.length > 68 ? `${text.slice(0, 67)}\u2026` : text;
21170
- return trimmed ? `${name} ${trimmed}` : name;
21169
+ function parseQuestionAnswer(q, line) {
21170
+ if (line === null) return null;
21171
+ const t = line.trim();
21172
+ if (!t) return null;
21173
+ if (/^\d+$/.test(t)) {
21174
+ const n = Number(t);
21175
+ if (n >= 1 && n <= q.options.length) return { [q.header]: [q.options[n - 1].label] };
21176
+ }
21177
+ return { [q.header]: [t] };
21171
21178
  }
21179
+ var REPL_BANNER = (model, cwd) => [
21180
+ `corenel \u2014 ${model}, working in ${cwd}`,
21181
+ "Type a request. /clear forgets the conversation, /exit leaves, Ctrl-C stops a turn.",
21182
+ ""
21183
+ ].join("\n");
21172
21184
  function makeEventRenderer(write) {
21173
21185
  let streaming = false;
21174
21186
  const endStream = () => {
@@ -21183,23 +21195,15 @@ function makeEventRenderer(write) {
21183
21195
  streaming = true;
21184
21196
  write(e.delta);
21185
21197
  break;
21186
- case "tool-call":
21198
+ case "tool-call": {
21187
21199
  endStream();
21188
- write(` \xB7 ${describeToolCall(e.name, e.args)}
21200
+ const subject = describeToolCall(e.name, e.args);
21201
+ write(` \xB7 ${e.name}${subject ? ` ${subject}` : ""}
21189
21202
  `);
21190
21203
  break;
21204
+ }
21191
21205
  case "tool-result":
21192
21206
  if (e.isError) write(` ! ${e.name} failed
21193
- `);
21194
- break;
21195
- case "policy-violation":
21196
- endStream();
21197
- write(` ! ${e.action === "stop" ? "blocked" : "warning"}: ${e.message}
21198
- `);
21199
- break;
21200
- case "error":
21201
- endStream();
21202
- write(` ! ${e.message}
21203
21207
  `);
21204
21208
  break;
21205
21209
  case "done":
@@ -21210,18 +21214,53 @@ function makeEventRenderer(write) {
21210
21214
  }
21211
21215
  };
21212
21216
  }
21213
- var REPL_BANNER = (model, cwd) => [
21214
- `corenel \u2014 ${model}, working in ${cwd}`,
21215
- "Type a request. /clear forgets the conversation, /exit leaves, Ctrl-C stops a turn.",
21216
- ""
21217
- ].join("\n");
21218
21217
  async function runRepl(deps) {
21219
21218
  const input = deps.input ?? process.stdin;
21220
21219
  const output = deps.output ?? process.stdout;
21220
+ let atLineStart = true;
21221
21221
  const write = (s) => {
21222
21222
  output.write(s);
21223
+ if (s.length > 0) atLineStart = s.endsWith("\n");
21224
+ };
21225
+ const writeLine = (s) => {
21226
+ if (!atLineStart) write("\n");
21227
+ write(s);
21223
21228
  };
21224
21229
  write(REPL_BANNER(deps.model, deps.cwd));
21230
+ const render = makeEventRenderer(write);
21231
+ const session = new TermSession({
21232
+ cwd: deps.cwd,
21233
+ model: deps.model,
21234
+ system: deps.system,
21235
+ client: deps.client,
21236
+ tools: deps.tools,
21237
+ toolCtx: (signal, askUser) => deps.toolCtx(signal, askUser),
21238
+ onEvent: render,
21239
+ ...deps.runner ? { runner: deps.runner } : {},
21240
+ ...deps.approveAll ? { approveAll: true } : {},
21241
+ ...deps.persistence ? { persistence: deps.persistence } : {}
21242
+ });
21243
+ let shown = 0;
21244
+ session.subscribe(() => {
21245
+ const items = session.getSnapshot().committed;
21246
+ shown = nextShownCursor(items.length, shown);
21247
+ for (; shown < items.length; shown++) {
21248
+ const it = items[shown];
21249
+ if (it.kind !== "note") continue;
21250
+ writeLine(` ${it.level === "info" ? "" : "! "}${it.text}
21251
+ `);
21252
+ if (it.detail) write(`${it.detail}
21253
+ `);
21254
+ }
21255
+ });
21256
+ if (deps.resume) {
21257
+ await session.resume(deps.resume.id);
21258
+ const restored = session.getSnapshot();
21259
+ if (restored.session) {
21260
+ writeLine(` resumed ${restored.session.id} - ${restored.committed.length} messages
21261
+ `);
21262
+ }
21263
+ }
21225
21264
  const rl = createInterface({ input, output });
21226
21265
  let closed = false;
21227
21266
  rl.on("close", () => {
@@ -21235,13 +21274,10 @@ async function runRepl(deps) {
21235
21274
  resolve2(answer);
21236
21275
  });
21237
21276
  });
21238
- let history = [];
21239
- let turn = null;
21277
+ let turnActive = false;
21240
21278
  const onSigint = () => {
21241
- if (turn) {
21242
- turn.abort();
21243
- write("\n(stopped)\n");
21244
- } else {
21279
+ if (turnActive) session.abort();
21280
+ else {
21245
21281
  write("\n");
21246
21282
  rl.close();
21247
21283
  }
@@ -21261,57 +21297,46 @@ async function runRepl(deps) {
21261
21297
  }
21262
21298
  }
21263
21299
  });
21300
+ session.attachPermission(permission);
21301
+ let questionOpen = false;
21302
+ session.subscribe(() => {
21303
+ const modal = session.getSnapshot().modal;
21304
+ if (!modal || modal.kind !== "user") {
21305
+ questionOpen = false;
21306
+ return;
21307
+ }
21308
+ if (questionOpen) return;
21309
+ questionOpen = true;
21310
+ const q = modal.questions[0];
21311
+ if (!q) {
21312
+ session.answer(null);
21313
+ return;
21314
+ }
21315
+ writeLine(formatQuestion(q));
21316
+ void ask(" answer (number, text, or empty to skip): ").then((line) => {
21317
+ if (session.getSnapshot().modal?.kind !== "user") return;
21318
+ session.answer(parseQuestionAnswer(q, line));
21319
+ });
21320
+ });
21264
21321
  for (; ; ) {
21265
21322
  const line = await ask("\u203A ");
21266
21323
  if (line === null) break;
21267
21324
  const text = line.trim();
21268
21325
  if (!text) continue;
21269
21326
  if (isQuit(text)) break;
21270
- if (isClear(text)) {
21271
- history = [];
21272
- write("(conversation cleared)\n");
21273
- continue;
21274
- }
21275
- if (isToolsCmd(text)) {
21276
- write(`${formatTools2(deps.tools)}
21277
- `);
21278
- continue;
21279
- }
21280
- history.push({ role: "user", content: text });
21281
- turn = new AbortController();
21282
- try {
21283
- let streamed = false;
21284
- const render = makeEventRenderer((s) => {
21285
- if (s.trim()) streamed = true;
21286
- write(s);
21287
- });
21288
- const result = await runAgent({
21289
- client: deps.client,
21290
- model: deps.model,
21291
- system: deps.system,
21292
- messages: history,
21293
- tools: deps.tools,
21294
- toolCtx: deps.toolCtx(turn.signal),
21295
- signal: turn.signal,
21296
- permission,
21297
- onEvent: render
21298
- });
21299
- history = result.messages;
21300
- if (!streamed && result.text) write(`${result.text}
21301
- `);
21302
- } catch (e) {
21303
- if (!turn.signal.aborted) write(`error: ${e instanceof Error ? e.message : String(e)}
21304
- `);
21305
- history = history.slice(0, -1);
21306
- } finally {
21307
- turn = null;
21308
- }
21327
+ turnActive = true;
21328
+ await session.submit(text);
21329
+ turnActive = false;
21330
+ if (session.wantsExit) break;
21309
21331
  }
21310
21332
  rl.off("SIGINT", onSigint);
21311
21333
  rl.close();
21312
21334
  return 0;
21313
21335
  }
21314
21336
 
21337
+ // src/cli.ts
21338
+ import { createSessionPersistence } from "@corenel/term/persist";
21339
+
21315
21340
  // src/shellTool.ts
21316
21341
  import { spawn as spawn2 } from "node:child_process";
21317
21342
  var MAX_OUTPUT = 3e4;
@@ -21417,6 +21442,31 @@ function shellTool(cwd) {
21417
21442
  };
21418
21443
  }
21419
21444
 
21445
+ // src/tuiChoice.ts
21446
+ function chooseFace(env) {
21447
+ if (env.argv.includes("--no-tui")) return "readline";
21448
+ if (env.argv.includes("--tui")) return "tui";
21449
+ if (!env.stdinTty || !env.stdoutTty) return "readline";
21450
+ if (env.term === "dumb") return "readline";
21451
+ if (env.ci) return "readline";
21452
+ return "tui";
21453
+ }
21454
+ var TuiUnavailableError = class extends Error {
21455
+ constructor(cause) {
21456
+ super(`the terminal face could not start: ${cause instanceof Error ? cause.message : String(cause)}`);
21457
+ this.name = "TuiUnavailableError";
21458
+ }
21459
+ };
21460
+ function currentFaceEnv(argv) {
21461
+ return {
21462
+ stdinTty: process.stdin.isTTY === true,
21463
+ stdoutTty: process.stdout.isTTY === true,
21464
+ term: process.env.TERM,
21465
+ ci: Boolean(process.env.CI),
21466
+ argv
21467
+ };
21468
+ }
21469
+
21420
21470
  // src/cli.ts
21421
21471
  function flag(argv, name, fallback) {
21422
21472
  const i = argv.indexOf(`--${name}`);
@@ -21469,9 +21519,15 @@ async function agentSetup(argv) {
21469
21519
  async function run(argv) {
21470
21520
  const prompt = promptOf(argv);
21471
21521
  if (!prompt) {
21472
- process.stderr.write('usage: corenel run "<prompt>" [--model M] [--base URL]\n');
21522
+ process.stderr.write('usage: corenel run "<prompt>" [--model M] [--base URL] [--session ID]\n');
21473
21523
  process.exit(2);
21474
21524
  }
21525
+ const session = flag(argv, "session", "");
21526
+ if (session) {
21527
+ const ext = findExtension("sessions");
21528
+ process.exitCode = ext ? await runExtension(ext, ["run", session, prompt]) : 2;
21529
+ return;
21530
+ }
21475
21531
  const { client, model, system, tools } = await agentSetup(argv);
21476
21532
  const ac = new AbortController();
21477
21533
  const result = await runAgent({
@@ -21487,16 +21543,47 @@ async function run(argv) {
21487
21543
  }
21488
21544
  async function interactive(argv) {
21489
21545
  const { client, model, system, tools } = await agentSetup(argv);
21546
+ const approveAll = argv.includes("--allow-shell");
21547
+ const cwd = process.cwd();
21548
+ const files = new NodeFileService(cwd);
21549
+ const persistence = createSessionPersistence(files);
21550
+ const resumeAt = argv.indexOf("--resume");
21551
+ const resumeId = resumeAt >= 0 && argv[resumeAt + 1] && !argv[resumeAt + 1].startsWith("--") ? argv[resumeAt + 1] : void 0;
21552
+ const resume = resumeAt >= 0 ? { id: resumeId } : void 0;
21553
+ if (chooseFace(currentFaceEnv(argv)) === "tui") {
21554
+ try {
21555
+ const { runTui } = await import("@corenel/term/ink");
21556
+ process.exitCode = await runTui({
21557
+ cwd,
21558
+ model,
21559
+ system,
21560
+ client,
21561
+ tools,
21562
+ version: version(),
21563
+ toolCtx: (signal, ask) => nodeToolCtx(signal, { files, ask }),
21564
+ ...approveAll ? { approveAll: true } : {},
21565
+ persistence,
21566
+ ...resume ? { resume } : {}
21567
+ });
21568
+ return;
21569
+ } catch (e) {
21570
+ if (argv.includes("--tui")) throw new TuiUnavailableError(e);
21571
+ process.stderr.write(`corenel: terminal face unavailable, falling back (${e instanceof Error ? e.message : String(e)})
21572
+ `);
21573
+ }
21574
+ }
21490
21575
  process.exitCode = await runRepl({
21491
21576
  client,
21492
21577
  model,
21493
21578
  system,
21494
21579
  tools,
21495
- toolCtx: (signal) => nodeToolCtx(signal, { files: new NodeFileService(process.cwd()) }),
21496
- cwd: process.cwd(),
21580
+ toolCtx: (signal, ask) => nodeToolCtx(signal, { files, ...ask ? { ask } : {} }),
21581
+ cwd,
21497
21582
  // --allow-shell skips the per-command prompt. Named after the sidecar's
21498
21583
  // flag, which grants the same thing.
21499
- approveAll: argv.includes("--allow-shell")
21584
+ approveAll,
21585
+ persistence,
21586
+ ...resume ? { resume } : {}
21500
21587
  });
21501
21588
  }
21502
21589
  async function main() {
package/package.json CHANGED
@@ -1,30 +1,24 @@
1
1
  {
2
2
  "name": "@corenel/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "Corenel CLI — runs the harness in node, in-proc, no transport. The headless proof the kernel is host-agnostic. (corenel run/ask/chat/login + start --sidecar to follow.)",
6
6
  "bin": {
7
7
  "corenel": "dist/cli.js"
8
8
  },
9
- "scripts": {
10
- "typecheck": "tsc --noEmit",
11
- "start": "tsx src/cli.ts",
12
- "verify": "tsx src/verify.ts",
13
- "build": "node build.mjs",
14
- "prepublishOnly": "node build.mjs"
15
- },
16
9
  "dependencies": {
17
- "openai": "^4.77.0"
10
+ "openai": "^4.77.0",
11
+ "@corenel/term": "0.1.0"
18
12
  },
19
13
  "devDependencies": {
20
- "@corenel/crew": "workspace:*",
21
- "@corenel/harness": "workspace:*",
22
- "@corenel/protocol": "workspace:*",
23
- "@corenel/tools-node": "workspace:*",
24
14
  "esbuild": "^0.21.5",
25
15
  "@types/node": "^24.13.1",
26
16
  "tsx": "^4.19.2",
27
- "typescript": "^5.9.3"
17
+ "typescript": "^5.9.3",
18
+ "@corenel/crew": "0.0.0",
19
+ "@corenel/protocol": "0.3.2",
20
+ "@corenel/harness": "0.3.2",
21
+ "@corenel/tools-node": "0.2.0"
28
22
  },
29
23
  "publishConfig": {
30
24
  "access": "public"
@@ -36,5 +30,11 @@
36
30
  ],
37
31
  "engines": {
38
32
  "node": ">=18"
33
+ },
34
+ "scripts": {
35
+ "typecheck": "tsc --noEmit",
36
+ "start": "tsx src/cli.ts",
37
+ "verify": "tsx src/verify.ts",
38
+ "build": "node build.mjs"
39
39
  }
40
- }
40
+ }