@usabledev/usable-chat 1.147.0 → 1.148.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/cli.js +394 -71
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -23716,8 +23716,7 @@ var init_harness_prompt = __esm({
23716
23716
  </communication>
23717
23717
 
23718
23718
  <grounding>
23719
- Usable is the user's long-term memory and your first source of truth \u2014 consult it before guessing.
23720
- - Before non-trivial work, search it with \`agentic-search-fragments\`: natural-language intent (what you're trying to achieve, not keywords), scoped to the workspace; iterate until the result is sufficient. Prefer it over external search or memory.
23719
+ Usable is the user's long-term memory. When a task touches project domain, past decisions, or conventions you're unsure of, ground yourself ONCE with a single targeted \`agentic-search-fragments\` (natural-language intent \u2014 what you're trying to achieve, not keywords \u2014 scoped to the workspace), then proceed. One good search is enough: don't re-run the same search, and don't search at all for routine local work you can just do (reading/editing files, running tests, authoring an extension from the \`extension_authoring_guide\`).
23721
23720
  - Cite the fragments you relied on (title + id). When you point the user at a fragment, give the link: https://usable.dev/dashboard/workspaces/<workspaceId>/fragments/<fragmentId>.
23722
23721
  - IDs \u2014 fragment, workspace, collection \u2014 come ONLY from tool results. NEVER invent, guess, or shorten a UUID.
23723
23722
  - After work is complete AND verified, capture durable insights with create/update-memory-fragment (tag by repo; note residual risk). Don't document speculatively.
@@ -23725,7 +23724,6 @@ Usable is the user's long-term memory and your first source of truth \u2014 cons
23725
23724
 
23726
23725
  <tools>
23727
23726
  - Resolve prerequisites before acting; run independent calls in parallel.
23728
- - Don't stop at the first plausible answer if another call would cut uncertainty. If a search comes back empty or suspiciously narrow, retry with a different strategy.
23729
23727
  - \`bash\` output is truncated to a preview; the full output spills to a temp file whose path is reported. Don't pipe through \`head\`/\`tail\` to work around the truncation.
23730
23728
  - Destructive or hard-to-reverse commands require approval \u2014 surface what you're about to do first.
23731
23729
  </tools>
@@ -23740,7 +23738,7 @@ Usable is the user's long-term memory and your first source of truth \u2014 cons
23740
23738
  </completeness>
23741
23739
 
23742
23740
  <workflow>
23743
- 1. Scope \u2014 for multi-file work, research the existing code and conventions (and search Usable) before writing. Reuse existing patterns; inventing a parallel convention is prohibited.
23741
+ 1. Scope \u2014 for multi-file work, research the existing code and conventions before writing (ground once in Usable if the task touches project conventions or past decisions). Reuse existing patterns; inventing a parallel convention is prohibited. A local coding task needs no plan-mode, task-status updates, or other workflow ceremony unless the user asks for it \u2014 just do the work.
23744
23742
  2. Edit \u2014 read whole sections, not snippets. Re-read a file if it changed since you last read it. Fix problems at the source and remove obsolete code rather than leaving aliases or re-exports behind.
23745
23743
  3. Verify \u2014 for non-trivial changes, prove it with tests you can actually run (prefer real over mocks; test behavior, not plumbing). All tests green before you call it done \u2014 "it was already broken" is not a skip reason; fix it or flag it.
23746
23744
  4. Report \u2014 say what changed, the evidence (the commands you ran and their results), and anything left or risky. Be brief in prose, not in evidence.
@@ -27325,7 +27323,7 @@ var init_sql = __esm({
27325
27323
  return new SQL([new StringChunk(str)]);
27326
27324
  }
27327
27325
  sql2.raw = raw;
27328
- function join20(chunks, separator) {
27326
+ function join21(chunks, separator) {
27329
27327
  const result = [];
27330
27328
  for (const [i18, chunk2] of chunks.entries()) {
27331
27329
  if (i18 > 0 && separator !== void 0) {
@@ -27335,7 +27333,7 @@ var init_sql = __esm({
27335
27333
  }
27336
27334
  return new SQL(result);
27337
27335
  }
27338
- sql2.join = join20;
27336
+ sql2.join = join21;
27339
27337
  function identifier(value) {
27340
27338
  return new Name(value);
27341
27339
  }
@@ -31722,7 +31720,7 @@ var init_select2 = __esm({
31722
31720
  const baseTableName = this.tableName;
31723
31721
  const tableName = getTableLikeName(table);
31724
31722
  for (const item of extractUsedTable(table)) this.usedTables.add(item);
31725
- if (typeof tableName === "string" && this.config.joins?.some((join20) => join20.alias === tableName)) {
31723
+ if (typeof tableName === "string" && this.config.joins?.some((join21) => join21.alias === tableName)) {
31726
31724
  throw new Error(`Alias "${tableName}" is already used in this query`);
31727
31725
  }
31728
31726
  if (!this.isPartialSelect) {
@@ -33257,7 +33255,7 @@ var init_update = __esm({
33257
33255
  createJoin(joinType) {
33258
33256
  return (table, on3) => {
33259
33257
  const tableName = getTableLikeName(table);
33260
- if (typeof tableName === "string" && this.config.joins.some((join20) => join20.alias === tableName)) {
33258
+ if (typeof tableName === "string" && this.config.joins.some((join21) => join21.alias === tableName)) {
33261
33259
  throw new Error(`Alias "${tableName}" is already used in this query`);
33262
33260
  }
33263
33261
  if (typeof on3 === "function") {
@@ -33353,10 +33351,10 @@ var init_update = __esm({
33353
33351
  const fromFields = this.getTableLikeFields(this.config.from);
33354
33352
  fields[tableName] = fromFields;
33355
33353
  }
33356
- for (const join20 of this.config.joins) {
33357
- const tableName2 = getTableLikeName(join20.table);
33358
- if (typeof tableName2 === "string" && !is(join20.table, SQL)) {
33359
- const fromFields = this.getTableLikeFields(join20.table);
33354
+ for (const join21 of this.config.joins) {
33355
+ const tableName2 = getTableLikeName(join21.table);
33356
+ if (typeof tableName2 === "string" && !is(join21.table, SQL)) {
33357
+ const fromFields = this.getTableLikeFields(join21.table);
33360
33358
  fields[tableName2] = fromFields;
33361
33359
  }
33362
33360
  }
@@ -84938,8 +84936,8 @@ var init_bedrock_sdk = __esm({
84938
84936
  });
84939
84937
 
84940
84938
  // src/lib/logger.ts
84941
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync8 } from "fs";
84942
- import { join as join10 } from "path";
84939
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync9 } from "fs";
84940
+ import { join as join11 } from "path";
84943
84941
  var LOG_LEVEL_PRIORITY, Logger, logger2, orchestrationLogger, streamLogger;
84944
84942
  var init_logger2 = __esm({
84945
84943
  "src/lib/logger.ts"() {
@@ -84952,7 +84950,7 @@ var init_logger2 = __esm({
84952
84950
  };
84953
84951
  Logger = class {
84954
84952
  constructor() {
84955
- this.logDir = join10(process.cwd(), "logs");
84953
+ this.logDir = join11(process.cwd(), "logs");
84956
84954
  this.enableFileLogging = process.env.ENABLE_FILE_LOGGING === "true";
84957
84955
  this.isDevelopment = process.env.NODE_ENV === "development";
84958
84956
  this.enableConsoleLogging = this.isDevelopment || process.env.ENABLE_CONSOLE_LOGGING === "true";
@@ -84960,7 +84958,7 @@ var init_logger2 = __esm({
84960
84958
  this.minLogLevel = ["debug", "info", "warn", "error"].includes(configuredLevel) ? configuredLevel : "info";
84961
84959
  if (this.enableFileLogging) {
84962
84960
  try {
84963
- mkdirSync8(this.logDir, { recursive: true });
84961
+ mkdirSync9(this.logDir, { recursive: true });
84964
84962
  } catch (error41) {
84965
84963
  console.error("Failed to create logs directory:", error41);
84966
84964
  this.enableFileLogging = false;
@@ -85042,7 +85040,7 @@ var init_logger2 = __esm({
85042
85040
  logToFile(entry) {
85043
85041
  try {
85044
85042
  const logFileName = this.getLogFileName(entry.category);
85045
- const logPath = join10(this.logDir, logFileName);
85043
+ const logPath = join11(this.logDir, logFileName);
85046
85044
  const logLine = JSON.stringify(entry) + "\n";
85047
85045
  appendFileSync3(logPath, logLine, "utf-8");
85048
85046
  } catch (error41) {
@@ -215465,14 +215463,14 @@ var require_turndown_cjs = __commonJS({
215465
215463
  } else if (node.nodeType === 1) {
215466
215464
  replacement = replacementForNode.call(self2, node);
215467
215465
  }
215468
- return join20(output, replacement);
215466
+ return join21(output, replacement);
215469
215467
  }, "");
215470
215468
  }
215471
215469
  function postProcess(output) {
215472
215470
  var self2 = this;
215473
215471
  this.rules.forEach(function(rule) {
215474
215472
  if (typeof rule.append === "function") {
215475
- output = join20(output, rule.append(self2.options));
215473
+ output = join21(output, rule.append(self2.options));
215476
215474
  }
215477
215475
  });
215478
215476
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -215484,7 +215482,7 @@ var require_turndown_cjs = __commonJS({
215484
215482
  if (whitespace.leading || whitespace.trailing) content = content.trim();
215485
215483
  return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
215486
215484
  }
215487
- function join20(output, replacement) {
215485
+ function join21(output, replacement) {
215488
215486
  var s17 = trimTrailingNewlines(output);
215489
215487
  var s22 = trimLeadingNewlines(replacement);
215490
215488
  var nls = Math.max(output.length - s17.length, replacement.length - s22.length);
@@ -244534,12 +244532,12 @@ var init_session_pathway = __esm({
244534
244532
  });
244535
244533
 
244536
244534
  // src/adapters/cli/prompt-files.ts
244537
- import { existsSync as existsSync9, readFileSync as readFileSync6, realpathSync as realpathSync4 } from "node:fs";
244538
- import { dirname as dirname14, join as join15 } from "node:path";
244535
+ import { existsSync as existsSync10, readFileSync as readFileSync7, realpathSync as realpathSync4 } from "node:fs";
244536
+ import { dirname as dirname14, join as join16 } from "node:path";
244539
244537
  function readFile(path11) {
244540
244538
  try {
244541
- if (!existsSync9(path11)) return void 0;
244542
- const content = readFileSync6(path11, "utf8").trim();
244539
+ if (!existsSync10(path11)) return void 0;
244540
+ const content = readFileSync7(path11, "utf8").trim();
244543
244541
  return content || void 0;
244544
244542
  } catch {
244545
244543
  return void 0;
@@ -244548,7 +244546,7 @@ function readFile(path11) {
244548
244546
  function gitRoot(start) {
244549
244547
  let dir = start;
244550
244548
  while (true) {
244551
- if (existsSync9(join15(dir, ".git"))) return dir;
244549
+ if (existsSync10(join16(dir, ".git"))) return dir;
244552
244550
  const parent = dirname14(dir);
244553
244551
  if (parent === dir) return start;
244554
244552
  dir = parent;
@@ -244566,16 +244564,16 @@ function projectChain(cwd) {
244566
244564
  return chain;
244567
244565
  }
244568
244566
  function resolvePromptFiles(cwd, dataDir2) {
244569
- const override = readFile(join15(cwd, ".usable", "SYSTEM.md")) ?? readFile(join15(dataDir2, "SYSTEM.md"));
244567
+ const override = readFile(join16(cwd, ".usable", "SYSTEM.md")) ?? readFile(join16(dataDir2, "SYSTEM.md"));
244570
244568
  const appends = [
244571
- readFile(join15(dataDir2, "SYSTEM.append.md")),
244572
- readFile(join15(cwd, ".usable", "SYSTEM.append.md"))
244569
+ readFile(join16(dataDir2, "SYSTEM.append.md")),
244570
+ readFile(join16(cwd, ".usable", "SYSTEM.append.md"))
244573
244571
  ].filter((s17) => Boolean(s17));
244574
244572
  const append = appends.length ? appends.join("\n\n") : void 0;
244575
244573
  const candidates = [];
244576
- for (const name14 of CONTEXT_FILE_NAMES) candidates.push(join15(dataDir2, name14));
244574
+ for (const name14 of CONTEXT_FILE_NAMES) candidates.push(join16(dataDir2, name14));
244577
244575
  for (const dir of projectChain(cwd)) {
244578
- for (const name14 of CONTEXT_FILE_NAMES) candidates.push(join15(dir, name14));
244576
+ for (const name14 of CONTEXT_FILE_NAMES) candidates.push(join16(dir, name14));
244579
244577
  }
244580
244578
  const seen = /* @__PURE__ */ new Set();
244581
244579
  const contextFiles = [];
@@ -245321,12 +245319,12 @@ var init_stdio2 = __esm({
245321
245319
  });
245322
245320
 
245323
245321
  // src/adapters/cli/mcp-servers.ts
245324
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
245325
- import { join as join16 } from "node:path";
245322
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
245323
+ import { join as join17 } from "node:path";
245326
245324
  function readConfig(path11) {
245327
245325
  try {
245328
- if (!existsSync10(path11)) return {};
245329
- const parsed = configSchema.safeParse(JSON.parse(readFileSync7(path11, "utf8")));
245326
+ if (!existsSync11(path11)) return {};
245327
+ const parsed = configSchema.safeParse(JSON.parse(readFileSync8(path11, "utf8")));
245330
245328
  if (!parsed.success || !parsed.data.mcpServers) return {};
245331
245329
  const out = {};
245332
245330
  for (const [name14, spec] of Object.entries(parsed.data.mcpServers)) out[name14] = { name: name14, ...spec };
@@ -245337,9 +245335,9 @@ function readConfig(path11) {
245337
245335
  }
245338
245336
  function loadMcpServerSpecs(cwd, dataDir2) {
245339
245337
  const merged = {
245340
- ...readConfig(join16(dataDir2, "mcp.json")),
245341
- ...readConfig(join16(cwd, ".mcp.json")),
245342
- ...readConfig(join16(cwd, ".usable", "mcp.json"))
245338
+ ...readConfig(join17(dataDir2, "mcp.json")),
245339
+ ...readConfig(join17(cwd, ".mcp.json")),
245340
+ ...readConfig(join17(cwd, ".usable", "mcp.json"))
245343
245341
  };
245344
245342
  return Object.values(merged).filter((s17) => !s17.disabled && (s17.command || s17.url));
245345
245343
  }
@@ -245759,8 +245757,8 @@ var init_usable_api = __esm({
245759
245757
  });
245760
245758
 
245761
245759
  // src/adapters/cli/media-tools.ts
245762
- import { writeFileSync as writeFileSync7 } from "node:fs";
245763
- import { join as join17 } from "node:path";
245760
+ import { writeFileSync as writeFileSync8 } from "node:fs";
245761
+ import { join as join18 } from "node:path";
245764
245762
  function stamp() {
245765
245763
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
245766
245764
  }
@@ -245800,8 +245798,8 @@ function buildMediaTools(opts) {
245800
245798
  const b64 = resp.data?.[0]?.b64_json;
245801
245799
  if (!b64) return { error: "no image returned" };
245802
245800
  const buf = Buffer.from(b64, "base64");
245803
- const file2 = join17(outDir ?? process.cwd(), `usable-image-${stamp()}.png`);
245804
- writeFileSync7(file2, buf);
245801
+ const file2 = join18(outDir ?? process.cwd(), `usable-image-${stamp()}.png`);
245802
+ writeFileSync8(file2, buf);
245805
245803
  return { path: file2, bytes: buf.length };
245806
245804
  } catch (err) {
245807
245805
  return { error: err.message || "image generation failed" };
@@ -245920,7 +245918,9 @@ Notes:
245920
245918
 
245921
245919
  ## Minimal example
245922
245920
  \`\`\`ts
245923
- import { z } from 'zod'
245921
+ // Type-only import (erased at runtime) \u2014 keep the file dependency-free so it
245922
+ // loads anywhere. Use plain JSON-schema for \`parameters\`; a zod schema also
245923
+ // works, but only if your project actually has zod installed on disk.
245924
245924
  import type { ExtensionAPI } from '@/extensions/api'
245925
245925
 
245926
245926
  export default (pi: ExtensionAPI) => {
@@ -245928,7 +245928,7 @@ export default (pi: ExtensionAPI) => {
245928
245928
  pi.registerTool({
245929
245929
  name: 'word_count',
245930
245930
  description: 'Count the words in a string.',
245931
- parameters: z.object({ text: z.string() }),
245931
+ parameters: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
245932
245932
  async execute({ text }) {
245933
245933
  return { success: true, data: { words: String(text).trim().split(/\\s+/).filter(Boolean).length } }
245934
245934
  },
@@ -245953,20 +245953,113 @@ export default (pi: ExtensionAPI) => {
245953
245953
  }
245954
245954
  \`\`\`
245955
245955
 
245956
- After creating it: \`/trust\` (project only) then \`/reload\` \u2014 or restart. Verify with \`/help\` (commands) and by asking the model to call your tool.`;
245956
+ ## Verifying it loads
245957
+ - Run \`/verify-extension <name>\` \u2014 it transpiles + loads the file in isolation (via jiti) and reports syntax / load-time errors and what it registers. Do NOT run \`tsc\` on the file: path aliases (\`@/extensions/api\`) won't resolve standalone, and the \`@/extensions/api\` import is types-only (erased at runtime). \`/verify-extension\` is the supported check.
245958
+ - Then \`/trust\` (project only) + \`/reload\` \u2014 or restart. Confirm with \`/help\` (commands) and by asking the model to call your tool.`;
245959
+ }
245960
+ });
245961
+
245962
+ // src/adapters/cli/diff.ts
245963
+ function unifiedDiff(oldStr, newStr, opts = {}) {
245964
+ if (oldStr === newStr) return "";
245965
+ const context = opts.context ?? 3;
245966
+ const maxLines = opts.maxLines ?? 80;
245967
+ const a21 = oldStr === "" ? [] : oldStr.split("\n");
245968
+ const b32 = newStr === "" ? [] : newStr.split("\n");
245969
+ const n30 = a21.length;
245970
+ const m33 = b32.length;
245971
+ if (n30 * m33 > 4e6) return `(diff omitted \u2014 ${n30}\u2192${m33} lines too large to render)`;
245972
+ const lcs = Array.from({ length: n30 + 1 }, () => new Array(m33 + 1).fill(0));
245973
+ for (let i19 = n30 - 1; i19 >= 0; i19--) {
245974
+ for (let j21 = m33 - 1; j21 >= 0; j21--) {
245975
+ lcs[i19][j21] = a21[i19] === b32[j21] ? lcs[i19 + 1][j21 + 1] + 1 : Math.max(lcs[i19 + 1][j21], lcs[i19][j21 + 1]);
245976
+ }
245977
+ }
245978
+ const ops = [];
245979
+ let i18 = 0;
245980
+ let j20 = 0;
245981
+ while (i18 < n30 && j20 < m33) {
245982
+ if (a21[i18] === b32[j20]) {
245983
+ ops.push({ t: " ", line: a21[i18] });
245984
+ i18++;
245985
+ j20++;
245986
+ } else if (lcs[i18 + 1][j20] >= lcs[i18][j20 + 1]) {
245987
+ ops.push({ t: "-", line: a21[i18] });
245988
+ i18++;
245989
+ } else {
245990
+ ops.push({ t: "+", line: b32[j20] });
245991
+ j20++;
245992
+ }
245993
+ }
245994
+ while (i18 < n30) ops.push({ t: "-", line: a21[i18++] });
245995
+ while (j20 < m33) ops.push({ t: "+", line: b32[j20++] });
245996
+ const keep = new Array(ops.length).fill(false);
245997
+ ops.forEach((op2, idx) => {
245998
+ if (op2.t === " ") return;
245999
+ for (let k23 = Math.max(0, idx - context); k23 <= Math.min(ops.length - 1, idx + context); k23++) {
246000
+ keep[k23] = true;
246001
+ }
246002
+ });
246003
+ const out = [];
246004
+ let oldNo = 1;
246005
+ let newNo = 1;
246006
+ let inHunk = false;
246007
+ let hunk = [];
246008
+ let hunkOldStart = 1;
246009
+ let hunkNewStart = 1;
246010
+ const flush = () => {
246011
+ if (hunk.length) {
246012
+ out.push(`@@ -${hunkOldStart} +${hunkNewStart} @@`);
246013
+ out.push(...hunk);
246014
+ hunk = [];
246015
+ }
246016
+ inHunk = false;
246017
+ };
246018
+ for (let idx = 0; idx < ops.length; idx++) {
246019
+ const op2 = ops[idx];
246020
+ if (keep[idx]) {
246021
+ if (!inHunk) {
246022
+ inHunk = true;
246023
+ hunkOldStart = oldNo;
246024
+ hunkNewStart = newNo;
246025
+ }
246026
+ hunk.push(`${op2.t}${op2.line}`);
246027
+ } else if (inHunk) {
246028
+ flush();
246029
+ }
246030
+ if (op2.t === " ") {
246031
+ oldNo++;
246032
+ newNo++;
246033
+ } else if (op2.t === "-") {
246034
+ oldNo++;
246035
+ } else {
246036
+ newNo++;
246037
+ }
246038
+ }
246039
+ flush();
246040
+ if (out.length > maxLines) {
246041
+ const kept = out.slice(0, maxLines);
246042
+ kept.push(`\u2026 (${out.length - maxLines} more diff lines)`);
246043
+ return kept.join("\n");
246044
+ }
246045
+ return out.join("\n");
246046
+ }
246047
+ var init_diff = __esm({
246048
+ "src/adapters/cli/diff.ts"() {
246049
+ "use strict";
245957
246050
  }
245958
246051
  });
245959
246052
 
245960
246053
  // src/adapters/cli/tools.ts
245961
246054
  import { spawn as spawn6 } from "node:child_process";
245962
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
246055
+ import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "node:fs";
245963
246056
  import { tmpdir } from "node:os";
245964
- import { dirname as dirname15, isAbsolute as isAbsolute4, join as join18, relative as relative2, resolve as resolve6, sep as sep2 } from "node:path";
246057
+ import { dirname as dirname15, isAbsolute as isAbsolute4, join as join19, relative as relative2, resolve as resolve6, sep as sep2 } from "node:path";
245965
246058
  function truncate2(text2, label) {
245966
246059
  if (text2.length <= MAX_OUTPUT_CHARS) return text2;
245967
- const file2 = join18(tmpdir(), `usable-${label}-${Date.now()}.txt`);
246060
+ const file2 = join19(tmpdir(), `usable-${label}-${Date.now()}.txt`);
245968
246061
  try {
245969
- writeFileSync8(file2, text2);
246062
+ writeFileSync9(file2, text2);
245970
246063
  } catch {
245971
246064
  }
245972
246065
  return text2.slice(0, MAX_OUTPUT_CHARS) + `
@@ -246090,9 +246183,9 @@ function createLocalTools(opts = {}) {
246090
246183
  const guarded = guardPath(parsed.data.path);
246091
246184
  if (!guarded.ok) return { error: guarded.error };
246092
246185
  const path11 = guarded.path;
246093
- if (!existsSync11(path11)) return { error: `No such file: ${rel(path11)}` };
246186
+ if (!existsSync12(path11)) return { error: `No such file: ${rel(path11)}` };
246094
246187
  try {
246095
- const lines = readFileSync8(path11, "utf8").split("\n");
246188
+ const lines = readFileSync9(path11, "utf8").split("\n");
246096
246189
  const offset = Math.max(1, parsed.data.offset ?? 1);
246097
246190
  const limit = Math.max(1, parsed.data.limit ?? MAX_READ_LINES);
246098
246191
  const slice = lines.slice(offset - 1, offset - 1 + limit);
@@ -246124,10 +246217,14 @@ function createLocalTools(opts = {}) {
246124
246217
  const path11 = guarded.path;
246125
246218
  try {
246126
246219
  const dir = dirname15(path11);
246127
- if (!existsSync11(dir)) mkdirSync9(dir, { recursive: true });
246128
- writeFileSync8(path11, parsed.data.content);
246220
+ const before = existsSync12(path11) ? readFileSync9(path11, "utf8") : "";
246221
+ if (!existsSync12(dir)) mkdirSync10(dir, { recursive: true });
246222
+ writeFileSync9(path11, parsed.data.content);
246129
246223
  readFiles.add(path11);
246130
- return `Wrote ${rel(path11)} (${parsed.data.content.length} bytes)`;
246224
+ const verb = before ? "Overwrote" : "Wrote";
246225
+ const diff = unifiedDiff(before, parsed.data.content, { maxLines: 60 });
246226
+ return `${verb} ${rel(path11)} (${parsed.data.content.length} bytes)` + (diff ? `
246227
+ ${diff}` : "");
246131
246228
  } catch (e14) {
246132
246229
  return { error: e14 instanceof Error ? e14.message : String(e14) };
246133
246230
  }
@@ -246157,11 +246254,12 @@ function createLocalTools(opts = {}) {
246157
246254
  const guarded = guardPath(parsed.data.path);
246158
246255
  if (!guarded.ok) return { error: guarded.error };
246159
246256
  const path11 = guarded.path;
246160
- if (!existsSync11(path11)) return { error: `No such file: ${rel(path11)}` };
246257
+ if (!existsSync12(path11)) return { error: `No such file: ${rel(path11)}` };
246161
246258
  if (!readFiles.has(path11)) return { error: `Read ${rel(path11)} before editing it.` };
246162
246259
  const edits = parsed.data.edits;
246163
246260
  try {
246164
- let content = readFileSync8(path11, "utf8");
246261
+ const original2 = readFileSync9(path11, "utf8");
246262
+ let content = original2;
246165
246263
  for (const [i18, edit2] of edits.entries()) {
246166
246264
  const { oldText, newText } = edit2;
246167
246265
  let count2 = content.split(oldText).length - 1;
@@ -246179,8 +246277,10 @@ function createLocalTools(opts = {}) {
246179
246277
  if (count2 > 1) return { error: `edit ${i18 + 1}: oldText appears ${count2}\xD7 \u2014 add more context to make it unique` };
246180
246278
  content = content.replace(target, () => newText);
246181
246279
  }
246182
- writeFileSync8(path11, content);
246183
- return `Applied ${edits.length} edit(s) to ${rel(path11)}`;
246280
+ writeFileSync9(path11, content);
246281
+ const diff = unifiedDiff(original2, content, { maxLines: 60 });
246282
+ return `Applied ${edits.length} edit(s) to ${rel(path11)}` + (diff ? `
246283
+ ${diff}` : "");
246184
246284
  } catch (e14) {
246185
246285
  return { error: e14 instanceof Error ? e14.message : String(e14) };
246186
246286
  }
@@ -246331,6 +246431,7 @@ var init_tools = __esm({
246331
246431
  "use strict";
246332
246432
  init_zod();
246333
246433
  init_extension_authoring();
246434
+ init_diff();
246334
246435
  MAX_READ_LINES = 2e3;
246335
246436
  MAX_OUTPUT_CHARS = 3e4;
246336
246437
  DEFAULT_BASH_TIMEOUT_S = 120;
@@ -261237,13 +261338,25 @@ async function launchTui(options2) {
261237
261338
  if (res.ok) {
261238
261339
  sys(
261239
261340
  `Scaffolded ${res.path}
261240
- Edit it, then /trust (project extensions need it) and /reload to load it.`
261341
+ Edit it, then /verify-extension ${arg.trim()} to check it loads, /trust (project extensions need it) and /reload to load it.`
261241
261342
  );
261242
261343
  } else {
261243
261344
  sys(`Could not scaffold: ${res.error}`, "error");
261244
261345
  }
261245
261346
  return;
261246
261347
  }
261348
+ case "verify-extension": {
261349
+ if (!extensions) {
261350
+ sys("No extension system available.", "error");
261351
+ return;
261352
+ }
261353
+ if (!arg.trim()) {
261354
+ sys("Usage: /verify-extension <name>", "error");
261355
+ return;
261356
+ }
261357
+ sys(await extensions.verify(arg.trim()));
261358
+ return;
261359
+ }
261247
261360
  case "login":
261248
261361
  try {
261249
261362
  const res = await actions.login((m33) => sys(m33));
@@ -261387,6 +261500,7 @@ Edit it, then /trust (project extensions need it) and /reload to load it.`
261387
261500
  { name: "sessions", description: "list sessions for this project" },
261388
261501
  { name: "provider", description: "switch model provider" },
261389
261502
  { name: "new-extension", description: "scaffold a starter extension", argumentHint: "<name>" },
261503
+ { name: "verify-extension", description: "check an extension loads", argumentHint: "<name>" },
261390
261504
  { name: "new", description: "start a fresh session" },
261391
261505
  { name: "clear", description: "clear the screen" },
261392
261506
  { name: "exit", description: "quit" },
@@ -261506,6 +261620,9 @@ Edit it, then /trust (project extensions need it) and /reload to load it.`
261506
261620
  tone: "info"
261507
261621
  });
261508
261622
  }
261623
+ if (options2.updateNotice) {
261624
+ push({ kind: "system", text: options2.updateNotice, tone: "info" });
261625
+ }
261509
261626
  ui2.addChild(root);
261510
261627
  ui2.setFocus(root);
261511
261628
  ui2.start();
@@ -261539,6 +261656,7 @@ var init_tui2 = __esm({
261539
261656
  "/reasoning [lvl] set reasoning effort (low|medium|high)",
261540
261657
  "/sessions list sessions for this project",
261541
261658
  "/new-extension <n> scaffold a starter extension in .usable/extensions/",
261659
+ "/verify-extension <n> check an extension loads (no tsc needed)",
261542
261660
  "/new start a fresh session",
261543
261661
  "/clear clear the screen",
261544
261662
  "/exit, /quit quit (or Ctrl-C twice)",
@@ -261573,8 +261691,8 @@ process.emitWarning = function patchedEmitWarning(warning, ...rest) {
261573
261691
 
261574
261692
  // src/cli/index.ts
261575
261693
  import { parseArgs } from "node:util";
261576
- import { appendFileSync as appendFileSync4, existsSync as existsSync12 } from "node:fs";
261577
- import { basename as basename10, dirname as dirname16, join as join19 } from "node:path";
261694
+ import { appendFileSync as appendFileSync4, existsSync as existsSync13 } from "node:fs";
261695
+ import { basename as basename10, dirname as dirname16, join as join20 } from "node:path";
261578
261696
 
261579
261697
  // src/lib/config.cli.ts
261580
261698
  init_zod();
@@ -262149,7 +262267,7 @@ init_tui_select();
262149
262267
  init_model_registry();
262150
262268
 
262151
262269
  // package.json
262152
- var version = "1.147.0";
262270
+ var version = "1.148.0";
262153
262271
 
262154
262272
  // src/adapters/cli/model-catalog.ts
262155
262273
  init_codex_auth();
@@ -262356,17 +262474,16 @@ function normalizeExtensionName(name14) {
262356
262474
  }
262357
262475
  function template(id) {
262358
262476
  const cmd = id.replace(/-/g, "");
262359
- return `import { z } from 'zod'
262360
- import type { ExtensionAPI } from '@/extensions/api'
262477
+ return `import type { ExtensionAPI } from '@/extensions/api'
262361
262478
 
262362
262479
  // ${id} \u2014 a usable-chat harness extension.
262363
- // Edit, then run /trust (project) + /reload to load it. /help lists commands.
262480
+ // Verify it loads with /verify-extension ${id}, then /trust (project) + /reload. /help lists commands.
262364
262481
  export default (pi: ExtensionAPI) => {
262365
- // A model-callable tool.
262482
+ // A model-callable tool (plain JSON-schema params \u2014 no external deps).
262366
262483
  pi.registerTool({
262367
262484
  name: '${cmd}_echo',
262368
262485
  description: 'Echo back the given text (example tool from the ${id} extension).',
262369
- parameters: z.object({ text: z.string() }),
262486
+ parameters: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
262370
262487
  async execute({ text }) {
262371
262488
  return { success: true, data: { echo: String(text) } }
262372
262489
  },
@@ -262378,8 +262495,9 @@ export default (pi: ExtensionAPI) => {
262378
262495
  run: (arg, ctx) => ctx.ui.notify(\`${id}: \${arg || '(no arg)'}\`),
262379
262496
  })
262380
262497
 
262381
- // Lifecycle/model hooks + UI contributions (uncomment to use):
262382
- // pi.on('token_usage', (e, ctx) => ctx.ui.setStatus('tok', \`\${e.totalTokens ?? 0} tok\`))
262498
+ // Live status-bar metrics (uncomment): token count + context % for this session.
262499
+ // pi.on('token_usage', (e, ctx) => ctx.ui.setStatus('${cmd}-tok', \`\${e.totalTokens ?? 0} tok\`))
262500
+ // pi.on('context', (e, ctx) => ctx.ui.setStatus('${cmd}-ctx', e.pct != null ? \`ctx \${Math.round(e.pct)}%\` : ''))
262383
262501
  // pi.on('error', (e, ctx) => ctx.ui.notify(\`error: \${e.message}\`, 'error'))
262384
262502
  // pi.setBanner([' ${id} ']) // replace the startup ASCII banner
262385
262503
  }
@@ -262589,6 +262707,45 @@ async function loadExtensions(opts) {
262589
262707
  }
262590
262708
  return { registry: registry2, untrustedProjectDir };
262591
262709
  }
262710
+ async function verifyExtension(cwd, name14) {
262711
+ const id = name14.replace(/^\//, "").trim();
262712
+ if (!id) return { ok: false, message: "usage: /verify-extension <name>" };
262713
+ const candidates = [];
262714
+ for (const base of [projectExtensionsDir(cwd), homeExtensionsDir()]) {
262715
+ for (const e14 of ["ts", "js", "mjs"]) candidates.push(join9(base, `${id}.${e14}`));
262716
+ for (const idx of ["index.ts", "index.js", "index.mjs"]) candidates.push(join9(base, id, idx));
262717
+ }
262718
+ const file2 = candidates.find((p28) => existsSync7(p28));
262719
+ if (!file2) {
262720
+ return {
262721
+ ok: false,
262722
+ message: `extension "${id}" not found in ./.usable/extensions or ~/.usable/extensions`
262723
+ };
262724
+ }
262725
+ const jiti = createJiti(import.meta.url, { moduleCache: false, fsCache: false });
262726
+ try {
262727
+ const mod = await jiti.import(file2);
262728
+ const fn4 = typeof mod === "function" ? mod : mod.default;
262729
+ if (typeof fn4 !== "function") {
262730
+ return {
262731
+ ok: false,
262732
+ message: `${basename2(file2)}: no default export \u2014 expected \`export default (pi) => { \u2026 }\``
262733
+ };
262734
+ }
262735
+ const registry2 = createRegistry();
262736
+ await fn4(recordingApi(registry2, id));
262737
+ const hooks = registry2.toolCall.length + registry2.toolResult.length + registry2.text.length + registry2.turnStart.length + registry2.turnEnd.length + registry2.reasoning.length + registry2.stepStart.length + registry2.stepEnd.length + registry2.tokenUsage.length + registry2.context.length + registry2.error.length + registry2.autoCompaction.length + registry2.sessionStart.length + registry2.sessionNew.length + registry2.sessionSwitch.length;
262738
+ return {
262739
+ ok: true,
262740
+ message: `\u2713 ${basename2(file2)} loads cleanly \u2014 ${registry2.tools.length} tool(s), ${registry2.commands.size} command(s), ${hooks} hook(s). Run /reload to activate.`
262741
+ };
262742
+ } catch (err) {
262743
+ return {
262744
+ ok: false,
262745
+ message: `\u2717 ${basename2(file2)} failed to load: ${err instanceof Error ? err.message : String(err)}`
262746
+ };
262747
+ }
262748
+ }
262592
262749
 
262593
262750
  // src/extensions/runtime.ts
262594
262751
  init_esm();
@@ -262698,6 +262855,153 @@ function buildExtensions(registry2, opts) {
262698
262855
  };
262699
262856
  }
262700
262857
 
262858
+ // src/adapters/cli/update-check.ts
262859
+ import { spawn as nodeSpawn } from "node:child_process";
262860
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "node:fs";
262861
+ import { join as join10 } from "node:path";
262862
+ var PKG_NAME = "@usabledev/usable-chat";
262863
+ var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
262864
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
262865
+ var FETCH_TIMEOUT_MS = 3e3;
262866
+ function parseVersion(version4) {
262867
+ const m33 = version4.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
262868
+ if (!m33) return void 0;
262869
+ return { major: +m33[1], minor: +m33[2], patch: +m33[3], prerelease: m33[4] };
262870
+ }
262871
+ function compareVersions(a21, b32) {
262872
+ const l13 = parseVersion(a21);
262873
+ const r17 = parseVersion(b32);
262874
+ if (!l13 || !r17) return void 0;
262875
+ if (l13.major !== r17.major) return l13.major - r17.major;
262876
+ if (l13.minor !== r17.minor) return l13.minor - r17.minor;
262877
+ if (l13.patch !== r17.patch) return l13.patch - r17.patch;
262878
+ if (l13.prerelease === r17.prerelease) return 0;
262879
+ if (!l13.prerelease) return 1;
262880
+ if (!r17.prerelease) return -1;
262881
+ return l13.prerelease.localeCompare(r17.prerelease);
262882
+ }
262883
+ function isNewerVersion(candidate, current) {
262884
+ const c21 = compareVersions(candidate, current);
262885
+ return c21 !== void 0 ? c21 > 0 : candidate.trim() !== current.trim();
262886
+ }
262887
+ function updateCheckEnabled() {
262888
+ if (process.env.USABLE_SKIP_UPDATE_CHECK || process.env.NO_UPDATE_NOTIFIER) return false;
262889
+ if (process.env.CI) return false;
262890
+ if (process.env.E2E_ALLOW_TEST_SEAMS === "true") return false;
262891
+ return true;
262892
+ }
262893
+ function statePath(dataDir2) {
262894
+ return join10(dataDir2, "update-check.json");
262895
+ }
262896
+ function readState(dataDir2) {
262897
+ const p28 = statePath(dataDir2);
262898
+ if (!existsSync8(p28)) return null;
262899
+ try {
262900
+ const s17 = JSON.parse(readFileSync6(p28, "utf8"));
262901
+ if (typeof s17.lastCheckAt === "number" && typeof s17.latestVersion === "string") {
262902
+ return { lastCheckAt: s17.lastCheckAt, latestVersion: s17.latestVersion };
262903
+ }
262904
+ } catch {
262905
+ }
262906
+ return null;
262907
+ }
262908
+ function writeState(dataDir2, state2) {
262909
+ try {
262910
+ if (!existsSync8(dataDir2)) mkdirSync8(dataDir2, { recursive: true });
262911
+ writeFileSync7(statePath(dataDir2), JSON.stringify(state2, null, 2), { mode: 384 });
262912
+ } catch {
262913
+ }
262914
+ }
262915
+ async function fetchLatestVersion(timeoutMs = FETCH_TIMEOUT_MS) {
262916
+ try {
262917
+ const res = await fetch(REGISTRY_URL, {
262918
+ signal: AbortSignal.timeout(timeoutMs),
262919
+ headers: { accept: "application/json" }
262920
+ });
262921
+ if (!res.ok) return null;
262922
+ const json3 = await res.json();
262923
+ return typeof json3.version === "string" ? json3.version : null;
262924
+ } catch {
262925
+ return null;
262926
+ }
262927
+ }
262928
+ function formatUpdateNotice(current, latest) {
262929
+ return `\u24D8 update available ${current} \u2192 ${latest} \xB7 run \`usable-chat update\``;
262930
+ }
262931
+ function getUpdateNotice(opts) {
262932
+ if (!updateCheckEnabled()) return null;
262933
+ const state2 = readState(opts.dataDir);
262934
+ if (state2 && isNewerVersion(state2.latestVersion, opts.currentVersion)) {
262935
+ return formatUpdateNotice(opts.currentVersion, state2.latestVersion);
262936
+ }
262937
+ return null;
262938
+ }
262939
+ async function maybeRefreshUpdateState(opts) {
262940
+ if (!updateCheckEnabled()) return;
262941
+ const now2 = opts.now ?? Date.now();
262942
+ const state2 = readState(opts.dataDir);
262943
+ if (state2 && now2 - state2.lastCheckAt < CHECK_INTERVAL_MS) return;
262944
+ const latest = await fetchLatestVersion();
262945
+ writeState(opts.dataDir, {
262946
+ lastCheckAt: now2,
262947
+ latestVersion: latest ?? state2?.latestVersion ?? "0.0.0"
262948
+ });
262949
+ }
262950
+ function refreshUpdateStateInBackground(opts) {
262951
+ void maybeRefreshUpdateState(opts).catch(() => {
262952
+ });
262953
+ }
262954
+ function detectInstall(entry = process.argv[1] ?? "") {
262955
+ const p28 = entry.replace(/\\/g, "/");
262956
+ if (p28.includes("/_npx/")) return "npx";
262957
+ if (process.env.npm_config_global === "true") return "global";
262958
+ if (p28.includes("/lib/node_modules/") || p28.includes("/pnpm/global/") || p28.includes("/pnpm-global/"))
262959
+ return "global";
262960
+ if (p28.includes("/node_modules/")) return "local";
262961
+ return "unknown";
262962
+ }
262963
+ async function runUpdateCommand(opts) {
262964
+ const kind = opts?.kind ?? detectInstall(opts?.entry);
262965
+ const out = (s17) => process.stdout.write(s17 + "\n");
262966
+ const err = (s17) => process.stderr.write(s17 + "\n");
262967
+ if (kind === "npx") {
262968
+ out("You're running via npx, which already fetches the latest on each run.");
262969
+ out(`To force the newest explicitly: npx ${PKG_NAME}@latest`);
262970
+ return 0;
262971
+ }
262972
+ const usePnpm = (opts?.entry ?? process.argv[1] ?? "").replace(/\\/g, "/").includes("/pnpm");
262973
+ const cmd = usePnpm ? "pnpm" : "npm";
262974
+ const args = usePnpm ? ["add", "-g", `${PKG_NAME}@latest`] : ["install", "-g", `${PKG_NAME}@latest`];
262975
+ out(`\u21BB installing ${PKG_NAME}@latest (${cmd} ${args.join(" ")}) \u2026`);
262976
+ const spawn7 = opts?.spawnFn ?? nodeSpawn;
262977
+ return await new Promise((resolve7) => {
262978
+ let settled = false;
262979
+ const done = (code) => {
262980
+ if (!settled) {
262981
+ settled = true;
262982
+ resolve7(code);
262983
+ }
262984
+ };
262985
+ const child = spawn7(cmd, args, { stdio: "inherit", shell: process.platform === "win32" });
262986
+ child.on("error", (e14) => {
262987
+ if (e14.code === "EACCES" || e14.code === "EPERM") {
262988
+ err(`\u2717 permission denied. Try: sudo ${cmd} ${args.join(" ")}`);
262989
+ } else if (e14.code === "ENOENT") {
262990
+ err(`\u2717 \`${cmd}\` not found. Install it, or run: npm install -g ${PKG_NAME}@latest`);
262991
+ } else {
262992
+ err(`\u2717 update failed: ${e14.message}
262993
+ Run manually: ${cmd} ${args.join(" ")}`);
262994
+ }
262995
+ done(1);
262996
+ });
262997
+ child.on("exit", (code) => {
262998
+ if (code === 0) out(`\u2713 updated \u2014 restart usable-chat to use the new version.`);
262999
+ else err(`\u2717 ${cmd} exited with code ${code ?? 1}`);
263000
+ done(code ?? 1);
263001
+ });
263002
+ });
263003
+ }
263004
+
262701
263005
  // src/cli/index.ts
262702
263006
  function usableCompletionsUrl(cfg) {
262703
263007
  const host = (cfg.ai.usableApiHost || "https://chat.usable.dev").replace(/\/+$/, "").replace(/\/api\/v1$/, "");
@@ -262756,7 +263060,7 @@ function userIdFromToken(token) {
262756
263060
  function resolveGitRoot(start) {
262757
263061
  let dir = start;
262758
263062
  while (true) {
262759
- if (existsSync12(join19(dir, ".git"))) return dir;
263063
+ if (existsSync13(join20(dir, ".git"))) return dir;
262760
263064
  const parent = dirname16(dir);
262761
263065
  if (parent === dir) return start;
262762
263066
  dir = parent;
@@ -262775,6 +263079,7 @@ function printUsage() {
262775
263079
  ' usable-chat -c -p "<prompt>" Continue the latest session for this project',
262776
263080
  ' usable-chat --resume <id> -p "..." Resume a specific session',
262777
263081
  " usable-chat sessions List sessions for this project",
263082
+ " usable-chat update Update to the latest version (npm i -g)",
262778
263083
  "",
262779
263084
  "Auth & keys:",
262780
263085
  " usable-chat login Usable/MCP token (Keycloak)",
@@ -262893,7 +263198,7 @@ function readScriptedLlm() {
262893
263198
  }
262894
263199
  async function runTui(cfg, store, projectId, values2) {
262895
263200
  applyEnv(cfg);
262896
- silenceConsole(join19(cfg.paths.dataDir, "usable.log"));
263201
+ silenceConsole(join20(cfg.paths.dataDir, "usable.log"));
262897
263202
  let session = await loadSession(cfg);
262898
263203
  let model = values2.model ?? cfg.ai.defaultModel;
262899
263204
  let reasoning = cfg.ai.reasoningEffort;
@@ -262922,7 +263227,7 @@ async function runTui(cfg, store, projectId, values2) {
262922
263227
  const scriptedLlm = readScriptedLlm();
262923
263228
  const { runCliTurn: runCliTurn2 } = await Promise.resolve().then(() => (init_run(), run_exports));
262924
263229
  const { launchTui: launchTui2 } = await Promise.resolve().then(() => (init_tui2(), tui_exports));
262925
- const extLogFile = join19(cfg.paths.dataDir, "usable.log");
263230
+ const extLogFile = join20(cfg.paths.dataDir, "usable.log");
262926
263231
  const extLog = (m33) => {
262927
263232
  try {
262928
263233
  appendFileSync4(extLogFile, `[ext] ${m33}
@@ -263025,6 +263330,11 @@ async function runTui(cfg, store, projectId, values2) {
263025
263330
  kind: m33.role === "assistant" ? "assistant" : "user",
263026
263331
  text: typeof m33.content === "string" ? m33.content : JSON.stringify(m33.content)
263027
263332
  }));
263333
+ const updateNotice = getUpdateNotice({
263334
+ currentVersion: version,
263335
+ dataDir: cfg.paths.dataDir
263336
+ });
263337
+ refreshUpdateStateInBackground({ dataDir: cfg.paths.dataDir });
263028
263338
  await launchTui2({
263029
263339
  runTurn,
263030
263340
  actions,
@@ -263032,6 +263342,7 @@ async function runTui(cfg, store, projectId, values2) {
263032
263342
  loggedIn: Boolean(session.user?.accessToken),
263033
263343
  user: session.user?.id,
263034
263344
  version,
263345
+ updateNotice: updateNotice ?? void 0,
263035
263346
  extensions: {
263036
263347
  hasCommand: (n30) => ext2.current.hasCommand(n30),
263037
263348
  runCommand: (n30, a21) => ext2.current.runCommand(n30, a21),
@@ -263044,6 +263355,7 @@ async function runTui(cfg, store, projectId, values2) {
263044
263355
  clearSkillCache2();
263045
263356
  await ext2.reload();
263046
263357
  },
263358
+ verify: (name14) => verifyExtension(process.cwd(), name14).then((r17) => r17.message),
263047
263359
  trust: () => ext2.trust(),
263048
263360
  bindNotify: (fn4) => {
263049
263361
  extUi.notify = fn4;
@@ -263085,6 +263397,9 @@ async function main() {
263085
263397
  if (positionals[0] === "config") {
263086
263398
  return runConfigCommand(cfg.paths.configFile, positionals.slice(1));
263087
263399
  }
263400
+ if (positionals[0] === "update") {
263401
+ return runUpdateCommand();
263402
+ }
263088
263403
  const store = new CliStore(cfg.paths.dbFile);
263089
263404
  const gitRoot2 = resolveGitRoot(process.cwd());
263090
263405
  const projectId = store.resolveProject(gitRoot2, basename10(gitRoot2));
@@ -263196,6 +263511,14 @@ async function main() {
263196
263511
  message: { role: "assistant", content: result.text }
263197
263512
  });
263198
263513
  }
263514
+ await maybeRefreshUpdateState({ dataDir: cfg.paths.dataDir });
263515
+ const updateNotice = getUpdateNotice({
263516
+ currentVersion: version,
263517
+ dataDir: cfg.paths.dataDir
263518
+ });
263519
+ if (updateNotice) process.stderr.write(`
263520
+ ${updateNotice}
263521
+ `);
263199
263522
  store.close();
263200
263523
  return 0;
263201
263524
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usabledev/usable-chat",
3
- "version": "1.147.0",
3
+ "version": "1.148.0",
4
4
  "description": "usable-chat — terminal harness for usable-chat (headless + TUI)",
5
5
  "type": "module",
6
6
  "bin": {