@corenel/cli 0.5.0 → 0.6.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 (2) hide show
  1. package/dist/main.js +257 -188
  2. package/package.json +7 -7
package/dist/main.js CHANGED
@@ -18404,6 +18404,25 @@ ${firstUserMsg.content}`;
18404
18404
  }
18405
18405
  });
18406
18406
 
18407
+ // ../tools-node/toolGuidance.ts
18408
+ function nodeToolGuidance(toolNames) {
18409
+ const has = new Set(toolNames);
18410
+ const parts = [];
18411
+ if (has.has(SHELL_TOOL)) parts.push(SHELL);
18412
+ if (PROC_TOOLS.some((t) => has.has(t))) parts.push(PROCS);
18413
+ return parts.join("\n\n");
18414
+ }
18415
+ var SHELL_TOOL, PROC_TOOLS, SHELL, PROCS;
18416
+ var init_toolGuidance = __esm({
18417
+ "../tools-node/toolGuidance.ts"() {
18418
+ "use strict";
18419
+ SHELL_TOOL = "shell";
18420
+ PROC_TOOLS = ["proc_start", "proc_stop", "proc_list"];
18421
+ SHELL = `Running a command is often the shortest correct answer: use \`${SHELL_TOOL}\` for anything a command already does well -- listing, searching, git, package managers, build and test runners -- instead of reconstructing it from file reads. It waits for the command to finish and returns its output, so it is the wrong tool for anything that does not exit on its own.`;
18422
+ PROCS = `A command that keeps running (a dev server, a watcher, a queue worker) belongs to \`${PROC_TOOLS[0]}\`, not \`${SHELL_TOOL}\` -- starting one with \`${SHELL_TOOL}\` hangs the turn until it is killed. Use \`${PROC_TOOLS[2]}\` to see what is already running before starting anything, and \`${PROC_TOOLS[1]}\` to stop what you started. Do not stop a process you did not start.`;
18423
+ }
18424
+ });
18425
+
18407
18426
  // ../tools-node/promptHost.ts
18408
18427
  import { normalizeFilePath as normalizeFilePath2, flattenFiles as flattenFiles2 } from "@corenel/protocol";
18409
18428
  function backend(svc) {
@@ -18462,8 +18481,22 @@ function nodePromptHost(workspace, project) {
18462
18481
  sources[p] = liveSrc;
18463
18482
  return compile("", values, new HybridFileSystem(sources, backend(workspace)), "/" + p);
18464
18483
  },
18465
- siteIndex: () => ""
18484
+ siteIndex: () => "",
18466
18485
  // no site index in node (the help chat is browser-only)
18486
+ /* Who the agent says it IS. Without this, `composeAgentSystem` falls back
18487
+ * to its `'Prompd'` default and the `corenel` binary's agent introduced
18488
+ * itself as "the Prompd assistant" -- the same leak already fixed for the
18489
+ * browser help chat (composeHelpSystem) and missed here, because the node
18490
+ * host is the one PromptHost that never implemented the hook.
18491
+ *
18492
+ * Constant, not a parameter: every node consumer of this host (the CLI and
18493
+ * the sidecar) IS Corenel. A host that is not should implement its own. */
18494
+ appName: () => "Corenel",
18495
+ /* The product's guidance for the tools this run actually has -- the shell,
18496
+ * long-running processes, and deferred MCP tools. Gated per tool inside
18497
+ * nodeToolGuidance, because this same host serves the sidecar, whose shell
18498
+ * is behind --allow-shell. */
18499
+ toolGuidance: (names) => nodeToolGuidance(names)
18467
18500
  };
18468
18501
  }
18469
18502
  var parser, compiler;
@@ -18471,6 +18504,7 @@ var init_promptHost = __esm({
18471
18504
  "../tools-node/promptHost.ts"() {
18472
18505
  "use strict";
18473
18506
  init_dist();
18507
+ init_toolGuidance();
18474
18508
  parser = new PrompdParser();
18475
18509
  compiler = new PrompdCompiler();
18476
18510
  }
@@ -22305,7 +22339,7 @@ var require_websocket_server = __commonJS({
22305
22339
  init_tools_node();
22306
22340
  import { runAgent as runAgent2 } from "@corenel/harness/core/loop";
22307
22341
  import { createChatClient as createChatClient2, setGatewayBase as setGatewayBase2 } from "@corenel/harness/providers/gateway";
22308
- import { resolveContextWindow as resolveContextWindow2 } from "@corenel/harness/providers/contextWindow";
22342
+ import { resolveContextWindow as resolveContextWindow2, resolveContextWindowWithHydration } from "@corenel/harness/providers/contextWindow";
22309
22343
  import { allTools as allTools3 } from "@corenel/harness/tools/builtins";
22310
22344
  import { registerGuard as registerGuard2 } from "@corenel/harness/guards";
22311
22345
  import { setPromptHost } from "@corenel/harness/prompts/host";
@@ -22355,8 +22389,8 @@ function inMemoryMemory() {
22355
22389
  async forget(name) {
22356
22390
  items.delete(name);
22357
22391
  }
22358
- // No core(): this stub has no tiers, and Task 9 injects nothing rather than
22359
- // pretending it has a memory block.
22392
+ // No core(): this stub has no tiers, and compose injects nothing rather
22393
+ // than pretending it has a memory block.
22360
22394
  };
22361
22395
  }
22362
22396
  function inMemoryKv() {
@@ -22416,168 +22450,6 @@ function nodeToolCtx(signal, opts = {}) {
22416
22450
  return { signal, ...base, ...spawn4 ? { spawn: spawn4 } : {} };
22417
22451
  }
22418
22452
 
22419
- // src/tokenGuard.ts
22420
- function tokenGuard(getToken) {
22421
- return {
22422
- id: "token",
22423
- appliesTo: (f) => f === "llm-execution",
22424
- check: () => getToken() ? { allowed: true } : { allowed: false, guardId: "token", reason: "no CORENEL_TOKEN \u2014 run `corenel login`", action: { kind: "sign-in" } }
22425
- };
22426
- }
22427
-
22428
- // src/login.ts
22429
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
22430
- var MIN_POLL_MS = 1e3;
22431
- var DEFAULT_LIFETIME_S = 900;
22432
- var ACCOUNT_LOOKUP_MS = 5e3;
22433
- function deadlineSignal(ms, outer) {
22434
- const ac = new AbortController();
22435
- const onOuter = () => ac.abort();
22436
- const timer = setTimeout(() => ac.abort(), ms);
22437
- if (outer) {
22438
- if (outer.aborted) ac.abort();
22439
- else outer.addEventListener("abort", onOuter, { once: true });
22440
- }
22441
- return {
22442
- signal: ac.signal,
22443
- dispose: () => {
22444
- clearTimeout(timer);
22445
- outer?.removeEventListener("abort", onOuter);
22446
- }
22447
- };
22448
- }
22449
- async function whoIsThis(base, token, f, outer, deadlineMs = ACCOUNT_LOOKUP_MS) {
22450
- const { signal, dispose } = deadlineSignal(deadlineMs, outer);
22451
- try {
22452
- const res = await f(`${base}/auth/me`, {
22453
- headers: { Authorization: `Bearer ${token}` },
22454
- signal
22455
- });
22456
- if (!res.ok) return void 0;
22457
- const body = JSON.parse(await res.text());
22458
- const email = body?.user?.email;
22459
- if (typeof email !== "string") return void 0;
22460
- const trimmed = email.trim();
22461
- return trimmed ? trimmed : void 0;
22462
- } catch {
22463
- return void 0;
22464
- } finally {
22465
- dispose();
22466
- }
22467
- }
22468
- function defaultOpen(uri, code2) {
22469
- process.stdout.write(`
22470
- To authorize, open: ${uri}
22471
- and enter the code: ${code2}
22472
-
22473
- Waiting for authorization\u2026
22474
- `);
22475
- }
22476
- async function deviceLogin(opts) {
22477
- const f = opts.fetchImpl ?? fetch;
22478
- const base = opts.base.replace(/\/$/, "");
22479
- const post = async (path, body) => {
22480
- const url = `${base}${path}`;
22481
- const res = await f(url, {
22482
- method: "POST",
22483
- headers: { "Content-Type": "application/json" },
22484
- body: JSON.stringify(body),
22485
- signal: opts.signal
22486
- });
22487
- const text = await res.text();
22488
- try {
22489
- return JSON.parse(text);
22490
- } catch {
22491
- const looksLikeHtml = /^\s*</.test(text);
22492
- throw new Error(
22493
- `${url} answered ${res.status} with ${looksLikeHtml ? "an HTML page" : "a non-JSON body"}, not JSON.` + (res.status === 404 ? `
22494
- That usually means --base is wrong. The device endpoints live under /api \u2014 try --base ${base.replace(/\/api$/, "")}/api` : "")
22495
- );
22496
- }
22497
- };
22498
- const dc = await post("/oauth/device/code", { client_id: "corenel-cli", scope: "agent" });
22499
- if (!dc.device_code) throw new Error("device authorization failed");
22500
- (opts.open ?? defaultOpen)(dc.verification_uri, dc.user_code);
22501
- let interval = opts.pollMs ?? Math.max(MIN_POLL_MS, (dc.interval ?? 5) * 1e3);
22502
- const lifetimeMs = (typeof dc.expires_in === "number" && dc.expires_in > 0 ? dc.expires_in : DEFAULT_LIFETIME_S) * 1e3;
22503
- const deadline = Date.now() + lifetimeMs;
22504
- for (; ; ) {
22505
- if (opts.signal?.aborted) throw new Error("login cancelled");
22506
- if (Date.now() >= deadline) {
22507
- throw new Error(`the code ${dc.user_code} expired before it was authorized -- run \`corenel login\` again`);
22508
- }
22509
- await sleep(interval);
22510
- const tok = await post("/oauth/token", {
22511
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
22512
- device_code: dc.device_code,
22513
- client_id: "corenel-cli"
22514
- });
22515
- if (tok.access_token) {
22516
- const claimed = typeof tok.account === "string" ? tok.account.trim() : "";
22517
- const account = claimed || await whoIsThis(base, tok.access_token, f, opts.signal, opts.accountLookupMs);
22518
- await opts.store(tok.access_token, account || void 0);
22519
- return tok.access_token;
22520
- }
22521
- if (tok.error === "authorization_pending") continue;
22522
- if (tok.error === "slow_down") {
22523
- interval += 5e3;
22524
- continue;
22525
- }
22526
- throw new Error(tok.error_description || tok.error || "login failed");
22527
- }
22528
- }
22529
-
22530
- // src/runAgent.ts
22531
- import { runAgent } from "@corenel/harness/core/loop";
22532
- import { createChatClient, setGatewayBase } from "@corenel/harness/providers/gateway";
22533
-
22534
- // src/apiBase.ts
22535
- init_tools_node();
22536
- import { DEFAULT_API_BASE as DEFAULT_API_BASE2 } from "@corenel/protocol";
22537
- var InvalidApiBaseError = class extends Error {
22538
- constructor(value, why) {
22539
- super(`invalid API base ${JSON.stringify(value)} -- ${why}`);
22540
- this.value = value;
22541
- this.why = why;
22542
- this.name = "InvalidApiBaseError";
22543
- }
22544
- };
22545
- function isLoopback(hostname) {
22546
- const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
22547
- return h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".localhost");
22548
- }
22549
- var warnedCleartext = false;
22550
- async function apiBase(explicit, opts = {}) {
22551
- const resolved = await resolveApiBase({
22552
- explicit,
22553
- env: opts.env ?? process.env,
22554
- ...opts.stateDir ? { stateDir: opts.stateDir } : {}
22555
- });
22556
- const value = resolved.base;
22557
- let url;
22558
- try {
22559
- url = new URL(value);
22560
- } catch {
22561
- throw new InvalidApiBaseError(value, "it is not a URL (a base needs a scheme, e.g. https://api.corenel.ai/api)");
22562
- }
22563
- if (url.protocol !== "https:" && url.protocol !== "http:") {
22564
- throw new InvalidApiBaseError(value, `the scheme must be https or http, not ${url.protocol.replace(/:$/, "")}`);
22565
- }
22566
- if (url.protocol === "http:" && !isLoopback(url.hostname) && !warnedCleartext) {
22567
- warnedCleartext = true;
22568
- process.stderr.write(
22569
- `corenel: warning: ${url.host} is plain http, so your access token is sent in cleartext. Use https unless this host is on a private network you trust.
22570
- `
22571
- );
22572
- }
22573
- return value;
22574
- }
22575
-
22576
- // src/runAgent.ts
22577
- import { allTools as allTools2 } from "@corenel/harness/tools/builtins";
22578
- import { registerGuard } from "@corenel/harness/guards";
22579
- import { composeAgentSystem } from "@corenel/harness/prompts";
22580
-
22581
22453
  // ../crew/frontmatter.ts
22582
22454
  var import_yaml = __toESM(require_dist(), 1);
22583
22455
  var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
@@ -22944,7 +22816,181 @@ import { makeRoomTools, ROOM_TOOL_NAMES } from "@corenel/harness/tools/roomTools
22944
22816
  import { describeParticipants, resolveAddressee } from "@corenel/harness/tools/roomTools";
22945
22817
  import { planFindingEdit, planFindingEdits } from "@corenel/harness/rooms/findings";
22946
22818
 
22819
+ // src/operatorMemory.ts
22820
+ init_tools_node();
22821
+ var OPERATOR_MEMORY_DIR = "memory";
22822
+ function operatorMemory() {
22823
+ const files = new NodeFileService(configWorkspaceRoot());
22824
+ return makeAgentMemoryProvider(() => files, OPERATOR_MEMORY_DIR);
22825
+ }
22826
+ function operatorMemoryIndex(memory) {
22827
+ return async () => {
22828
+ const items = await memory.list().catch(() => []);
22829
+ return items.map((i) => `- **${i.name}** \u2014 ${i.description || "(no description)"}`).join("\n");
22830
+ };
22831
+ }
22832
+
22833
+ // src/tokenGuard.ts
22834
+ function tokenGuard(getToken) {
22835
+ return {
22836
+ id: "token",
22837
+ appliesTo: (f) => f === "llm-execution",
22838
+ check: () => getToken() ? { allowed: true } : { allowed: false, guardId: "token", reason: "no CORENEL_TOKEN \u2014 run `corenel login`", action: { kind: "sign-in" } }
22839
+ };
22840
+ }
22841
+
22842
+ // src/login.ts
22843
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
22844
+ var MIN_POLL_MS = 1e3;
22845
+ var DEFAULT_LIFETIME_S = 900;
22846
+ var ACCOUNT_LOOKUP_MS = 5e3;
22847
+ function deadlineSignal(ms, outer) {
22848
+ const ac = new AbortController();
22849
+ const onOuter = () => ac.abort();
22850
+ const timer = setTimeout(() => ac.abort(), ms);
22851
+ if (outer) {
22852
+ if (outer.aborted) ac.abort();
22853
+ else outer.addEventListener("abort", onOuter, { once: true });
22854
+ }
22855
+ return {
22856
+ signal: ac.signal,
22857
+ dispose: () => {
22858
+ clearTimeout(timer);
22859
+ outer?.removeEventListener("abort", onOuter);
22860
+ }
22861
+ };
22862
+ }
22863
+ async function whoIsThis(base, token, f, outer, deadlineMs = ACCOUNT_LOOKUP_MS) {
22864
+ const { signal, dispose } = deadlineSignal(deadlineMs, outer);
22865
+ try {
22866
+ const res = await f(`${base}/auth/me`, {
22867
+ headers: { Authorization: `Bearer ${token}` },
22868
+ signal
22869
+ });
22870
+ if (!res.ok) return void 0;
22871
+ const body = JSON.parse(await res.text());
22872
+ const email = body?.user?.email;
22873
+ if (typeof email !== "string") return void 0;
22874
+ const trimmed = email.trim();
22875
+ return trimmed ? trimmed : void 0;
22876
+ } catch {
22877
+ return void 0;
22878
+ } finally {
22879
+ dispose();
22880
+ }
22881
+ }
22882
+ function defaultOpen(uri, code2) {
22883
+ process.stdout.write(`
22884
+ To authorize, open: ${uri}
22885
+ and enter the code: ${code2}
22886
+
22887
+ Waiting for authorization\u2026
22888
+ `);
22889
+ }
22890
+ async function deviceLogin(opts) {
22891
+ const f = opts.fetchImpl ?? fetch;
22892
+ const base = opts.base.replace(/\/$/, "");
22893
+ const post = async (path, body) => {
22894
+ const url = `${base}${path}`;
22895
+ const res = await f(url, {
22896
+ method: "POST",
22897
+ headers: { "Content-Type": "application/json" },
22898
+ body: JSON.stringify(body),
22899
+ signal: opts.signal
22900
+ });
22901
+ const text = await res.text();
22902
+ try {
22903
+ return JSON.parse(text);
22904
+ } catch {
22905
+ const looksLikeHtml = /^\s*</.test(text);
22906
+ throw new Error(
22907
+ `${url} answered ${res.status} with ${looksLikeHtml ? "an HTML page" : "a non-JSON body"}, not JSON.` + (res.status === 404 ? `
22908
+ That usually means --base is wrong. The device endpoints live under /api \u2014 try --base ${base.replace(/\/api$/, "")}/api` : "")
22909
+ );
22910
+ }
22911
+ };
22912
+ const dc = await post("/oauth/device/code", { client_id: "corenel-cli", scope: "agent" });
22913
+ if (!dc.device_code) throw new Error("device authorization failed");
22914
+ (opts.open ?? defaultOpen)(dc.verification_uri, dc.user_code);
22915
+ let interval = opts.pollMs ?? Math.max(MIN_POLL_MS, (dc.interval ?? 5) * 1e3);
22916
+ const lifetimeMs = (typeof dc.expires_in === "number" && dc.expires_in > 0 ? dc.expires_in : DEFAULT_LIFETIME_S) * 1e3;
22917
+ const deadline = Date.now() + lifetimeMs;
22918
+ for (; ; ) {
22919
+ if (opts.signal?.aborted) throw new Error("login cancelled");
22920
+ if (Date.now() >= deadline) {
22921
+ throw new Error(`the code ${dc.user_code} expired before it was authorized -- run \`corenel login\` again`);
22922
+ }
22923
+ await sleep(interval);
22924
+ const tok = await post("/oauth/token", {
22925
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
22926
+ device_code: dc.device_code,
22927
+ client_id: "corenel-cli"
22928
+ });
22929
+ if (tok.access_token) {
22930
+ const claimed = typeof tok.account === "string" ? tok.account.trim() : "";
22931
+ const account = claimed || await whoIsThis(base, tok.access_token, f, opts.signal, opts.accountLookupMs);
22932
+ await opts.store(tok.access_token, account || void 0);
22933
+ return tok.access_token;
22934
+ }
22935
+ if (tok.error === "authorization_pending") continue;
22936
+ if (tok.error === "slow_down") {
22937
+ interval += 5e3;
22938
+ continue;
22939
+ }
22940
+ throw new Error(tok.error_description || tok.error || "login failed");
22941
+ }
22942
+ }
22943
+
22944
+ // src/runAgent.ts
22945
+ import { runAgent } from "@corenel/harness/core/loop";
22946
+ import { createChatClient, setGatewayBase } from "@corenel/harness/providers/gateway";
22947
+
22948
+ // src/apiBase.ts
22949
+ init_tools_node();
22950
+ import { DEFAULT_API_BASE as DEFAULT_API_BASE2 } from "@corenel/protocol";
22951
+ var InvalidApiBaseError = class extends Error {
22952
+ constructor(value, why) {
22953
+ super(`invalid API base ${JSON.stringify(value)} -- ${why}`);
22954
+ this.value = value;
22955
+ this.why = why;
22956
+ this.name = "InvalidApiBaseError";
22957
+ }
22958
+ };
22959
+ function isLoopback(hostname) {
22960
+ const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
22961
+ return h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".localhost");
22962
+ }
22963
+ var warnedCleartext = false;
22964
+ async function apiBase(explicit, opts = {}) {
22965
+ const resolved = await resolveApiBase({
22966
+ explicit,
22967
+ env: opts.env ?? process.env,
22968
+ ...opts.stateDir ? { stateDir: opts.stateDir } : {}
22969
+ });
22970
+ const value = resolved.base;
22971
+ let url;
22972
+ try {
22973
+ url = new URL(value);
22974
+ } catch {
22975
+ throw new InvalidApiBaseError(value, "it is not a URL (a base needs a scheme, e.g. https://api.corenel.ai/api)");
22976
+ }
22977
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
22978
+ throw new InvalidApiBaseError(value, `the scheme must be https or http, not ${url.protocol.replace(/:$/, "")}`);
22979
+ }
22980
+ if (url.protocol === "http:" && !isLoopback(url.hostname) && !warnedCleartext) {
22981
+ warnedCleartext = true;
22982
+ process.stderr.write(
22983
+ `corenel: warning: ${url.host} is plain http, so your access token is sent in cleartext. Use https unless this host is on a private network you trust.
22984
+ `
22985
+ );
22986
+ }
22987
+ return value;
22988
+ }
22989
+
22947
22990
  // src/runAgent.ts
22991
+ import { allTools as allTools2 } from "@corenel/harness/tools/builtins";
22992
+ import { registerGuard } from "@corenel/harness/guards";
22993
+ import { composeAgentSystem } from "@corenel/harness/prompts";
22948
22994
  init_tools_node();
22949
22995
  import { stateDirName as stateDirName3 } from "@corenel/protocol";
22950
22996
  import { PermissionService as PermissionService2 } from "@corenel/harness/guardrail/permission-service";
@@ -24275,7 +24321,7 @@ function trim(v) {
24275
24321
  }
24276
24322
 
24277
24323
  // src/cli.ts
24278
- import { hydratePrices } from "@corenel/harness/core/pricing";
24324
+ import { hydratePrices, hydrateCatalog } from "@corenel/harness/core/pricing";
24279
24325
 
24280
24326
  // src/modelCatalog.ts
24281
24327
  function modelCatalogEntries(table) {
@@ -25796,9 +25842,22 @@ async function agentSetup(argv) {
25796
25842
  }
25797
25843
  setPromptHost(nodePromptHost(new NodeFileService(configWorkspaceRoot()), new NodeFileService(process.cwd())));
25798
25844
  const identity = createIdentityState();
25845
+ const memory = operatorMemory();
25799
25846
  let system = "You are corenel, a concise CLI assistant.";
25800
25847
  try {
25801
- system = await composeAgentSystem3({ mode: identity.mode, tools: tools.map((t) => ({ name: t.name, description: t.description })) });
25848
+ system = await composeAgentSystem3({
25849
+ mode: identity.mode,
25850
+ tools: tools.map((t) => ({ name: t.name, description: t.description })),
25851
+ // What this face knows that the model cannot infer. `process.cwd()` and
25852
+ // not the `--root` union: this is where the shell tool runs and where a
25853
+ // relative path resolves, which is the question the facts answer.
25854
+ runFacts: [
25855
+ "Surface: terminal",
25856
+ `Working directory: ${process.cwd()}`,
25857
+ `Platform: ${process.platform}`
25858
+ ],
25859
+ memoryIndex: operatorMemoryIndex(memory)
25860
+ });
25802
25861
  } catch {
25803
25862
  }
25804
25863
  let policy;
@@ -25822,6 +25881,7 @@ async function agentSetup(argv) {
25822
25881
  identity,
25823
25882
  policy,
25824
25883
  policySource,
25884
+ memory,
25825
25885
  mcp,
25826
25886
  disconnect: () => sidecarProcs?.disconnect()
25827
25887
  };
@@ -25842,7 +25902,7 @@ async function run(argv) {
25842
25902
  process.exitCode = ext ? await runExtension(ext, ["run", session, prompt]) : 2;
25843
25903
  return;
25844
25904
  }
25845
- const { client, model, system, tools, policy, disconnect } = await agentSetup(argv);
25905
+ const { client, model, system, tools, policy, memory, disconnect } = await agentSetup(argv);
25846
25906
  try {
25847
25907
  await runOnce();
25848
25908
  } finally {
@@ -25915,7 +25975,8 @@ async function run(argv) {
25915
25975
  // looser, so it can't be used to route around the default-deny above.
25916
25976
  toolCtx: nodeToolCtx(ac.signal, {
25917
25977
  files: new NodeFileService(process.cwd()),
25918
- spawn: { tools, client, defaultModel: model, permission }
25978
+ spawn: { tools, client, defaultModel: model, permission },
25979
+ memory
25919
25980
  }),
25920
25981
  signal: ac.signal,
25921
25982
  // The meter is fed from the SAME event stream, one `usage` per model call.
@@ -25943,7 +26004,7 @@ async function run(argv) {
25943
26004
  }
25944
26005
  }
25945
26006
  async function interactive(argv) {
25946
- const { client, model, tools, identity, policy, policySource, mcp, disconnect } = await agentSetup(argv);
26007
+ const { client, model, tools, identity, policy, policySource, memory, mcp, disconnect } = await agentSetup(argv);
25947
26008
  try {
25948
26009
  await runInteractive();
25949
26010
  } finally {
@@ -25954,10 +26015,18 @@ async function interactive(argv) {
25954
26015
  const cwd = process.cwd();
25955
26016
  let modelRates = null;
25956
26017
  let priceTable = /* @__PURE__ */ new Map();
26018
+ const resolvedWindow = resolveContextWindow2(model);
26019
+ alog("context window", model, resolvedWindow.window, `(${resolvedWindow.source})`);
26020
+ const compression = { mode: "balanced", contextWindow: resolvedWindow.window, strategy: "deferred" };
25957
26021
  const priceBase = await apiBase(flag(argv, "base", ""));
25958
- void hydratePrices(priceBase, nodeAuthToken().getToken).then((table) => {
25959
- priceTable = table;
25960
- modelRates = table.get(model) ?? null;
26022
+ void hydrateCatalog(priceBase, nodeAuthToken().getToken).then(({ prices, windows }) => {
26023
+ priceTable = prices;
26024
+ modelRates = prices.get(model) ?? null;
26025
+ const rehydrated = resolveContextWindowWithHydration(model, windows);
26026
+ if (rehydrated.window !== compression.contextWindow) {
26027
+ compression.contextWindow = rehydrated.window;
26028
+ alog("context window", model, rehydrated.window, `(${rehydrated.source})`);
26029
+ }
25961
26030
  }).catch(() => {
25962
26031
  });
25963
26032
  const files = new NodeFileService(cwd);
@@ -26014,9 +26083,6 @@ async function interactive(argv) {
26014
26083
  const onSessionId = (id) => {
26015
26084
  currentSessionId = id;
26016
26085
  };
26017
- const resolvedWindow = resolveContextWindow2(model);
26018
- alog("context window", model, resolvedWindow.window, `(${resolvedWindow.source})`);
26019
- const compression = { mode: "balanced", contextWindow: resolvedWindow.window, strategy: "deferred" };
26020
26086
  const compaction = {
26021
26087
  summarize: makeSummarizer(client, model),
26022
26088
  onCheckpoint: (body) => makeCheckpointStore(() => files, sessionsRoot()).append(currentSessionId, body)
@@ -26030,13 +26096,14 @@ async function interactive(argv) {
26030
26096
  model,
26031
26097
  rates: () => modelRates,
26032
26098
  // `/context`'s fill fraction. A getter for the reason
26033
- // TermSessionDeps.contextWindow's field doc gives -- the model is
26034
- // fixed for a session, so this returns the same number every call
26035
- // today, but pulling it from `resolvedWindow` (computed once, right
26036
- // above, from the SAME resolveContextWindow(model) call compaction
26037
- // itself is sized from) rather than a captured literal keeps it that
26038
- // way as a consequence of the wiring, not an assumption baked in here.
26039
- contextWindow: () => resolvedWindow.window,
26099
+ // TermSessionDeps.contextWindow's field doc gives -- and, unlike a
26100
+ // captured literal, it reads `compression.contextWindow` LIVE: that
26101
+ // is the same object the price/window hydration above mutates in
26102
+ // place once the backend answers, so `/context` reports the
26103
+ // hydrated window from the very next turn on, not just what was
26104
+ // known synchronously at launch. Same object compaction itself is
26105
+ // sized from (`compression`, above), so the two can never disagree.
26106
+ contextWindow: () => compression.contextWindow ?? resolvedWindow.window,
26040
26107
  system: systemSource,
26041
26108
  client,
26042
26109
  tools,
@@ -26052,7 +26119,8 @@ async function interactive(argv) {
26052
26119
  files,
26053
26120
  ask,
26054
26121
  spawn: { tools, client, defaultModel: model, permission },
26055
- recall
26122
+ recall,
26123
+ memory
26056
26124
  }),
26057
26125
  ...approveAll ? { approveAll: true } : {},
26058
26126
  policy,
@@ -26078,8 +26146,8 @@ async function interactive(argv) {
26078
26146
  client,
26079
26147
  model,
26080
26148
  rates: () => modelRates,
26081
- // See the TUI branch above -- same getter, same `resolvedWindow`.
26082
- contextWindow: () => resolvedWindow.window,
26149
+ // See the TUI branch above -- same getter, same `compression` object.
26150
+ contextWindow: () => compression.contextWindow ?? resolvedWindow.window,
26083
26151
  system: systemSource,
26084
26152
  tools,
26085
26153
  // The readline face attaches its own PermissionService too (repl.ts) and
@@ -26090,7 +26158,8 @@ async function interactive(argv) {
26090
26158
  files,
26091
26159
  ...ask ? { ask } : {},
26092
26160
  spawn: { tools, client, defaultModel: model, permission },
26093
- recall
26161
+ recall,
26162
+ memory
26094
26163
  }),
26095
26164
  cwd,
26096
26165
  // --allow-mutations (or its --allow-shell alias) skips the per-command
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corenel/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.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.1.0",
13
- "@corenel/harness": "0.8.0",
14
- "@corenel/mcp": "0.1.0",
15
- "@corenel/term": "0.3.0",
16
- "@corenel/protocol": "0.7.3"
12
+ "@corenel/config": "0.2.0",
13
+ "@corenel/harness": "0.9.0",
14
+ "@corenel/protocol": "0.8.0",
15
+ "@corenel/mcp": "0.2.0",
16
+ "@corenel/term": "0.4.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.4.0"
25
+ "@corenel/tools-node": "0.5.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"