@corenel/cli 0.8.0 → 0.9.0

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 +77 -68
  2. package/dist/main.js +199 -68
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -1,68 +1,77 @@
1
- # @corenel/cli
2
-
3
- The Corenel CLI — runs the host-agnostic agent harness in Node, in-process (no
4
- transport). Early/beta: the headless proof the kernel runs outside the browser.
5
-
6
- ```bash
7
- npx @corenel/cli
8
- ```
9
-
10
- Requires Node 22+. 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/main.js` -- see the note at the top of `build.mjs`. (`dist/cli.js`,
25
- the bin, is only a Node >=22 check that then imports `main.js`.) `@corenel/term`'s
26
- own dist is bundled but keeps `@corenel/harness` and `@corenel/protocol`
27
- EXTERNAL (see `packages/term/build.mjs`) -- both the CLI and term import the
28
- harness directly, and they must resolve to the SAME installed copy, or the
29
- `llm-execution` feature guard and `--debug` logging silently apply to the
30
- wrong module instance. That makes all four packages real runtime dependencies
31
- of each other, and publish order matters:
32
-
33
- pnpm --filter @corenel/protocol build && pnpm --filter @corenel/protocol publish
34
- pnpm --filter @corenel/harness build && pnpm --filter @corenel/harness publish
35
- pnpm --filter @corenel/term build && pnpm --filter @corenel/term publish
36
- pnpm --filter @corenel/cli build && pnpm --filter @corenel/cli publish
37
-
38
- Publishing any package against an unpublished dependency yields one that
39
- cannot install at all, so `npx @corenel/cli` fails before any verb runs.
40
-
41
- Publish with **pnpm, never npm**: this CLI depends on `@corenel/term` as
42
- `workspace:*`, and pnpm rewrites the `workspace:` protocol to a real version at
43
- pack/publish time while npm ships it verbatim -- an `npm publish` from this
44
- workspace produces a manifest whose install fails with
45
- `EUNSUPPORTEDPROTOCOL / Unsupported URL Type "workspace:"`, which is the very
46
- breakage this section exists to prevent.
47
-
48
- **All four versions must be bumped and published together, in the order
49
- above, every release -- even one that only touches a single package.** pnpm
50
- rewrites every `workspace:*` dependency to the EXACT current version of that
51
- dependency at pack time. All four packages are pinned to the same harness
52
- version today, so a normal `npm install` of `@corenel/cli` resolves to one
53
- harness copy. Bump `@corenel/harness` and publish a new `@corenel/term`
54
- without also republishing `@corenel/cli`, and the tree that installs
55
- `@corenel/cli` gets the OLD harness version nested under it (frozen into the
56
- manifest `@corenel/cli` published against) plus the NEW harness version
57
- hoisted at the top level for `@corenel/term` -- two separate harness module
58
- instances again, silently. That is exactly the bug this section's ordering
59
- exists to prevent (see `harnessSingleInstance.test.ts` in this package's
60
- `test/`): `registerGuard()`/`setAgentLogging()` calls made through one
61
- instance configure a module the other never reads, so the `llm-execution`
62
- feature gate silently stops running and `--debug` logging can't be turned
63
- off. Every test in this repo stays green when that happens, because the
64
- in-repo workspace is symlinked (one instance by construction) and the
65
- build-output regression test only exercises that same symlinked layout --
66
- neither one can see a real, version-skewed `npm install`.
67
-
68
- Licensed under Elastic-2.0.
1
+ # @corenel/cli
2
+
3
+ The Corenel CLI — runs the host-agnostic agent harness in Node, in-process (no
4
+ transport). Early/beta: the headless proof the kernel runs outside the browser.
5
+
6
+ ```bash
7
+ npx @corenel/cli
8
+ ```
9
+
10
+ Requires Node 22+. 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
+ The API base is `--base`, then `CORENEL_API_BASE`, then the base recorded at
22
+ `corenel login`, then `https://api.corenel.ai/api`. Run from source
23
+ (`tsx src/cli.ts`), that last default becomes the local API
24
+ (`http://localhost:3010/api`) instead; `corenel auth` and `corenel doctor`
25
+ say so, and a `login` from source records the local base on its credential.
26
+ `--base` and `CORENEL_API_BASE` turn that off, and a recorded login base wins over
27
+ it; to move a credential between production and the local API, run
28
+ `corenel logout` before `corenel login`.
29
+
30
+ ## Releasing
31
+
32
+ `@corenel/term` (the terminal face) is a real runtime dependency, not bundled
33
+ into `dist/main.js` -- see the note at the top of `build.mjs`. (`dist/cli.js`,
34
+ the bin, is only a Node >=22 check that then imports `main.js`.) `@corenel/term`'s
35
+ own dist is bundled but keeps `@corenel/harness` and `@corenel/protocol`
36
+ EXTERNAL (see `packages/term/build.mjs`) -- both the CLI and term import the
37
+ harness directly, and they must resolve to the SAME installed copy, or the
38
+ `llm-execution` feature guard and `--debug` logging silently apply to the
39
+ wrong module instance. That makes all four packages real runtime dependencies
40
+ of each other, and publish order matters:
41
+
42
+ pnpm --filter @corenel/protocol build && pnpm --filter @corenel/protocol publish
43
+ pnpm --filter @corenel/harness build && pnpm --filter @corenel/harness publish
44
+ pnpm --filter @corenel/term build && pnpm --filter @corenel/term publish
45
+ pnpm --filter @corenel/cli build && pnpm --filter @corenel/cli publish
46
+
47
+ Publishing any package against an unpublished dependency yields one that
48
+ cannot install at all, so `npx @corenel/cli` fails before any verb runs.
49
+
50
+ Publish with **pnpm, never npm**: this CLI depends on `@corenel/term` as
51
+ `workspace:*`, and pnpm rewrites the `workspace:` protocol to a real version at
52
+ pack/publish time while npm ships it verbatim -- an `npm publish` from this
53
+ workspace produces a manifest whose install fails with
54
+ `EUNSUPPORTEDPROTOCOL / Unsupported URL Type "workspace:"`, which is the very
55
+ breakage this section exists to prevent.
56
+
57
+ **All four versions must be bumped and published together, in the order
58
+ above, every release -- even one that only touches a single package.** pnpm
59
+ rewrites every `workspace:*` dependency to the EXACT current version of that
60
+ dependency at pack time. All four packages are pinned to the same harness
61
+ version today, so a normal `npm install` of `@corenel/cli` resolves to one
62
+ harness copy. Bump `@corenel/harness` and publish a new `@corenel/term`
63
+ without also republishing `@corenel/cli`, and the tree that installs
64
+ `@corenel/cli` gets the OLD harness version nested under it (frozen into the
65
+ manifest `@corenel/cli` published against) plus the NEW harness version
66
+ hoisted at the top level for `@corenel/term` -- two separate harness module
67
+ instances again, silently. That is exactly the bug this section's ordering
68
+ exists to prevent (see `harnessSingleInstance.test.ts` in this package's
69
+ `test/`): `registerGuard()`/`setAgentLogging()` calls made through one
70
+ instance configure a module the other never reads, so the `llm-execution`
71
+ feature gate silently stops running and `--debug` logging can't be turned
72
+ off. Every test in this repo stays green when that happens, because the
73
+ in-repo workspace is symlinked (one instance by construction) and the
74
+ build-output regression test only exercises that same symlinked layout --
75
+ neither one can see a real, version-skewed `npm install`.
76
+
77
+ Licensed under Elastic-2.0.
package/dist/main.js CHANGED
@@ -15259,8 +15259,8 @@ var require_precompile = __commonJS({
15259
15259
  fs6.readdirSync(dir).forEach(function(file) {
15260
15260
  var filepath = path.join(dir, file);
15261
15261
  var subpath = filepath.substr(path.join(input, "/").length);
15262
- var stat = fs6.statSync(filepath);
15263
- if (stat && stat.isDirectory()) {
15262
+ var stat2 = fs6.statSync(filepath);
15263
+ if (stat2 && stat2.isDirectory()) {
15264
15264
  subpath += "/";
15265
15265
  if (!match(subpath, opts.exclude)) {
15266
15266
  addTemplates(filepath);
@@ -16439,8 +16439,8 @@ ${yamlContent}` : yamlContent;
16439
16439
  if (resolvedInheritsRef.startsWith("./") || resolvedInheritsRef.startsWith("../") || resolvedInheritsRef.startsWith("/") || !resolvedInheritsRef.startsWith("@") && isPrompdFile(resolvedInheritsRef)) {
16440
16440
  const sourceDir = context.fileSystem.dirname(context.sourceFile);
16441
16441
  const parentPath = context.fileSystem.resolve(sourceDir, resolvedInheritsRef);
16442
- const fileExists = await context.fileSystem.exists(parentPath);
16443
- if (!fileExists) {
16442
+ const fileExists2 = await context.fileSystem.exists(parentPath);
16443
+ if (!fileExists2) {
16444
16444
  const location = context.findLocation(/inherits:/);
16445
16445
  context.addDiagnostic({
16446
16446
  message: `Inherited file not found: "${resolvedInheritsRef}" (resolved to: ${parentPath})`,
@@ -16467,8 +16467,8 @@ ${yamlContent}` : yamlContent;
16467
16467
  }
16468
16468
  const packagePath = await this.resolvePackage(context, packageRef);
16469
16469
  const fullFilePath = resolvePackageFile(packagePath, filePathInPackage);
16470
- const fileExists = await context.fileSystem.exists(fullFilePath);
16471
- if (!fileExists) {
16470
+ const fileExists2 = await context.fileSystem.exists(fullFilePath);
16471
+ if (!fileExists2) {
16472
16472
  const location = context.findLocation(/inherits:/);
16473
16473
  context.addDiagnostic({
16474
16474
  message: `Inherited file not found in package: "${filePathInPackage}" (package: ${packageRef})`,
@@ -22704,7 +22704,7 @@ var require_websocket_server = __commonJS({
22704
22704
  init_tools_node();
22705
22705
  import { runAgent as runAgent2 } from "@corenel/harness/core/loop";
22706
22706
  import { createChatClient as createChatClient2, setGatewayBase as setGatewayBase2 } from "@corenel/harness/providers/gateway";
22707
- import { resolveContextWindow as resolveContextWindow2, resolveContextWindowWithHydration } from "@corenel/harness/providers/contextWindow";
22707
+ import { resolveContextWindow as resolveContextWindow3, resolveContextWindowWithHydration } from "@corenel/harness/providers/contextWindow";
22708
22708
  import { basename as basename5 } from "node:path";
22709
22709
  import { randomUUID } from "node:crypto";
22710
22710
  import { allTools as allTools3 } from "@corenel/harness/tools/builtins";
@@ -22721,9 +22721,9 @@ function configWorkspaceRoot(base) {
22721
22721
 
22722
22722
  // src/cli.ts
22723
22723
  import { FileRecallStore, UNATTACHED_SESSION } from "@corenel/harness/sessions/fileRecallStore";
22724
- import { makeCheckpointStore } from "@corenel/harness/sessions/checkpointStore";
22724
+ import { makeSessionFoldStorage } from "@corenel/harness/sessions/foldStorage";
22725
22725
  import { makeSummarizer } from "@corenel/harness/core/summarize";
22726
- import { stateDirName as stateDirName7, sessionsRoot } from "@corenel/protocol";
22726
+ import { stateDirName as stateDirName7, sessionsRoot, isSourceRun } from "@corenel/protocol";
22727
22727
  import { PermissionService as PermissionService6 } from "@corenel/harness/guardrail/permission-service";
22728
22728
 
22729
22729
  // src/nodeContext.ts
@@ -24918,6 +24918,7 @@ import { createChatClient, setGatewayBase } from "@corenel/harness/providers/gat
24918
24918
  // src/apiBase.ts
24919
24919
  init_tools_node();
24920
24920
  import { DEFAULT_API_BASE as DEFAULT_API_BASE2 } from "@corenel/protocol";
24921
+ import { DEV_PROFILE } from "@corenel/protocol";
24921
24922
  var InvalidApiBaseError = class extends Error {
24922
24923
  constructor(value, why) {
24923
24924
  super(`invalid API base ${JSON.stringify(value)} -- ${why}`);
@@ -24931,12 +24932,34 @@ function isLoopback(hostname2) {
24931
24932
  return h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".localhost");
24932
24933
  }
24933
24934
  var warnedCleartext = false;
24935
+ var sourceRunDefault = false;
24936
+ function setSourceRun(sourceRun) {
24937
+ sourceRunDefault = sourceRun;
24938
+ }
24939
+ function applySourceRunDevBase(resolved, sourceRun) {
24940
+ if (!sourceRun || resolved.source !== "default") return resolved;
24941
+ return { base: DEV_PROFILE.apiBase, source: "dev-profile" };
24942
+ }
24943
+ function cliDevRelayBases(resolved, sourceRun = sourceRunDefault) {
24944
+ if (resolved.source === "dev-profile") return [resolved.base];
24945
+ if (sourceRun && resolved.source === "credential" && resolved.base.replace(/\/+$/, "") === DEV_PROFILE.apiBase) return [resolved.base];
24946
+ return [];
24947
+ }
24948
+ function apiBaseNote(source) {
24949
+ return source === "dev-profile" ? "dev profile: running from source" : void 0;
24950
+ }
24934
24951
  async function apiBase(explicit, opts = {}) {
24935
- const resolved = await resolveApiBase({
24936
- explicit,
24937
- env: opts.env ?? process.env,
24938
- ...opts.stateDir ? { stateDir: opts.stateDir } : {}
24939
- });
24952
+ return (await resolveCliApiBase(explicit, opts)).base;
24953
+ }
24954
+ async function resolveCliApiBase(explicit, opts = {}) {
24955
+ const resolved = applySourceRunDevBase(
24956
+ await resolveApiBase({
24957
+ explicit,
24958
+ env: opts.env ?? process.env,
24959
+ ...opts.stateDir ? { stateDir: opts.stateDir } : {}
24960
+ }),
24961
+ opts.sourceRun ?? sourceRunDefault
24962
+ );
24940
24963
  const value = resolved.base;
24941
24964
  let url;
24942
24965
  try {
@@ -24954,7 +24977,7 @@ async function apiBase(explicit, opts = {}) {
24954
24977
  `
24955
24978
  );
24956
24979
  }
24957
- return value;
24980
+ return resolved;
24958
24981
  }
24959
24982
 
24960
24983
  // src/runAgent.ts
@@ -25270,7 +25293,9 @@ function helpText(version2) {
25270
25293
  " --model <id> model for this turn (default gpt-4o-mini)",
25271
25294
  " --base <url> API base (default https://api.corenel.ai/api, or CORENEL_API_BASE)",
25272
25295
  " --policy <name|path> a builtin policy name (e.g. read-only) or a policy YAML file;",
25273
- " default is a project/user policy.yaml if one exists, else standard",
25296
+ " default is the per-project policy (policies/ws-<id>.yaml in your home",
25297
+ " state dir), else the user policy.yaml there, else standard. A",
25298
+ " policy.yaml inside the project folder is ignored.",
25274
25299
  " --allow-mutations allow write/shell/other mutating tools (run/run-agent refuse them",
25275
25300
  " outright without this; interactively it skips the per-command",
25276
25301
  " prompt instead). --allow-shell still works as an alias.",
@@ -25355,23 +25380,27 @@ async function authState(resolve4 = (env2) => resolveNodeCredential({ env: env2
25355
25380
  const source = r.source === "env" ? "env" : "file";
25356
25381
  return { source, path: r.path ?? authTokenPath(), hint: hintOf(r.token) };
25357
25382
  }
25358
- function describeAuth(s, base) {
25383
+ function describeAuth(s, base, source) {
25384
+ const note = source ? apiBaseNote(source) : void 0;
25385
+ const apiLine = ` api ${base}${note ? ` (${note})` : ""}`;
25359
25386
  if (s.source === "none") {
25360
25387
  return [
25361
25388
  "Not signed in.",
25362
25389
  ` Run \`corenel login\`, or set CORENEL_TOKEN=<token>.`,
25363
- ` Token would be stored at ${s.path}`
25364
- ].join("\n");
25390
+ ` Token would be stored at ${s.path}`,
25391
+ note ? apiLine : null
25392
+ ].filter(Boolean).join("\n");
25365
25393
  }
25366
25394
  return [
25367
25395
  `Signed in (${s.source === "env" ? "CORENEL_TOKEN environment variable" : s.path}).`,
25368
25396
  ` token ${s.hint}`,
25369
- ` api ${base}`,
25397
+ apiLine,
25370
25398
  s.source === "env" ? " NOTE: the environment wins over any stored token, so `corenel logout` will not change this." : null
25371
25399
  ].filter(Boolean).join("\n");
25372
25400
  }
25373
25401
  async function runAuth(out = (s) => process.stdout.write(s)) {
25374
- out(`${describeAuth(await authState(), await apiBase())}
25402
+ const { base, source } = await resolveCliApiBase();
25403
+ out(`${describeAuth(await authState(), base, source)}
25375
25404
  `);
25376
25405
  }
25377
25406
  async function runLogout(out = (s) => process.stdout.write(s)) {
@@ -25721,17 +25750,19 @@ async function checkSidecarEnv(base, env, stateDir = corenelDir()) {
25721
25750
  return { name: "sidecar", status: "fail", detail: `${base} (env: ${env}) -- ${e instanceof Error ? e.message : String(e)}` };
25722
25751
  }
25723
25752
  }
25753
+ function apiCheck(resolved) {
25754
+ const note = apiBaseNote(resolved.source);
25755
+ const detail = note ? `${resolved.base} (${note}; installed default is ${DEFAULT_API_BASE2})` : resolved.base === DEFAULT_API_BASE2 ? resolved.base : `${resolved.base} (overridden; default is ${DEFAULT_API_BASE2})`;
25756
+ return { name: "api", status: "ok", detail };
25757
+ }
25724
25758
  async function collectChecks(f = fetch, modelId = DEFAULT_MODEL, env) {
25725
- const base = await apiBase();
25759
+ const resolvedBase = await resolveCliApiBase();
25760
+ const base = resolvedBase.base;
25726
25761
  const checks = [
25727
25762
  { name: "binary", status: "ok", detail: selfPath() },
25728
25763
  { name: "version", status: "ok", detail: `@corenel/cli ${version()}` },
25729
25764
  { name: "node", status: "ok", detail: process.version },
25730
- {
25731
- name: "api",
25732
- status: "ok",
25733
- detail: base === DEFAULT_API_BASE2 ? base : `${base} (overridden; default is ${DEFAULT_API_BASE2})`
25734
- }
25765
+ apiCheck(resolvedBase)
25735
25766
  ];
25736
25767
  try {
25737
25768
  const u = new URL(base);
@@ -25779,7 +25810,7 @@ import { createInterface } from "node:readline";
25779
25810
  import { PermissionService as PermissionService4 } from "@corenel/harness/guardrail/permission-service";
25780
25811
  import { STANDARD_POLICY as STANDARD_POLICY2 } from "@corenel/harness/guardrail/builtins";
25781
25812
  import { approvalsHooks } from "@corenel/harness/approvals";
25782
- import { TermSession, parseSuggestionChoice, suggestionKeys, suggestionLine, suggestionOutcome } from "@corenel/term/session";
25813
+ import { TermSession, startupResumeOptions, parseSuggestionChoice, suggestionKeys, suggestionLine, suggestionOutcome } from "@corenel/term/session";
25783
25814
  import { describeToolCall, formatOrd } from "@corenel/term/describe";
25784
25815
  import { buildWritePreview } from "@corenel/term/writePreview";
25785
25816
  import { formatEndingChange } from "@corenel/term/diff";
@@ -25937,10 +25968,10 @@ async function runRepl(deps) {
25937
25968
  }
25938
25969
  });
25939
25970
  if (deps.resume) {
25940
- await session.resume(deps.resume.id);
25971
+ await session.resume(deps.resume.id, startupResumeOptions(deps.resume));
25941
25972
  const restored = session.getSnapshot();
25942
25973
  if (restored.session) {
25943
- writeLine(` resumed ${restored.session.id} - ${restored.committed.length} messages
25974
+ writeLine(` resumed ${restored.session.id} - ${restored.committed.filter((i) => i.kind !== "note").length} messages
25944
25975
  `);
25945
25976
  }
25946
25977
  }
@@ -26227,6 +26258,44 @@ ${rules(r.workspace)}`);
26227
26258
  import { approvalsHooks as approvalsHooks2 } from "@corenel/harness/approvals";
26228
26259
  import { createSessionPersistence } from "@corenel/term/persist";
26229
26260
 
26261
+ // src/resumeModel.ts
26262
+ import { estimateFill, COMPACT_AT_FILL } from "@corenel/harness/core/compactHistory";
26263
+ import { resolveContextWindow as resolveContextWindow2 } from "@corenel/harness/providers/contextWindow";
26264
+ var DEFAULT_MODEL2 = "gpt-4o-mini";
26265
+ function resumeModel(flagModel, stored, fallback) {
26266
+ return flagModel || stored || fallback;
26267
+ }
26268
+ async function storedModelOf(resume, persistence) {
26269
+ if (!resume) return void 0;
26270
+ const loaded = await persistence.load(resume.id);
26271
+ if (!loaded) return void 0;
26272
+ return { id: loaded.id, messages: loaded.messages, ...loaded.model ? { model: loaded.model } : {} };
26273
+ }
26274
+ async function resolveSessionModel(input) {
26275
+ const { argv, persistence, fallback, isKnownModel } = input;
26276
+ const windowOf = input.contextWindow ?? ((m) => resolveContextWindow2(m).window);
26277
+ const flagModel = hasValueFlag(argv, "model") ? flag(argv, "model", "") || void 0 : void 0;
26278
+ const resume = resumeFlag(argv);
26279
+ let chat;
26280
+ try {
26281
+ chat = await storedModelOf(resume, persistence);
26282
+ } catch {
26283
+ const model = flagModel ?? fallback;
26284
+ return { model, notice: `could not read this chat's model, so it will run on ${model}.` };
26285
+ }
26286
+ const stored = chat?.model;
26287
+ if (flagModel) {
26288
+ if (!chat || !stored || stored === flagModel) return { model: flagModel };
26289
+ const folds = estimateFill(chat.messages, windowOf(flagModel)) >= COMPACT_AT_FILL;
26290
+ const notice = `chat ${chat.id} was on ${stored}; continuing on ${flagModel} (this changes the chat's model)` + (folds ? ", and older history will be folded to fit." : ".");
26291
+ return { model: flagModel, notice };
26292
+ }
26293
+ if (stored && isKnownModel && !isKnownModel(stored)) {
26294
+ return { model: fallback, notice: `this chat used ${stored}, which is not available. It will run on ${fallback}.` };
26295
+ }
26296
+ return { model: resumeModel(void 0, stored, fallback) };
26297
+ }
26298
+
26230
26299
  // src/shellTool.ts
26231
26300
  import { spawn as spawn3 } from "node:child_process";
26232
26301
  var MAX_OUTPUT = 3e4;
@@ -27031,8 +27100,8 @@ async function resolveStrategyPrompt(files, id) {
27031
27100
  }
27032
27101
 
27033
27102
  // src/policyResolve.ts
27034
- import { readFile as readFile2 } from "node:fs/promises";
27035
- import { homedir as homedir3 } from "node:os";
27103
+ init_tools_node();
27104
+ import { readFile as readFile2, stat } from "node:fs/promises";
27036
27105
  import { join as join11 } from "node:path";
27037
27106
  import { stateDirName as stateDirName6 } from "@corenel/protocol";
27038
27107
  import { BUILTIN_POLICIES, STANDARD_POLICY as STANDARD_POLICY3, policyFromYaml } from "@corenel/harness/guardrail";
@@ -27081,25 +27150,55 @@ function validateOrThrow(text, path) {
27081
27150
  }
27082
27151
  return result.policy;
27083
27152
  }
27153
+ async function projectPolicyPath(cwd, homeBase) {
27154
+ return join11(corenelDir(homeBase), "policies", `ws-${await workspaceApprovalsId(cwd)}.yaml`);
27155
+ }
27156
+ async function fileExists(path) {
27157
+ try {
27158
+ await stat(path);
27159
+ return true;
27160
+ } catch (e) {
27161
+ return !(isErrnoException(e) && e.code === "ENOENT");
27162
+ }
27163
+ }
27084
27164
  async function resolvePolicy(opts) {
27085
- const { flag: flag2, cwd } = opts;
27165
+ const { flag: flag2, cwd, homeBase } = opts;
27166
+ const warnings = [];
27086
27167
  if (flag2) {
27087
27168
  if (Object.prototype.hasOwnProperty.call(BUILTIN_POLICIES, flag2)) {
27088
- return { policy: BUILTIN_POLICIES[flag2], source: { kind: "builtin", name: flag2 } };
27169
+ return { policy: BUILTIN_POLICIES[flag2], source: { kind: "builtin", name: flag2 }, warnings };
27089
27170
  }
27090
27171
  const policy = await loadRequiredPolicyFile(
27091
27172
  flag2,
27092
27173
  `corenel: unknown --policy "${flag2}" -- not a builtin (${Object.keys(BUILTIN_POLICIES).join(", ")}) and no file exists at that path.`
27093
27174
  );
27094
- return { policy, source: { kind: "file", path: flag2 } };
27175
+ return { policy, source: { kind: "file", path: flag2 }, warnings };
27176
+ }
27177
+ const stateDir = corenelDir(homeBase);
27178
+ const projectPath = await projectPolicyPath(cwd, homeBase);
27179
+ const userPath = join11(stateDir, "policy.yaml");
27180
+ if (await insideAnyRoot([cwd], stateDir) !== null) {
27181
+ const present2 = [];
27182
+ if (await fileExists(projectPath)) present2.push(projectPath);
27183
+ if (await fileExists(userPath)) present2.push(userPath);
27184
+ if (present2.length) {
27185
+ throw new PolicyResolutionError(
27186
+ `corenel: not loading ${present2.join(" or ")}: ${stateDir} is inside this folder (${cwd}), where the agent's file tools could have written it. Start corenel from a folder that does not contain it, or pass --policy <name|path> explicitly.`
27187
+ );
27188
+ }
27189
+ return { policy: STANDARD_POLICY3, source: { kind: "default" }, warnings };
27190
+ }
27191
+ const workspacePath = join11(cwd, stateDirName6(), "policy.yaml");
27192
+ if (await fileExists(workspacePath)) {
27193
+ warnings.push(
27194
+ `corenel: ${workspacePath} ignored -- a policy inside the workspace can be written by the agent or arrive in a cloned repo. To use it for this folder, move it to ${projectPath}, or pass --policy ${workspacePath} explicitly.`
27195
+ );
27095
27196
  }
27096
- const projectPath = join11(cwd, stateDirName6(), "policy.yaml");
27097
27197
  const project = await loadDiscoveredPolicyFile(projectPath);
27098
- if (project) return { policy: project, source: { kind: "project", path: projectPath } };
27099
- const userPath = join11(homedir3(), stateDirName6(), "policy.yaml");
27198
+ if (project) return { policy: project, source: { kind: "project", path: projectPath }, warnings };
27100
27199
  const user = await loadDiscoveredPolicyFile(userPath);
27101
- if (user) return { policy: user, source: { kind: "user", path: userPath } };
27102
- return { policy: STANDARD_POLICY3, source: { kind: "default" } };
27200
+ if (user) return { policy: user, source: { kind: "user", path: userPath }, warnings };
27201
+ return { policy: STANDARD_POLICY3, source: { kind: "default" }, warnings };
27103
27202
  }
27104
27203
 
27105
27204
  // ../mcp/src/httpClient.ts
@@ -27572,7 +27671,7 @@ function isExpired(cred, nowMs) {
27572
27671
  }
27573
27672
 
27574
27673
  // ../mcp/src/auth.ts
27575
- import { DEFAULT_API_BASE as DEFAULT_API_BASE3 } from "@corenel/protocol";
27674
+ import { DEFAULT_API_BASE as DEFAULT_API_BASE4 } from "@corenel/protocol";
27576
27675
  function safeOrigin(url) {
27577
27676
  try {
27578
27677
  return new URL(url).origin;
@@ -27580,24 +27679,30 @@ function safeOrigin(url) {
27580
27679
  return "its URL";
27581
27680
  }
27582
27681
  }
27583
- function isCorenelRelayUrl(url, bases) {
27682
+ function isCorenelRelayUrl(url, bases, devApiBases = []) {
27584
27683
  let u;
27585
27684
  try {
27586
27685
  u = new URL(url);
27587
27686
  } catch {
27588
27687
  return false;
27589
27688
  }
27590
- if (u.protocol !== "https:") return false;
27591
27689
  if (!/\/mcp\/hosted\/[^/]+\/?$/.test(u.pathname)) return false;
27592
- return bases.some((b) => {
27690
+ const sameOrigin = (b) => {
27593
27691
  try {
27594
27692
  return new URL(b).origin === u.origin;
27595
27693
  } catch {
27596
27694
  return false;
27597
27695
  }
27598
- });
27696
+ };
27697
+ if (u.protocol === "https:") return [...bases, ...devApiBases].some(sameOrigin);
27698
+ if (u.protocol === "http:" && isLoopbackHost(u.hostname)) return devApiBases.some(sameOrigin);
27699
+ return false;
27700
+ }
27701
+ function isLoopbackHost(hostname2) {
27702
+ const h = hostname2.replace(/^\[|\]$/g, "").toLowerCase();
27703
+ return h === "localhost" || h === "127.0.0.1" || h === "::1";
27599
27704
  }
27600
- function authProviderFor(stateDir, server, fetchImpl = fetch, accountToken = async () => null, relayBases = async () => []) {
27705
+ function authProviderFor(stateDir, server, fetchImpl = fetch, accountToken = async () => null, relayBases = async () => [], devApiBases = []) {
27601
27706
  async function doRefresh(cred) {
27602
27707
  if (!cred.refreshToken) {
27603
27708
  throw new Error(`"${server.name}" has no refresh token on file -- run: corenel mcp login ${server.name}`);
@@ -27610,7 +27715,7 @@ function authProviderFor(stateDir, server, fetchImpl = fetch, accountToken = asy
27610
27715
  return {
27611
27716
  async headers() {
27612
27717
  const cred = await readMcpCredential(stateDir, server.name);
27613
- if (server.hosted && !isCorenelRelayUrl(server.url, [DEFAULT_API_BASE3, ...await relayBases()])) {
27718
+ if (server.hosted && !isCorenelRelayUrl(server.url, [DEFAULT_API_BASE4, ...await relayBases()], devApiBases)) {
27614
27719
  throw new Error(`"${server.name}" is marked hosted but ${safeOrigin(server.url)} is not a Corenel relay; refusing to send the account token there.`);
27615
27720
  }
27616
27721
  if (!cred && !server.hosted) {
@@ -27661,10 +27766,17 @@ async function accountTokenFor(stateDir) {
27661
27766
  return null;
27662
27767
  }
27663
27768
  }
27769
+ function mcpClientFactory(opts = {}) {
27770
+ const devApiBases = [...opts.devApiBases ?? []];
27771
+ return (stateDir, server) => buildClient(stateDir, server, devApiBases);
27772
+ }
27664
27773
  function defaultClientFactory(stateDir, server) {
27774
+ return buildClient(stateDir, server, []);
27775
+ }
27776
+ function buildClient(stateDir, server, devApiBases) {
27665
27777
  return new McpHttpClient(
27666
27778
  server,
27667
- authProviderFor(stateDir, server, timedFetch(AUTH_REQUEST_TIMEOUT_MS), () => accountTokenFor(stateDir), () => relayBasesFor(stateDir))
27779
+ authProviderFor(stateDir, server, timedFetch(AUTH_REQUEST_TIMEOUT_MS), () => accountTokenFor(stateDir), () => relayBasesFor(stateDir), devApiBases)
27668
27780
  );
27669
27781
  }
27670
27782
  async function relayBasesFor(stateDir) {
@@ -28002,11 +28114,11 @@ function cacheEntryFor(cache, server, cred) {
28002
28114
  }
28003
28115
 
28004
28116
  // src/mcpRegistry.ts
28005
- async function openCliMcp(stateDir, policy) {
28117
+ async function openCliMcp(stateDir, policy, opts = {}) {
28006
28118
  try {
28007
28119
  const servers = await readMcpServers(stateDir);
28008
28120
  if (servers.length === 0) return null;
28009
- const registry = new McpRegistry(stateDir);
28121
+ const registry = new McpRegistry(stateDir, mcpClientFactory({ devApiBases: opts.devApiBases ?? [] }));
28010
28122
  const cache = await readToolCache(stateDir);
28011
28123
  const usable = {};
28012
28124
  for (const s of servers) {
@@ -28399,8 +28511,18 @@ import { lookup as dnsLookup } from "node:dns/promises";
28399
28511
  import { setOutboundResolver } from "@corenel/harness/tools/urlGuard";
28400
28512
  var EXIT_POLICY_PARK_REFUSED = 3;
28401
28513
  var EXIT_BUDGET_EXCEEDED = 4;
28402
- async function agentSetup(argv) {
28403
- setGatewayBase2(await apiBase(flag(argv, "base", "")));
28514
+ function replSessionId(id) {
28515
+ return id === UNATTACHED_SESSION ? null : id;
28516
+ }
28517
+ function replCompaction(client, model, contextWindow, files, getSessionId) {
28518
+ return {
28519
+ summarize: makeSummarizer(client, model, { contextWindow }),
28520
+ storage: makeSessionFoldStorage({ getFiles: () => files, root: sessionsRoot(), sessionId: getSessionId })
28521
+ };
28522
+ }
28523
+ async function agentSetup(argv, chosenModel) {
28524
+ const gatewayBase = await resolveCliApiBase(flag(argv, "base", ""));
28525
+ setGatewayBase2(gatewayBase.base);
28404
28526
  const token = await nodeAuthToken().getToken();
28405
28527
  if (!token) {
28406
28528
  process.stderr.write("corenel: not signed in.\n Run `corenel login`, or set CORENEL_TOKEN=<token> (a gateway bearer token), then try again.\n");
@@ -28442,19 +28564,22 @@ async function agentSetup(argv) {
28442
28564
  let policy;
28443
28565
  let policySource;
28444
28566
  try {
28445
- ({ policy, source: policySource } = await resolvePolicy({
28567
+ const resolved = await resolvePolicy({
28446
28568
  ...hasValueFlag(argv, "policy") ? { flag: flag(argv, "policy", "") } : {},
28447
28569
  cwd: process.cwd()
28448
- }));
28570
+ });
28571
+ ({ policy, source: policySource } = resolved);
28572
+ for (const w of resolved.warnings) process.stderr.write(`${w}
28573
+ `);
28449
28574
  } catch (e) {
28450
28575
  sidecarProcs?.disconnect();
28451
28576
  throw e;
28452
28577
  }
28453
- const mcp = await openCliMcp(configWorkspaceRoot(), policy);
28578
+ const mcp = await openCliMcp(configWorkspaceRoot(), policy, { devApiBases: cliDevRelayBases(gatewayBase) });
28454
28579
  if (mcp) tools.push(...mcp.tools);
28455
28580
  return {
28456
28581
  client: createChatClient2({ getToken: async () => token }),
28457
- model: flag(argv, "model", "gpt-4o-mini"),
28582
+ model: chosenModel ?? flag(argv, "model", DEFAULT_MODEL2),
28458
28583
  system,
28459
28584
  tools,
28460
28585
  identity: identity2,
@@ -28558,10 +28683,10 @@ async function run(argv) {
28558
28683
  * with a real window buys exactly that awareness without turning on
28559
28684
  * rung-1 offload here (this path builds no recall store).
28560
28685
  *
28561
- * No `onCheckpoint`: a one-shot run has no session to write one beside.
28686
+ * No fold storage: a one-shot run has no session to write one beside.
28562
28687
  * It still compacts — surviving the wall matters more than recording why. */
28563
- compression: { mode: "off", contextWindow: resolveContextWindow2(model).window },
28564
- compaction: { summarize: makeSummarizer(client, model) },
28688
+ compression: { mode: "off", contextWindow: resolveContextWindow3(model).window },
28689
+ compaction: { summarize: makeSummarizer(client, model, { contextWindow: resolveContextWindow3(model).window }) },
28565
28690
  // A spawned sub-agent runs under the SAME gate as the parent turn -- never
28566
28691
  // looser, so it can't be used to route around the default-deny above.
28567
28692
  toolCtx: nodeToolCtx(ac.signal, {
@@ -28599,7 +28724,12 @@ async function run(argv) {
28599
28724
  }
28600
28725
  }
28601
28726
  async function interactive(argv) {
28602
- const { client, model, tools, identity: identity2, policy, policySource, memory, mcp, disconnect } = await agentSetup(argv);
28727
+ const chosen = await resolveSessionModel({
28728
+ argv,
28729
+ persistence: createSessionPersistence(new NodeFileService(process.cwd())),
28730
+ fallback: DEFAULT_MODEL2
28731
+ });
28732
+ const { client, model, tools, identity: identity2, policy, policySource, memory, mcp, disconnect } = await agentSetup(argv, chosen.model);
28603
28733
  try {
28604
28734
  await runInteractive();
28605
28735
  } finally {
@@ -28614,7 +28744,7 @@ async function interactive(argv) {
28614
28744
  const approvalsContext = { runId: randomUUID(), workspace: basename5(cwd) };
28615
28745
  let modelRates = null;
28616
28746
  let priceTable = /* @__PURE__ */ new Map();
28617
- const resolvedWindow = resolveContextWindow2(model);
28747
+ const resolvedWindow = resolveContextWindow3(model);
28618
28748
  alog("context window", model, resolvedWindow.window, `(${resolvedWindow.source})`);
28619
28749
  const compression = { mode: "balanced", contextWindow: resolvedWindow.window, strategy: "deferred" };
28620
28750
  const priceBase = await apiBase(flag(argv, "base", ""));
@@ -28682,11 +28812,9 @@ async function interactive(argv) {
28682
28812
  const onSessionId = (id) => {
28683
28813
  currentSessionId = id;
28684
28814
  };
28685
- const compaction = {
28686
- summarize: makeSummarizer(client, model),
28687
- onCheckpoint: (body) => makeCheckpointStore(() => files, sessionsRoot()).append(currentSessionId, body)
28688
- };
28689
- const resume = resumeFlag(argv);
28815
+ const compaction = replCompaction(client, model, () => compression.contextWindow, files, () => replSessionId(currentSessionId));
28816
+ const resumeBase = resumeFlag(argv);
28817
+ const resume = resumeBase ? { ...resumeBase, ...chosen.notice ? { notice: chosen.notice } : {} } : void 0;
28690
28818
  if (chooseFace(currentFaceEnv(argv)) === "tui") {
28691
28819
  try {
28692
28820
  const { runTui } = await import("@corenel/term/ink");
@@ -28785,6 +28913,7 @@ async function interactive(argv) {
28785
28913
  }
28786
28914
  async function main() {
28787
28915
  const [cmd, ...rest] = process.argv.slice(2);
28916
+ setSourceRun(isSourceRun(import.meta.url));
28788
28917
  setOutboundResolver(async (h) => (await dnsLookup(h, { all: true, verbatim: true })).map((a) => a.address));
28789
28918
  try {
28790
28919
  setAgentLogging(hasBooleanFlag(process.argv, "debug") || process.env.CORENEL_DEBUG === "1");
@@ -28862,7 +28991,7 @@ Logged in. Token saved to ${authTokenPath()}
28862
28991
  await runLogout();
28863
28992
  break;
28864
28993
  case "doctor": {
28865
- process.exitCode = await runDoctor(void 0, void 0, flag(rest, "model", "gpt-4o-mini"), resolveDoctorEnv(rest));
28994
+ process.exitCode = await runDoctor(void 0, void 0, flag(rest, "model", DEFAULT_MODEL2), resolveDoctorEnv(rest));
28866
28995
  break;
28867
28996
  }
28868
28997
  case "approvals":
@@ -28935,5 +29064,7 @@ main().catch((e) => {
28935
29064
  export {
28936
29065
  EXIT_BUDGET_EXCEEDED,
28937
29066
  EXIT_POLICY_PARK_REFUSED,
28938
- buildRunPermission
29067
+ buildRunPermission,
29068
+ replCompaction,
29069
+ replSessionId
28939
29070
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corenel/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
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": {
@@ -9,11 +9,11 @@
9
9
  "dependencies": {
10
10
  "openai": "^4.77.0",
11
11
  "ws": "^8.18.0",
12
- "@corenel/config": "0.6.0",
13
- "@corenel/mcp": "0.4.0",
14
- "@corenel/protocol": "0.12.0",
15
- "@corenel/harness": "0.13.0",
16
- "@corenel/term": "0.7.0"
12
+ "@corenel/config": "0.6.1",
13
+ "@corenel/harness": "0.14.0",
14
+ "@corenel/mcp": "0.5.0",
15
+ "@corenel/protocol": "0.13.0",
16
+ "@corenel/term": "0.8.0"
17
17
  },
18
18
  "devDependencies": {
19
19
  "esbuild": "^0.21.5",
@@ -22,7 +22,7 @@
22
22
  "tsx": "^4.19.2",
23
23
  "typescript": "^5.9.3",
24
24
  "@corenel/crew": "0.0.0",
25
- "@corenel/tools-node": "0.7.0"
25
+ "@corenel/tools-node": "0.7.1"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"