@usabledev/usable-chat 1.147.0 → 1.149.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 +410 -75
  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) {
@@ -155289,7 +155287,7 @@ var init_registry2 = __esm({
155289
155287
  * @returns Array of sub-agents that can handle the query
155290
155288
  */
155291
155289
  findHandlers(query, context) {
155292
- return this.getAll().filter((agent) => agent.canHandle(query, context));
155290
+ return this.getAll().filter((agent) => agent.canHandle(query, context)).sort((a21, b32) => (b32.priority ?? 0) - (a21.priority ?? 0));
155293
155291
  }
155294
155292
  /**
155295
155293
  * Check if a sub-agent is registered
@@ -156928,8 +156926,8 @@ ${effectiveSystemPrompt}`;
156928
156926
  apiKey: options2.apiKey,
156929
156927
  getAccessToken: options2.getAccessToken,
156930
156928
  // Pass token provider for MCP tool authentication
156931
- maxSteps: 10,
156932
- // Allow multi-step tool calling
156929
+ maxSteps: subAgent.maxTurns ?? 10,
156930
+ // Allow multi-step tool calling; expert maxTurns overrides default
156933
156931
  timeoutMs: 6e4,
156934
156932
  // 60 second timeout
156935
156933
  temperature,
@@ -157405,7 +157403,11 @@ var init_custom_expert = __esm({
157405
157403
  (val) => val.startsWith("data:") || val.startsWith("http://") || val.startsWith("https://") || val.startsWith("/"),
157406
157404
  "Must be a valid URL, data URL, or path"
157407
157405
  ).optional(),
157408
- parentTools: external_exports.array(parentToolDefinitionSchema).max(MAX_PARENT_TOOLS).optional()
157406
+ parentTools: external_exports.array(parentToolDefinitionSchema).max(MAX_PARENT_TOOLS).optional(),
157407
+ excludeKeywords: external_exports.array(external_exports.string().min(1).max(50)).max(20).optional(),
157408
+ priority: external_exports.number().int().min(0).max(100).optional(),
157409
+ maxTurns: external_exports.number().int().min(1).max(50).optional(),
157410
+ uiShortName: external_exports.string().max(30).optional()
157409
157411
  });
157410
157412
  createCustomExpertSchema = external_exports.object({
157411
157413
  key: external_exports.string().min(1).max(100).regex(/^[a-z0-9-]+$/, "Key must be lowercase alphanumeric with hyphens").optional(),
@@ -215465,14 +215467,14 @@ var require_turndown_cjs = __commonJS({
215465
215467
  } else if (node.nodeType === 1) {
215466
215468
  replacement = replacementForNode.call(self2, node);
215467
215469
  }
215468
- return join20(output, replacement);
215470
+ return join21(output, replacement);
215469
215471
  }, "");
215470
215472
  }
215471
215473
  function postProcess(output) {
215472
215474
  var self2 = this;
215473
215475
  this.rules.forEach(function(rule) {
215474
215476
  if (typeof rule.append === "function") {
215475
- output = join20(output, rule.append(self2.options));
215477
+ output = join21(output, rule.append(self2.options));
215476
215478
  }
215477
215479
  });
215478
215480
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -215484,7 +215486,7 @@ var require_turndown_cjs = __commonJS({
215484
215486
  if (whitespace.leading || whitespace.trailing) content = content.trim();
215485
215487
  return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
215486
215488
  }
215487
- function join20(output, replacement) {
215489
+ function join21(output, replacement) {
215488
215490
  var s17 = trimTrailingNewlines(output);
215489
215491
  var s22 = trimLeadingNewlines(replacement);
215490
215492
  var nls = Math.max(output.length - s17.length, replacement.length - s22.length);
@@ -242472,6 +242474,14 @@ Never treat a markdown file in working memory as a completed deliverable.
242472
242474
  systemMessageParts.push(LANGUAGE_PROMPT);
242473
242475
  orchestrationLogger.info("Multilingual mode enabled - Faroese language prompt added");
242474
242476
  }
242477
+ if (config3.language) {
242478
+ systemMessageParts.push(
242479
+ `The user interface requested language "${config3.language}". Answer in that language unless the user explicitly asks for another language.`
242480
+ );
242481
+ if (config3.language === "fo" && !config3.isMultilingual) {
242482
+ systemMessageParts.push(LANGUAGE_PROMPT);
242483
+ }
242484
+ }
242475
242485
  if (chatMode === "discussion") {
242476
242486
  systemMessageParts.push(`<discussion-mode>
242477
242487
  CRITICAL: You are in DISCUSSION MODE (read-only). This is a hard constraint you MUST follow.
@@ -244534,12 +244544,12 @@ var init_session_pathway = __esm({
244534
244544
  });
244535
244545
 
244536
244546
  // 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";
244547
+ import { existsSync as existsSync10, readFileSync as readFileSync7, realpathSync as realpathSync4 } from "node:fs";
244548
+ import { dirname as dirname14, join as join16 } from "node:path";
244539
244549
  function readFile(path11) {
244540
244550
  try {
244541
- if (!existsSync9(path11)) return void 0;
244542
- const content = readFileSync6(path11, "utf8").trim();
244551
+ if (!existsSync10(path11)) return void 0;
244552
+ const content = readFileSync7(path11, "utf8").trim();
244543
244553
  return content || void 0;
244544
244554
  } catch {
244545
244555
  return void 0;
@@ -244548,7 +244558,7 @@ function readFile(path11) {
244548
244558
  function gitRoot(start) {
244549
244559
  let dir = start;
244550
244560
  while (true) {
244551
- if (existsSync9(join15(dir, ".git"))) return dir;
244561
+ if (existsSync10(join16(dir, ".git"))) return dir;
244552
244562
  const parent = dirname14(dir);
244553
244563
  if (parent === dir) return start;
244554
244564
  dir = parent;
@@ -244566,16 +244576,16 @@ function projectChain(cwd) {
244566
244576
  return chain;
244567
244577
  }
244568
244578
  function resolvePromptFiles(cwd, dataDir2) {
244569
- const override = readFile(join15(cwd, ".usable", "SYSTEM.md")) ?? readFile(join15(dataDir2, "SYSTEM.md"));
244579
+ const override = readFile(join16(cwd, ".usable", "SYSTEM.md")) ?? readFile(join16(dataDir2, "SYSTEM.md"));
244570
244580
  const appends = [
244571
- readFile(join15(dataDir2, "SYSTEM.append.md")),
244572
- readFile(join15(cwd, ".usable", "SYSTEM.append.md"))
244581
+ readFile(join16(dataDir2, "SYSTEM.append.md")),
244582
+ readFile(join16(cwd, ".usable", "SYSTEM.append.md"))
244573
244583
  ].filter((s17) => Boolean(s17));
244574
244584
  const append = appends.length ? appends.join("\n\n") : void 0;
244575
244585
  const candidates = [];
244576
- for (const name14 of CONTEXT_FILE_NAMES) candidates.push(join15(dataDir2, name14));
244586
+ for (const name14 of CONTEXT_FILE_NAMES) candidates.push(join16(dataDir2, name14));
244577
244587
  for (const dir of projectChain(cwd)) {
244578
- for (const name14 of CONTEXT_FILE_NAMES) candidates.push(join15(dir, name14));
244588
+ for (const name14 of CONTEXT_FILE_NAMES) candidates.push(join16(dir, name14));
244579
244589
  }
244580
244590
  const seen = /* @__PURE__ */ new Set();
244581
244591
  const contextFiles = [];
@@ -245321,12 +245331,12 @@ var init_stdio2 = __esm({
245321
245331
  });
245322
245332
 
245323
245333
  // 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";
245334
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
245335
+ import { join as join17 } from "node:path";
245326
245336
  function readConfig(path11) {
245327
245337
  try {
245328
- if (!existsSync10(path11)) return {};
245329
- const parsed = configSchema.safeParse(JSON.parse(readFileSync7(path11, "utf8")));
245338
+ if (!existsSync11(path11)) return {};
245339
+ const parsed = configSchema.safeParse(JSON.parse(readFileSync8(path11, "utf8")));
245330
245340
  if (!parsed.success || !parsed.data.mcpServers) return {};
245331
245341
  const out = {};
245332
245342
  for (const [name14, spec] of Object.entries(parsed.data.mcpServers)) out[name14] = { name: name14, ...spec };
@@ -245337,9 +245347,9 @@ function readConfig(path11) {
245337
245347
  }
245338
245348
  function loadMcpServerSpecs(cwd, dataDir2) {
245339
245349
  const merged = {
245340
- ...readConfig(join16(dataDir2, "mcp.json")),
245341
- ...readConfig(join16(cwd, ".mcp.json")),
245342
- ...readConfig(join16(cwd, ".usable", "mcp.json"))
245350
+ ...readConfig(join17(dataDir2, "mcp.json")),
245351
+ ...readConfig(join17(cwd, ".mcp.json")),
245352
+ ...readConfig(join17(cwd, ".usable", "mcp.json"))
245343
245353
  };
245344
245354
  return Object.values(merged).filter((s17) => !s17.disabled && (s17.command || s17.url));
245345
245355
  }
@@ -245759,8 +245769,8 @@ var init_usable_api = __esm({
245759
245769
  });
245760
245770
 
245761
245771
  // src/adapters/cli/media-tools.ts
245762
- import { writeFileSync as writeFileSync7 } from "node:fs";
245763
- import { join as join17 } from "node:path";
245772
+ import { writeFileSync as writeFileSync8 } from "node:fs";
245773
+ import { join as join18 } from "node:path";
245764
245774
  function stamp() {
245765
245775
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
245766
245776
  }
@@ -245800,8 +245810,8 @@ function buildMediaTools(opts) {
245800
245810
  const b64 = resp.data?.[0]?.b64_json;
245801
245811
  if (!b64) return { error: "no image returned" };
245802
245812
  const buf = Buffer.from(b64, "base64");
245803
- const file2 = join17(outDir ?? process.cwd(), `usable-image-${stamp()}.png`);
245804
- writeFileSync7(file2, buf);
245813
+ const file2 = join18(outDir ?? process.cwd(), `usable-image-${stamp()}.png`);
245814
+ writeFileSync8(file2, buf);
245805
245815
  return { path: file2, bytes: buf.length };
245806
245816
  } catch (err) {
245807
245817
  return { error: err.message || "image generation failed" };
@@ -245920,7 +245930,9 @@ Notes:
245920
245930
 
245921
245931
  ## Minimal example
245922
245932
  \`\`\`ts
245923
- import { z } from 'zod'
245933
+ // Type-only import (erased at runtime) \u2014 keep the file dependency-free so it
245934
+ // loads anywhere. Use plain JSON-schema for \`parameters\`; a zod schema also
245935
+ // works, but only if your project actually has zod installed on disk.
245924
245936
  import type { ExtensionAPI } from '@/extensions/api'
245925
245937
 
245926
245938
  export default (pi: ExtensionAPI) => {
@@ -245928,7 +245940,7 @@ export default (pi: ExtensionAPI) => {
245928
245940
  pi.registerTool({
245929
245941
  name: 'word_count',
245930
245942
  description: 'Count the words in a string.',
245931
- parameters: z.object({ text: z.string() }),
245943
+ parameters: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
245932
245944
  async execute({ text }) {
245933
245945
  return { success: true, data: { words: String(text).trim().split(/\\s+/).filter(Boolean).length } }
245934
245946
  },
@@ -245953,20 +245965,113 @@ export default (pi: ExtensionAPI) => {
245953
245965
  }
245954
245966
  \`\`\`
245955
245967
 
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.`;
245968
+ ## Verifying it loads
245969
+ - 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.
245970
+ - Then \`/trust\` (project only) + \`/reload\` \u2014 or restart. Confirm with \`/help\` (commands) and by asking the model to call your tool.`;
245971
+ }
245972
+ });
245973
+
245974
+ // src/adapters/cli/diff.ts
245975
+ function unifiedDiff(oldStr, newStr, opts = {}) {
245976
+ if (oldStr === newStr) return "";
245977
+ const context = opts.context ?? 3;
245978
+ const maxLines = opts.maxLines ?? 80;
245979
+ const a21 = oldStr === "" ? [] : oldStr.split("\n");
245980
+ const b32 = newStr === "" ? [] : newStr.split("\n");
245981
+ const n30 = a21.length;
245982
+ const m33 = b32.length;
245983
+ if (n30 * m33 > 4e6) return `(diff omitted \u2014 ${n30}\u2192${m33} lines too large to render)`;
245984
+ const lcs = Array.from({ length: n30 + 1 }, () => new Array(m33 + 1).fill(0));
245985
+ for (let i19 = n30 - 1; i19 >= 0; i19--) {
245986
+ for (let j21 = m33 - 1; j21 >= 0; j21--) {
245987
+ lcs[i19][j21] = a21[i19] === b32[j21] ? lcs[i19 + 1][j21 + 1] + 1 : Math.max(lcs[i19 + 1][j21], lcs[i19][j21 + 1]);
245988
+ }
245989
+ }
245990
+ const ops = [];
245991
+ let i18 = 0;
245992
+ let j20 = 0;
245993
+ while (i18 < n30 && j20 < m33) {
245994
+ if (a21[i18] === b32[j20]) {
245995
+ ops.push({ t: " ", line: a21[i18] });
245996
+ i18++;
245997
+ j20++;
245998
+ } else if (lcs[i18 + 1][j20] >= lcs[i18][j20 + 1]) {
245999
+ ops.push({ t: "-", line: a21[i18] });
246000
+ i18++;
246001
+ } else {
246002
+ ops.push({ t: "+", line: b32[j20] });
246003
+ j20++;
246004
+ }
246005
+ }
246006
+ while (i18 < n30) ops.push({ t: "-", line: a21[i18++] });
246007
+ while (j20 < m33) ops.push({ t: "+", line: b32[j20++] });
246008
+ const keep = new Array(ops.length).fill(false);
246009
+ ops.forEach((op2, idx) => {
246010
+ if (op2.t === " ") return;
246011
+ for (let k23 = Math.max(0, idx - context); k23 <= Math.min(ops.length - 1, idx + context); k23++) {
246012
+ keep[k23] = true;
246013
+ }
246014
+ });
246015
+ const out = [];
246016
+ let oldNo = 1;
246017
+ let newNo = 1;
246018
+ let inHunk = false;
246019
+ let hunk = [];
246020
+ let hunkOldStart = 1;
246021
+ let hunkNewStart = 1;
246022
+ const flush = () => {
246023
+ if (hunk.length) {
246024
+ out.push(`@@ -${hunkOldStart} +${hunkNewStart} @@`);
246025
+ out.push(...hunk);
246026
+ hunk = [];
246027
+ }
246028
+ inHunk = false;
246029
+ };
246030
+ for (let idx = 0; idx < ops.length; idx++) {
246031
+ const op2 = ops[idx];
246032
+ if (keep[idx]) {
246033
+ if (!inHunk) {
246034
+ inHunk = true;
246035
+ hunkOldStart = oldNo;
246036
+ hunkNewStart = newNo;
246037
+ }
246038
+ hunk.push(`${op2.t}${op2.line}`);
246039
+ } else if (inHunk) {
246040
+ flush();
246041
+ }
246042
+ if (op2.t === " ") {
246043
+ oldNo++;
246044
+ newNo++;
246045
+ } else if (op2.t === "-") {
246046
+ oldNo++;
246047
+ } else {
246048
+ newNo++;
246049
+ }
246050
+ }
246051
+ flush();
246052
+ if (out.length > maxLines) {
246053
+ const kept = out.slice(0, maxLines);
246054
+ kept.push(`\u2026 (${out.length - maxLines} more diff lines)`);
246055
+ return kept.join("\n");
246056
+ }
246057
+ return out.join("\n");
246058
+ }
246059
+ var init_diff = __esm({
246060
+ "src/adapters/cli/diff.ts"() {
246061
+ "use strict";
245957
246062
  }
245958
246063
  });
245959
246064
 
245960
246065
  // src/adapters/cli/tools.ts
245961
246066
  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";
246067
+ import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "node:fs";
245963
246068
  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";
246069
+ import { dirname as dirname15, isAbsolute as isAbsolute4, join as join19, relative as relative2, resolve as resolve6, sep as sep2 } from "node:path";
245965
246070
  function truncate2(text2, label) {
245966
246071
  if (text2.length <= MAX_OUTPUT_CHARS) return text2;
245967
- const file2 = join18(tmpdir(), `usable-${label}-${Date.now()}.txt`);
246072
+ const file2 = join19(tmpdir(), `usable-${label}-${Date.now()}.txt`);
245968
246073
  try {
245969
- writeFileSync8(file2, text2);
246074
+ writeFileSync9(file2, text2);
245970
246075
  } catch {
245971
246076
  }
245972
246077
  return text2.slice(0, MAX_OUTPUT_CHARS) + `
@@ -246090,9 +246195,9 @@ function createLocalTools(opts = {}) {
246090
246195
  const guarded = guardPath(parsed.data.path);
246091
246196
  if (!guarded.ok) return { error: guarded.error };
246092
246197
  const path11 = guarded.path;
246093
- if (!existsSync11(path11)) return { error: `No such file: ${rel(path11)}` };
246198
+ if (!existsSync12(path11)) return { error: `No such file: ${rel(path11)}` };
246094
246199
  try {
246095
- const lines = readFileSync8(path11, "utf8").split("\n");
246200
+ const lines = readFileSync9(path11, "utf8").split("\n");
246096
246201
  const offset = Math.max(1, parsed.data.offset ?? 1);
246097
246202
  const limit = Math.max(1, parsed.data.limit ?? MAX_READ_LINES);
246098
246203
  const slice = lines.slice(offset - 1, offset - 1 + limit);
@@ -246124,10 +246229,14 @@ function createLocalTools(opts = {}) {
246124
246229
  const path11 = guarded.path;
246125
246230
  try {
246126
246231
  const dir = dirname15(path11);
246127
- if (!existsSync11(dir)) mkdirSync9(dir, { recursive: true });
246128
- writeFileSync8(path11, parsed.data.content);
246232
+ const before = existsSync12(path11) ? readFileSync9(path11, "utf8") : "";
246233
+ if (!existsSync12(dir)) mkdirSync10(dir, { recursive: true });
246234
+ writeFileSync9(path11, parsed.data.content);
246129
246235
  readFiles.add(path11);
246130
- return `Wrote ${rel(path11)} (${parsed.data.content.length} bytes)`;
246236
+ const verb = before ? "Overwrote" : "Wrote";
246237
+ const diff = unifiedDiff(before, parsed.data.content, { maxLines: 60 });
246238
+ return `${verb} ${rel(path11)} (${parsed.data.content.length} bytes)` + (diff ? `
246239
+ ${diff}` : "");
246131
246240
  } catch (e14) {
246132
246241
  return { error: e14 instanceof Error ? e14.message : String(e14) };
246133
246242
  }
@@ -246157,11 +246266,12 @@ function createLocalTools(opts = {}) {
246157
246266
  const guarded = guardPath(parsed.data.path);
246158
246267
  if (!guarded.ok) return { error: guarded.error };
246159
246268
  const path11 = guarded.path;
246160
- if (!existsSync11(path11)) return { error: `No such file: ${rel(path11)}` };
246269
+ if (!existsSync12(path11)) return { error: `No such file: ${rel(path11)}` };
246161
246270
  if (!readFiles.has(path11)) return { error: `Read ${rel(path11)} before editing it.` };
246162
246271
  const edits = parsed.data.edits;
246163
246272
  try {
246164
- let content = readFileSync8(path11, "utf8");
246273
+ const original2 = readFileSync9(path11, "utf8");
246274
+ let content = original2;
246165
246275
  for (const [i18, edit2] of edits.entries()) {
246166
246276
  const { oldText, newText } = edit2;
246167
246277
  let count2 = content.split(oldText).length - 1;
@@ -246179,8 +246289,10 @@ function createLocalTools(opts = {}) {
246179
246289
  if (count2 > 1) return { error: `edit ${i18 + 1}: oldText appears ${count2}\xD7 \u2014 add more context to make it unique` };
246180
246290
  content = content.replace(target, () => newText);
246181
246291
  }
246182
- writeFileSync8(path11, content);
246183
- return `Applied ${edits.length} edit(s) to ${rel(path11)}`;
246292
+ writeFileSync9(path11, content);
246293
+ const diff = unifiedDiff(original2, content, { maxLines: 60 });
246294
+ return `Applied ${edits.length} edit(s) to ${rel(path11)}` + (diff ? `
246295
+ ${diff}` : "");
246184
246296
  } catch (e14) {
246185
246297
  return { error: e14 instanceof Error ? e14.message : String(e14) };
246186
246298
  }
@@ -246331,6 +246443,7 @@ var init_tools = __esm({
246331
246443
  "use strict";
246332
246444
  init_zod();
246333
246445
  init_extension_authoring();
246446
+ init_diff();
246334
246447
  MAX_READ_LINES = 2e3;
246335
246448
  MAX_OUTPUT_CHARS = 3e4;
246336
246449
  DEFAULT_BASH_TIMEOUT_S = 120;
@@ -261237,13 +261350,25 @@ async function launchTui(options2) {
261237
261350
  if (res.ok) {
261238
261351
  sys(
261239
261352
  `Scaffolded ${res.path}
261240
- Edit it, then /trust (project extensions need it) and /reload to load it.`
261353
+ Edit it, then /verify-extension ${arg.trim()} to check it loads, /trust (project extensions need it) and /reload to load it.`
261241
261354
  );
261242
261355
  } else {
261243
261356
  sys(`Could not scaffold: ${res.error}`, "error");
261244
261357
  }
261245
261358
  return;
261246
261359
  }
261360
+ case "verify-extension": {
261361
+ if (!extensions) {
261362
+ sys("No extension system available.", "error");
261363
+ return;
261364
+ }
261365
+ if (!arg.trim()) {
261366
+ sys("Usage: /verify-extension <name>", "error");
261367
+ return;
261368
+ }
261369
+ sys(await extensions.verify(arg.trim()));
261370
+ return;
261371
+ }
261247
261372
  case "login":
261248
261373
  try {
261249
261374
  const res = await actions.login((m33) => sys(m33));
@@ -261387,6 +261512,7 @@ Edit it, then /trust (project extensions need it) and /reload to load it.`
261387
261512
  { name: "sessions", description: "list sessions for this project" },
261388
261513
  { name: "provider", description: "switch model provider" },
261389
261514
  { name: "new-extension", description: "scaffold a starter extension", argumentHint: "<name>" },
261515
+ { name: "verify-extension", description: "check an extension loads", argumentHint: "<name>" },
261390
261516
  { name: "new", description: "start a fresh session" },
261391
261517
  { name: "clear", description: "clear the screen" },
261392
261518
  { name: "exit", description: "quit" },
@@ -261506,6 +261632,9 @@ Edit it, then /trust (project extensions need it) and /reload to load it.`
261506
261632
  tone: "info"
261507
261633
  });
261508
261634
  }
261635
+ if (options2.updateNotice) {
261636
+ push({ kind: "system", text: options2.updateNotice, tone: "info" });
261637
+ }
261509
261638
  ui2.addChild(root);
261510
261639
  ui2.setFocus(root);
261511
261640
  ui2.start();
@@ -261539,6 +261668,7 @@ var init_tui2 = __esm({
261539
261668
  "/reasoning [lvl] set reasoning effort (low|medium|high)",
261540
261669
  "/sessions list sessions for this project",
261541
261670
  "/new-extension <n> scaffold a starter extension in .usable/extensions/",
261671
+ "/verify-extension <n> check an extension loads (no tsc needed)",
261542
261672
  "/new start a fresh session",
261543
261673
  "/clear clear the screen",
261544
261674
  "/exit, /quit quit (or Ctrl-C twice)",
@@ -261573,8 +261703,8 @@ process.emitWarning = function patchedEmitWarning(warning, ...rest) {
261573
261703
 
261574
261704
  // src/cli/index.ts
261575
261705
  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";
261706
+ import { appendFileSync as appendFileSync4, existsSync as existsSync13 } from "node:fs";
261707
+ import { basename as basename10, dirname as dirname16, join as join20 } from "node:path";
261578
261708
 
261579
261709
  // src/lib/config.cli.ts
261580
261710
  init_zod();
@@ -262149,7 +262279,7 @@ init_tui_select();
262149
262279
  init_model_registry();
262150
262280
 
262151
262281
  // package.json
262152
- var version = "1.147.0";
262282
+ var version = "1.149.0";
262153
262283
 
262154
262284
  // src/adapters/cli/model-catalog.ts
262155
262285
  init_codex_auth();
@@ -262356,17 +262486,16 @@ function normalizeExtensionName(name14) {
262356
262486
  }
262357
262487
  function template(id) {
262358
262488
  const cmd = id.replace(/-/g, "");
262359
- return `import { z } from 'zod'
262360
- import type { ExtensionAPI } from '@/extensions/api'
262489
+ return `import type { ExtensionAPI } from '@/extensions/api'
262361
262490
 
262362
262491
  // ${id} \u2014 a usable-chat harness extension.
262363
- // Edit, then run /trust (project) + /reload to load it. /help lists commands.
262492
+ // Verify it loads with /verify-extension ${id}, then /trust (project) + /reload. /help lists commands.
262364
262493
  export default (pi: ExtensionAPI) => {
262365
- // A model-callable tool.
262494
+ // A model-callable tool (plain JSON-schema params \u2014 no external deps).
262366
262495
  pi.registerTool({
262367
262496
  name: '${cmd}_echo',
262368
262497
  description: 'Echo back the given text (example tool from the ${id} extension).',
262369
- parameters: z.object({ text: z.string() }),
262498
+ parameters: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
262370
262499
  async execute({ text }) {
262371
262500
  return { success: true, data: { echo: String(text) } }
262372
262501
  },
@@ -262378,8 +262507,9 @@ export default (pi: ExtensionAPI) => {
262378
262507
  run: (arg, ctx) => ctx.ui.notify(\`${id}: \${arg || '(no arg)'}\`),
262379
262508
  })
262380
262509
 
262381
- // Lifecycle/model hooks + UI contributions (uncomment to use):
262382
- // pi.on('token_usage', (e, ctx) => ctx.ui.setStatus('tok', \`\${e.totalTokens ?? 0} tok\`))
262510
+ // Live status-bar metrics (uncomment): token count + context % for this session.
262511
+ // pi.on('token_usage', (e, ctx) => ctx.ui.setStatus('${cmd}-tok', \`\${e.totalTokens ?? 0} tok\`))
262512
+ // pi.on('context', (e, ctx) => ctx.ui.setStatus('${cmd}-ctx', e.pct != null ? \`ctx \${Math.round(e.pct)}%\` : ''))
262383
262513
  // pi.on('error', (e, ctx) => ctx.ui.notify(\`error: \${e.message}\`, 'error'))
262384
262514
  // pi.setBanner([' ${id} ']) // replace the startup ASCII banner
262385
262515
  }
@@ -262589,6 +262719,45 @@ async function loadExtensions(opts) {
262589
262719
  }
262590
262720
  return { registry: registry2, untrustedProjectDir };
262591
262721
  }
262722
+ async function verifyExtension(cwd, name14) {
262723
+ const id = name14.replace(/^\//, "").trim();
262724
+ if (!id) return { ok: false, message: "usage: /verify-extension <name>" };
262725
+ const candidates = [];
262726
+ for (const base of [projectExtensionsDir(cwd), homeExtensionsDir()]) {
262727
+ for (const e14 of ["ts", "js", "mjs"]) candidates.push(join9(base, `${id}.${e14}`));
262728
+ for (const idx of ["index.ts", "index.js", "index.mjs"]) candidates.push(join9(base, id, idx));
262729
+ }
262730
+ const file2 = candidates.find((p28) => existsSync7(p28));
262731
+ if (!file2) {
262732
+ return {
262733
+ ok: false,
262734
+ message: `extension "${id}" not found in ./.usable/extensions or ~/.usable/extensions`
262735
+ };
262736
+ }
262737
+ const jiti = createJiti(import.meta.url, { moduleCache: false, fsCache: false });
262738
+ try {
262739
+ const mod = await jiti.import(file2);
262740
+ const fn4 = typeof mod === "function" ? mod : mod.default;
262741
+ if (typeof fn4 !== "function") {
262742
+ return {
262743
+ ok: false,
262744
+ message: `${basename2(file2)}: no default export \u2014 expected \`export default (pi) => { \u2026 }\``
262745
+ };
262746
+ }
262747
+ const registry2 = createRegistry();
262748
+ await fn4(recordingApi(registry2, id));
262749
+ 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;
262750
+ return {
262751
+ ok: true,
262752
+ message: `\u2713 ${basename2(file2)} loads cleanly \u2014 ${registry2.tools.length} tool(s), ${registry2.commands.size} command(s), ${hooks} hook(s). Run /reload to activate.`
262753
+ };
262754
+ } catch (err) {
262755
+ return {
262756
+ ok: false,
262757
+ message: `\u2717 ${basename2(file2)} failed to load: ${err instanceof Error ? err.message : String(err)}`
262758
+ };
262759
+ }
262760
+ }
262592
262761
 
262593
262762
  // src/extensions/runtime.ts
262594
262763
  init_esm();
@@ -262698,6 +262867,153 @@ function buildExtensions(registry2, opts) {
262698
262867
  };
262699
262868
  }
262700
262869
 
262870
+ // src/adapters/cli/update-check.ts
262871
+ import { spawn as nodeSpawn } from "node:child_process";
262872
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "node:fs";
262873
+ import { join as join10 } from "node:path";
262874
+ var PKG_NAME = "@usabledev/usable-chat";
262875
+ var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
262876
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
262877
+ var FETCH_TIMEOUT_MS = 3e3;
262878
+ function parseVersion(version4) {
262879
+ const m33 = version4.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
262880
+ if (!m33) return void 0;
262881
+ return { major: +m33[1], minor: +m33[2], patch: +m33[3], prerelease: m33[4] };
262882
+ }
262883
+ function compareVersions(a21, b32) {
262884
+ const l13 = parseVersion(a21);
262885
+ const r17 = parseVersion(b32);
262886
+ if (!l13 || !r17) return void 0;
262887
+ if (l13.major !== r17.major) return l13.major - r17.major;
262888
+ if (l13.minor !== r17.minor) return l13.minor - r17.minor;
262889
+ if (l13.patch !== r17.patch) return l13.patch - r17.patch;
262890
+ if (l13.prerelease === r17.prerelease) return 0;
262891
+ if (!l13.prerelease) return 1;
262892
+ if (!r17.prerelease) return -1;
262893
+ return l13.prerelease.localeCompare(r17.prerelease);
262894
+ }
262895
+ function isNewerVersion(candidate, current) {
262896
+ const c21 = compareVersions(candidate, current);
262897
+ return c21 !== void 0 ? c21 > 0 : candidate.trim() !== current.trim();
262898
+ }
262899
+ function updateCheckEnabled() {
262900
+ if (process.env.USABLE_SKIP_UPDATE_CHECK || process.env.NO_UPDATE_NOTIFIER) return false;
262901
+ if (process.env.CI) return false;
262902
+ if (process.env.E2E_ALLOW_TEST_SEAMS === "true") return false;
262903
+ return true;
262904
+ }
262905
+ function statePath(dataDir2) {
262906
+ return join10(dataDir2, "update-check.json");
262907
+ }
262908
+ function readState(dataDir2) {
262909
+ const p28 = statePath(dataDir2);
262910
+ if (!existsSync8(p28)) return null;
262911
+ try {
262912
+ const s17 = JSON.parse(readFileSync6(p28, "utf8"));
262913
+ if (typeof s17.lastCheckAt === "number" && typeof s17.latestVersion === "string") {
262914
+ return { lastCheckAt: s17.lastCheckAt, latestVersion: s17.latestVersion };
262915
+ }
262916
+ } catch {
262917
+ }
262918
+ return null;
262919
+ }
262920
+ function writeState(dataDir2, state2) {
262921
+ try {
262922
+ if (!existsSync8(dataDir2)) mkdirSync8(dataDir2, { recursive: true });
262923
+ writeFileSync7(statePath(dataDir2), JSON.stringify(state2, null, 2), { mode: 384 });
262924
+ } catch {
262925
+ }
262926
+ }
262927
+ async function fetchLatestVersion(timeoutMs = FETCH_TIMEOUT_MS) {
262928
+ try {
262929
+ const res = await fetch(REGISTRY_URL, {
262930
+ signal: AbortSignal.timeout(timeoutMs),
262931
+ headers: { accept: "application/json" }
262932
+ });
262933
+ if (!res.ok) return null;
262934
+ const json3 = await res.json();
262935
+ return typeof json3.version === "string" ? json3.version : null;
262936
+ } catch {
262937
+ return null;
262938
+ }
262939
+ }
262940
+ function formatUpdateNotice(current, latest) {
262941
+ return `\u24D8 update available ${current} \u2192 ${latest} \xB7 run \`usable-chat update\``;
262942
+ }
262943
+ function getUpdateNotice(opts) {
262944
+ if (!updateCheckEnabled()) return null;
262945
+ const state2 = readState(opts.dataDir);
262946
+ if (state2 && isNewerVersion(state2.latestVersion, opts.currentVersion)) {
262947
+ return formatUpdateNotice(opts.currentVersion, state2.latestVersion);
262948
+ }
262949
+ return null;
262950
+ }
262951
+ async function maybeRefreshUpdateState(opts) {
262952
+ if (!updateCheckEnabled()) return;
262953
+ const now2 = opts.now ?? Date.now();
262954
+ const state2 = readState(opts.dataDir);
262955
+ if (state2 && now2 - state2.lastCheckAt < CHECK_INTERVAL_MS) return;
262956
+ const latest = await fetchLatestVersion();
262957
+ writeState(opts.dataDir, {
262958
+ lastCheckAt: now2,
262959
+ latestVersion: latest ?? state2?.latestVersion ?? "0.0.0"
262960
+ });
262961
+ }
262962
+ function refreshUpdateStateInBackground(opts) {
262963
+ void maybeRefreshUpdateState(opts).catch(() => {
262964
+ });
262965
+ }
262966
+ function detectInstall(entry = process.argv[1] ?? "") {
262967
+ const p28 = entry.replace(/\\/g, "/");
262968
+ if (p28.includes("/_npx/")) return "npx";
262969
+ if (process.env.npm_config_global === "true") return "global";
262970
+ if (p28.includes("/lib/node_modules/") || p28.includes("/pnpm/global/") || p28.includes("/pnpm-global/"))
262971
+ return "global";
262972
+ if (p28.includes("/node_modules/")) return "local";
262973
+ return "unknown";
262974
+ }
262975
+ async function runUpdateCommand(opts) {
262976
+ const kind = opts?.kind ?? detectInstall(opts?.entry);
262977
+ const out = (s17) => process.stdout.write(s17 + "\n");
262978
+ const err = (s17) => process.stderr.write(s17 + "\n");
262979
+ if (kind === "npx") {
262980
+ out("You're running via npx, which already fetches the latest on each run.");
262981
+ out(`To force the newest explicitly: npx ${PKG_NAME}@latest`);
262982
+ return 0;
262983
+ }
262984
+ const usePnpm = (opts?.entry ?? process.argv[1] ?? "").replace(/\\/g, "/").includes("/pnpm");
262985
+ const cmd = usePnpm ? "pnpm" : "npm";
262986
+ const args = usePnpm ? ["add", "-g", `${PKG_NAME}@latest`] : ["install", "-g", `${PKG_NAME}@latest`];
262987
+ out(`\u21BB installing ${PKG_NAME}@latest (${cmd} ${args.join(" ")}) \u2026`);
262988
+ const spawn7 = opts?.spawnFn ?? nodeSpawn;
262989
+ return await new Promise((resolve7) => {
262990
+ let settled = false;
262991
+ const done = (code) => {
262992
+ if (!settled) {
262993
+ settled = true;
262994
+ resolve7(code);
262995
+ }
262996
+ };
262997
+ const child = spawn7(cmd, args, { stdio: "inherit", shell: process.platform === "win32" });
262998
+ child.on("error", (e14) => {
262999
+ if (e14.code === "EACCES" || e14.code === "EPERM") {
263000
+ err(`\u2717 permission denied. Try: sudo ${cmd} ${args.join(" ")}`);
263001
+ } else if (e14.code === "ENOENT") {
263002
+ err(`\u2717 \`${cmd}\` not found. Install it, or run: npm install -g ${PKG_NAME}@latest`);
263003
+ } else {
263004
+ err(`\u2717 update failed: ${e14.message}
263005
+ Run manually: ${cmd} ${args.join(" ")}`);
263006
+ }
263007
+ done(1);
263008
+ });
263009
+ child.on("exit", (code) => {
263010
+ if (code === 0) out(`\u2713 updated \u2014 restart usable-chat to use the new version.`);
263011
+ else err(`\u2717 ${cmd} exited with code ${code ?? 1}`);
263012
+ done(code ?? 1);
263013
+ });
263014
+ });
263015
+ }
263016
+
262701
263017
  // src/cli/index.ts
262702
263018
  function usableCompletionsUrl(cfg) {
262703
263019
  const host = (cfg.ai.usableApiHost || "https://chat.usable.dev").replace(/\/+$/, "").replace(/\/api\/v1$/, "");
@@ -262756,7 +263072,7 @@ function userIdFromToken(token) {
262756
263072
  function resolveGitRoot(start) {
262757
263073
  let dir = start;
262758
263074
  while (true) {
262759
- if (existsSync12(join19(dir, ".git"))) return dir;
263075
+ if (existsSync13(join20(dir, ".git"))) return dir;
262760
263076
  const parent = dirname16(dir);
262761
263077
  if (parent === dir) return start;
262762
263078
  dir = parent;
@@ -262775,6 +263091,7 @@ function printUsage() {
262775
263091
  ' usable-chat -c -p "<prompt>" Continue the latest session for this project',
262776
263092
  ' usable-chat --resume <id> -p "..." Resume a specific session',
262777
263093
  " usable-chat sessions List sessions for this project",
263094
+ " usable-chat update Update to the latest version (npm i -g)",
262778
263095
  "",
262779
263096
  "Auth & keys:",
262780
263097
  " usable-chat login Usable/MCP token (Keycloak)",
@@ -262893,7 +263210,7 @@ function readScriptedLlm() {
262893
263210
  }
262894
263211
  async function runTui(cfg, store, projectId, values2) {
262895
263212
  applyEnv(cfg);
262896
- silenceConsole(join19(cfg.paths.dataDir, "usable.log"));
263213
+ silenceConsole(join20(cfg.paths.dataDir, "usable.log"));
262897
263214
  let session = await loadSession(cfg);
262898
263215
  let model = values2.model ?? cfg.ai.defaultModel;
262899
263216
  let reasoning = cfg.ai.reasoningEffort;
@@ -262922,7 +263239,7 @@ async function runTui(cfg, store, projectId, values2) {
262922
263239
  const scriptedLlm = readScriptedLlm();
262923
263240
  const { runCliTurn: runCliTurn2 } = await Promise.resolve().then(() => (init_run(), run_exports));
262924
263241
  const { launchTui: launchTui2 } = await Promise.resolve().then(() => (init_tui2(), tui_exports));
262925
- const extLogFile = join19(cfg.paths.dataDir, "usable.log");
263242
+ const extLogFile = join20(cfg.paths.dataDir, "usable.log");
262926
263243
  const extLog = (m33) => {
262927
263244
  try {
262928
263245
  appendFileSync4(extLogFile, `[ext] ${m33}
@@ -263025,6 +263342,11 @@ async function runTui(cfg, store, projectId, values2) {
263025
263342
  kind: m33.role === "assistant" ? "assistant" : "user",
263026
263343
  text: typeof m33.content === "string" ? m33.content : JSON.stringify(m33.content)
263027
263344
  }));
263345
+ const updateNotice = getUpdateNotice({
263346
+ currentVersion: version,
263347
+ dataDir: cfg.paths.dataDir
263348
+ });
263349
+ refreshUpdateStateInBackground({ dataDir: cfg.paths.dataDir });
263028
263350
  await launchTui2({
263029
263351
  runTurn,
263030
263352
  actions,
@@ -263032,6 +263354,7 @@ async function runTui(cfg, store, projectId, values2) {
263032
263354
  loggedIn: Boolean(session.user?.accessToken),
263033
263355
  user: session.user?.id,
263034
263356
  version,
263357
+ updateNotice: updateNotice ?? void 0,
263035
263358
  extensions: {
263036
263359
  hasCommand: (n30) => ext2.current.hasCommand(n30),
263037
263360
  runCommand: (n30, a21) => ext2.current.runCommand(n30, a21),
@@ -263044,6 +263367,7 @@ async function runTui(cfg, store, projectId, values2) {
263044
263367
  clearSkillCache2();
263045
263368
  await ext2.reload();
263046
263369
  },
263370
+ verify: (name14) => verifyExtension(process.cwd(), name14).then((r17) => r17.message),
263047
263371
  trust: () => ext2.trust(),
263048
263372
  bindNotify: (fn4) => {
263049
263373
  extUi.notify = fn4;
@@ -263085,6 +263409,9 @@ async function main() {
263085
263409
  if (positionals[0] === "config") {
263086
263410
  return runConfigCommand(cfg.paths.configFile, positionals.slice(1));
263087
263411
  }
263412
+ if (positionals[0] === "update") {
263413
+ return runUpdateCommand();
263414
+ }
263088
263415
  const store = new CliStore(cfg.paths.dbFile);
263089
263416
  const gitRoot2 = resolveGitRoot(process.cwd());
263090
263417
  const projectId = store.resolveProject(gitRoot2, basename10(gitRoot2));
@@ -263196,6 +263523,14 @@ async function main() {
263196
263523
  message: { role: "assistant", content: result.text }
263197
263524
  });
263198
263525
  }
263526
+ await maybeRefreshUpdateState({ dataDir: cfg.paths.dataDir });
263527
+ const updateNotice = getUpdateNotice({
263528
+ currentVersion: version,
263529
+ dataDir: cfg.paths.dataDir
263530
+ });
263531
+ if (updateNotice) process.stderr.write(`
263532
+ ${updateNotice}
263533
+ `);
263199
263534
  store.close();
263200
263535
  return 0;
263201
263536
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usabledev/usable-chat",
3
- "version": "1.147.0",
3
+ "version": "1.149.0",
4
4
  "description": "usable-chat — terminal harness for usable-chat (headless + TUI)",
5
5
  "type": "module",
6
6
  "bin": {