@sunilaleti/hx 0.3.0-beta.1 → 1.1.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.
package/dist/hx.js CHANGED
@@ -825,7 +825,7 @@ var init_json = __esm({
825
825
 
826
826
  // src/paths.ts
827
827
  import { createHash } from "node:crypto";
828
- import { closeSync, mkdirSync, openSync } from "node:fs";
828
+ import { chmodSync, closeSync, mkdirSync, openSync, statSync as statSync3 } from "node:fs";
829
829
  function userHome() {
830
830
  const override = process.env[HX_HOME_ENV];
831
831
  return override ? purePath(expandUser(override)) : join(purePath(home()), ".hx");
@@ -883,7 +883,13 @@ function projectInstructionsFiles(cwd, root) {
883
883
  return chain2.slice(0, index + 1).reverse().map((directory) => join(directory, "AGENTS.md"));
884
884
  }
885
885
  function ensureUserDirs() {
886
- for (const path of [userHome(), sessionsDir(), userSkillsDir(), userAgentsDir(), logsDir()]) {
886
+ const root = userHome();
887
+ mkdirSync(root, { recursive: true, mode: 448 });
888
+ try {
889
+ if (statSync3(root).mode & 63) chmodSync(root, 448);
890
+ } catch {
891
+ }
892
+ for (const path of [sessionsDir(), userSkillsDir(), userAgentsDir(), logsDir()]) {
887
893
  mkdirSync(path, { recursive: true });
888
894
  }
889
895
  try {
@@ -932,7 +938,7 @@ var init_paths = __esm({
932
938
  });
933
939
 
934
940
  // src/auth/store.ts
935
- import { chmodSync, closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, renameSync, rmSync, writeSync } from "node:fs";
941
+ import { chmodSync as chmodSync2, closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, renameSync, rmSync, writeSync } from "node:fs";
936
942
  function expired(credential, leeway = 300) {
937
943
  return Date.now() / 1e3 >= credential.expires - leeway;
938
944
  }
@@ -1088,7 +1094,7 @@ var init_store = __esm({
1088
1094
  throw error;
1089
1095
  }
1090
1096
  closeSync2(descriptor);
1091
- chmodSync(tmp, 384);
1097
+ chmodSync2(tmp, 384);
1092
1098
  renameSync(tmp, path);
1093
1099
  }
1094
1100
  };
@@ -1744,13 +1750,13 @@ var init_base = __esm({
1744
1750
  });
1745
1751
 
1746
1752
  // src/which.ts
1747
- import { accessSync, constants, statSync as statSync3 } from "node:fs";
1753
+ import { accessSync, constants, statSync as statSync4 } from "node:fs";
1748
1754
  function which(name, path = process.env.PATH) {
1749
1755
  const candidates2 = name.includes("/") ? [name] : (path ?? "").split(":").map((dir) => join(dir || ".", name));
1750
1756
  for (const candidate of candidates2) {
1751
1757
  try {
1752
1758
  accessSync(candidate, constants.X_OK);
1753
- if (statSync3(candidate).isFile()) return candidate;
1759
+ if (statSync4(candidate).isFile()) return candidate;
1754
1760
  } catch {
1755
1761
  }
1756
1762
  }
@@ -2385,12 +2391,13 @@ function quote(text3) {
2385
2391
  if (SAFE.test(text3)) return text3;
2386
2392
  return `'${text3.replaceAll("'", `'"'"'`)}'`;
2387
2393
  }
2388
- var WHITESPACE, SAFE;
2394
+ var WHITESPACE, SAFE, join2;
2389
2395
  var init_shlex = __esm({
2390
2396
  "src/shlex.ts"() {
2391
2397
  "use strict";
2392
2398
  WHITESPACE = " \r\n";
2393
2399
  SAFE = /^[%+,\-./0-9:=@A-Z_a-z]+$/;
2400
+ join2 = (parts2) => parts2.map(quote).join(" ");
2394
2401
  }
2395
2402
  });
2396
2403
 
@@ -2584,7 +2591,7 @@ var init_messages = __esm({
2584
2591
  });
2585
2592
 
2586
2593
  // src/core/images.ts
2587
- import { readFileSync as readFileSync2, statSync as statSync4 } from "node:fs";
2594
+ import { readFileSync as readFileSync2, statSync as statSync5 } from "node:fs";
2588
2595
  async function loadSharp() {
2589
2596
  sharpModule ??= (await import("sharp")).default;
2590
2597
  return sharpModule;
@@ -2627,7 +2634,7 @@ async function loadImage(data, label = "") {
2627
2634
  async function loadImageFile(path, label) {
2628
2635
  let bytes;
2629
2636
  try {
2630
- bytes = statSync4(path).size;
2637
+ bytes = statSync5(path).size;
2631
2638
  } catch (error) {
2632
2639
  throw new ImageError(`cannot read ${path}: ${strerror(error)}`);
2633
2640
  }
@@ -5079,7 +5086,7 @@ import {
5079
5086
  openSync as openSync3,
5080
5087
  readdirSync as readdirSync2,
5081
5088
  realpathSync as realpathSync2,
5082
- statSync as statSync5,
5089
+ statSync as statSync6,
5083
5090
  writeSync as writeSync2
5084
5091
  } from "node:fs";
5085
5092
  function servedName(name) {
@@ -5213,7 +5220,7 @@ function projectDir2(cwd) {
5213
5220
  const prefix = sanitized.slice(0, 200);
5214
5221
  try {
5215
5222
  for (const entry of readdirSync2(projects))
5216
- if (entry.startsWith(`${prefix}-`) && statSync5(join(projects, entry)).isDirectory())
5223
+ if (entry.startsWith(`${prefix}-`) && statSync6(join(projects, entry)).isDirectory())
5217
5224
  return join(projects, entry);
5218
5225
  } catch {
5219
5226
  }
@@ -6344,7 +6351,8 @@ function buildSettings(data, cwd) {
6344
6351
  tui: { enterWhileBusy: busyRaw, fullscreen: truthy(tui.fullscreen ?? false) },
6345
6352
  tools: { hashline: truthy(pick(tools, "hashline", true)) },
6346
6353
  theme: pick(data, "theme", "dark"),
6347
- quietStartup: truthy(pick(data, "quietStartup", pick(data, "quiet_startup", false)))
6354
+ quietStartup: truthy(pick(data, "quietStartup", pick(data, "quiet_startup", false))),
6355
+ autoUpdates: truthy(pick(data, "autoUpdates", pick(data, "auto_updates", true)))
6348
6356
  };
6349
6357
  }
6350
6358
  function iterate(value) {
@@ -16962,7 +16970,7 @@ var require_abnf = __commonJS({
16962
16970
  scope: "attribute",
16963
16971
  match: regex.concat(IDENT, /(?=\s*=)/)
16964
16972
  };
16965
- const ASSIGNMENT = {
16973
+ const ASSIGNMENT2 = {
16966
16974
  scope: "operator",
16967
16975
  match: /=\/?/
16968
16976
  };
@@ -16971,7 +16979,7 @@ var require_abnf = __commonJS({
16971
16979
  illegal: /[!@#$^&',?+~`|:]/,
16972
16980
  keywords: KEYWORDS,
16973
16981
  contains: [
16974
- ASSIGNMENT,
16982
+ ASSIGNMENT2,
16975
16983
  RULE_DECLARATION,
16976
16984
  COMMENT,
16977
16985
  TERMINAL_BINARY,
@@ -30310,7 +30318,7 @@ var require_gams = __commonJS({
30310
30318
  illegal: "\\n",
30311
30319
  contains: [hljs.BACKSLASH_ESCAPE]
30312
30320
  };
30313
- const ASSIGNMENT = {
30321
+ const ASSIGNMENT2 = {
30314
30322
  begin: "/",
30315
30323
  end: "/",
30316
30324
  keywords: KEYWORDS,
@@ -30332,7 +30340,7 @@ var require_gams = __commonJS({
30332
30340
  endsWithParent: true,
30333
30341
  contains: [
30334
30342
  QSTR,
30335
- ASSIGNMENT,
30343
+ ASSIGNMENT2,
30336
30344
  {
30337
30345
  className: "comment",
30338
30346
  // one comment word, then possibly more
@@ -30379,7 +30387,7 @@ var require_gams = __commonJS({
30379
30387
  hljs.C_BLOCK_COMMENT_MODE,
30380
30388
  hljs.QUOTE_STRING_MODE,
30381
30389
  hljs.APOS_STRING_MODE,
30382
- ASSIGNMENT,
30390
+ ASSIGNMENT2,
30383
30391
  DESCTEXT
30384
30392
  ]
30385
30393
  },
@@ -40434,7 +40442,7 @@ var require_makefile = __commonJS({
40434
40442
  // Added QUOTE_STRING as they can be a part of functions
40435
40443
  ]
40436
40444
  };
40437
- const ASSIGNMENT = { begin: "^" + hljs.UNDERSCORE_IDENT_RE + "\\s*(?=[:+?]?=)" };
40445
+ const ASSIGNMENT2 = { begin: "^" + hljs.UNDERSCORE_IDENT_RE + "\\s*(?=[:+?]?=)" };
40438
40446
  const META = {
40439
40447
  className: "meta",
40440
40448
  begin: /^\.PHONY:/,
@@ -40466,7 +40474,7 @@ var require_makefile = __commonJS({
40466
40474
  VARIABLE,
40467
40475
  QUOTE_STRING,
40468
40476
  FUNC,
40469
- ASSIGNMENT,
40477
+ ASSIGNMENT2,
40470
40478
  META,
40471
40479
  TARGET
40472
40480
  ]
@@ -67725,7 +67733,7 @@ init_net();
67725
67733
  init_py();
67726
67734
 
67727
67735
  // src/version.ts
67728
- var VERSION = "0.3.0-beta.1";
67736
+ var VERSION = "1.1.0";
67729
67737
  var PACKAGE = "@sunilaleti/hx";
67730
67738
 
67731
67739
  // src/providers/codex.ts
@@ -68165,12 +68173,12 @@ init_images();
68165
68173
 
68166
68174
  // src/core/session.ts
68167
68175
  import { randomBytes as randomBytes3 } from "node:crypto";
68168
- import { appendFileSync, mkdirSync as mkdirSync7, statSync as statSync8, writeFileSync as writeFileSync4 } from "node:fs";
68176
+ import { appendFileSync, mkdirSync as mkdirSync7, statSync as statSync9, writeFileSync as writeFileSync4 } from "node:fs";
68169
68177
 
68170
68178
  // src/glob.ts
68171
68179
  init_pathlib();
68172
68180
  init_py();
68173
- import { opendirSync, statSync as statSync6 } from "node:fs";
68181
+ import { opendirSync, statSync as statSync7 } from "node:fs";
68174
68182
  function translate(pattern) {
68175
68183
  let out = "";
68176
68184
  const chars = [...pattern];
@@ -68222,7 +68230,7 @@ function isDirEntry(directory, entry) {
68222
68230
  if (entry.isDirectory()) return true;
68223
68231
  if (!entry.isSymbolicLink()) return false;
68224
68232
  try {
68225
- return statSync6(join(directory, entry.name)).isDirectory();
68233
+ return statSync7(join(directory, entry.name)).isDirectory();
68226
68234
  } catch {
68227
68235
  return false;
68228
68236
  }
@@ -68259,7 +68267,7 @@ function select(path, rest, out) {
68259
68267
  const child = join(path, part);
68260
68268
  if (tail3.length === 0) {
68261
68269
  try {
68262
- statSync6(child);
68270
+ statSync7(child);
68263
68271
  out.push(child);
68264
68272
  } catch {
68265
68273
  }
@@ -68477,7 +68485,7 @@ function digestOf(path) {
68477
68485
  init_messages();
68478
68486
 
68479
68487
  // src/core/reads.ts
68480
- import { statSync as statSync7 } from "node:fs";
68488
+ import { statSync as statSync8 } from "node:fs";
68481
68489
  var fileReadToDict = (read2) => ({
68482
68490
  index: read2.index,
68483
68491
  path: read2.path,
@@ -68516,7 +68524,7 @@ var STAT_DIGESTS = /* @__PURE__ */ new Map();
68516
68524
  function cachedDigest(key2) {
68517
68525
  let stamp;
68518
68526
  try {
68519
- const info = statSync7(key2, { bigint: true });
68527
+ const info = statSync8(key2, { bigint: true });
68520
68528
  stamp = `${info.ino}:${info.size}:${info.mtimeNs}`;
68521
68529
  } catch {
68522
68530
  STAT_DIGESTS.delete(key2);
@@ -68908,7 +68916,7 @@ var BACKFILL_MAX_BYTES = 4e6;
68908
68916
  function backfilled(meta) {
68909
68917
  if (meta.promptCount || meta.messageCount <= 0) return meta;
68910
68918
  try {
68911
- if (statSync8(sessionTranscriptFile(meta.sessionId)).size > BACKFILL_MAX_BYTES) return meta;
68919
+ if (statSync9(sessionTranscriptFile(meta.sessionId)).size > BACKFILL_MAX_BYTES) return meta;
68912
68920
  } catch {
68913
68921
  return meta;
68914
68922
  }
@@ -69034,6 +69042,75 @@ function changelog(version = null) {
69034
69042
  );
69035
69043
  }
69036
69044
 
69045
+ // src/mcp/approval.ts
69046
+ init_json();
69047
+ init_pathlib();
69048
+ init_paths();
69049
+ init_shlex();
69050
+ import { createHash as createHash8 } from "node:crypto";
69051
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "node:fs";
69052
+ var approvalsFile = (cwd) => join(projectStateDir(cwd), "mcp-approved.json");
69053
+ function fingerprint2(config2) {
69054
+ const fields = {
69055
+ transport: config2.transport,
69056
+ command: config2.command,
69057
+ args: config2.args,
69058
+ env: config2.env,
69059
+ url: config2.url,
69060
+ headers: config2.headers
69061
+ };
69062
+ return createHash8("sha256").update(dumps(fields, { sortKeys: true })).digest("hex");
69063
+ }
69064
+ function readApprovals(cwd) {
69065
+ const path = approvalsFile(cwd);
69066
+ if (!isFile(path)) return {};
69067
+ try {
69068
+ const data = JSON.parse(readText(path));
69069
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return {};
69070
+ return Object.fromEntries(
69071
+ Object.entries(data).filter(
69072
+ (entry) => typeof entry[1] === "string"
69073
+ )
69074
+ );
69075
+ } catch {
69076
+ return {};
69077
+ }
69078
+ }
69079
+ function writeApprovals(cwd, approvals) {
69080
+ const path = approvalsFile(cwd);
69081
+ mkdirSync8(parent(path), { recursive: true });
69082
+ writeFileSync5(path, `${dumps(approvals, { indent: 2, sortKeys: true })}
69083
+ `);
69084
+ }
69085
+ function approve(config2, cwd) {
69086
+ writeApprovals(cwd, { ...readApprovals(cwd), [config2.name]: fingerprint2(config2) });
69087
+ }
69088
+ function revoke(name, cwd) {
69089
+ const approvals = readApprovals(cwd);
69090
+ if (!(name in approvals)) return;
69091
+ delete approvals[name];
69092
+ writeApprovals(cwd, approvals);
69093
+ }
69094
+ function partition(configs, cwd) {
69095
+ const approvals = readApprovals(cwd);
69096
+ const startable = [];
69097
+ const waiting = [];
69098
+ for (const config2 of configs) {
69099
+ const approved = approvals[config2.name];
69100
+ if (config2.source === "user" || approved === fingerprint2(config2)) startable.push(config2);
69101
+ else waiting.push({ config: config2, changed: approved !== void 0 });
69102
+ }
69103
+ return [startable, waiting];
69104
+ }
69105
+ function describeServer(config2) {
69106
+ if (config2.url) return config2.url;
69107
+ return join2([config2.command ?? "", ...config2.args]);
69108
+ }
69109
+ function waitingNotice({ config: config2, changed }) {
69110
+ const why = changed ? "has changed since you approved it" : "came with the project and has not been approved";
69111
+ return `MCP server ${config2.name} in .hx/mcp.json ${why}, so HX has not started it. It ${config2.url ? "connects to" : "runs"}: ${describeServer(config2)}. Run \`hx mcp approve ${config2.name}\` to start it.`;
69112
+ }
69113
+
69037
69114
  // src/mcp/client.ts
69038
69115
  init_json();
69039
69116
  init_net();
@@ -69614,7 +69691,7 @@ init_json();
69614
69691
  init_pathlib();
69615
69692
  init_paths();
69616
69693
  init_py();
69617
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "node:fs";
69694
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync7 } from "node:fs";
69618
69695
 
69619
69696
  // src/tools/base.ts
69620
69697
  function toolResult(content, fields = {}) {
@@ -69671,7 +69748,7 @@ function isAbort(error, signal) {
69671
69748
  init_pathlib();
69672
69749
  init_paths();
69673
69750
  init_py();
69674
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "node:fs";
69751
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "node:fs";
69675
69752
  var MIN_BODY_CHARS = 200;
69676
69753
  function elision(omittedLines, omittedChars, path) {
69677
69754
  return `
@@ -69726,10 +69803,10 @@ function split2(lines2, charCap, lineCap, headRatio) {
69726
69803
  }
69727
69804
  function spill(text3, sessionId, toolUseId) {
69728
69805
  const directory = sessionOutputsDir(sessionId);
69729
- mkdirSync8(directory, { recursive: true });
69806
+ mkdirSync9(directory, { recursive: true });
69730
69807
  const safe = toolUseId.replace(/[^A-Za-z0-9_.-]/g, "_") || "output";
69731
69808
  const path = join(directory, `${safe}.txt`);
69732
- writeFileSync5(path, text3, "utf8");
69809
+ writeFileSync6(path, text3, "utf8");
69733
69810
  return path;
69734
69811
  }
69735
69812
  function summarizeForUi(text3, maxLen = 120) {
@@ -70295,6 +70372,7 @@ function serverConfig(fields) {
70295
70372
  enabled: true,
70296
70373
  timeout: 30,
70297
70374
  oauth: noOptions(),
70375
+ source: "user",
70298
70376
  ...fields
70299
70377
  };
70300
70378
  }
@@ -70369,11 +70447,13 @@ async function normalise(raw, tool) {
70369
70447
  return [images2, problems];
70370
70448
  }
70371
70449
  var MCPManager = class {
70372
- constructor(configs, store = new AuthStore()) {
70450
+ constructor(configs, store = new AuthStore(), waiting = []) {
70373
70451
  this.store = store;
70452
+ this.waiting = waiting;
70374
70453
  this.configs = configs.filter((config2) => config2.enabled);
70375
70454
  }
70376
70455
  store;
70456
+ waiting;
70377
70457
  configs;
70378
70458
  clients = /* @__PURE__ */ new Map();
70379
70459
  statuses = /* @__PURE__ */ new Map();
@@ -70585,13 +70665,16 @@ function buildTransport(config2, store) {
70585
70665
  var userMcpFile = () => join(userHome(), "mcp.json");
70586
70666
  function loadConfigs(cwd) {
70587
70667
  const merged = /* @__PURE__ */ new Map();
70588
- for (const path of [userMcpFile(), projectMcpFile(cwd)])
70589
- for (const config2 of readFile(path)) merged.set(config2.name, config2);
70668
+ for (const [path, source] of [
70669
+ [userMcpFile(), "user"],
70670
+ [projectMcpFile(cwd), "project"]
70671
+ ])
70672
+ for (const config2 of readFile(path, source)) merged.set(config2.name, config2);
70590
70673
  return [...merged.keys()].sort(compareCodePoints).map((name) => merged.get(name));
70591
70674
  }
70592
70675
  var isObject7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
70593
70676
  var strings = (raw) => Object.fromEntries(Object.entries(isObject7(raw) ? raw : {}).map(([k, v]) => [k, String(v)]));
70594
- function readFile(path) {
70677
+ function readFile(path, source) {
70595
70678
  if (!isFile(path)) return [];
70596
70679
  let data;
70597
70680
  try {
@@ -70617,7 +70700,8 @@ function readFile(path) {
70617
70700
  headers: strings(raw.headers),
70618
70701
  enabled: "enabled" in raw ? truthy(raw.enabled) : true,
70619
70702
  timeout: "timeout" in raw ? toFloat(raw.timeout) : 30,
70620
- oauth: optionsFromJson(raw.oauth)
70703
+ oauth: optionsFromJson(raw.oauth),
70704
+ source
70621
70705
  })
70622
70706
  );
70623
70707
  }
@@ -70632,7 +70716,7 @@ function readJson(path) {
70632
70716
  return {};
70633
70717
  }
70634
70718
  }
70635
- var writeJson = (path, data) => writeFileSync6(path, `${dumps(data, { indent: 2, sortKeys: true })}
70719
+ var writeJson = (path, data) => writeFileSync7(path, `${dumps(data, { indent: 2, sortKeys: true })}
70636
70720
  `);
70637
70721
  function saveConfig(config2, cwd, userLevel = false) {
70638
70722
  const path = userLevel ? userMcpFile() : projectMcpFile(cwd);
@@ -70648,7 +70732,7 @@ function saveConfig(config2, cwd, userLevel = false) {
70648
70732
  const block2 = optionsToJson(config2.oauth);
70649
70733
  if (Object.keys(block2).length) entry.oauth = block2;
70650
70734
  servers[config2.name] = entry;
70651
- mkdirSync9(parent(path), { recursive: true });
70735
+ mkdirSync10(parent(path), { recursive: true });
70652
70736
  writeJson(path, data);
70653
70737
  return path;
70654
70738
  }
@@ -70673,6 +70757,69 @@ function removeConfig(name, cwd, userLevel = false) {
70673
70757
  init_net();
70674
70758
  init_pathlib();
70675
70759
  init_paths();
70760
+
70761
+ // src/providers/deferred.ts
70762
+ init_resolve();
70763
+ init_base();
70764
+ init_registry();
70765
+ var NOT_SIGNED_IN = "Not signed in yet. Type /login to sign in with ChatGPT Plus/Pro, Claude Pro/Max, an OpenRouter API key or Devin.";
70766
+ var NEEDS = {
70767
+ openrouter: "an OpenRouter API key",
70768
+ "openai-codex": "a ChatGPT Plus/Pro sign-in",
70769
+ "claude-code": "a Claude Pro/Max sign-in",
70770
+ devin: "a Devin sign-in"
70771
+ };
70772
+ function signInHint(modelId, resolver) {
70773
+ if (!available(resolver).length) return NOT_SIGNED_IN;
70774
+ const route = providerFor(modelId);
70775
+ const needs = NEEDS[route.id] ?? `a ${route.label} sign-in`;
70776
+ return `${modelId} needs ${needs}, which is not set up yet. Type /login to sign in, or /model to pick a model you can use.`;
70777
+ }
70778
+ var DeferredProvider = class {
70779
+ constructor(modelId, resolver, sessionId, models) {
70780
+ this.modelId = modelId;
70781
+ this.resolver = resolver;
70782
+ this.sessionId = sessionId;
70783
+ this.models = models;
70784
+ }
70785
+ modelId;
70786
+ resolver;
70787
+ sessionId;
70788
+ models;
70789
+ inner = null;
70790
+ get name() {
70791
+ return this.inner?.name ?? providerFor(this.modelId).id;
70792
+ }
70793
+ /** The prompt-cache key, kept for the provider this becomes. */
70794
+ setSessionId(sessionId) {
70795
+ this.sessionId = sessionId;
70796
+ this.inner?.setSessionId?.(sessionId);
70797
+ }
70798
+ /** A key saved now is read from the store when the provider is built. */
70799
+ setApiKey(key2) {
70800
+ this.inner?.setApiKey?.(key2);
70801
+ }
70802
+ async *astream(request2, signal) {
70803
+ if (this.inner === null) {
70804
+ try {
70805
+ this.inner = buildProvider(request2.model, this.resolver, {
70806
+ sessionId: this.sessionId,
70807
+ models: this.models
70808
+ });
70809
+ } catch (error) {
70810
+ if (error instanceof MissingCredential || error instanceof ExpiredCredential)
70811
+ throw new ProviderError(signInHint(request2.model, this.resolver));
70812
+ throw error;
70813
+ }
70814
+ }
70815
+ yield* this.inner.astream(request2, signal);
70816
+ }
70817
+ async aclose() {
70818
+ await this.inner?.aclose();
70819
+ }
70820
+ };
70821
+
70822
+ // src/cli.ts
70676
70823
  init_models();
70677
70824
  init_openrouter();
70678
70825
  init_registry();
@@ -71228,7 +71375,7 @@ init_py();
71228
71375
 
71229
71376
  // src/tools/anchors.ts
71230
71377
  init_py();
71231
- import { createHash as createHash8 } from "node:crypto";
71378
+ import { createHash as createHash9 } from "node:crypto";
71232
71379
  var WIDTHS = [4, 6, 8];
71233
71380
  var SEPARATOR = "\0";
71234
71381
  function split3(content) {
@@ -71243,7 +71390,7 @@ function window2(lines2, index) {
71243
71390
  return [previous, lines2[index], following].join(SEPARATOR);
71244
71391
  }
71245
71392
  function digests(lines2) {
71246
- return lines2.map((_, i) => createHash8("sha1").update(window2(lines2, i), "utf8").digest("hex"));
71393
+ return lines2.map((_, i) => createHash9("sha1").update(window2(lines2, i), "utf8").digest("hex"));
71247
71394
  }
71248
71395
  function anchorWidth(full) {
71249
71396
  const distinct = new Set(full).size;
@@ -71289,7 +71436,7 @@ var AnchorError = class extends Error {
71289
71436
  init_images();
71290
71437
  init_pathlib();
71291
71438
  init_py();
71292
- import { closeSync as closeSync6, existsSync as existsSync2, openSync as openSync6, readFileSync as readFileSync4, readSync as readSync2, statSync as statSync9 } from "node:fs";
71439
+ import { closeSync as closeSync6, existsSync as existsSync2, openSync as openSync6, readFileSync as readFileSync4, readSync as readSync2, statSync as statSync10 } from "node:fs";
71293
71440
  var DESCRIPTION = `Read a file from the local filesystem.
71294
71441
 
71295
71442
  Returns numbered lines, each labelled with a short content anchor:
@@ -71326,7 +71473,7 @@ var ReadTool = class extends Tool {
71326
71473
  if (isImagePath(path) || isBinary(path) && looksLikeImage(path)) return readImage(path);
71327
71474
  if (isBinary(path)) {
71328
71475
  throw new ToolError(
71329
- `${path}: binary file (${count(statSync9(path).size, "byte")}), not read as text`
71476
+ `${path}: binary file (${count(statSync10(path).size, "byte")}), not read as text`
71330
71477
  );
71331
71478
  }
71332
71479
  const offset = Math.max(toInt(params.offset || 1), 1);
@@ -71417,16 +71564,16 @@ function clip2(line) {
71417
71564
  init_pathlib();
71418
71565
  import { randomBytes as randomBytes4 } from "node:crypto";
71419
71566
  import {
71420
- chmodSync as chmodSync2,
71567
+ chmodSync as chmodSync3,
71421
71568
  closeSync as closeSync7,
71422
71569
  existsSync as existsSync3,
71423
71570
  lstatSync as lstatSync2,
71424
- mkdirSync as mkdirSync10,
71571
+ mkdirSync as mkdirSync11,
71425
71572
  openSync as openSync7,
71426
71573
  renameSync as renameSync5,
71427
71574
  rmSync as rmSync3,
71428
- statSync as statSync10,
71429
- writeFileSync as writeFileSync7
71575
+ statSync as statSync11,
71576
+ writeFileSync as writeFileSync8
71430
71577
  } from "node:fs";
71431
71578
  var DESCRIPTION2 = `Write a file, overwriting it if it exists.
71432
71579
 
@@ -71472,7 +71619,7 @@ var WriteTool = class extends Tool {
71472
71619
  }
71473
71620
  }
71474
71621
  const existed = existsSync3(path);
71475
- mkdirSync10(parent(path), { recursive: true });
71622
+ mkdirSync11(parent(path), { recursive: true });
71476
71623
  this.checkpoints?.capture(path);
71477
71624
  writeAtomic(path, content);
71478
71625
  ctx.reads.markRead(path);
@@ -71506,7 +71653,7 @@ function writeAtomic(path, content) {
71506
71653
  }
71507
71654
  let mode2;
71508
71655
  try {
71509
- mode2 = statSync10(target).mode & 4095;
71656
+ mode2 = statSync11(target).mode & 4095;
71510
71657
  } catch (error) {
71511
71658
  if (error.code !== "ENOENT") throw error;
71512
71659
  mode2 = null;
@@ -71515,11 +71662,11 @@ function writeAtomic(path, content) {
71515
71662
  const fd = openSync7(tmp, "wx", mode2 ?? 438);
71516
71663
  try {
71517
71664
  try {
71518
- writeFileSync7(fd, content, "utf8");
71665
+ writeFileSync8(fd, content, "utf8");
71519
71666
  } finally {
71520
71667
  closeSync7(fd);
71521
71668
  }
71522
- if (mode2 !== null) chmodSync2(tmp, mode2);
71669
+ if (mode2 !== null) chmodSync3(tmp, mode2);
71523
71670
  renameSync5(tmp, target);
71524
71671
  } catch (error) {
71525
71672
  rmSync3(tmp, { force: true });
@@ -72415,7 +72562,7 @@ ${gate.contextText}`;
72415
72562
  };
72416
72563
  for (const call of calls) {
72417
72564
  if (this.runsSerially(call)) {
72418
- for (const pending of concurrent) await run(pending);
72565
+ if (concurrent.length) await gather(concurrent);
72419
72566
  concurrent = [];
72420
72567
  await run(call);
72421
72568
  } else concurrent.push(call);
@@ -72950,7 +73097,7 @@ init_lateinject();
72950
73097
  init_pathlib();
72951
73098
  init_py();
72952
73099
  import { spawnSync as spawnSync3 } from "node:child_process";
72953
- import { statSync as statSync11 } from "node:fs";
73100
+ import { statSync as statSync12 } from "node:fs";
72954
73101
  var STATUS_COMMAND = ["git", "status", "--porcelain=v2", "--branch", "-z"];
72955
73102
  var ROOT_COMMAND = ["git", "rev-parse", "--show-toplevel"];
72956
73103
  var TIMEOUT_SECONDS = 2;
@@ -73089,7 +73236,7 @@ var BranchWatcher = class {
73089
73236
  if (head4 === null) return null;
73090
73237
  let stamp;
73091
73238
  try {
73092
- const info = statSync11(head4, { bigint: true });
73239
+ const info = statSync12(head4, { bigint: true });
73093
73240
  stamp = `${info.mtimeNs}:${info.size}`;
73094
73241
  } catch {
73095
73242
  return this.current;
@@ -73193,7 +73340,7 @@ function statusCode(xy) {
73193
73340
  }
73194
73341
  function signature(path) {
73195
73342
  try {
73196
- const info = statSync11(path, { bigint: true });
73343
+ const info = statSync12(path, { bigint: true });
73197
73344
  return `${info.mtimeNs}:${info.size}`;
73198
73345
  } catch {
73199
73346
  return "none";
@@ -73383,6 +73530,29 @@ init_py();
73383
73530
  init_shlex();
73384
73531
  var OPERATORS = ["&&", "||", ";;", ";", "|&", "|", "\n", "&"];
73385
73532
  var DYNAMIC_EXECUTABLES = /* @__PURE__ */ new Set(["eval", "exec", "source", ".", "env"]);
73533
+ var WRAPPERS = /* @__PURE__ */ new Set([
73534
+ "builtin",
73535
+ "caffeinate",
73536
+ "chrt",
73537
+ "command",
73538
+ "doas",
73539
+ "flock",
73540
+ "gtimeout",
73541
+ "ionice",
73542
+ "nice",
73543
+ "nohup",
73544
+ "setsid",
73545
+ "stdbuf",
73546
+ "strace",
73547
+ "sudo",
73548
+ "taskset",
73549
+ "time",
73550
+ "timeout",
73551
+ "unbuffer",
73552
+ "watch",
73553
+ "xargs"
73554
+ ]);
73555
+ var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*\+?=/;
73386
73556
  var FD_DUP = /^\d*>&\d*-?$/;
73387
73557
  var OUT_OP = /^(?:\d*>>?|&>>?)$/;
73388
73558
  var IN_OP = /^\d*<<?-?$/;
@@ -73468,7 +73638,17 @@ var READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
73468
73638
  var MUTATING_FLAGS = {
73469
73639
  sed: /* @__PURE__ */ new Set(["-i", "--in-place"]),
73470
73640
  awk: /* @__PURE__ */ new Set(["-i", "--include"]),
73471
- find: /* @__PURE__ */ new Set(["-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprintf", "-fls"]),
73641
+ find: /* @__PURE__ */ new Set([
73642
+ "-delete",
73643
+ "-exec",
73644
+ "-execdir",
73645
+ "-ok",
73646
+ "-okdir",
73647
+ "-fprint",
73648
+ "-fprint0",
73649
+ "-fprintf",
73650
+ "-fls"
73651
+ ]),
73472
73652
  fd: /* @__PURE__ */ new Set(["-x", "--exec", "-X", "--exec-batch"]),
73473
73653
  sort: /* @__PURE__ */ new Set(["-o", "--output", "--compress-program"]),
73474
73654
  tree: /* @__PURE__ */ new Set(["-o"]),
@@ -73643,10 +73823,18 @@ function toSegment(text3, inSub) {
73643
73823
  if (tokens.length === 0) return [null, [], true];
73644
73824
  const [words, redirects] = splitRedirections(tokens);
73645
73825
  if (words.length === 0) return [null, redirects, true];
73646
- const executable = words[0];
73826
+ let first2 = 0;
73827
+ while (first2 < words.length && ASSIGNMENT.test(words[first2])) first2++;
73828
+ const executable = words[first2] ?? "";
73647
73829
  const ok = !executable.includes("$") && !executable.includes("`");
73648
73830
  return [
73649
- { raw: stripped, executable, args: words.slice(1), inSubstitution: inSub },
73831
+ {
73832
+ raw: stripped,
73833
+ executable,
73834
+ args: words.slice(first2 + 1),
73835
+ assignments: words.slice(0, first2),
73836
+ inSubstitution: inSub
73837
+ },
73650
73838
  redirects,
73651
73839
  ok
73652
73840
  ];
@@ -73726,13 +73914,20 @@ function scan(text3) {
73726
73914
  index++;
73727
73915
  continue;
73728
73916
  }
73729
- if (text3.startsWith("$(", index) || char === "`") {
73917
+ if (text3.startsWith("$(", index) || text3.startsWith("<(", index) || text3.startsWith(">(", index) || char === "`") {
73730
73918
  const [body, consumed, closed] = readSubstitution(text3, index);
73731
73919
  substitutions.push(...recurse(body, substitutions));
73732
73920
  ok = ok && closed;
73733
73921
  index += consumed;
73734
73922
  continue;
73735
73923
  }
73924
+ if (char === "(" && strip(buffer)) {
73925
+ const [body, consumed] = readSubstitution(text3, index);
73926
+ substitutions.push(...recurse(body, substitutions));
73927
+ ok = false;
73928
+ index += consumed;
73929
+ continue;
73930
+ }
73736
73931
  if (char === "(" || char === ")") {
73737
73932
  index++;
73738
73933
  continue;
@@ -73770,27 +73965,51 @@ function readSubstitution(text3, index) {
73770
73965
  if (end === -1) return [text3.slice(index + 1), text3.length - index, false];
73771
73966
  return [text3.slice(index + 1, end), end - index + 1, true];
73772
73967
  }
73968
+ const open = text3.indexOf("(", index);
73773
73969
  let depth = 0;
73774
- for (let cursor = index + 1; cursor < text3.length; cursor++) {
73970
+ for (let cursor = open; cursor < text3.length; cursor++) {
73775
73971
  if (text3[cursor] === "(") depth++;
73776
73972
  else if (text3[cursor] === ")") {
73777
73973
  depth--;
73778
- if (depth === 0) return [text3.slice(index + 2, cursor), cursor - index + 1, true];
73974
+ if (depth === 0) return [text3.slice(open + 1, cursor), cursor - index + 1, true];
73779
73975
  }
73780
73976
  }
73781
- return [text3.slice(index + 2), text3.length - index, false];
73977
+ return [text3.slice(open + 1), text3.length - index, false];
73782
73978
  }
73783
- function matchSpecifier(segment, pattern) {
73979
+ function matchSpecifier(segment, pattern, exact = false) {
73784
73980
  if (pattern === "*" || pattern === ":*") return true;
73785
- const commandLine = [segment.executable, ...segment.args].join(" ");
73981
+ if (exact) return matchWords(written(segment), pattern);
73982
+ return commandForms(segment).some((words) => matchWords(words, pattern));
73983
+ }
73984
+ function matchWords(words, pattern) {
73985
+ const commandLine = words.join(" ");
73786
73986
  if (pattern.endsWith(":*")) {
73787
73987
  const prefix = strip(pattern.slice(0, -2));
73788
73988
  return commandLine === prefix || commandLine.startsWith(`${prefix} `);
73789
73989
  }
73790
73990
  if (pattern.includes(" ")) return commandLine === pattern;
73791
- return segment.executable === pattern;
73991
+ return words[0] === pattern;
73992
+ }
73993
+ var written = (segment) => [...segment.assignments, segment.executable, ...segment.args].filter(Boolean);
73994
+ function commandForms(segment) {
73995
+ const words = [segment.executable, ...segment.args].filter(Boolean);
73996
+ const forms = [];
73997
+ const add2 = (command2) => {
73998
+ forms.push(command2);
73999
+ const name = basename2(command2[0]);
74000
+ if (name !== command2[0]) forms.push([name, ...command2.slice(1)]);
74001
+ };
74002
+ if (words.length === 0) return forms;
74003
+ add2(words);
74004
+ if (WRAPPERS.has(basename2(words[0]))) {
74005
+ for (let index = 1; index < words.length; index++)
74006
+ if (!words[index].startsWith("-")) add2(words.slice(index));
74007
+ }
74008
+ return forms;
73792
74009
  }
74010
+ var basename2 = (path) => path.slice(path.lastIndexOf("/") + 1);
73793
74011
  function isReadOnly(segment) {
74012
+ if (segment.assignments.length) return false;
73794
74013
  if (segment.executable === "git") return gitIsReadOnly(segment.args);
73795
74014
  if (!READ_ONLY_COMMANDS.has(segment.executable)) return false;
73796
74015
  if (segment.executable === "env") return !envRunsACommand(segment.args);
@@ -73803,13 +74022,13 @@ function isReadOnly(segment) {
73803
74022
  );
73804
74023
  }
73805
74024
  var PROGRAM_TEXT_COMMANDS = /* @__PURE__ */ new Set(["awk", "sed"]);
73806
- var AWK_WRITES = /system\s*\(|\|\s*["']|["']\s*\||print[f]?[^;}\n]*>/;
74025
+ var AWK_WRITES = /system\s*\(|\|\s*["']|["']\s*\||\|\s*getline\b|\|&|print[f]?[^;}\n]*[>|]|@(?:include|load)\b/;
73807
74026
  var PROGRAM_TEXT_OPTIONS = {
73808
74027
  awk: ["-e", "--source"],
73809
74028
  sed: ["-e", "--expression"]
73810
74029
  };
73811
74030
  var PROGRAM_FILE_OPTIONS = {
73812
- awk: ["-f", "--file", "-E", "--exec"],
74031
+ awk: ["-f", "--file", "-E", "--exec", "-l", "--load"],
73813
74032
  sed: ["-f", "--file"]
73814
74033
  };
73815
74034
  function namesAnOutput(segment) {
@@ -73980,7 +74199,182 @@ function splitGitSubcommand(args) {
73980
74199
  return [null, []];
73981
74200
  }
73982
74201
 
74202
+ // src/permissions/sandbox.ts
74203
+ init_pathlib();
74204
+ init_which();
74205
+ import { randomBytes as randomBytes6 } from "node:crypto";
74206
+ import { closeSync as closeSync8, existsSync as existsSync4, mkdirSync as mkdirSync12, openSync as openSync8, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "node:fs";
74207
+ import { tmpdir as tmpdir2 } from "node:os";
74208
+ var CREDENTIAL_PATHS = [
74209
+ "~/.ssh/",
74210
+ "~/.aws/",
74211
+ "~/.gnupg/",
74212
+ "~/.kube/",
74213
+ "~/.docker/config.json",
74214
+ "~/.config/gh/",
74215
+ "~/.netrc",
74216
+ "~/.npmrc",
74217
+ "~/.pypirc",
74218
+ "~/.hx/auth.json"
74219
+ ];
74220
+ var Sandbox = class {
74221
+ constructor(policy, backend) {
74222
+ this.policy = policy;
74223
+ this.backend = backend ?? detectBackend();
74224
+ }
74225
+ policy;
74226
+ backend;
74227
+ profilePath = null;
74228
+ /** False when degraded to `none`. */
74229
+ get active() {
74230
+ return this.backend !== "none";
74231
+ }
74232
+ /** The argv to actually exec. */
74233
+ wrap(argv) {
74234
+ if (this.backend === "seatbelt")
74235
+ return ["/usr/bin/sandbox-exec", "-f", this.writeProfile(), ...argv];
74236
+ if (this.backend === "bubblewrap") return buildBwrapArgv(this.policy, argv);
74237
+ return [...argv];
74238
+ }
74239
+ /** The generated Seatbelt profile - shown by `/permissions`. */
74240
+ profileText() {
74241
+ return buildSeatbeltProfile(this.policy);
74242
+ }
74243
+ writeProfile() {
74244
+ if (this.profilePath === null || !existsSync4(this.profilePath)) {
74245
+ const path = join(tmpdir2(), `hx-sandbox-${randomBytes6(4).toString("hex")}.sb`);
74246
+ writeFileSync9(path, this.profileText(), { mode: 384 });
74247
+ this.profilePath = path;
74248
+ }
74249
+ return this.profilePath;
74250
+ }
74251
+ cleanup() {
74252
+ if (this.profilePath !== null) {
74253
+ rmSync4(this.profilePath, { force: true });
74254
+ this.profilePath = null;
74255
+ }
74256
+ }
74257
+ };
74258
+ function detectBackend() {
74259
+ if (process.platform === "darwin" && existsSync4("/usr/bin/sandbox-exec")) return "seatbelt";
74260
+ if (process.platform === "linux" && which("bwrap")) return "bubblewrap";
74261
+ return "none";
74262
+ }
74263
+ function defaultPolicy(cwd, allowNetwork = false) {
74264
+ const temp = resolve(tmpdir2());
74265
+ const writable = [resolve(cwd), ...tempContainer(temp)];
74266
+ for (const extra of ["~/.cache", "~/Library/Caches"]) {
74267
+ const candidate = expandUser(extra);
74268
+ if (isDir(candidate)) writable.push(resolve(candidate));
74269
+ }
74270
+ return {
74271
+ writablePaths: [...new Set(writable)],
74272
+ readablePaths: ["/"],
74273
+ denyPaths: CREDENTIAL_PATHS.map((p) => ({
74274
+ path: real(absolute(expandUser(p))),
74275
+ isDir: p.endsWith("/")
74276
+ })),
74277
+ allowNetwork,
74278
+ allowSubprocess: true,
74279
+ cwd: resolve(cwd)
74280
+ };
74281
+ }
74282
+ var DARWIN_TEMP_ROOT = "/private/var/folders";
74283
+ var isRelativeTo = (path, root) => path === root || path.startsWith(`${root.replace(/\/$/, "")}/`);
74284
+ function tempContainer(temp) {
74285
+ const paths = [temp];
74286
+ if (isRelativeTo(temp, DARWIN_TEMP_ROOT)) {
74287
+ const container = parent(temp);
74288
+ if (isRelativeTo(container, DARWIN_TEMP_ROOT) && container !== DARWIN_TEMP_ROOT)
74289
+ paths.push(container);
74290
+ }
74291
+ return paths;
74292
+ }
74293
+ function buildSeatbeltProfile(policy) {
74294
+ const lines2 = [
74295
+ "(version 1)",
74296
+ "(deny default)",
74297
+ "",
74298
+ ";; Process management",
74299
+ "(allow process-fork)",
74300
+ "(allow signal (target same-sandbox))",
74301
+ "(allow sysctl-read)",
74302
+ "(allow mach-lookup)",
74303
+ "(allow ipc-posix-shm)",
74304
+ policy.allowSubprocess ? "(allow process-exec*)" : "(deny process-exec*)",
74305
+ "",
74306
+ ";; Reads"
74307
+ ];
74308
+ for (const path of policy.readablePaths.length ? policy.readablePaths : ["/"]) {
74309
+ lines2.push(`(allow file-read* (subpath ${sbplString(real(path))}))`);
74310
+ }
74311
+ lines2.push("", ";; Writes");
74312
+ for (const path of policy.writablePaths)
74313
+ lines2.push(`(allow file-write* (subpath ${sbplString(real(path))}))`);
74314
+ for (const device of ["/dev/null", "/dev/stdout", "/dev/stderr", "/dev/tty", "/dev/urandom"]) {
74315
+ lines2.push(`(allow file-write-data file-read-data (literal ${sbplString(device)}))`);
74316
+ }
74317
+ lines2.push("", ";; Network", policy.allowNetwork ? "(allow network*)" : "(deny network*)");
74318
+ if (policy.denyPaths.length) {
74319
+ lines2.push("", ";; Credentials - last match wins, so these override both allowances");
74320
+ for (const denied of policy.denyPaths) {
74321
+ lines2.push(`(deny file-read* file-write* (subpath ${sbplString(real(denied.path))}))`);
74322
+ }
74323
+ }
74324
+ return `${lines2.join("\n")}
74325
+ `;
74326
+ }
74327
+ function credentialPathOf(path) {
74328
+ for (const entry of CREDENTIAL_PATHS) {
74329
+ const denied = real(absolute(expandUser(entry)));
74330
+ if (path === denied || entry.endsWith("/") && isRelativeTo(path, denied)) return entry;
74331
+ }
74332
+ return null;
74333
+ }
74334
+ var real = (path) => resolve(path);
74335
+ function sbplString(path) {
74336
+ return `"${path.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
74337
+ }
74338
+ function buildBwrapArgv(policy, argv) {
74339
+ const command2 = [
74340
+ "bwrap",
74341
+ "--ro-bind",
74342
+ "/",
74343
+ "/",
74344
+ "--dev",
74345
+ "/dev",
74346
+ "--proc",
74347
+ "/proc",
74348
+ "--die-with-parent"
74349
+ ];
74350
+ const writable = policy.writablePaths.map(real);
74351
+ for (const path of writable) command2.push("--bind", path, path);
74352
+ for (const denied of policy.denyPaths) {
74353
+ const path = real(denied.path);
74354
+ if (!existsSync4(path) && !ensureMountpoint(denied, writable)) continue;
74355
+ if (isDir(path)) command2.push("--tmpfs", path, "--remount-ro", path);
74356
+ else command2.push("--ro-bind", "/dev/null", path);
74357
+ }
74358
+ if (!policy.allowNetwork) command2.push("--unshare-net");
74359
+ if (policy.cwd !== null) command2.push("--chdir", real(policy.cwd));
74360
+ return [...command2, "--", ...argv];
74361
+ }
74362
+ function ensureMountpoint(denied, writable) {
74363
+ const path = real(denied.path);
74364
+ if (!writable.some((root) => isRelativeTo(path, root))) return false;
74365
+ try {
74366
+ mkdirSync12(parent(path), { recursive: true });
74367
+ if (denied.isDir) mkdirSync12(path, { mode: 448, recursive: true });
74368
+ else closeSync8(openSync8(path, "a", 384));
74369
+ } catch {
74370
+ return false;
74371
+ }
74372
+ return existsSync4(path);
74373
+ }
74374
+
73983
74375
  // src/permissions/engine.ts
74376
+ var FILE_TOOLS = /* @__PURE__ */ new Set(["Read", "Write", "Edit", "Grep", "Glob", "Symbols"]);
74377
+ var PROTECTED_DIRS = /* @__PURE__ */ new Set([".git", ".hx"]);
73984
74378
  var DEFAULT_MUTATING_TOOLS = /* @__PURE__ */ new Set(["Bash", "Write", "Edit", "Task"]);
73985
74379
  function allSpecifiers(request2) {
73986
74380
  if (request2.specifiers?.length) return request2.specifiers;
@@ -74018,6 +74412,12 @@ var PermissionEngine = class _PermissionEngine {
74018
74412
  if (denial) return outcome("deny", "matched a deny rule", denial);
74019
74413
  if (this.sessionGrants.has(this.grantKey(request2)))
74020
74414
  return outcome("allow", "granted for this session");
74415
+ const credential = this.credentialTouched(request2);
74416
+ if (credential !== null) {
74417
+ const named = this.matchAllow(request2, true);
74418
+ if (named) return outcome("allow", "matched an allow rule", named);
74419
+ return outcome("ask", `${credential} holds credentials`);
74420
+ }
74021
74421
  if (request2.toolName === "Bash" && request2.specifier) {
74022
74422
  const parsed = parse4(request2.specifier);
74023
74423
  if (parsed.unparseable) {
@@ -74039,7 +74439,7 @@ var PermissionEngine = class _PermissionEngine {
74039
74439
  return outcome("allow", "read-only tool in plan mode");
74040
74440
  }
74041
74441
  if (!request2.mutating) return outcome("allow", "read-only tool");
74042
- if (this.mode === "acceptEdits" && _PermissionEngine.EDIT_TOOLS.has(request2.toolName)) {
74442
+ if (this.mode === "acceptEdits" && _PermissionEngine.EDIT_TOOLS.has(request2.toolName) && this.editsProject(request2)) {
74043
74443
  return outcome("allow", "acceptEdits mode");
74044
74444
  }
74045
74445
  if (request2.toolName === "Bash" && request2.specifier) {
@@ -74051,6 +74451,33 @@ var PermissionEngine = class _PermissionEngine {
74051
74451
  }
74052
74452
  return outcome("ask", "mutating tool");
74053
74453
  }
74454
+ /** Where `specifier` really is: absolute, with `..` and symlinks resolved. */
74455
+ target(specifier) {
74456
+ return realPath(purePath(expandUser(specifier)), resolve(this.cwd));
74457
+ }
74458
+ /** The credential file or folder a file tool's call reaches, as written in
74459
+ * `CREDENTIAL_PATHS`, or `null`. */
74460
+ credentialTouched(request2) {
74461
+ if (!FILE_TOOLS.has(request2.toolName)) return null;
74462
+ for (const specifier of allSpecifiers(request2)) {
74463
+ const credential = credentialPathOf(this.target(specifier));
74464
+ if (credential !== null) return credential;
74465
+ }
74466
+ return null;
74467
+ }
74468
+ /**
74469
+ * Whether every file an edit changes is the project's own, which is all
74470
+ * acceptEdits covers: inside the working directory once symlinks are
74471
+ * resolved, and not in a folder whose files run code or grant permissions.
74472
+ */
74473
+ editsProject(request2) {
74474
+ const root = resolve(this.cwd);
74475
+ const prefix = root === "/" ? "/" : `${root}/`;
74476
+ const targets = allSpecifiers(request2).map((specifier) => this.target(specifier));
74477
+ return targets.length > 0 && targets.every(
74478
+ (target) => target.startsWith(prefix) && !target.slice(prefix.length).split("/").some((part) => PROTECTED_DIRS.has(part))
74479
+ );
74480
+ }
74054
74481
  applies(rule2, request2) {
74055
74482
  return rule2.tool === request2.toolName || rule2.tool === "*";
74056
74483
  }
@@ -74062,13 +74489,14 @@ var PermissionEngine = class _PermissionEngine {
74062
74489
  return null;
74063
74490
  }
74064
74491
  /**
74065
- * Allow requires *every* segment to be covered. Any-segment matching is
74066
- * right for deny (`safe && danger` must be caught) but catastrophic for
74067
- * allow: `allow: Bash(git status:*)` would green-light `git status && rm -rf /`.
74492
+ * Allow requires *every* segment to be covered, each as it is written.
74493
+ * Any-segment matching is right for deny (`safe && danger` must be caught)
74494
+ * but catastrophic for allow: `allow: Bash(git status:*)` would green-light
74495
+ * `git status && rm -rf /`.
74068
74496
  */
74069
- matchAllow(request2) {
74497
+ matchAllow(request2, named = false) {
74070
74498
  const candidates2 = this.rules.filter(
74071
- (rule2) => rule2.decision === "allow" && this.applies(rule2, request2)
74499
+ (rule2) => rule2.decision === "allow" && this.applies(rule2, request2) && (!named || rule2.specifier !== null)
74072
74500
  );
74073
74501
  if (candidates2.length === 0) return null;
74074
74502
  if (request2.toolName !== "Bash" || !request2.specifier) {
@@ -74086,7 +74514,7 @@ var PermissionEngine = class _PermissionEngine {
74086
74514
  let first2 = null;
74087
74515
  for (const segment of parsed.segments) {
74088
74516
  const covering = candidates2.find(
74089
- (rule2) => rule2.specifier === null || matchSpecifier(segment, rule2.specifier)
74517
+ (rule2) => rule2.specifier === null || matchSpecifier(segment, rule2.specifier, true)
74090
74518
  );
74091
74519
  if (covering === void 0) return null;
74092
74520
  first2 ??= covering;
@@ -74193,6 +74621,7 @@ function persistableRules(toolName, specifier) {
74193
74621
  return rules;
74194
74622
  }
74195
74623
  function rulePrefix(segment) {
74624
+ if (segment.assignments.length || !segment.executable) return null;
74196
74625
  const first2 = segment.args[0];
74197
74626
  if (first2 !== void 0 && !first2.startsWith("-")) return `${segment.executable} ${first2}`;
74198
74627
  return null;
@@ -74361,172 +74790,6 @@ var InvalidRule = class extends Error {
74361
74790
  name = "InvalidRule";
74362
74791
  };
74363
74792
 
74364
- // src/permissions/sandbox.ts
74365
- init_pathlib();
74366
- init_which();
74367
- import { randomBytes as randomBytes6 } from "node:crypto";
74368
- import { closeSync as closeSync8, existsSync as existsSync4, mkdirSync as mkdirSync11, openSync as openSync8, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "node:fs";
74369
- import { tmpdir as tmpdir2 } from "node:os";
74370
- var CREDENTIAL_PATHS = [
74371
- "~/.ssh/",
74372
- "~/.aws/",
74373
- "~/.gnupg/",
74374
- "~/.kube/",
74375
- "~/.docker/config.json",
74376
- "~/.config/gh/",
74377
- "~/.netrc",
74378
- "~/.npmrc",
74379
- "~/.pypirc",
74380
- "~/.hx/auth.json"
74381
- ];
74382
- var Sandbox = class {
74383
- constructor(policy, backend) {
74384
- this.policy = policy;
74385
- this.backend = backend ?? detectBackend();
74386
- }
74387
- policy;
74388
- backend;
74389
- profilePath = null;
74390
- /** False when degraded to `none`. */
74391
- get active() {
74392
- return this.backend !== "none";
74393
- }
74394
- /** The argv to actually exec. */
74395
- wrap(argv) {
74396
- if (this.backend === "seatbelt")
74397
- return ["/usr/bin/sandbox-exec", "-f", this.writeProfile(), ...argv];
74398
- if (this.backend === "bubblewrap") return buildBwrapArgv(this.policy, argv);
74399
- return [...argv];
74400
- }
74401
- /** The generated Seatbelt profile - shown by `/permissions`. */
74402
- profileText() {
74403
- return buildSeatbeltProfile(this.policy);
74404
- }
74405
- writeProfile() {
74406
- if (this.profilePath === null || !existsSync4(this.profilePath)) {
74407
- const path = join(tmpdir2(), `hx-sandbox-${randomBytes6(4).toString("hex")}.sb`);
74408
- writeFileSync8(path, this.profileText(), { mode: 384 });
74409
- this.profilePath = path;
74410
- }
74411
- return this.profilePath;
74412
- }
74413
- cleanup() {
74414
- if (this.profilePath !== null) {
74415
- rmSync4(this.profilePath, { force: true });
74416
- this.profilePath = null;
74417
- }
74418
- }
74419
- };
74420
- function detectBackend() {
74421
- if (process.platform === "darwin" && existsSync4("/usr/bin/sandbox-exec")) return "seatbelt";
74422
- if (process.platform === "linux" && which("bwrap")) return "bubblewrap";
74423
- return "none";
74424
- }
74425
- function defaultPolicy(cwd, allowNetwork = false) {
74426
- const temp = resolve(tmpdir2());
74427
- const writable = [resolve(cwd), ...tempContainer(temp)];
74428
- for (const extra of ["~/.cache", "~/Library/Caches"]) {
74429
- const candidate = expandUser(extra);
74430
- if (isDir(candidate)) writable.push(resolve(candidate));
74431
- }
74432
- return {
74433
- writablePaths: [...new Set(writable)],
74434
- readablePaths: ["/"],
74435
- denyPaths: CREDENTIAL_PATHS.map((p) => ({
74436
- path: real(absolute(expandUser(p))),
74437
- isDir: p.endsWith("/")
74438
- })),
74439
- allowNetwork,
74440
- allowSubprocess: true,
74441
- cwd: resolve(cwd)
74442
- };
74443
- }
74444
- var DARWIN_TEMP_ROOT = "/private/var/folders";
74445
- var isRelativeTo = (path, root) => path === root || path.startsWith(`${root.replace(/\/$/, "")}/`);
74446
- function tempContainer(temp) {
74447
- const paths = [temp];
74448
- if (isRelativeTo(temp, DARWIN_TEMP_ROOT)) {
74449
- const container = parent(temp);
74450
- if (isRelativeTo(container, DARWIN_TEMP_ROOT) && container !== DARWIN_TEMP_ROOT)
74451
- paths.push(container);
74452
- }
74453
- return paths;
74454
- }
74455
- function buildSeatbeltProfile(policy) {
74456
- const lines2 = [
74457
- "(version 1)",
74458
- "(deny default)",
74459
- "",
74460
- ";; Process management",
74461
- "(allow process-fork)",
74462
- "(allow signal (target same-sandbox))",
74463
- "(allow sysctl-read)",
74464
- "(allow mach-lookup)",
74465
- "(allow ipc-posix-shm)",
74466
- policy.allowSubprocess ? "(allow process-exec*)" : "(deny process-exec*)",
74467
- "",
74468
- ";; Reads"
74469
- ];
74470
- for (const path of policy.readablePaths.length ? policy.readablePaths : ["/"]) {
74471
- lines2.push(`(allow file-read* (subpath ${sbplString(real(path))}))`);
74472
- }
74473
- lines2.push("", ";; Writes");
74474
- for (const path of policy.writablePaths)
74475
- lines2.push(`(allow file-write* (subpath ${sbplString(real(path))}))`);
74476
- for (const device of ["/dev/null", "/dev/stdout", "/dev/stderr", "/dev/tty", "/dev/urandom"]) {
74477
- lines2.push(`(allow file-write-data file-read-data (literal ${sbplString(device)}))`);
74478
- }
74479
- lines2.push("", ";; Network", policy.allowNetwork ? "(allow network*)" : "(deny network*)");
74480
- if (policy.denyPaths.length) {
74481
- lines2.push("", ";; Credentials - last match wins, so these override both allowances");
74482
- for (const denied of policy.denyPaths) {
74483
- lines2.push(`(deny file-read* file-write* (subpath ${sbplString(real(denied.path))}))`);
74484
- }
74485
- }
74486
- return `${lines2.join("\n")}
74487
- `;
74488
- }
74489
- var real = (path) => resolve(path);
74490
- function sbplString(path) {
74491
- return `"${path.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
74492
- }
74493
- function buildBwrapArgv(policy, argv) {
74494
- const command2 = [
74495
- "bwrap",
74496
- "--ro-bind",
74497
- "/",
74498
- "/",
74499
- "--dev",
74500
- "/dev",
74501
- "--proc",
74502
- "/proc",
74503
- "--die-with-parent"
74504
- ];
74505
- const writable = policy.writablePaths.map(real);
74506
- for (const path of writable) command2.push("--bind", path, path);
74507
- for (const denied of policy.denyPaths) {
74508
- const path = real(denied.path);
74509
- if (!existsSync4(path) && !ensureMountpoint(denied, writable)) continue;
74510
- if (isDir(path)) command2.push("--tmpfs", path, "--remount-ro", path);
74511
- else command2.push("--ro-bind", "/dev/null", path);
74512
- }
74513
- if (!policy.allowNetwork) command2.push("--unshare-net");
74514
- if (policy.cwd !== null) command2.push("--chdir", real(policy.cwd));
74515
- return [...command2, "--", ...argv];
74516
- }
74517
- function ensureMountpoint(denied, writable) {
74518
- const path = real(denied.path);
74519
- if (!writable.some((root) => isRelativeTo(path, root))) return false;
74520
- try {
74521
- mkdirSync11(parent(path), { recursive: true });
74522
- if (denied.isDir) mkdirSync11(path, { mode: 448, recursive: true });
74523
- else closeSync8(openSync8(path, "a", 384));
74524
- } catch {
74525
- return false;
74526
- }
74527
- return existsSync4(path);
74528
- }
74529
-
74530
74793
  // src/runtime.ts
74531
74794
  init_models();
74532
74795
  init_registry();
@@ -74668,7 +74931,7 @@ init_py();
74668
74931
  init_shlex();
74669
74932
  import { execFile as execFile2, spawn as spawn7 } from "node:child_process";
74670
74933
  import { randomBytes as randomBytes7 } from "node:crypto";
74671
- import { closeSync as closeSync9, mkdirSync as mkdirSync12, openSync as openSync9, readFileSync as readFileSync6 } from "node:fs";
74934
+ import { closeSync as closeSync9, mkdirSync as mkdirSync13, openSync as openSync9, readFileSync as readFileSync6 } from "node:fs";
74672
74935
  import { constants as constants3 } from "node:os";
74673
74936
  var DESCRIPTION5 = `Run a shell command in a persistent session.
74674
74937
 
@@ -74742,11 +75005,17 @@ var Reader = class {
74742
75005
  return this.chunks.shift() ?? Buffer.alloc(0);
74743
75006
  }
74744
75007
  };
75008
+ var POSIX_SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh", "ksh93", "mksh", "ash", "yash"]);
75009
+ function posixShell(shell = null) {
75010
+ const chosen = shell || process.env.SHELL || "";
75011
+ if (POSIX_SHELLS.has(chosen.slice(chosen.lastIndexOf("/") + 1))) return chosen;
75012
+ return isFile("/bin/bash") ? "/bin/bash" : "/bin/sh";
75013
+ }
74745
75014
  var PersistentShell = class {
74746
75015
  constructor(currentCwd, shell = null, env = {}, sandbox = null) {
74747
75016
  this.currentCwd = currentCwd;
74748
75017
  this.sandbox = sandbox;
74749
- this.shell = shell || process.env.SHELL || "/bin/bash";
75018
+ this.shell = posixShell(shell);
74750
75019
  this.env = commandEnvironment({ ...env, HX: "1", TERM: "dumb" });
74751
75020
  }
74752
75021
  currentCwd;
@@ -74949,7 +75218,7 @@ var BackgroundJobs = class {
74949
75218
  */
74950
75219
  async start(command2, cwd, env, sandbox = null) {
74951
75220
  const jobId = `bg_${randomBytes7(4).toString("hex")}`;
74952
- mkdirSync12(this.logDir, { recursive: true });
75221
+ mkdirSync13(this.logDir, { recursive: true });
74953
75222
  const logPath = join(this.logDir, `${jobId}.log`);
74954
75223
  let argv = ["/bin/sh", "-c", command2];
74955
75224
  if (sandbox !== null) argv = sandbox.wrap(argv);
@@ -75228,7 +75497,7 @@ init_json();
75228
75497
  init_py();
75229
75498
 
75230
75499
  // src/tools/glob.ts
75231
- import { statSync as statSync12 } from "node:fs";
75500
+ import { statSync as statSync13 } from "node:fs";
75232
75501
  init_pathlib();
75233
75502
  init_py();
75234
75503
  var DESCRIPTION6 = `Find files by glob pattern (e.g. \`**/*.py\`, \`src/**/*.ts\`).
@@ -75259,6 +75528,10 @@ var GlobTool = class extends Tool {
75259
75528
  required: ["pattern"]
75260
75529
  };
75261
75530
  }
75531
+ /** The folder searched, so a path rule covers a search of it. */
75532
+ permissionSpecifier(params) {
75533
+ return typeof params.path === "string" && params.path ? params.path : null;
75534
+ }
75262
75535
  async run(params, ctx) {
75263
75536
  const root = searchRoot(params.path, ctx.cwd);
75264
75537
  if (!isDir(root)) throw new ToolError(`${root}: not a directory`);
@@ -75301,7 +75574,7 @@ function checkPattern(pattern) {
75301
75574
  var ignored = (path) => parts(path).some((part) => IGNORED_DIRS.has(part));
75302
75575
  function mtime(path) {
75303
75576
  try {
75304
- return statSync12(path).mtimeMs;
75577
+ return statSync13(path).mtimeMs;
75305
75578
  } catch {
75306
75579
  return 0;
75307
75580
  }
@@ -75343,6 +75616,10 @@ var GrepTool = class extends Tool {
75343
75616
  required: ["pattern"]
75344
75617
  };
75345
75618
  }
75619
+ /** The file or folder searched, so a path rule covers a search of it. */
75620
+ permissionSpecifier(params) {
75621
+ return typeof params.path === "string" && params.path ? params.path : null;
75622
+ }
75346
75623
  async run(params, ctx) {
75347
75624
  const pattern = String(params.pattern);
75348
75625
  const ripgrep = which("rg");
@@ -75397,7 +75674,28 @@ function runRipgrep(rg, pattern, root, mode2, params) {
75397
75674
  }
75398
75675
  throw new ToolError(stderr || `ripgrep exited ${result2.status}`);
75399
75676
  }
75400
- return splitLines(result2.stdout ?? "").filter(Boolean);
75677
+ const hidden = credentialsUnder(root);
75678
+ return splitLines(result2.stdout ?? "").filter(
75679
+ (line) => line && !hidden.some((credential) => credential.covers(line))
75680
+ );
75681
+ }
75682
+ function credentialsUnder(root) {
75683
+ const base = root.replace(/\/$/, "");
75684
+ const real2 = resolve(root);
75685
+ const found = [];
75686
+ for (const entry of CREDENTIAL_PATHS) {
75687
+ const spellings = /* @__PURE__ */ new Set([purePath(absolute(expandUser(entry))), resolve(expandUser(entry))]);
75688
+ for (const path of spellings) {
75689
+ let shown;
75690
+ if (path.startsWith(`${real2}/`)) shown = base + path.slice(real2.length);
75691
+ else if (path.startsWith(`${base}/`)) shown = path;
75692
+ else continue;
75693
+ found.push({
75694
+ covers: entry.endsWith("/") ? (line) => line.startsWith(`${shown}/`) : (line) => line === shown || line.startsWith(`${shown}:`) || line.startsWith(`${shown}-`)
75695
+ });
75696
+ }
75697
+ }
75698
+ return found;
75401
75699
  }
75402
75700
  function search(regex, root, mode2, params) {
75403
75701
  const globFilter = String(params.glob || "");
@@ -75424,7 +75722,7 @@ function search(regex, root, mode2, params) {
75424
75722
  function walk(root) {
75425
75723
  if (isFile(root)) return [root];
75426
75724
  return glob(root, "**/*").filter(
75427
- (path) => isFile(path) && !parts(path).some((part) => IGNORED_DIRS.has(part))
75725
+ (path) => isFile(path) && !parts(path).some((part) => IGNORED_DIRS.has(part)) && credentialPathOf(resolve(path)) === null
75428
75726
  );
75429
75727
  }
75430
75728
  function renderContext(path, lines2, hits, before2, after2) {
@@ -75437,7 +75735,7 @@ function renderContext(path, lines2, hits, before2, after2) {
75437
75735
  }
75438
75736
 
75439
75737
  // src/tools/symbols.ts
75440
- import { readFileSync as readFileSync8, statSync as statSync13 } from "node:fs";
75738
+ import { readFileSync as readFileSync8, statSync as statSync14 } from "node:fs";
75441
75739
  import { createRequire } from "node:module";
75442
75740
  import { dirname } from "node:path";
75443
75741
  init_json();
@@ -75569,6 +75867,12 @@ var SymbolsTool = class extends Tool {
75569
75867
  required: ["mode"]
75570
75868
  };
75571
75869
  }
75870
+ /** The file or folder read, so a path rule covers it. */
75871
+ permissionSpecifier(params) {
75872
+ for (const key2 of ["file_path", "path"])
75873
+ if (typeof params[key2] === "string" && params[key2]) return params[key2];
75874
+ return null;
75875
+ }
75572
75876
  async run(params, ctx) {
75573
75877
  const mode2 = String(params.mode);
75574
75878
  const limit = toInt(params.limit || DEFAULT_LIMIT3);
@@ -75677,7 +75981,7 @@ function candidates(params, ctx) {
75677
75981
  }
75678
75982
  function mtime2(path) {
75679
75983
  try {
75680
- return statSync13(path).mtimeMs;
75984
+ return statSync14(path).mtimeMs;
75681
75985
  } catch {
75682
75986
  return 0;
75683
75987
  }
@@ -75706,7 +76010,7 @@ function specFor(path) {
75706
76010
  async function parse5(path, spec2) {
75707
76011
  let size3;
75708
76012
  try {
75709
- size3 = statSync13(path).size;
76013
+ size3 = statSync14(path).size;
75710
76014
  } catch (error) {
75711
76015
  throw new ToolError(`${path}: ${osErrorText(error)}`);
75712
76016
  }
@@ -76413,7 +76717,7 @@ ${listed2}
76413
76717
  They still apply to this project on this machine. Earlier versions of HX wrote them into the checkout; it no longer writes there at all.`
76414
76718
  ];
76415
76719
  }
76416
- function buildRuntime(parsed, resume = null) {
76720
+ function buildRuntime(parsed, resume = null, signedOut = false) {
76417
76721
  ensureUserDirs();
76418
76722
  const settings = loadSettings(parsed.cwd, parsed.overrides);
76419
76723
  const bus = new EventBus();
@@ -76424,10 +76728,16 @@ function buildRuntime(parsed, resume = null) {
76424
76728
  models.loadCache();
76425
76729
  const modelInfo2 = models.getOrDefault(settings.models.model);
76426
76730
  const session = resume ? loadSession(resume) : newSession(settings.cwd, settings.models.model);
76427
- const provider = buildProvider(settings.models.model, auth, {
76428
- sessionId: session.meta.sessionId,
76429
- models
76430
- });
76731
+ let provider;
76732
+ try {
76733
+ provider = buildProvider(settings.models.model, auth, {
76734
+ sessionId: session.meta.sessionId,
76735
+ models
76736
+ });
76737
+ } catch (error) {
76738
+ if (!(signedOut && error instanceof MissingCredential)) throw error;
76739
+ provider = new DeferredProvider(settings.models.model, auth, session.meta.sessionId, models);
76740
+ }
76431
76741
  const sandbox = settings.permissions.sandbox ? new Sandbox(defaultPolicy(settings.cwd, settings.permissions.allowNetwork)) : null;
76432
76742
  const shell = new PersistentShell(settings.cwd, settings.bash.shell, {}, sandbox);
76433
76743
  const jobs = new BackgroundJobs(join(sessionOutputsDir(session.meta.sessionId), "jobs"));
@@ -76460,6 +76770,13 @@ function buildRuntime(parsed, resume = null) {
76460
76770
  `Ignored ${hooks.ignored.length} hook(s) declared in the project's shared settings - hooks run shell commands, so they are only loaded from your own settings. See /hooks.`
76461
76771
  );
76462
76772
  }
76773
+ if (settings.bash.shell && posixShell(settings.bash.shell) !== settings.bash.shell)
76774
+ notices.push(
76775
+ `bash.shell is ${settings.bash.shell}, which does not speak POSIX sh; commands run in ${shell.shell}.`
76776
+ );
76777
+ const [mcpServers, waitingServers] = partition(loadConfigs(settings.cwd), settings.cwd);
76778
+ const waiting = waitingServers.filter((server) => server.config.enabled);
76779
+ for (const server of waiting) notices.push(waitingNotice(server));
76463
76780
  const agents = new Map(discover2(settings.cwd).map((agent) => [agent.name, agent]));
76464
76781
  const subagents = new SubagentRunner({
76465
76782
  definitions: agents,
@@ -76520,7 +76837,7 @@ function buildRuntime(parsed, resume = null) {
76520
76837
  tools,
76521
76838
  checkpoints,
76522
76839
  hooks,
76523
- new MCPManager(loadConfigs(settings.cwd)),
76840
+ new MCPManager(mcpServers, void 0, waiting),
76524
76841
  notices
76525
76842
  );
76526
76843
  }
@@ -76540,7 +76857,7 @@ ${listed2}`,
76540
76857
 
76541
76858
  // src/trace.ts
76542
76859
  import { spawn as spawn8 } from "node:child_process";
76543
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync9 } from "node:fs";
76860
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync10 } from "node:fs";
76544
76861
  import { pathToFileURL } from "node:url";
76545
76862
  init_messages();
76546
76863
  init_json();
@@ -76584,8 +76901,8 @@ function renderHtml(trace) {
76584
76901
  return trace_template_default.replace(/__HX_TRACE_(?:TITLE|DATA)__/g, (match4) => filled[match4]);
76585
76902
  }
76586
76903
  function writeTrace(trace, path) {
76587
- mkdirSync13(parent(path), { recursive: true });
76588
- writeFileSync9(path, renderHtml(trace), "utf8");
76904
+ mkdirSync14(parent(path), { recursive: true });
76905
+ writeFileSync10(path, renderHtml(trace), "utf8");
76589
76906
  return path;
76590
76907
  }
76591
76908
  var defaultTracePath = (session) => join(sessionDir(session.meta.sessionId), TRACE_FILENAME);
@@ -77883,8 +78200,8 @@ var ProcessTerminal = class {
77883
78200
  let buffer = Buffer.from(data);
77884
78201
  while (buffer.length) {
77885
78202
  try {
77886
- const written = writeSync5(1, buffer);
77887
- buffer = buffer.subarray(written);
78203
+ const written2 = writeSync5(1, buffer);
78204
+ buffer = buffer.subarray(written2);
77888
78205
  } catch (error) {
77889
78206
  if (error.code === "EAGAIN") continue;
77890
78207
  return;
@@ -78292,13 +78609,302 @@ var ShiftEnter = class {
78292
78609
  }
78293
78610
  };
78294
78611
 
78612
+ // src/term/component.ts
78613
+ var Widget = class {
78614
+ cache = [];
78615
+ cacheWidth = -1;
78616
+ isDirty = true;
78617
+ render(width) {
78618
+ if (this.isDirty || width !== this.cacheWidth) {
78619
+ this.cache = this.draw(width);
78620
+ this.cacheWidth = width;
78621
+ this.isDirty = false;
78622
+ }
78623
+ return this.cache;
78624
+ }
78625
+ invalidate() {
78626
+ this.isDirty = true;
78627
+ }
78628
+ get dirty() {
78629
+ return this.isDirty;
78630
+ }
78631
+ };
78632
+ var Container = class extends Widget {
78633
+ children;
78634
+ constructor(...children) {
78635
+ super();
78636
+ this.children = children;
78637
+ }
78638
+ add(child) {
78639
+ this.children.push(child);
78640
+ this.invalidate();
78641
+ return child;
78642
+ }
78643
+ extend(children) {
78644
+ this.children.push(...children);
78645
+ this.invalidate();
78646
+ }
78647
+ clear() {
78648
+ this.children = [];
78649
+ this.invalidate();
78650
+ }
78651
+ remove(child) {
78652
+ const index = this.children.indexOf(child);
78653
+ if (index >= 0) {
78654
+ this.children.splice(index, 1);
78655
+ this.invalidate();
78656
+ }
78657
+ }
78658
+ /** Concatenate the children, asking each: a clean child returns its cache,
78659
+ * so a frame costs what actually changed. */
78660
+ render(width) {
78661
+ const lines2 = [];
78662
+ for (const child of this.children) lines2.push(...child.render(width));
78663
+ this.isDirty = false;
78664
+ return lines2;
78665
+ }
78666
+ draw(width) {
78667
+ return this.render(width);
78668
+ }
78669
+ /** Offer the key to each child until one takes it. */
78670
+ handleInput(key2, data) {
78671
+ for (const child of this.children) {
78672
+ const handler = child.handleInput;
78673
+ if (handler?.call(child, key2, data)) return true;
78674
+ }
78675
+ return false;
78676
+ }
78677
+ };
78678
+
78679
+ // src/term/primitives.ts
78680
+ var Text = class extends Widget {
78681
+ constructor(content = "", paddingX = 1, paddingY = 0, tint2 = null) {
78682
+ super();
78683
+ this.content = content;
78684
+ this.paddingX = paddingX;
78685
+ this.paddingY = paddingY;
78686
+ this.tint = tint2;
78687
+ }
78688
+ content;
78689
+ paddingX;
78690
+ paddingY;
78691
+ tint;
78692
+ get text() {
78693
+ return this.content;
78694
+ }
78695
+ setText(text3) {
78696
+ if (text3 !== this.content) {
78697
+ this.content = text3;
78698
+ this.invalidate();
78699
+ }
78700
+ }
78701
+ draw(width) {
78702
+ const inner = Math.max(1, width - 2 * this.paddingX);
78703
+ const pad = " ".repeat(this.paddingX);
78704
+ const body = wrap2(this.content, inner).map((line) => `${pad}${line}`);
78705
+ const blank = Array(this.paddingY).fill("");
78706
+ return [...blank, ...body, ...blank].map(
78707
+ (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
78708
+ );
78709
+ }
78710
+ };
78711
+ var COLUMN = /^(\s*\S.*?[^\s.!?]\s{2,})\S/;
78712
+ function wrapAligned(paragraph2, width) {
78713
+ const lines2 = wrap2(paragraph2, width);
78714
+ if (lines2.length < 2) return lines2;
78715
+ const plain2 = stripAnsi(paragraph2);
78716
+ const column = COLUMN.exec(plain2);
78717
+ const lead = column ? cellWidth(column[1]) : plain2.length - plain2.replace(/^ +/, "").length;
78718
+ if (!(lead > 0 && lead <= Math.floor(width / 2))) return lines2;
78719
+ const [head4, body] = splitAtCell(paragraph2, lead);
78720
+ const rest = wrap2(body, width - lead);
78721
+ return [head4 + rest[0], ...rest.slice(1).map((line) => " ".repeat(lead) + line)];
78722
+ }
78723
+ function splitAtCell(text3, cells) {
78724
+ const tokens = tokenize(text3);
78725
+ let seen = 0;
78726
+ for (const [index, [isEscape, chunk]] of tokens.entries()) {
78727
+ if (isEscape) continue;
78728
+ if (seen >= cells) {
78729
+ const head4 = tokens.slice(0, index);
78730
+ const style = head4.filter(([isEscape2]) => isEscape2).map(([, c]) => c).join("");
78731
+ return [
78732
+ head4.map(([, c]) => c).join(""),
78733
+ style + tokens.slice(index).map(([, c]) => c).join("")
78734
+ ];
78735
+ }
78736
+ seen += cellWidth(chunk);
78737
+ }
78738
+ return [text3, ""];
78739
+ }
78740
+ var HangingText = class extends Widget {
78741
+ constructor(prefix, body, paddingX = 1, tint2 = null) {
78742
+ super();
78743
+ this.prefix = prefix;
78744
+ this.body = body;
78745
+ this.paddingX = paddingX;
78746
+ this.tint = tint2;
78747
+ this.prefixWidth = cellWidth(prefix);
78748
+ }
78749
+ prefix;
78750
+ body;
78751
+ paddingX;
78752
+ tint;
78753
+ prefixWidth;
78754
+ setBody(body) {
78755
+ if (body !== this.body) {
78756
+ this.body = body;
78757
+ this.invalidate();
78758
+ }
78759
+ }
78760
+ draw(width) {
78761
+ const pad = " ".repeat(this.paddingX);
78762
+ const inner = Math.max(1, width - 2 * this.paddingX - this.prefixWidth);
78763
+ const hang = " ".repeat(this.prefixWidth);
78764
+ const out = [];
78765
+ for (const paragraph2 of this.body.split("\n")) {
78766
+ wrapAligned(paragraph2, inner).forEach((line, index) => {
78767
+ out.push(`${pad}${out.length === 0 && index === 0 ? this.prefix : hang}${line}`);
78768
+ });
78769
+ }
78770
+ return out.map(
78771
+ (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
78772
+ );
78773
+ }
78774
+ };
78775
+ var Box = class extends Widget {
78776
+ constructor(paddingX = 1, paddingY = 1, tint2 = null, ...children) {
78777
+ super();
78778
+ this.paddingX = paddingX;
78779
+ this.paddingY = paddingY;
78780
+ this.tint = tint2;
78781
+ this.children = children;
78782
+ }
78783
+ paddingX;
78784
+ paddingY;
78785
+ tint;
78786
+ children;
78787
+ add(child) {
78788
+ this.children.push(child);
78789
+ this.invalidate();
78790
+ return child;
78791
+ }
78792
+ clear() {
78793
+ this.children = [];
78794
+ this.invalidate();
78795
+ }
78796
+ render(width) {
78797
+ const inner = Math.max(1, width - 2 * this.paddingX);
78798
+ const pad = " ".repeat(this.paddingX);
78799
+ const body = this.children.flatMap(
78800
+ (child) => child.render(inner).map((line) => `${pad}${stripTerminator(line)}`)
78801
+ );
78802
+ const blank = Array(this.paddingY).fill("");
78803
+ return [...blank, ...body, ...blank].map(
78804
+ (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
78805
+ );
78806
+ }
78807
+ draw(width) {
78808
+ return this.render(width);
78809
+ }
78810
+ };
78811
+ var Spacer = class extends Widget {
78812
+ constructor(lines2 = 1) {
78813
+ super();
78814
+ this.lines = lines2;
78815
+ }
78816
+ lines;
78817
+ draw() {
78818
+ return Array(this.lines).fill("");
78819
+ }
78820
+ };
78821
+ var Rule = class extends Widget {
78822
+ constructor(color2 = null, char = "\u2500") {
78823
+ super();
78824
+ this.color = color2;
78825
+ this.char = char;
78826
+ }
78827
+ color;
78828
+ char;
78829
+ /** What the rule is painted with. */
78830
+ get paint() {
78831
+ return this.color;
78832
+ }
78833
+ setColor(color2) {
78834
+ this.color = color2;
78835
+ this.invalidate();
78836
+ }
78837
+ draw(width) {
78838
+ const line = this.char.repeat(Math.max(1, width));
78839
+ return [terminate(this.color ? this.color(line) : line)];
78840
+ }
78841
+ };
78842
+ var LabelledRule = class extends Rule {
78843
+ constructor(color2 = null, label = "", align = "left", char = "\u2500") {
78844
+ super(color2, char);
78845
+ this.label = label;
78846
+ this.align = align;
78847
+ }
78848
+ label;
78849
+ align;
78850
+ setLabel(label) {
78851
+ if (label !== this.label) {
78852
+ this.label = label;
78853
+ this.invalidate();
78854
+ }
78855
+ }
78856
+ draw(width) {
78857
+ const w = Math.max(1, width);
78858
+ if (!this.label) return super.draw(w);
78859
+ const label = ` ${this.label} `;
78860
+ const labelWidth = cellWidth(label);
78861
+ if (labelWidth + 4 > w) return super.draw(w);
78862
+ const left = this.align === "center" ? Math.floor((w - labelWidth) / 2) : 2;
78863
+ const right = w - labelWidth - left;
78864
+ const paint = this.color ?? ((text3) => text3);
78865
+ return [terminate(paint(this.char.repeat(left)) + label + paint(this.char.repeat(right)))];
78866
+ }
78867
+ };
78868
+ var Lines = class extends Widget {
78869
+ constructor(lines2, paddingX = 1, tint2 = null) {
78870
+ super();
78871
+ this.lines = lines2;
78872
+ this.paddingX = paddingX;
78873
+ this.tint = tint2;
78874
+ }
78875
+ lines;
78876
+ paddingX;
78877
+ tint;
78878
+ setLines(lines2) {
78879
+ this.lines = lines2;
78880
+ this.invalidate();
78881
+ }
78882
+ draw(width) {
78883
+ const pad = " ".repeat(this.paddingX);
78884
+ const inner = Math.max(1, width - 2 * this.paddingX);
78885
+ return this.lines.map((line) => `${pad}${truncateToWidth(stripTerminator(line), inner)}`).map(
78886
+ (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
78887
+ );
78888
+ }
78889
+ };
78890
+ var stripTerminator = (line) => line.endsWith(SEGMENT_RESET) ? line.slice(0, -SEGMENT_RESET.length) : line;
78891
+ function padTo(line, width) {
78892
+ let fitted = line;
78893
+ let used2 = cellWidth(fitted);
78894
+ if (used2 > width) {
78895
+ fitted = truncateToWidth(fitted, width);
78896
+ used2 = cellWidth(fitted);
78897
+ }
78898
+ return fitted + " ".repeat(Math.max(0, width - used2));
78899
+ }
78900
+
78295
78901
  // src/tui/clipboard.ts
78296
78902
  init_images();
78297
78903
  init_pathlib();
78298
78904
  init_py();
78299
78905
  import { spawn as spawn9 } from "node:child_process";
78300
78906
  import { randomBytes as randomBytes8 } from "node:crypto";
78301
- import { readFileSync as readFileSync9, rmSync as rmSync5, statSync as statSync14, writeFileSync as writeFileSync10 } from "node:fs";
78907
+ import { readFileSync as readFileSync9, rmSync as rmSync5, statSync as statSync15, writeFileSync as writeFileSync11 } from "node:fs";
78302
78908
  import { tmpdir as tmpdir3 } from "node:os";
78303
78909
  var MAX_OSC52_ENCODED = 1e5;
78304
78910
  var TIMEOUT_MS = 5e3;
@@ -78403,7 +79009,7 @@ async function readImage2(platform = process.platform, env = process.env) {
78403
79009
  }
78404
79010
  async function withScratch(fn) {
78405
79011
  const target = join(tmpdir3(), `hx-clipboard-${randomBytes8(4).toString("hex")}.img`);
78406
- writeFileSync10(target, "");
79012
+ writeFileSync11(target, "");
78407
79013
  try {
78408
79014
  return await fn(target);
78409
79015
  } finally {
@@ -78465,7 +79071,7 @@ async function readLinux(env) {
78465
79071
  function copiedFile(path) {
78466
79072
  if (!isImagePath(path)) return null;
78467
79073
  try {
78468
- if (statSync14(path).size > MAX_SOURCE_BYTES)
79074
+ if (statSync15(path).size > MAX_SOURCE_BYTES)
78469
79075
  throw new ClipboardError(`${basename(path)} is too large to attach`);
78470
79076
  return { data: readFileSync9(path), name: basename(path) };
78471
79077
  } catch (error) {
@@ -78481,7 +79087,7 @@ init_resolve();
78481
79087
  init_store();
78482
79088
  init_config();
78483
79089
  init_context();
78484
- import { statSync as statSync15 } from "node:fs";
79090
+ import { statSync as statSync16 } from "node:fs";
78485
79091
  init_usage();
78486
79092
  init_json();
78487
79093
  init_net();
@@ -78944,295 +79550,6 @@ var THEME = new Theme();
78944
79550
  // src/tui/views/login.ts
78945
79551
  init_py();
78946
79552
 
78947
- // src/term/component.ts
78948
- var Widget = class {
78949
- cache = [];
78950
- cacheWidth = -1;
78951
- isDirty = true;
78952
- render(width) {
78953
- if (this.isDirty || width !== this.cacheWidth) {
78954
- this.cache = this.draw(width);
78955
- this.cacheWidth = width;
78956
- this.isDirty = false;
78957
- }
78958
- return this.cache;
78959
- }
78960
- invalidate() {
78961
- this.isDirty = true;
78962
- }
78963
- get dirty() {
78964
- return this.isDirty;
78965
- }
78966
- };
78967
- var Container = class extends Widget {
78968
- children;
78969
- constructor(...children) {
78970
- super();
78971
- this.children = children;
78972
- }
78973
- add(child) {
78974
- this.children.push(child);
78975
- this.invalidate();
78976
- return child;
78977
- }
78978
- extend(children) {
78979
- this.children.push(...children);
78980
- this.invalidate();
78981
- }
78982
- clear() {
78983
- this.children = [];
78984
- this.invalidate();
78985
- }
78986
- remove(child) {
78987
- const index = this.children.indexOf(child);
78988
- if (index >= 0) {
78989
- this.children.splice(index, 1);
78990
- this.invalidate();
78991
- }
78992
- }
78993
- /** Concatenate the children, asking each: a clean child returns its cache,
78994
- * so a frame costs what actually changed. */
78995
- render(width) {
78996
- const lines2 = [];
78997
- for (const child of this.children) lines2.push(...child.render(width));
78998
- this.isDirty = false;
78999
- return lines2;
79000
- }
79001
- draw(width) {
79002
- return this.render(width);
79003
- }
79004
- /** Offer the key to each child until one takes it. */
79005
- handleInput(key2, data) {
79006
- for (const child of this.children) {
79007
- const handler = child.handleInput;
79008
- if (handler?.call(child, key2, data)) return true;
79009
- }
79010
- return false;
79011
- }
79012
- };
79013
-
79014
- // src/term/primitives.ts
79015
- var Text = class extends Widget {
79016
- constructor(content = "", paddingX = 1, paddingY = 0, tint2 = null) {
79017
- super();
79018
- this.content = content;
79019
- this.paddingX = paddingX;
79020
- this.paddingY = paddingY;
79021
- this.tint = tint2;
79022
- }
79023
- content;
79024
- paddingX;
79025
- paddingY;
79026
- tint;
79027
- get text() {
79028
- return this.content;
79029
- }
79030
- setText(text3) {
79031
- if (text3 !== this.content) {
79032
- this.content = text3;
79033
- this.invalidate();
79034
- }
79035
- }
79036
- draw(width) {
79037
- const inner = Math.max(1, width - 2 * this.paddingX);
79038
- const pad = " ".repeat(this.paddingX);
79039
- const body = wrap2(this.content, inner).map((line) => `${pad}${line}`);
79040
- const blank = Array(this.paddingY).fill("");
79041
- return [...blank, ...body, ...blank].map(
79042
- (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
79043
- );
79044
- }
79045
- };
79046
- var COLUMN = /^(\s*\S.*?[^\s.!?]\s{2,})\S/;
79047
- function wrapAligned(paragraph2, width) {
79048
- const lines2 = wrap2(paragraph2, width);
79049
- if (lines2.length < 2) return lines2;
79050
- const plain2 = stripAnsi(paragraph2);
79051
- const column = COLUMN.exec(plain2);
79052
- const lead = column ? cellWidth(column[1]) : plain2.length - plain2.replace(/^ +/, "").length;
79053
- if (!(lead > 0 && lead <= Math.floor(width / 2))) return lines2;
79054
- const [head4, body] = splitAtCell(paragraph2, lead);
79055
- const rest = wrap2(body, width - lead);
79056
- return [head4 + rest[0], ...rest.slice(1).map((line) => " ".repeat(lead) + line)];
79057
- }
79058
- function splitAtCell(text3, cells) {
79059
- const tokens = tokenize(text3);
79060
- let seen = 0;
79061
- for (const [index, [isEscape, chunk]] of tokens.entries()) {
79062
- if (isEscape) continue;
79063
- if (seen >= cells) {
79064
- const head4 = tokens.slice(0, index);
79065
- const style = head4.filter(([isEscape2]) => isEscape2).map(([, c]) => c).join("");
79066
- return [
79067
- head4.map(([, c]) => c).join(""),
79068
- style + tokens.slice(index).map(([, c]) => c).join("")
79069
- ];
79070
- }
79071
- seen += cellWidth(chunk);
79072
- }
79073
- return [text3, ""];
79074
- }
79075
- var HangingText = class extends Widget {
79076
- constructor(prefix, body, paddingX = 1, tint2 = null) {
79077
- super();
79078
- this.prefix = prefix;
79079
- this.body = body;
79080
- this.paddingX = paddingX;
79081
- this.tint = tint2;
79082
- this.prefixWidth = cellWidth(prefix);
79083
- }
79084
- prefix;
79085
- body;
79086
- paddingX;
79087
- tint;
79088
- prefixWidth;
79089
- setBody(body) {
79090
- if (body !== this.body) {
79091
- this.body = body;
79092
- this.invalidate();
79093
- }
79094
- }
79095
- draw(width) {
79096
- const pad = " ".repeat(this.paddingX);
79097
- const inner = Math.max(1, width - 2 * this.paddingX - this.prefixWidth);
79098
- const hang = " ".repeat(this.prefixWidth);
79099
- const out = [];
79100
- for (const paragraph2 of this.body.split("\n")) {
79101
- wrapAligned(paragraph2, inner).forEach((line, index) => {
79102
- out.push(`${pad}${out.length === 0 && index === 0 ? this.prefix : hang}${line}`);
79103
- });
79104
- }
79105
- return out.map(
79106
- (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
79107
- );
79108
- }
79109
- };
79110
- var Box = class extends Widget {
79111
- constructor(paddingX = 1, paddingY = 1, tint2 = null, ...children) {
79112
- super();
79113
- this.paddingX = paddingX;
79114
- this.paddingY = paddingY;
79115
- this.tint = tint2;
79116
- this.children = children;
79117
- }
79118
- paddingX;
79119
- paddingY;
79120
- tint;
79121
- children;
79122
- add(child) {
79123
- this.children.push(child);
79124
- this.invalidate();
79125
- return child;
79126
- }
79127
- clear() {
79128
- this.children = [];
79129
- this.invalidate();
79130
- }
79131
- render(width) {
79132
- const inner = Math.max(1, width - 2 * this.paddingX);
79133
- const pad = " ".repeat(this.paddingX);
79134
- const body = this.children.flatMap(
79135
- (child) => child.render(inner).map((line) => `${pad}${stripTerminator(line)}`)
79136
- );
79137
- const blank = Array(this.paddingY).fill("");
79138
- return [...blank, ...body, ...blank].map(
79139
- (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
79140
- );
79141
- }
79142
- draw(width) {
79143
- return this.render(width);
79144
- }
79145
- };
79146
- var Spacer = class extends Widget {
79147
- constructor(lines2 = 1) {
79148
- super();
79149
- this.lines = lines2;
79150
- }
79151
- lines;
79152
- draw() {
79153
- return Array(this.lines).fill("");
79154
- }
79155
- };
79156
- var Rule = class extends Widget {
79157
- constructor(color2 = null, char = "\u2500") {
79158
- super();
79159
- this.color = color2;
79160
- this.char = char;
79161
- }
79162
- color;
79163
- char;
79164
- /** What the rule is painted with. */
79165
- get paint() {
79166
- return this.color;
79167
- }
79168
- setColor(color2) {
79169
- this.color = color2;
79170
- this.invalidate();
79171
- }
79172
- draw(width) {
79173
- const line = this.char.repeat(Math.max(1, width));
79174
- return [terminate(this.color ? this.color(line) : line)];
79175
- }
79176
- };
79177
- var LabelledRule = class extends Rule {
79178
- constructor(color2 = null, label = "", align = "left", char = "\u2500") {
79179
- super(color2, char);
79180
- this.label = label;
79181
- this.align = align;
79182
- }
79183
- label;
79184
- align;
79185
- setLabel(label) {
79186
- if (label !== this.label) {
79187
- this.label = label;
79188
- this.invalidate();
79189
- }
79190
- }
79191
- draw(width) {
79192
- const w = Math.max(1, width);
79193
- if (!this.label) return super.draw(w);
79194
- const label = ` ${this.label} `;
79195
- const labelWidth = cellWidth(label);
79196
- if (labelWidth + 4 > w) return super.draw(w);
79197
- const left = this.align === "center" ? Math.floor((w - labelWidth) / 2) : 2;
79198
- const right = w - labelWidth - left;
79199
- const paint = this.color ?? ((text3) => text3);
79200
- return [terminate(paint(this.char.repeat(left)) + label + paint(this.char.repeat(right)))];
79201
- }
79202
- };
79203
- var Lines = class extends Widget {
79204
- constructor(lines2, paddingX = 1, tint2 = null) {
79205
- super();
79206
- this.lines = lines2;
79207
- this.paddingX = paddingX;
79208
- this.tint = tint2;
79209
- }
79210
- lines;
79211
- paddingX;
79212
- tint;
79213
- setLines(lines2) {
79214
- this.lines = lines2;
79215
- this.invalidate();
79216
- }
79217
- draw(width) {
79218
- const pad = " ".repeat(this.paddingX);
79219
- const inner = Math.max(1, width - 2 * this.paddingX);
79220
- return this.lines.map((line) => `${pad}${truncateToWidth(stripTerminator(line), inner)}`).map(
79221
- (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
79222
- );
79223
- }
79224
- };
79225
- var stripTerminator = (line) => line.endsWith(SEGMENT_RESET) ? line.slice(0, -SEGMENT_RESET.length) : line;
79226
- function padTo(line, width) {
79227
- let fitted = line;
79228
- let used2 = cellWidth(fitted);
79229
- if (used2 > width) {
79230
- fitted = truncateToWidth(fitted, width);
79231
- used2 = cellWidth(fitted);
79232
- }
79233
- return fitted + " ".repeat(Math.max(0, width - used2));
79234
- }
79235
-
79236
79553
  // src/tui/format.ts
79237
79554
  init_py();
79238
79555
 
@@ -86255,7 +86572,7 @@ async function cmdAgents(ctx, _args) {
86255
86572
  }
86256
86573
  async function cmdMcp(ctx, args) {
86257
86574
  const manager = ctx.app.mcp;
86258
- if (manager === null || !manager.configs.length) {
86575
+ if (manager === null || !manager.configs.length && !manager.waiting.length) {
86259
86576
  ctx.app.notice("No MCP servers configured. Add one with `hx mcp add`.", "warning");
86260
86577
  return;
86261
86578
  }
@@ -86297,7 +86614,10 @@ async function cmdMcp(ctx, args) {
86297
86614
  lines2.push(` ${ljust(status3.name, 20)} ${status3.error} - /mcp login ${status3.name}`);
86298
86615
  else lines2.push(` ${ljust(status3.name, 20)} unavailable - ${status3.error}`);
86299
86616
  }
86300
- ctx.app.notice(lines2.join("\n"), manager.status().every((s) => s.connected) ? "info" : "warning");
86617
+ for (const { config: config2 } of manager.waiting)
86618
+ lines2.push(` ${ljust(config2.name, 20)} not approved - run \`hx mcp approve ${config2.name}\``);
86619
+ const healthy = manager.status().every((s) => s.connected) && !manager.waiting.length;
86620
+ ctx.app.notice(lines2.join("\n"), healthy ? "info" : "warning");
86301
86621
  }
86302
86622
  function onlyServerNeedingLogin(manager) {
86303
86623
  const waiting = manager.status().filter((s) => s.needsLogin);
@@ -86663,18 +86983,18 @@ async function cmdCopy(ctx, _args) {
86663
86983
  }
86664
86984
  async function cmdTrace(ctx, args) {
86665
86985
  const [target, openAfter] = traceArgs(args);
86666
- let written;
86986
+ let written2;
86667
86987
  try {
86668
- written = writeSessionTrace(ctx, target);
86988
+ written2 = writeSessionTrace(ctx, target);
86669
86989
  } catch (error) {
86670
86990
  if (!error.code) throw error;
86671
86991
  ctx.app.notice(`Could not write the trace: ${osErrorText(error)}`, "error");
86672
86992
  return;
86673
86993
  }
86674
- const opened = openAfter && await openInBrowser(written);
86994
+ const opened = openAfter && await openInBrowser(written2);
86675
86995
  const trailer = openAfter && !opened ? " No browser could be launched here; open it yourself." : "";
86676
86996
  ctx.app.notice(
86677
- `Trace written to ${written} (${formatBytes(statSync15(written).size)}).${trailer}`,
86997
+ `Trace written to ${written2} (${formatBytes(statSync16(written2).size)}).${trailer}`,
86678
86998
  "success"
86679
86999
  );
86680
87000
  }
@@ -87771,7 +88091,7 @@ var PermissionPrompt = class extends Framed {
87771
88091
 
87772
88092
  // src/tui/views/prompt.ts
87773
88093
  init_images();
87774
- import { statSync as statSync16 } from "node:fs";
88094
+ import { statSync as statSync17 } from "node:fs";
87775
88095
  init_pathlib();
87776
88096
  init_py();
87777
88097
 
@@ -88249,7 +88569,7 @@ var FileCompleter = class _FileCompleter {
88249
88569
  }
88250
88570
  const mtime3 = (path) => {
88251
88571
  try {
88252
- return statSync16(path).mtimeMs;
88572
+ return statSync17(path).mtimeMs;
88253
88573
  } catch {
88254
88574
  return 0;
88255
88575
  }
@@ -89042,6 +89362,13 @@ var HintsBar = class extends Widget {
89042
89362
  return [fillLine(` ${parts2.join(fg2("dim", SEPARATOR2))}`, width)];
89043
89363
  }
89044
89364
  };
89365
+ var LOGO = [
89366
+ " __ __ _ __",
89367
+ " / / / / | |/ /",
89368
+ " / /_/ / | /",
89369
+ " / __ / / |",
89370
+ "/_/ /_/ /_/|_|"
89371
+ ];
89045
89372
  var Header = class extends Widget {
89046
89373
  constructor(version = "", quiet = false) {
89047
89374
  super();
@@ -89051,6 +89378,7 @@ var Header = class extends Widget {
89051
89378
  quiet;
89052
89379
  versionText;
89053
89380
  isExpanded = false;
89381
+ showLogo = false;
89054
89382
  get version() {
89055
89383
  return this.versionText;
89056
89384
  }
@@ -89068,10 +89396,19 @@ var Header = class extends Widget {
89068
89396
  toggle() {
89069
89397
  this.expanded = !this.isExpanded;
89070
89398
  }
89399
+ /** The logo above the title: on the first screen of someone not signed in. */
89400
+ get logo() {
89401
+ return this.showLogo;
89402
+ }
89403
+ set logo(value) {
89404
+ this.showLogo = value;
89405
+ this.invalidate();
89406
+ }
89071
89407
  draw(width) {
89072
89408
  if (this.quiet) return [];
89073
89409
  const keys = keymap();
89074
- const lines2 = [fg2("accent", "hx", { bold: true }) + fg2("dim", ` v${this.versionText}`)];
89410
+ const lines2 = this.showLogo ? [...LOGO.map((row) => fg2("accent", row, { bold: true })), ""] : [];
89411
+ lines2.push(fg2("accent", "hx", { bold: true }) + fg2("dim", ` v${this.versionText}`));
89075
89412
  if (!this.isExpanded) {
89076
89413
  const inner = Math.max(1, width - 2);
89077
89414
  lines2.push(
@@ -89342,6 +89679,18 @@ var HXSession = class {
89342
89679
  this.view.dock.hints.setHints(HINTS);
89343
89680
  this.refreshStatus();
89344
89681
  if (this.loop.session.activeMessages().length) this.replayTranscript();
89682
+ if (this.extra.signIn) this.showSignIn(this.extra.signIn);
89683
+ void this.extra.update?.then((outcome2) => {
89684
+ if (outcome2 === null) return;
89685
+ const level = outcome2.ok ? "success" : "warning";
89686
+ if (this.view.transcript.blocks.length) {
89687
+ this.notice(outcome2.message, level);
89688
+ return;
89689
+ }
89690
+ this.view.transcript.add(new Spacer(1));
89691
+ this.view.transcript.add(new Notice(outcome2.message, level));
89692
+ this.runner.requestRender();
89693
+ });
89345
89694
  if (this.loop.permissions !== null)
89346
89695
  this.loop.permissions.asker = (request2) => this.askPermission(request2);
89347
89696
  const events = this.consumeEvents();
@@ -89385,6 +89734,13 @@ var HXSession = class {
89385
89734
  this.runner.requestRender();
89386
89735
  }
89387
89736
  }
89737
+ /** Say how to sign in, a row below the header, under the logo - unless a
89738
+ * resumed conversation is already on screen. */
89739
+ showSignIn(hint2) {
89740
+ if (!this.view.transcript.blocks.length) this.view.header.logo = true;
89741
+ this.view.transcript.add(new Spacer(1));
89742
+ this.view.transcript.add(new Notice(hint2, "warning"));
89743
+ }
89388
89744
  // -- the surface the slash commands expect ---------------------------------
89389
89745
  get status() {
89390
89746
  return this.view.dock.status;
@@ -90337,6 +90693,145 @@ async function runSession(loop, bus, settings, extra = {}) {
90337
90693
  return session.relaunch;
90338
90694
  }
90339
90695
 
90696
+ // src/update.ts
90697
+ init_json();
90698
+ init_net();
90699
+ init_pathlib();
90700
+ init_paths();
90701
+ init_py();
90702
+ import { spawn as spawn10 } from "node:child_process";
90703
+ import { mkdirSync as mkdirSync15, openSync as openSync10, readFileSync as readFileSync10, realpathSync as realpathSync3, writeFileSync as writeFileSync12 } from "node:fs";
90704
+ init_which();
90705
+ var RELEASES_URL2 = `https://registry.npmjs.org/-/package/${PACKAGE}/dist-tags`;
90706
+ var RELEASES_ENV2 = "HX_RELEASES_URL";
90707
+ var DISABLE_ENV = "HX_DISABLE_AUTOUPDATER";
90708
+ var RELEASES_TIMEOUT2 = 5;
90709
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
90710
+ var stateFile = () => join(userHome(), "update.json");
90711
+ async function latestRelease(signal) {
90712
+ const url = strip(process.env[RELEASES_ENV2] ?? "") || RELEASES_URL2;
90713
+ try {
90714
+ const response = await request(url, {
90715
+ timeout: RELEASES_TIMEOUT2,
90716
+ ...signal ? { signal } : {}
90717
+ });
90718
+ try {
90719
+ if (response.status < 200 || response.status >= 300) return null;
90720
+ const tags = await response.json();
90721
+ const latest = typeof tags === "object" && tags !== null ? tags.latest : null;
90722
+ return typeof latest === "string" && RELEASE2.test(latest) ? latest : null;
90723
+ } finally {
90724
+ response.close();
90725
+ }
90726
+ } catch {
90727
+ return null;
90728
+ }
90729
+ }
90730
+ var RELEASE2 = /^\d+\.\d+\.\d+$/;
90731
+ function compareReleases(a, b) {
90732
+ const left = a.split(".").map(Number);
90733
+ const right = b.split(".").map(Number);
90734
+ for (let index = 0; index < 3; index++) {
90735
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
90736
+ if (difference) return difference;
90737
+ }
90738
+ return 0;
90739
+ }
90740
+ function notAnNpmInstall() {
90741
+ const script = process.argv[1] ?? "";
90742
+ let real2;
90743
+ try {
90744
+ real2 = realpathSync3(script);
90745
+ } catch {
90746
+ return script;
90747
+ }
90748
+ return real2.includes(`/node_modules/${PACKAGE}/`) ? null : parent(parent(real2));
90749
+ }
90750
+ function npm() {
90751
+ return which("npm") ?? join(parent(process.execPath), "npm");
90752
+ }
90753
+ var INSTALL_COMMAND = `npm install --global ${PACKAGE}`;
90754
+ function runNpm(version, background) {
90755
+ const args = ["install", "--global", `${PACKAGE}@${version}`];
90756
+ let stdio = "inherit";
90757
+ if (background) {
90758
+ mkdirSync15(logsDir(), { recursive: true });
90759
+ const log = openSync10(join(logsDir(), "update.log"), "a");
90760
+ stdio = ["ignore", log, log];
90761
+ }
90762
+ return new Promise((resolve2) => {
90763
+ const child = spawn10(npm(), args, { stdio, detached: background });
90764
+ child.once("error", () => resolve2(null));
90765
+ child.once("exit", (code2, signal) => resolve2(code2 ?? (signal ? 1 : 0)));
90766
+ if (background) child.unref();
90767
+ });
90768
+ }
90769
+ async function runUpdate(print2) {
90770
+ const elsewhere2 = notAnNpmInstall();
90771
+ if (elsewhere2 !== null) {
90772
+ print2(
90773
+ `hx is running from ${elsewhere2}, not from an npm install, so it cannot update itself. Update a checkout with git pull and npm run build, or install the release: ${INSTALL_COMMAND}`,
90774
+ process.stderr
90775
+ );
90776
+ return 1;
90777
+ }
90778
+ const latest = await latestRelease();
90779
+ if (latest === null) {
90780
+ print2("Could not reach the npm registry to check for a newer hx.", process.stderr);
90781
+ return 1;
90782
+ }
90783
+ if (compareReleases(latest, VERSION) <= 0) {
90784
+ print2(`hx ${VERSION} is up to date.`);
90785
+ return 0;
90786
+ }
90787
+ print2(`Updating hx ${VERSION} to ${latest}...`);
90788
+ const status3 = await runNpm(latest, false);
90789
+ if (status3 === null) {
90790
+ print2(
90791
+ `npm is not installed, so hx cannot update itself. See https://nodejs.org/`,
90792
+ process.stderr
90793
+ );
90794
+ return 1;
90795
+ }
90796
+ if (status3 !== 0) {
90797
+ print2(`npm could not install ${PACKAGE}@${latest} (exit ${status3}).`, process.stderr);
90798
+ return 1;
90799
+ }
90800
+ print2(`Updated hx to ${latest}.`);
90801
+ return 0;
90802
+ }
90803
+ function lastChecked() {
90804
+ try {
90805
+ const state = JSON.parse(readFileSync10(stateFile(), "utf8"));
90806
+ const at = typeof state === "object" && state !== null ? state.checkedAt : 0;
90807
+ return typeof at === "number" ? at : 0;
90808
+ } catch {
90809
+ return 0;
90810
+ }
90811
+ }
90812
+ async function autoUpdate(enabled, signal) {
90813
+ const disabled = strip(process.env[DISABLE_ENV] ?? "").toLowerCase();
90814
+ if (!enabled || ["1", "true", "yes"].includes(disabled)) return null;
90815
+ if (notAnNpmInstall() !== null) return null;
90816
+ const now3 = Date.now();
90817
+ if (now3 - lastChecked() < CHECK_INTERVAL_MS) return null;
90818
+ try {
90819
+ writeFileSync12(stateFile(), `${dumps({ checkedAt: now3 })}
90820
+ `);
90821
+ } catch {
90822
+ return null;
90823
+ }
90824
+ const latest = await latestRelease(signal);
90825
+ if (signal.aborted || latest === null || compareReleases(latest, VERSION) <= 0) return null;
90826
+ const status3 = await runNpm(latest, true);
90827
+ if (status3 === 0) return { message: `Updated hx to ${latest}. Restart hx to use it.`, ok: true };
90828
+ const why = status3 === null ? "npm could not be started" : `npm exited ${status3}`;
90829
+ return {
90830
+ message: `Could not update hx to ${latest}: ${why}. Run hx update to see why.`,
90831
+ ok: false
90832
+ };
90833
+ }
90834
+
90340
90835
  // src/cli.ts
90341
90836
  var USAGE = `hx - an agent harness for the terminal
90342
90837
 
@@ -90352,7 +90847,7 @@ Usage:
90352
90847
  hx auth [set|clear] Show or change the OpenRouter API key
90353
90848
  hx docs [SECTION|--all] Print the manual, or one section of it
90354
90849
  hx changelog [VERSION] Print what shipped in each version
90355
- hx upgrade Update hx to the latest version
90850
+ hx update Update hx to the newest release
90356
90851
  hx --version Show version
90357
90852
  hx --help Show this message
90358
90853
 
@@ -90416,25 +90911,13 @@ ${USAGE}`, process.stderr);
90416
90911
  return runMcpCommand(parsed.rest);
90417
90912
  case "trace":
90418
90913
  return runTraceCommand(parsed);
90914
+ case "update":
90419
90915
  case "upgrade":
90420
- return runUpgradeCommand();
90916
+ return runUpdate(print);
90421
90917
  default:
90422
90918
  return runTuiCommand(parsed);
90423
90919
  }
90424
90920
  }
90425
- function runUpgradeCommand() {
90426
- const channel = VERSION.includes("-") ? "next" : "latest";
90427
- const npm = process.platform === "win32" ? "npm.cmd" : "npm";
90428
- const result2 = spawnSync5(npm, ["install", "--global", `${PACKAGE}@${channel}`], {
90429
- stdio: "inherit"
90430
- });
90431
- if (result2.error?.code === "ENOENT") {
90432
- print("npm is not installed. See https://nodejs.org/", process.stderr);
90433
- return 1;
90434
- }
90435
- if (result2.error) throw result2.error;
90436
- return result2.status ?? 1;
90437
- }
90438
90921
  function parseArgs(args) {
90439
90922
  const parsed = {
90440
90923
  command: "tui",
@@ -90489,7 +90972,7 @@ function parseArgs(args) {
90489
90972
  }
90490
90973
  if (positional.length) {
90491
90974
  const [head4, ...tail3] = positional;
90492
- if (["resume", "prompt", "trace", "mcp", "upgrade", "auth"].includes(head4)) {
90975
+ if (["resume", "prompt", "trace", "mcp", "auth", "update", "upgrade"].includes(head4)) {
90493
90976
  parsed.command = head4;
90494
90977
  parsed.rest = tail3;
90495
90978
  if (head4 === "resume" && tail3.length) parsed.sessionId = tail3[0];
@@ -90952,25 +91435,28 @@ function runTraceCommand(parsed) {
90952
91435
  destination = purePath(expandUser(target));
90953
91436
  if (isDir(destination)) destination = join(destination, TRACE_FILENAME);
90954
91437
  }
90955
- let written;
91438
+ let written2;
90956
91439
  try {
90957
- written = writeTrace(buildTrace(session), destination);
91440
+ written2 = writeTrace(buildTrace(session), destination);
90958
91441
  } catch (error) {
90959
91442
  if (!error.code) throw error;
90960
91443
  print(`error: could not write the trace: ${osErrorText(error)}`, process.stderr);
90961
91444
  return 1;
90962
91445
  }
90963
- print(written);
91446
+ print(written2);
90964
91447
  return 0;
90965
91448
  }
90966
91449
  var MCP_USAGE = `hx mcp list Show configured servers
90967
91450
  hx mcp add NAME COMMAND [ARGS...] Add a stdio server
90968
91451
  hx mcp add NAME --url URL [--header K:V]... Add an HTTP server
91452
+ hx mcp approve NAME Let a server from the project's .hx/mcp.json start
90969
91453
  hx mcp login NAME Sign in to an HTTP server in the browser
90970
91454
  hx mcp logout NAME Forget a server's sign-in
90971
91455
  hx mcp remove NAME Remove a server
90972
91456
 
90973
91457
  Add --user to write to ~/.hx/mcp.json instead of the project's .hx/mcp.json.
91458
+ A server in the project's .hx/mcp.json came with the project, so it starts only
91459
+ once approved: \`hx mcp add\` approves what it adds, and \`hx mcp approve\` the rest.
90974
91460
  An HTTP server that asks for sign-in gets it over OAuth unless a --header
90975
91461
  already sends Authorization.
90976
91462
  `;
@@ -90989,7 +91475,14 @@ async function runMcpCommand(given) {
90989
91475
  print(MCP_USAGE);
90990
91476
  return 0;
90991
91477
  }
90992
- for (const status3 of await probeServers(configs)) {
91478
+ const [startable, waiting] = partition(configs, cwd);
91479
+ for (const { config: config2, changed } of waiting) {
91480
+ const why = changed ? "changed since approved" : "not approved";
91481
+ print(
91482
+ `${ljust2(config2.name, 20)} ${why} - runs ${describeServer(config2)}; run \`hx mcp approve ${config2.name}\``
91483
+ );
91484
+ }
91485
+ for (const status3 of await probeServers(startable)) {
90993
91486
  if (status3.needsLogin) {
90994
91487
  print(`${ljust2(status3.name, 20)} ${status3.error} - run \`hx mcp login ${status3.name}\``);
90995
91488
  continue;
@@ -91029,9 +91522,10 @@ ${MCP_USAGE}`, process.stderr);
91029
91522
  });
91030
91523
  }
91031
91524
  print(`Added ${name} to ${saveConfig(config2, cwd, userLevel)}`);
91525
+ if (!userLevel) approve({ ...config2, source: "project" }, cwd);
91032
91526
  return 0;
91033
91527
  }
91034
- if (args[0] === "login" || args[0] === "logout") {
91528
+ if (args[0] === "approve") {
91035
91529
  if (args.length < 2) {
91036
91530
  print(MCP_USAGE, process.stderr);
91037
91531
  return 2;
@@ -91041,6 +91535,33 @@ ${MCP_USAGE}`, process.stderr);
91041
91535
  print(`No server named ${args[1]}.`, process.stderr);
91042
91536
  return 1;
91043
91537
  }
91538
+ if (server.source === "user") {
91539
+ print(`${server.name} is in your own ~/.hx/mcp.json; it needs no approval.`);
91540
+ return 0;
91541
+ }
91542
+ approve(server, cwd);
91543
+ const does = server.url ? "connects to" : "runs";
91544
+ print(`Approved ${server.name}. It ${does}: ${describeServer(server)}`);
91545
+ return 0;
91546
+ }
91547
+ if (args[0] === "login" || args[0] === "logout") {
91548
+ if (args.length < 2) {
91549
+ print(MCP_USAGE, process.stderr);
91550
+ return 2;
91551
+ }
91552
+ const configs = loadConfigs(cwd);
91553
+ const server = configs.find((c) => c.name === args[1]);
91554
+ if (server === void 0) {
91555
+ print(`No server named ${args[1]}.`, process.stderr);
91556
+ return 1;
91557
+ }
91558
+ if (args[0] === "login" && partition([server], cwd)[1].length) {
91559
+ print(
91560
+ `${server.name} has not been approved. Run \`hx mcp approve ${server.name}\` first.`,
91561
+ process.stderr
91562
+ );
91563
+ return 1;
91564
+ }
91044
91565
  return args[0] === "login" ? mcpLogin2(server) : mcpLogout(server);
91045
91566
  }
91046
91567
  if (args[0] === "remove") {
@@ -91049,6 +91570,7 @@ ${MCP_USAGE}`, process.stderr);
91049
91570
  return 2;
91050
91571
  }
91051
91572
  if (removeConfig(args[1], cwd, userLevel)) {
91573
+ if (!userLevel) revoke(args[1], cwd);
91052
91574
  print(`Removed ${args[1]}.`);
91053
91575
  return 0;
91054
91576
  }
@@ -91132,19 +91654,16 @@ async function runTuiCommand(parsed) {
91132
91654
  try {
91133
91655
  resume = resumeTarget(parsed);
91134
91656
  moved = adoptSessionDirectory(parsed, resume);
91135
- runtime2 = buildRuntime(parsed, resume);
91657
+ runtime2 = buildRuntime(parsed, resume, true);
91136
91658
  } catch (error) {
91137
- if (!(error instanceof MissingCredential)) return report(error);
91138
- if (await runLogin(error.providerId) !== 0) return report(error);
91139
- try {
91140
- runtime2 = buildRuntime(parsed, resume);
91141
- } catch (retry) {
91142
- return report(retry);
91143
- }
91659
+ return report(error);
91144
91660
  }
91145
91661
  if (moved) runtime2.notices.push(moved);
91662
+ const signIn2 = runtime2.provider instanceof DeferredProvider ? signInHint(runtime2.loop.model, runtime2.auth) : null;
91146
91663
  let relaunch = null;
91664
+ const sessionOver = new AbortController();
91147
91665
  try {
91666
+ const update = autoUpdate(runtime2.settings.autoUpdates, sessionOver.signal);
91148
91667
  await runtime2.connectMcp();
91149
91668
  await runtime2.refreshModelsIfStale();
91150
91669
  relaunch = await runSession(runtime2.loop, runtime2.bus, runtime2.settings, {
@@ -91157,9 +91676,12 @@ async function runTuiCommand(parsed) {
91157
91676
  mcp: runtime2.mcp,
91158
91677
  notices: runtime2.notices,
91159
91678
  checkpoints: runtime2.checkpoints,
91160
- renameLeft
91679
+ renameLeft,
91680
+ signIn: signIn2,
91681
+ update
91161
91682
  });
91162
91683
  } finally {
91684
+ sessionOver.abort();
91163
91685
  runtime2.bus.close();
91164
91686
  await runtime2.aclose();
91165
91687
  }