@sunilaleti/hx 0.3.0-beta.1 → 1.0.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
  }
@@ -16962,7 +16969,7 @@ var require_abnf = __commonJS({
16962
16969
  scope: "attribute",
16963
16970
  match: regex.concat(IDENT, /(?=\s*=)/)
16964
16971
  };
16965
- const ASSIGNMENT = {
16972
+ const ASSIGNMENT2 = {
16966
16973
  scope: "operator",
16967
16974
  match: /=\/?/
16968
16975
  };
@@ -16971,7 +16978,7 @@ var require_abnf = __commonJS({
16971
16978
  illegal: /[!@#$^&',?+~`|:]/,
16972
16979
  keywords: KEYWORDS,
16973
16980
  contains: [
16974
- ASSIGNMENT,
16981
+ ASSIGNMENT2,
16975
16982
  RULE_DECLARATION,
16976
16983
  COMMENT,
16977
16984
  TERMINAL_BINARY,
@@ -30310,7 +30317,7 @@ var require_gams = __commonJS({
30310
30317
  illegal: "\\n",
30311
30318
  contains: [hljs.BACKSLASH_ESCAPE]
30312
30319
  };
30313
- const ASSIGNMENT = {
30320
+ const ASSIGNMENT2 = {
30314
30321
  begin: "/",
30315
30322
  end: "/",
30316
30323
  keywords: KEYWORDS,
@@ -30332,7 +30339,7 @@ var require_gams = __commonJS({
30332
30339
  endsWithParent: true,
30333
30340
  contains: [
30334
30341
  QSTR,
30335
- ASSIGNMENT,
30342
+ ASSIGNMENT2,
30336
30343
  {
30337
30344
  className: "comment",
30338
30345
  // one comment word, then possibly more
@@ -30379,7 +30386,7 @@ var require_gams = __commonJS({
30379
30386
  hljs.C_BLOCK_COMMENT_MODE,
30380
30387
  hljs.QUOTE_STRING_MODE,
30381
30388
  hljs.APOS_STRING_MODE,
30382
- ASSIGNMENT,
30389
+ ASSIGNMENT2,
30383
30390
  DESCTEXT
30384
30391
  ]
30385
30392
  },
@@ -40434,7 +40441,7 @@ var require_makefile = __commonJS({
40434
40441
  // Added QUOTE_STRING as they can be a part of functions
40435
40442
  ]
40436
40443
  };
40437
- const ASSIGNMENT = { begin: "^" + hljs.UNDERSCORE_IDENT_RE + "\\s*(?=[:+?]?=)" };
40444
+ const ASSIGNMENT2 = { begin: "^" + hljs.UNDERSCORE_IDENT_RE + "\\s*(?=[:+?]?=)" };
40438
40445
  const META = {
40439
40446
  className: "meta",
40440
40447
  begin: /^\.PHONY:/,
@@ -40466,7 +40473,7 @@ var require_makefile = __commonJS({
40466
40473
  VARIABLE,
40467
40474
  QUOTE_STRING,
40468
40475
  FUNC,
40469
- ASSIGNMENT,
40476
+ ASSIGNMENT2,
40470
40477
  META,
40471
40478
  TARGET
40472
40479
  ]
@@ -67725,8 +67732,7 @@ init_net();
67725
67732
  init_py();
67726
67733
 
67727
67734
  // src/version.ts
67728
- var VERSION = "0.3.0-beta.1";
67729
- var PACKAGE = "@sunilaleti/hx";
67735
+ var VERSION = "1.0.0";
67730
67736
 
67731
67737
  // src/providers/codex.ts
67732
67738
  init_base();
@@ -68165,12 +68171,12 @@ init_images();
68165
68171
 
68166
68172
  // src/core/session.ts
68167
68173
  import { randomBytes as randomBytes3 } from "node:crypto";
68168
- import { appendFileSync, mkdirSync as mkdirSync7, statSync as statSync8, writeFileSync as writeFileSync4 } from "node:fs";
68174
+ import { appendFileSync, mkdirSync as mkdirSync7, statSync as statSync9, writeFileSync as writeFileSync4 } from "node:fs";
68169
68175
 
68170
68176
  // src/glob.ts
68171
68177
  init_pathlib();
68172
68178
  init_py();
68173
- import { opendirSync, statSync as statSync6 } from "node:fs";
68179
+ import { opendirSync, statSync as statSync7 } from "node:fs";
68174
68180
  function translate(pattern) {
68175
68181
  let out = "";
68176
68182
  const chars = [...pattern];
@@ -68222,7 +68228,7 @@ function isDirEntry(directory, entry) {
68222
68228
  if (entry.isDirectory()) return true;
68223
68229
  if (!entry.isSymbolicLink()) return false;
68224
68230
  try {
68225
- return statSync6(join(directory, entry.name)).isDirectory();
68231
+ return statSync7(join(directory, entry.name)).isDirectory();
68226
68232
  } catch {
68227
68233
  return false;
68228
68234
  }
@@ -68259,7 +68265,7 @@ function select(path, rest, out) {
68259
68265
  const child = join(path, part);
68260
68266
  if (tail3.length === 0) {
68261
68267
  try {
68262
- statSync6(child);
68268
+ statSync7(child);
68263
68269
  out.push(child);
68264
68270
  } catch {
68265
68271
  }
@@ -68477,7 +68483,7 @@ function digestOf(path) {
68477
68483
  init_messages();
68478
68484
 
68479
68485
  // src/core/reads.ts
68480
- import { statSync as statSync7 } from "node:fs";
68486
+ import { statSync as statSync8 } from "node:fs";
68481
68487
  var fileReadToDict = (read2) => ({
68482
68488
  index: read2.index,
68483
68489
  path: read2.path,
@@ -68516,7 +68522,7 @@ var STAT_DIGESTS = /* @__PURE__ */ new Map();
68516
68522
  function cachedDigest(key2) {
68517
68523
  let stamp;
68518
68524
  try {
68519
- const info = statSync7(key2, { bigint: true });
68525
+ const info = statSync8(key2, { bigint: true });
68520
68526
  stamp = `${info.ino}:${info.size}:${info.mtimeNs}`;
68521
68527
  } catch {
68522
68528
  STAT_DIGESTS.delete(key2);
@@ -68908,7 +68914,7 @@ var BACKFILL_MAX_BYTES = 4e6;
68908
68914
  function backfilled(meta) {
68909
68915
  if (meta.promptCount || meta.messageCount <= 0) return meta;
68910
68916
  try {
68911
- if (statSync8(sessionTranscriptFile(meta.sessionId)).size > BACKFILL_MAX_BYTES) return meta;
68917
+ if (statSync9(sessionTranscriptFile(meta.sessionId)).size > BACKFILL_MAX_BYTES) return meta;
68912
68918
  } catch {
68913
68919
  return meta;
68914
68920
  }
@@ -69034,6 +69040,75 @@ function changelog(version = null) {
69034
69040
  );
69035
69041
  }
69036
69042
 
69043
+ // src/mcp/approval.ts
69044
+ init_json();
69045
+ init_pathlib();
69046
+ init_paths();
69047
+ init_shlex();
69048
+ import { createHash as createHash8 } from "node:crypto";
69049
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "node:fs";
69050
+ var approvalsFile = (cwd) => join(projectStateDir(cwd), "mcp-approved.json");
69051
+ function fingerprint2(config2) {
69052
+ const fields = {
69053
+ transport: config2.transport,
69054
+ command: config2.command,
69055
+ args: config2.args,
69056
+ env: config2.env,
69057
+ url: config2.url,
69058
+ headers: config2.headers
69059
+ };
69060
+ return createHash8("sha256").update(dumps(fields, { sortKeys: true })).digest("hex");
69061
+ }
69062
+ function readApprovals(cwd) {
69063
+ const path = approvalsFile(cwd);
69064
+ if (!isFile(path)) return {};
69065
+ try {
69066
+ const data = JSON.parse(readText(path));
69067
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return {};
69068
+ return Object.fromEntries(
69069
+ Object.entries(data).filter(
69070
+ (entry) => typeof entry[1] === "string"
69071
+ )
69072
+ );
69073
+ } catch {
69074
+ return {};
69075
+ }
69076
+ }
69077
+ function writeApprovals(cwd, approvals) {
69078
+ const path = approvalsFile(cwd);
69079
+ mkdirSync8(parent(path), { recursive: true });
69080
+ writeFileSync5(path, `${dumps(approvals, { indent: 2, sortKeys: true })}
69081
+ `);
69082
+ }
69083
+ function approve(config2, cwd) {
69084
+ writeApprovals(cwd, { ...readApprovals(cwd), [config2.name]: fingerprint2(config2) });
69085
+ }
69086
+ function revoke(name, cwd) {
69087
+ const approvals = readApprovals(cwd);
69088
+ if (!(name in approvals)) return;
69089
+ delete approvals[name];
69090
+ writeApprovals(cwd, approvals);
69091
+ }
69092
+ function partition(configs, cwd) {
69093
+ const approvals = readApprovals(cwd);
69094
+ const startable = [];
69095
+ const waiting = [];
69096
+ for (const config2 of configs) {
69097
+ const approved = approvals[config2.name];
69098
+ if (config2.source === "user" || approved === fingerprint2(config2)) startable.push(config2);
69099
+ else waiting.push({ config: config2, changed: approved !== void 0 });
69100
+ }
69101
+ return [startable, waiting];
69102
+ }
69103
+ function describeServer(config2) {
69104
+ if (config2.url) return config2.url;
69105
+ return join2([config2.command ?? "", ...config2.args]);
69106
+ }
69107
+ function waitingNotice({ config: config2, changed }) {
69108
+ const why = changed ? "has changed since you approved it" : "came with the project and has not been approved";
69109
+ 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.`;
69110
+ }
69111
+
69037
69112
  // src/mcp/client.ts
69038
69113
  init_json();
69039
69114
  init_net();
@@ -69614,7 +69689,7 @@ init_json();
69614
69689
  init_pathlib();
69615
69690
  init_paths();
69616
69691
  init_py();
69617
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "node:fs";
69692
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync7 } from "node:fs";
69618
69693
 
69619
69694
  // src/tools/base.ts
69620
69695
  function toolResult(content, fields = {}) {
@@ -69671,7 +69746,7 @@ function isAbort(error, signal) {
69671
69746
  init_pathlib();
69672
69747
  init_paths();
69673
69748
  init_py();
69674
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "node:fs";
69749
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "node:fs";
69675
69750
  var MIN_BODY_CHARS = 200;
69676
69751
  function elision(omittedLines, omittedChars, path) {
69677
69752
  return `
@@ -69726,10 +69801,10 @@ function split2(lines2, charCap, lineCap, headRatio) {
69726
69801
  }
69727
69802
  function spill(text3, sessionId, toolUseId) {
69728
69803
  const directory = sessionOutputsDir(sessionId);
69729
- mkdirSync8(directory, { recursive: true });
69804
+ mkdirSync9(directory, { recursive: true });
69730
69805
  const safe = toolUseId.replace(/[^A-Za-z0-9_.-]/g, "_") || "output";
69731
69806
  const path = join(directory, `${safe}.txt`);
69732
- writeFileSync5(path, text3, "utf8");
69807
+ writeFileSync6(path, text3, "utf8");
69733
69808
  return path;
69734
69809
  }
69735
69810
  function summarizeForUi(text3, maxLen = 120) {
@@ -70295,6 +70370,7 @@ function serverConfig(fields) {
70295
70370
  enabled: true,
70296
70371
  timeout: 30,
70297
70372
  oauth: noOptions(),
70373
+ source: "user",
70298
70374
  ...fields
70299
70375
  };
70300
70376
  }
@@ -70369,11 +70445,13 @@ async function normalise(raw, tool) {
70369
70445
  return [images2, problems];
70370
70446
  }
70371
70447
  var MCPManager = class {
70372
- constructor(configs, store = new AuthStore()) {
70448
+ constructor(configs, store = new AuthStore(), waiting = []) {
70373
70449
  this.store = store;
70450
+ this.waiting = waiting;
70374
70451
  this.configs = configs.filter((config2) => config2.enabled);
70375
70452
  }
70376
70453
  store;
70454
+ waiting;
70377
70455
  configs;
70378
70456
  clients = /* @__PURE__ */ new Map();
70379
70457
  statuses = /* @__PURE__ */ new Map();
@@ -70585,13 +70663,16 @@ function buildTransport(config2, store) {
70585
70663
  var userMcpFile = () => join(userHome(), "mcp.json");
70586
70664
  function loadConfigs(cwd) {
70587
70665
  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);
70666
+ for (const [path, source] of [
70667
+ [userMcpFile(), "user"],
70668
+ [projectMcpFile(cwd), "project"]
70669
+ ])
70670
+ for (const config2 of readFile(path, source)) merged.set(config2.name, config2);
70590
70671
  return [...merged.keys()].sort(compareCodePoints).map((name) => merged.get(name));
70591
70672
  }
70592
70673
  var isObject7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
70593
70674
  var strings = (raw) => Object.fromEntries(Object.entries(isObject7(raw) ? raw : {}).map(([k, v]) => [k, String(v)]));
70594
- function readFile(path) {
70675
+ function readFile(path, source) {
70595
70676
  if (!isFile(path)) return [];
70596
70677
  let data;
70597
70678
  try {
@@ -70617,7 +70698,8 @@ function readFile(path) {
70617
70698
  headers: strings(raw.headers),
70618
70699
  enabled: "enabled" in raw ? truthy(raw.enabled) : true,
70619
70700
  timeout: "timeout" in raw ? toFloat(raw.timeout) : 30,
70620
- oauth: optionsFromJson(raw.oauth)
70701
+ oauth: optionsFromJson(raw.oauth),
70702
+ source
70621
70703
  })
70622
70704
  );
70623
70705
  }
@@ -70632,7 +70714,7 @@ function readJson(path) {
70632
70714
  return {};
70633
70715
  }
70634
70716
  }
70635
- var writeJson = (path, data) => writeFileSync6(path, `${dumps(data, { indent: 2, sortKeys: true })}
70717
+ var writeJson = (path, data) => writeFileSync7(path, `${dumps(data, { indent: 2, sortKeys: true })}
70636
70718
  `);
70637
70719
  function saveConfig(config2, cwd, userLevel = false) {
70638
70720
  const path = userLevel ? userMcpFile() : projectMcpFile(cwd);
@@ -70648,7 +70730,7 @@ function saveConfig(config2, cwd, userLevel = false) {
70648
70730
  const block2 = optionsToJson(config2.oauth);
70649
70731
  if (Object.keys(block2).length) entry.oauth = block2;
70650
70732
  servers[config2.name] = entry;
70651
- mkdirSync9(parent(path), { recursive: true });
70733
+ mkdirSync10(parent(path), { recursive: true });
70652
70734
  writeJson(path, data);
70653
70735
  return path;
70654
70736
  }
@@ -70673,6 +70755,69 @@ function removeConfig(name, cwd, userLevel = false) {
70673
70755
  init_net();
70674
70756
  init_pathlib();
70675
70757
  init_paths();
70758
+
70759
+ // src/providers/deferred.ts
70760
+ init_resolve();
70761
+ init_base();
70762
+ init_registry();
70763
+ 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.";
70764
+ var NEEDS = {
70765
+ openrouter: "an OpenRouter API key",
70766
+ "openai-codex": "a ChatGPT Plus/Pro sign-in",
70767
+ "claude-code": "a Claude Pro/Max sign-in",
70768
+ devin: "a Devin sign-in"
70769
+ };
70770
+ function signInHint(modelId, resolver) {
70771
+ if (!available(resolver).length) return NOT_SIGNED_IN;
70772
+ const route = providerFor(modelId);
70773
+ const needs = NEEDS[route.id] ?? `a ${route.label} sign-in`;
70774
+ return `${modelId} needs ${needs}, which is not set up yet. Type /login to sign in, or /model to pick a model you can use.`;
70775
+ }
70776
+ var DeferredProvider = class {
70777
+ constructor(modelId, resolver, sessionId, models) {
70778
+ this.modelId = modelId;
70779
+ this.resolver = resolver;
70780
+ this.sessionId = sessionId;
70781
+ this.models = models;
70782
+ }
70783
+ modelId;
70784
+ resolver;
70785
+ sessionId;
70786
+ models;
70787
+ inner = null;
70788
+ get name() {
70789
+ return this.inner?.name ?? providerFor(this.modelId).id;
70790
+ }
70791
+ /** The prompt-cache key, kept for the provider this becomes. */
70792
+ setSessionId(sessionId) {
70793
+ this.sessionId = sessionId;
70794
+ this.inner?.setSessionId?.(sessionId);
70795
+ }
70796
+ /** A key saved now is read from the store when the provider is built. */
70797
+ setApiKey(key2) {
70798
+ this.inner?.setApiKey?.(key2);
70799
+ }
70800
+ async *astream(request2, signal) {
70801
+ if (this.inner === null) {
70802
+ try {
70803
+ this.inner = buildProvider(request2.model, this.resolver, {
70804
+ sessionId: this.sessionId,
70805
+ models: this.models
70806
+ });
70807
+ } catch (error) {
70808
+ if (error instanceof MissingCredential || error instanceof ExpiredCredential)
70809
+ throw new ProviderError(signInHint(request2.model, this.resolver));
70810
+ throw error;
70811
+ }
70812
+ }
70813
+ yield* this.inner.astream(request2, signal);
70814
+ }
70815
+ async aclose() {
70816
+ await this.inner?.aclose();
70817
+ }
70818
+ };
70819
+
70820
+ // src/cli.ts
70676
70821
  init_models();
70677
70822
  init_openrouter();
70678
70823
  init_registry();
@@ -71228,7 +71373,7 @@ init_py();
71228
71373
 
71229
71374
  // src/tools/anchors.ts
71230
71375
  init_py();
71231
- import { createHash as createHash8 } from "node:crypto";
71376
+ import { createHash as createHash9 } from "node:crypto";
71232
71377
  var WIDTHS = [4, 6, 8];
71233
71378
  var SEPARATOR = "\0";
71234
71379
  function split3(content) {
@@ -71243,7 +71388,7 @@ function window2(lines2, index) {
71243
71388
  return [previous, lines2[index], following].join(SEPARATOR);
71244
71389
  }
71245
71390
  function digests(lines2) {
71246
- return lines2.map((_, i) => createHash8("sha1").update(window2(lines2, i), "utf8").digest("hex"));
71391
+ return lines2.map((_, i) => createHash9("sha1").update(window2(lines2, i), "utf8").digest("hex"));
71247
71392
  }
71248
71393
  function anchorWidth(full) {
71249
71394
  const distinct = new Set(full).size;
@@ -71289,7 +71434,7 @@ var AnchorError = class extends Error {
71289
71434
  init_images();
71290
71435
  init_pathlib();
71291
71436
  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";
71437
+ import { closeSync as closeSync6, existsSync as existsSync2, openSync as openSync6, readFileSync as readFileSync4, readSync as readSync2, statSync as statSync10 } from "node:fs";
71293
71438
  var DESCRIPTION = `Read a file from the local filesystem.
71294
71439
 
71295
71440
  Returns numbered lines, each labelled with a short content anchor:
@@ -71326,7 +71471,7 @@ var ReadTool = class extends Tool {
71326
71471
  if (isImagePath(path) || isBinary(path) && looksLikeImage(path)) return readImage(path);
71327
71472
  if (isBinary(path)) {
71328
71473
  throw new ToolError(
71329
- `${path}: binary file (${count(statSync9(path).size, "byte")}), not read as text`
71474
+ `${path}: binary file (${count(statSync10(path).size, "byte")}), not read as text`
71330
71475
  );
71331
71476
  }
71332
71477
  const offset = Math.max(toInt(params.offset || 1), 1);
@@ -71417,16 +71562,16 @@ function clip2(line) {
71417
71562
  init_pathlib();
71418
71563
  import { randomBytes as randomBytes4 } from "node:crypto";
71419
71564
  import {
71420
- chmodSync as chmodSync2,
71565
+ chmodSync as chmodSync3,
71421
71566
  closeSync as closeSync7,
71422
71567
  existsSync as existsSync3,
71423
71568
  lstatSync as lstatSync2,
71424
- mkdirSync as mkdirSync10,
71569
+ mkdirSync as mkdirSync11,
71425
71570
  openSync as openSync7,
71426
71571
  renameSync as renameSync5,
71427
71572
  rmSync as rmSync3,
71428
- statSync as statSync10,
71429
- writeFileSync as writeFileSync7
71573
+ statSync as statSync11,
71574
+ writeFileSync as writeFileSync8
71430
71575
  } from "node:fs";
71431
71576
  var DESCRIPTION2 = `Write a file, overwriting it if it exists.
71432
71577
 
@@ -71472,7 +71617,7 @@ var WriteTool = class extends Tool {
71472
71617
  }
71473
71618
  }
71474
71619
  const existed = existsSync3(path);
71475
- mkdirSync10(parent(path), { recursive: true });
71620
+ mkdirSync11(parent(path), { recursive: true });
71476
71621
  this.checkpoints?.capture(path);
71477
71622
  writeAtomic(path, content);
71478
71623
  ctx.reads.markRead(path);
@@ -71506,7 +71651,7 @@ function writeAtomic(path, content) {
71506
71651
  }
71507
71652
  let mode2;
71508
71653
  try {
71509
- mode2 = statSync10(target).mode & 4095;
71654
+ mode2 = statSync11(target).mode & 4095;
71510
71655
  } catch (error) {
71511
71656
  if (error.code !== "ENOENT") throw error;
71512
71657
  mode2 = null;
@@ -71515,11 +71660,11 @@ function writeAtomic(path, content) {
71515
71660
  const fd = openSync7(tmp, "wx", mode2 ?? 438);
71516
71661
  try {
71517
71662
  try {
71518
- writeFileSync7(fd, content, "utf8");
71663
+ writeFileSync8(fd, content, "utf8");
71519
71664
  } finally {
71520
71665
  closeSync7(fd);
71521
71666
  }
71522
- if (mode2 !== null) chmodSync2(tmp, mode2);
71667
+ if (mode2 !== null) chmodSync3(tmp, mode2);
71523
71668
  renameSync5(tmp, target);
71524
71669
  } catch (error) {
71525
71670
  rmSync3(tmp, { force: true });
@@ -72415,7 +72560,7 @@ ${gate.contextText}`;
72415
72560
  };
72416
72561
  for (const call of calls) {
72417
72562
  if (this.runsSerially(call)) {
72418
- for (const pending of concurrent) await run(pending);
72563
+ if (concurrent.length) await gather(concurrent);
72419
72564
  concurrent = [];
72420
72565
  await run(call);
72421
72566
  } else concurrent.push(call);
@@ -72950,7 +73095,7 @@ init_lateinject();
72950
73095
  init_pathlib();
72951
73096
  init_py();
72952
73097
  import { spawnSync as spawnSync3 } from "node:child_process";
72953
- import { statSync as statSync11 } from "node:fs";
73098
+ import { statSync as statSync12 } from "node:fs";
72954
73099
  var STATUS_COMMAND = ["git", "status", "--porcelain=v2", "--branch", "-z"];
72955
73100
  var ROOT_COMMAND = ["git", "rev-parse", "--show-toplevel"];
72956
73101
  var TIMEOUT_SECONDS = 2;
@@ -73089,7 +73234,7 @@ var BranchWatcher = class {
73089
73234
  if (head4 === null) return null;
73090
73235
  let stamp;
73091
73236
  try {
73092
- const info = statSync11(head4, { bigint: true });
73237
+ const info = statSync12(head4, { bigint: true });
73093
73238
  stamp = `${info.mtimeNs}:${info.size}`;
73094
73239
  } catch {
73095
73240
  return this.current;
@@ -73193,7 +73338,7 @@ function statusCode(xy) {
73193
73338
  }
73194
73339
  function signature(path) {
73195
73340
  try {
73196
- const info = statSync11(path, { bigint: true });
73341
+ const info = statSync12(path, { bigint: true });
73197
73342
  return `${info.mtimeNs}:${info.size}`;
73198
73343
  } catch {
73199
73344
  return "none";
@@ -73383,6 +73528,29 @@ init_py();
73383
73528
  init_shlex();
73384
73529
  var OPERATORS = ["&&", "||", ";;", ";", "|&", "|", "\n", "&"];
73385
73530
  var DYNAMIC_EXECUTABLES = /* @__PURE__ */ new Set(["eval", "exec", "source", ".", "env"]);
73531
+ var WRAPPERS = /* @__PURE__ */ new Set([
73532
+ "builtin",
73533
+ "caffeinate",
73534
+ "chrt",
73535
+ "command",
73536
+ "doas",
73537
+ "flock",
73538
+ "gtimeout",
73539
+ "ionice",
73540
+ "nice",
73541
+ "nohup",
73542
+ "setsid",
73543
+ "stdbuf",
73544
+ "strace",
73545
+ "sudo",
73546
+ "taskset",
73547
+ "time",
73548
+ "timeout",
73549
+ "unbuffer",
73550
+ "watch",
73551
+ "xargs"
73552
+ ]);
73553
+ var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*\+?=/;
73386
73554
  var FD_DUP = /^\d*>&\d*-?$/;
73387
73555
  var OUT_OP = /^(?:\d*>>?|&>>?)$/;
73388
73556
  var IN_OP = /^\d*<<?-?$/;
@@ -73468,7 +73636,17 @@ var READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
73468
73636
  var MUTATING_FLAGS = {
73469
73637
  sed: /* @__PURE__ */ new Set(["-i", "--in-place"]),
73470
73638
  awk: /* @__PURE__ */ new Set(["-i", "--include"]),
73471
- find: /* @__PURE__ */ new Set(["-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprintf", "-fls"]),
73639
+ find: /* @__PURE__ */ new Set([
73640
+ "-delete",
73641
+ "-exec",
73642
+ "-execdir",
73643
+ "-ok",
73644
+ "-okdir",
73645
+ "-fprint",
73646
+ "-fprint0",
73647
+ "-fprintf",
73648
+ "-fls"
73649
+ ]),
73472
73650
  fd: /* @__PURE__ */ new Set(["-x", "--exec", "-X", "--exec-batch"]),
73473
73651
  sort: /* @__PURE__ */ new Set(["-o", "--output", "--compress-program"]),
73474
73652
  tree: /* @__PURE__ */ new Set(["-o"]),
@@ -73643,10 +73821,18 @@ function toSegment(text3, inSub) {
73643
73821
  if (tokens.length === 0) return [null, [], true];
73644
73822
  const [words, redirects] = splitRedirections(tokens);
73645
73823
  if (words.length === 0) return [null, redirects, true];
73646
- const executable = words[0];
73824
+ let first2 = 0;
73825
+ while (first2 < words.length && ASSIGNMENT.test(words[first2])) first2++;
73826
+ const executable = words[first2] ?? "";
73647
73827
  const ok = !executable.includes("$") && !executable.includes("`");
73648
73828
  return [
73649
- { raw: stripped, executable, args: words.slice(1), inSubstitution: inSub },
73829
+ {
73830
+ raw: stripped,
73831
+ executable,
73832
+ args: words.slice(first2 + 1),
73833
+ assignments: words.slice(0, first2),
73834
+ inSubstitution: inSub
73835
+ },
73650
73836
  redirects,
73651
73837
  ok
73652
73838
  ];
@@ -73726,13 +73912,20 @@ function scan(text3) {
73726
73912
  index++;
73727
73913
  continue;
73728
73914
  }
73729
- if (text3.startsWith("$(", index) || char === "`") {
73915
+ if (text3.startsWith("$(", index) || text3.startsWith("<(", index) || text3.startsWith(">(", index) || char === "`") {
73730
73916
  const [body, consumed, closed] = readSubstitution(text3, index);
73731
73917
  substitutions.push(...recurse(body, substitutions));
73732
73918
  ok = ok && closed;
73733
73919
  index += consumed;
73734
73920
  continue;
73735
73921
  }
73922
+ if (char === "(" && strip(buffer)) {
73923
+ const [body, consumed] = readSubstitution(text3, index);
73924
+ substitutions.push(...recurse(body, substitutions));
73925
+ ok = false;
73926
+ index += consumed;
73927
+ continue;
73928
+ }
73736
73929
  if (char === "(" || char === ")") {
73737
73930
  index++;
73738
73931
  continue;
@@ -73770,27 +73963,51 @@ function readSubstitution(text3, index) {
73770
73963
  if (end === -1) return [text3.slice(index + 1), text3.length - index, false];
73771
73964
  return [text3.slice(index + 1, end), end - index + 1, true];
73772
73965
  }
73966
+ const open = text3.indexOf("(", index);
73773
73967
  let depth = 0;
73774
- for (let cursor = index + 1; cursor < text3.length; cursor++) {
73968
+ for (let cursor = open; cursor < text3.length; cursor++) {
73775
73969
  if (text3[cursor] === "(") depth++;
73776
73970
  else if (text3[cursor] === ")") {
73777
73971
  depth--;
73778
- if (depth === 0) return [text3.slice(index + 2, cursor), cursor - index + 1, true];
73972
+ if (depth === 0) return [text3.slice(open + 1, cursor), cursor - index + 1, true];
73779
73973
  }
73780
73974
  }
73781
- return [text3.slice(index + 2), text3.length - index, false];
73975
+ return [text3.slice(open + 1), text3.length - index, false];
73782
73976
  }
73783
- function matchSpecifier(segment, pattern) {
73977
+ function matchSpecifier(segment, pattern, exact = false) {
73784
73978
  if (pattern === "*" || pattern === ":*") return true;
73785
- const commandLine = [segment.executable, ...segment.args].join(" ");
73979
+ if (exact) return matchWords(written(segment), pattern);
73980
+ return commandForms(segment).some((words) => matchWords(words, pattern));
73981
+ }
73982
+ function matchWords(words, pattern) {
73983
+ const commandLine = words.join(" ");
73786
73984
  if (pattern.endsWith(":*")) {
73787
73985
  const prefix = strip(pattern.slice(0, -2));
73788
73986
  return commandLine === prefix || commandLine.startsWith(`${prefix} `);
73789
73987
  }
73790
73988
  if (pattern.includes(" ")) return commandLine === pattern;
73791
- return segment.executable === pattern;
73989
+ return words[0] === pattern;
73990
+ }
73991
+ var written = (segment) => [...segment.assignments, segment.executable, ...segment.args].filter(Boolean);
73992
+ function commandForms(segment) {
73993
+ const words = [segment.executable, ...segment.args].filter(Boolean);
73994
+ const forms = [];
73995
+ const add2 = (command2) => {
73996
+ forms.push(command2);
73997
+ const name = basename2(command2[0]);
73998
+ if (name !== command2[0]) forms.push([name, ...command2.slice(1)]);
73999
+ };
74000
+ if (words.length === 0) return forms;
74001
+ add2(words);
74002
+ if (WRAPPERS.has(basename2(words[0]))) {
74003
+ for (let index = 1; index < words.length; index++)
74004
+ if (!words[index].startsWith("-")) add2(words.slice(index));
74005
+ }
74006
+ return forms;
73792
74007
  }
74008
+ var basename2 = (path) => path.slice(path.lastIndexOf("/") + 1);
73793
74009
  function isReadOnly(segment) {
74010
+ if (segment.assignments.length) return false;
73794
74011
  if (segment.executable === "git") return gitIsReadOnly(segment.args);
73795
74012
  if (!READ_ONLY_COMMANDS.has(segment.executable)) return false;
73796
74013
  if (segment.executable === "env") return !envRunsACommand(segment.args);
@@ -73803,13 +74020,13 @@ function isReadOnly(segment) {
73803
74020
  );
73804
74021
  }
73805
74022
  var PROGRAM_TEXT_COMMANDS = /* @__PURE__ */ new Set(["awk", "sed"]);
73806
- var AWK_WRITES = /system\s*\(|\|\s*["']|["']\s*\||print[f]?[^;}\n]*>/;
74023
+ var AWK_WRITES = /system\s*\(|\|\s*["']|["']\s*\||\|\s*getline\b|\|&|print[f]?[^;}\n]*[>|]|@(?:include|load)\b/;
73807
74024
  var PROGRAM_TEXT_OPTIONS = {
73808
74025
  awk: ["-e", "--source"],
73809
74026
  sed: ["-e", "--expression"]
73810
74027
  };
73811
74028
  var PROGRAM_FILE_OPTIONS = {
73812
- awk: ["-f", "--file", "-E", "--exec"],
74029
+ awk: ["-f", "--file", "-E", "--exec", "-l", "--load"],
73813
74030
  sed: ["-f", "--file"]
73814
74031
  };
73815
74032
  function namesAnOutput(segment) {
@@ -73980,7 +74197,182 @@ function splitGitSubcommand(args) {
73980
74197
  return [null, []];
73981
74198
  }
73982
74199
 
74200
+ // src/permissions/sandbox.ts
74201
+ init_pathlib();
74202
+ init_which();
74203
+ import { randomBytes as randomBytes6 } from "node:crypto";
74204
+ import { closeSync as closeSync8, existsSync as existsSync4, mkdirSync as mkdirSync12, openSync as openSync8, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "node:fs";
74205
+ import { tmpdir as tmpdir2 } from "node:os";
74206
+ var CREDENTIAL_PATHS = [
74207
+ "~/.ssh/",
74208
+ "~/.aws/",
74209
+ "~/.gnupg/",
74210
+ "~/.kube/",
74211
+ "~/.docker/config.json",
74212
+ "~/.config/gh/",
74213
+ "~/.netrc",
74214
+ "~/.npmrc",
74215
+ "~/.pypirc",
74216
+ "~/.hx/auth.json"
74217
+ ];
74218
+ var Sandbox = class {
74219
+ constructor(policy, backend) {
74220
+ this.policy = policy;
74221
+ this.backend = backend ?? detectBackend();
74222
+ }
74223
+ policy;
74224
+ backend;
74225
+ profilePath = null;
74226
+ /** False when degraded to `none`. */
74227
+ get active() {
74228
+ return this.backend !== "none";
74229
+ }
74230
+ /** The argv to actually exec. */
74231
+ wrap(argv) {
74232
+ if (this.backend === "seatbelt")
74233
+ return ["/usr/bin/sandbox-exec", "-f", this.writeProfile(), ...argv];
74234
+ if (this.backend === "bubblewrap") return buildBwrapArgv(this.policy, argv);
74235
+ return [...argv];
74236
+ }
74237
+ /** The generated Seatbelt profile - shown by `/permissions`. */
74238
+ profileText() {
74239
+ return buildSeatbeltProfile(this.policy);
74240
+ }
74241
+ writeProfile() {
74242
+ if (this.profilePath === null || !existsSync4(this.profilePath)) {
74243
+ const path = join(tmpdir2(), `hx-sandbox-${randomBytes6(4).toString("hex")}.sb`);
74244
+ writeFileSync9(path, this.profileText(), { mode: 384 });
74245
+ this.profilePath = path;
74246
+ }
74247
+ return this.profilePath;
74248
+ }
74249
+ cleanup() {
74250
+ if (this.profilePath !== null) {
74251
+ rmSync4(this.profilePath, { force: true });
74252
+ this.profilePath = null;
74253
+ }
74254
+ }
74255
+ };
74256
+ function detectBackend() {
74257
+ if (process.platform === "darwin" && existsSync4("/usr/bin/sandbox-exec")) return "seatbelt";
74258
+ if (process.platform === "linux" && which("bwrap")) return "bubblewrap";
74259
+ return "none";
74260
+ }
74261
+ function defaultPolicy(cwd, allowNetwork = false) {
74262
+ const temp = resolve(tmpdir2());
74263
+ const writable = [resolve(cwd), ...tempContainer(temp)];
74264
+ for (const extra of ["~/.cache", "~/Library/Caches"]) {
74265
+ const candidate = expandUser(extra);
74266
+ if (isDir(candidate)) writable.push(resolve(candidate));
74267
+ }
74268
+ return {
74269
+ writablePaths: [...new Set(writable)],
74270
+ readablePaths: ["/"],
74271
+ denyPaths: CREDENTIAL_PATHS.map((p) => ({
74272
+ path: real(absolute(expandUser(p))),
74273
+ isDir: p.endsWith("/")
74274
+ })),
74275
+ allowNetwork,
74276
+ allowSubprocess: true,
74277
+ cwd: resolve(cwd)
74278
+ };
74279
+ }
74280
+ var DARWIN_TEMP_ROOT = "/private/var/folders";
74281
+ var isRelativeTo = (path, root) => path === root || path.startsWith(`${root.replace(/\/$/, "")}/`);
74282
+ function tempContainer(temp) {
74283
+ const paths = [temp];
74284
+ if (isRelativeTo(temp, DARWIN_TEMP_ROOT)) {
74285
+ const container = parent(temp);
74286
+ if (isRelativeTo(container, DARWIN_TEMP_ROOT) && container !== DARWIN_TEMP_ROOT)
74287
+ paths.push(container);
74288
+ }
74289
+ return paths;
74290
+ }
74291
+ function buildSeatbeltProfile(policy) {
74292
+ const lines2 = [
74293
+ "(version 1)",
74294
+ "(deny default)",
74295
+ "",
74296
+ ";; Process management",
74297
+ "(allow process-fork)",
74298
+ "(allow signal (target same-sandbox))",
74299
+ "(allow sysctl-read)",
74300
+ "(allow mach-lookup)",
74301
+ "(allow ipc-posix-shm)",
74302
+ policy.allowSubprocess ? "(allow process-exec*)" : "(deny process-exec*)",
74303
+ "",
74304
+ ";; Reads"
74305
+ ];
74306
+ for (const path of policy.readablePaths.length ? policy.readablePaths : ["/"]) {
74307
+ lines2.push(`(allow file-read* (subpath ${sbplString(real(path))}))`);
74308
+ }
74309
+ lines2.push("", ";; Writes");
74310
+ for (const path of policy.writablePaths)
74311
+ lines2.push(`(allow file-write* (subpath ${sbplString(real(path))}))`);
74312
+ for (const device of ["/dev/null", "/dev/stdout", "/dev/stderr", "/dev/tty", "/dev/urandom"]) {
74313
+ lines2.push(`(allow file-write-data file-read-data (literal ${sbplString(device)}))`);
74314
+ }
74315
+ lines2.push("", ";; Network", policy.allowNetwork ? "(allow network*)" : "(deny network*)");
74316
+ if (policy.denyPaths.length) {
74317
+ lines2.push("", ";; Credentials - last match wins, so these override both allowances");
74318
+ for (const denied of policy.denyPaths) {
74319
+ lines2.push(`(deny file-read* file-write* (subpath ${sbplString(real(denied.path))}))`);
74320
+ }
74321
+ }
74322
+ return `${lines2.join("\n")}
74323
+ `;
74324
+ }
74325
+ function credentialPathOf(path) {
74326
+ for (const entry of CREDENTIAL_PATHS) {
74327
+ const denied = real(absolute(expandUser(entry)));
74328
+ if (path === denied || entry.endsWith("/") && isRelativeTo(path, denied)) return entry;
74329
+ }
74330
+ return null;
74331
+ }
74332
+ var real = (path) => resolve(path);
74333
+ function sbplString(path) {
74334
+ return `"${path.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
74335
+ }
74336
+ function buildBwrapArgv(policy, argv) {
74337
+ const command2 = [
74338
+ "bwrap",
74339
+ "--ro-bind",
74340
+ "/",
74341
+ "/",
74342
+ "--dev",
74343
+ "/dev",
74344
+ "--proc",
74345
+ "/proc",
74346
+ "--die-with-parent"
74347
+ ];
74348
+ const writable = policy.writablePaths.map(real);
74349
+ for (const path of writable) command2.push("--bind", path, path);
74350
+ for (const denied of policy.denyPaths) {
74351
+ const path = real(denied.path);
74352
+ if (!existsSync4(path) && !ensureMountpoint(denied, writable)) continue;
74353
+ if (isDir(path)) command2.push("--tmpfs", path, "--remount-ro", path);
74354
+ else command2.push("--ro-bind", "/dev/null", path);
74355
+ }
74356
+ if (!policy.allowNetwork) command2.push("--unshare-net");
74357
+ if (policy.cwd !== null) command2.push("--chdir", real(policy.cwd));
74358
+ return [...command2, "--", ...argv];
74359
+ }
74360
+ function ensureMountpoint(denied, writable) {
74361
+ const path = real(denied.path);
74362
+ if (!writable.some((root) => isRelativeTo(path, root))) return false;
74363
+ try {
74364
+ mkdirSync12(parent(path), { recursive: true });
74365
+ if (denied.isDir) mkdirSync12(path, { mode: 448, recursive: true });
74366
+ else closeSync8(openSync8(path, "a", 384));
74367
+ } catch {
74368
+ return false;
74369
+ }
74370
+ return existsSync4(path);
74371
+ }
74372
+
73983
74373
  // src/permissions/engine.ts
74374
+ var FILE_TOOLS = /* @__PURE__ */ new Set(["Read", "Write", "Edit", "Grep", "Glob", "Symbols"]);
74375
+ var PROTECTED_DIRS = /* @__PURE__ */ new Set([".git", ".hx"]);
73984
74376
  var DEFAULT_MUTATING_TOOLS = /* @__PURE__ */ new Set(["Bash", "Write", "Edit", "Task"]);
73985
74377
  function allSpecifiers(request2) {
73986
74378
  if (request2.specifiers?.length) return request2.specifiers;
@@ -74018,6 +74410,12 @@ var PermissionEngine = class _PermissionEngine {
74018
74410
  if (denial) return outcome("deny", "matched a deny rule", denial);
74019
74411
  if (this.sessionGrants.has(this.grantKey(request2)))
74020
74412
  return outcome("allow", "granted for this session");
74413
+ const credential = this.credentialTouched(request2);
74414
+ if (credential !== null) {
74415
+ const named = this.matchAllow(request2, true);
74416
+ if (named) return outcome("allow", "matched an allow rule", named);
74417
+ return outcome("ask", `${credential} holds credentials`);
74418
+ }
74021
74419
  if (request2.toolName === "Bash" && request2.specifier) {
74022
74420
  const parsed = parse4(request2.specifier);
74023
74421
  if (parsed.unparseable) {
@@ -74039,7 +74437,7 @@ var PermissionEngine = class _PermissionEngine {
74039
74437
  return outcome("allow", "read-only tool in plan mode");
74040
74438
  }
74041
74439
  if (!request2.mutating) return outcome("allow", "read-only tool");
74042
- if (this.mode === "acceptEdits" && _PermissionEngine.EDIT_TOOLS.has(request2.toolName)) {
74440
+ if (this.mode === "acceptEdits" && _PermissionEngine.EDIT_TOOLS.has(request2.toolName) && this.editsProject(request2)) {
74043
74441
  return outcome("allow", "acceptEdits mode");
74044
74442
  }
74045
74443
  if (request2.toolName === "Bash" && request2.specifier) {
@@ -74051,6 +74449,33 @@ var PermissionEngine = class _PermissionEngine {
74051
74449
  }
74052
74450
  return outcome("ask", "mutating tool");
74053
74451
  }
74452
+ /** Where `specifier` really is: absolute, with `..` and symlinks resolved. */
74453
+ target(specifier) {
74454
+ return realPath(purePath(expandUser(specifier)), resolve(this.cwd));
74455
+ }
74456
+ /** The credential file or folder a file tool's call reaches, as written in
74457
+ * `CREDENTIAL_PATHS`, or `null`. */
74458
+ credentialTouched(request2) {
74459
+ if (!FILE_TOOLS.has(request2.toolName)) return null;
74460
+ for (const specifier of allSpecifiers(request2)) {
74461
+ const credential = credentialPathOf(this.target(specifier));
74462
+ if (credential !== null) return credential;
74463
+ }
74464
+ return null;
74465
+ }
74466
+ /**
74467
+ * Whether every file an edit changes is the project's own, which is all
74468
+ * acceptEdits covers: inside the working directory once symlinks are
74469
+ * resolved, and not in a folder whose files run code or grant permissions.
74470
+ */
74471
+ editsProject(request2) {
74472
+ const root = resolve(this.cwd);
74473
+ const prefix = root === "/" ? "/" : `${root}/`;
74474
+ const targets = allSpecifiers(request2).map((specifier) => this.target(specifier));
74475
+ return targets.length > 0 && targets.every(
74476
+ (target) => target.startsWith(prefix) && !target.slice(prefix.length).split("/").some((part) => PROTECTED_DIRS.has(part))
74477
+ );
74478
+ }
74054
74479
  applies(rule2, request2) {
74055
74480
  return rule2.tool === request2.toolName || rule2.tool === "*";
74056
74481
  }
@@ -74062,13 +74487,14 @@ var PermissionEngine = class _PermissionEngine {
74062
74487
  return null;
74063
74488
  }
74064
74489
  /**
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 /`.
74490
+ * Allow requires *every* segment to be covered, each as it is written.
74491
+ * Any-segment matching is right for deny (`safe && danger` must be caught)
74492
+ * but catastrophic for allow: `allow: Bash(git status:*)` would green-light
74493
+ * `git status && rm -rf /`.
74068
74494
  */
74069
- matchAllow(request2) {
74495
+ matchAllow(request2, named = false) {
74070
74496
  const candidates2 = this.rules.filter(
74071
- (rule2) => rule2.decision === "allow" && this.applies(rule2, request2)
74497
+ (rule2) => rule2.decision === "allow" && this.applies(rule2, request2) && (!named || rule2.specifier !== null)
74072
74498
  );
74073
74499
  if (candidates2.length === 0) return null;
74074
74500
  if (request2.toolName !== "Bash" || !request2.specifier) {
@@ -74086,7 +74512,7 @@ var PermissionEngine = class _PermissionEngine {
74086
74512
  let first2 = null;
74087
74513
  for (const segment of parsed.segments) {
74088
74514
  const covering = candidates2.find(
74089
- (rule2) => rule2.specifier === null || matchSpecifier(segment, rule2.specifier)
74515
+ (rule2) => rule2.specifier === null || matchSpecifier(segment, rule2.specifier, true)
74090
74516
  );
74091
74517
  if (covering === void 0) return null;
74092
74518
  first2 ??= covering;
@@ -74193,6 +74619,7 @@ function persistableRules(toolName, specifier) {
74193
74619
  return rules;
74194
74620
  }
74195
74621
  function rulePrefix(segment) {
74622
+ if (segment.assignments.length || !segment.executable) return null;
74196
74623
  const first2 = segment.args[0];
74197
74624
  if (first2 !== void 0 && !first2.startsWith("-")) return `${segment.executable} ${first2}`;
74198
74625
  return null;
@@ -74361,172 +74788,6 @@ var InvalidRule = class extends Error {
74361
74788
  name = "InvalidRule";
74362
74789
  };
74363
74790
 
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
74791
  // src/runtime.ts
74531
74792
  init_models();
74532
74793
  init_registry();
@@ -74668,7 +74929,7 @@ init_py();
74668
74929
  init_shlex();
74669
74930
  import { execFile as execFile2, spawn as spawn7 } from "node:child_process";
74670
74931
  import { randomBytes as randomBytes7 } from "node:crypto";
74671
- import { closeSync as closeSync9, mkdirSync as mkdirSync12, openSync as openSync9, readFileSync as readFileSync6 } from "node:fs";
74932
+ import { closeSync as closeSync9, mkdirSync as mkdirSync13, openSync as openSync9, readFileSync as readFileSync6 } from "node:fs";
74672
74933
  import { constants as constants3 } from "node:os";
74673
74934
  var DESCRIPTION5 = `Run a shell command in a persistent session.
74674
74935
 
@@ -74742,11 +75003,17 @@ var Reader = class {
74742
75003
  return this.chunks.shift() ?? Buffer.alloc(0);
74743
75004
  }
74744
75005
  };
75006
+ var POSIX_SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh", "ksh93", "mksh", "ash", "yash"]);
75007
+ function posixShell(shell = null) {
75008
+ const chosen = shell || process.env.SHELL || "";
75009
+ if (POSIX_SHELLS.has(chosen.slice(chosen.lastIndexOf("/") + 1))) return chosen;
75010
+ return isFile("/bin/bash") ? "/bin/bash" : "/bin/sh";
75011
+ }
74745
75012
  var PersistentShell = class {
74746
75013
  constructor(currentCwd, shell = null, env = {}, sandbox = null) {
74747
75014
  this.currentCwd = currentCwd;
74748
75015
  this.sandbox = sandbox;
74749
- this.shell = shell || process.env.SHELL || "/bin/bash";
75016
+ this.shell = posixShell(shell);
74750
75017
  this.env = commandEnvironment({ ...env, HX: "1", TERM: "dumb" });
74751
75018
  }
74752
75019
  currentCwd;
@@ -74949,7 +75216,7 @@ var BackgroundJobs = class {
74949
75216
  */
74950
75217
  async start(command2, cwd, env, sandbox = null) {
74951
75218
  const jobId = `bg_${randomBytes7(4).toString("hex")}`;
74952
- mkdirSync12(this.logDir, { recursive: true });
75219
+ mkdirSync13(this.logDir, { recursive: true });
74953
75220
  const logPath = join(this.logDir, `${jobId}.log`);
74954
75221
  let argv = ["/bin/sh", "-c", command2];
74955
75222
  if (sandbox !== null) argv = sandbox.wrap(argv);
@@ -75228,7 +75495,7 @@ init_json();
75228
75495
  init_py();
75229
75496
 
75230
75497
  // src/tools/glob.ts
75231
- import { statSync as statSync12 } from "node:fs";
75498
+ import { statSync as statSync13 } from "node:fs";
75232
75499
  init_pathlib();
75233
75500
  init_py();
75234
75501
  var DESCRIPTION6 = `Find files by glob pattern (e.g. \`**/*.py\`, \`src/**/*.ts\`).
@@ -75259,6 +75526,10 @@ var GlobTool = class extends Tool {
75259
75526
  required: ["pattern"]
75260
75527
  };
75261
75528
  }
75529
+ /** The folder searched, so a path rule covers a search of it. */
75530
+ permissionSpecifier(params) {
75531
+ return typeof params.path === "string" && params.path ? params.path : null;
75532
+ }
75262
75533
  async run(params, ctx) {
75263
75534
  const root = searchRoot(params.path, ctx.cwd);
75264
75535
  if (!isDir(root)) throw new ToolError(`${root}: not a directory`);
@@ -75301,7 +75572,7 @@ function checkPattern(pattern) {
75301
75572
  var ignored = (path) => parts(path).some((part) => IGNORED_DIRS.has(part));
75302
75573
  function mtime(path) {
75303
75574
  try {
75304
- return statSync12(path).mtimeMs;
75575
+ return statSync13(path).mtimeMs;
75305
75576
  } catch {
75306
75577
  return 0;
75307
75578
  }
@@ -75343,6 +75614,10 @@ var GrepTool = class extends Tool {
75343
75614
  required: ["pattern"]
75344
75615
  };
75345
75616
  }
75617
+ /** The file or folder searched, so a path rule covers a search of it. */
75618
+ permissionSpecifier(params) {
75619
+ return typeof params.path === "string" && params.path ? params.path : null;
75620
+ }
75346
75621
  async run(params, ctx) {
75347
75622
  const pattern = String(params.pattern);
75348
75623
  const ripgrep = which("rg");
@@ -75397,7 +75672,28 @@ function runRipgrep(rg, pattern, root, mode2, params) {
75397
75672
  }
75398
75673
  throw new ToolError(stderr || `ripgrep exited ${result2.status}`);
75399
75674
  }
75400
- return splitLines(result2.stdout ?? "").filter(Boolean);
75675
+ const hidden = credentialsUnder(root);
75676
+ return splitLines(result2.stdout ?? "").filter(
75677
+ (line) => line && !hidden.some((credential) => credential.covers(line))
75678
+ );
75679
+ }
75680
+ function credentialsUnder(root) {
75681
+ const base = root.replace(/\/$/, "");
75682
+ const real2 = resolve(root);
75683
+ const found = [];
75684
+ for (const entry of CREDENTIAL_PATHS) {
75685
+ const spellings = /* @__PURE__ */ new Set([purePath(absolute(expandUser(entry))), resolve(expandUser(entry))]);
75686
+ for (const path of spellings) {
75687
+ let shown;
75688
+ if (path.startsWith(`${real2}/`)) shown = base + path.slice(real2.length);
75689
+ else if (path.startsWith(`${base}/`)) shown = path;
75690
+ else continue;
75691
+ found.push({
75692
+ covers: entry.endsWith("/") ? (line) => line.startsWith(`${shown}/`) : (line) => line === shown || line.startsWith(`${shown}:`) || line.startsWith(`${shown}-`)
75693
+ });
75694
+ }
75695
+ }
75696
+ return found;
75401
75697
  }
75402
75698
  function search(regex, root, mode2, params) {
75403
75699
  const globFilter = String(params.glob || "");
@@ -75424,7 +75720,7 @@ function search(regex, root, mode2, params) {
75424
75720
  function walk(root) {
75425
75721
  if (isFile(root)) return [root];
75426
75722
  return glob(root, "**/*").filter(
75427
- (path) => isFile(path) && !parts(path).some((part) => IGNORED_DIRS.has(part))
75723
+ (path) => isFile(path) && !parts(path).some((part) => IGNORED_DIRS.has(part)) && credentialPathOf(resolve(path)) === null
75428
75724
  );
75429
75725
  }
75430
75726
  function renderContext(path, lines2, hits, before2, after2) {
@@ -75437,7 +75733,7 @@ function renderContext(path, lines2, hits, before2, after2) {
75437
75733
  }
75438
75734
 
75439
75735
  // src/tools/symbols.ts
75440
- import { readFileSync as readFileSync8, statSync as statSync13 } from "node:fs";
75736
+ import { readFileSync as readFileSync8, statSync as statSync14 } from "node:fs";
75441
75737
  import { createRequire } from "node:module";
75442
75738
  import { dirname } from "node:path";
75443
75739
  init_json();
@@ -75511,11 +75807,11 @@ var LANGUAGES = {
75511
75807
  ".go": GO,
75512
75808
  ".rs": RUST
75513
75809
  };
75514
- var PACKAGE2 = "@vscode/tree-sitter-wasm";
75810
+ var PACKAGE = "@vscode/tree-sitter-wasm";
75515
75811
  var load = createRequire(import.meta.url);
75516
75812
  function packageDir() {
75517
75813
  try {
75518
- return dirname(load.resolve(PACKAGE2));
75814
+ return dirname(load.resolve(PACKAGE));
75519
75815
  } catch {
75520
75816
  return null;
75521
75817
  }
@@ -75529,9 +75825,9 @@ function parserFor(grammar) {
75529
75825
  parser = (async () => {
75530
75826
  const directory = packageDir();
75531
75827
  if (directory === null)
75532
- throw new ToolError(`tree-sitter is not installed: npm install -g ${PACKAGE2}`);
75828
+ throw new ToolError(`tree-sitter is not installed: npm install -g ${PACKAGE}`);
75533
75829
  runtime ??= (async () => {
75534
- const module = load(PACKAGE2);
75830
+ const module = load(PACKAGE);
75535
75831
  await module.Parser.init({ locateFile: (file) => join(directory, file) });
75536
75832
  return module;
75537
75833
  })();
@@ -75569,6 +75865,12 @@ var SymbolsTool = class extends Tool {
75569
75865
  required: ["mode"]
75570
75866
  };
75571
75867
  }
75868
+ /** The file or folder read, so a path rule covers it. */
75869
+ permissionSpecifier(params) {
75870
+ for (const key2 of ["file_path", "path"])
75871
+ if (typeof params[key2] === "string" && params[key2]) return params[key2];
75872
+ return null;
75873
+ }
75572
75874
  async run(params, ctx) {
75573
75875
  const mode2 = String(params.mode);
75574
75876
  const limit = toInt(params.limit || DEFAULT_LIMIT3);
@@ -75677,7 +75979,7 @@ function candidates(params, ctx) {
75677
75979
  }
75678
75980
  function mtime2(path) {
75679
75981
  try {
75680
- return statSync13(path).mtimeMs;
75982
+ return statSync14(path).mtimeMs;
75681
75983
  } catch {
75682
75984
  return 0;
75683
75985
  }
@@ -75706,7 +76008,7 @@ function specFor(path) {
75706
76008
  async function parse5(path, spec2) {
75707
76009
  let size3;
75708
76010
  try {
75709
- size3 = statSync13(path).size;
76011
+ size3 = statSync14(path).size;
75710
76012
  } catch (error) {
75711
76013
  throw new ToolError(`${path}: ${osErrorText(error)}`);
75712
76014
  }
@@ -76413,7 +76715,7 @@ ${listed2}
76413
76715
  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
76716
  ];
76415
76717
  }
76416
- function buildRuntime(parsed, resume = null) {
76718
+ function buildRuntime(parsed, resume = null, signedOut = false) {
76417
76719
  ensureUserDirs();
76418
76720
  const settings = loadSettings(parsed.cwd, parsed.overrides);
76419
76721
  const bus = new EventBus();
@@ -76424,10 +76726,16 @@ function buildRuntime(parsed, resume = null) {
76424
76726
  models.loadCache();
76425
76727
  const modelInfo2 = models.getOrDefault(settings.models.model);
76426
76728
  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
- });
76729
+ let provider;
76730
+ try {
76731
+ provider = buildProvider(settings.models.model, auth, {
76732
+ sessionId: session.meta.sessionId,
76733
+ models
76734
+ });
76735
+ } catch (error) {
76736
+ if (!(signedOut && error instanceof MissingCredential)) throw error;
76737
+ provider = new DeferredProvider(settings.models.model, auth, session.meta.sessionId, models);
76738
+ }
76431
76739
  const sandbox = settings.permissions.sandbox ? new Sandbox(defaultPolicy(settings.cwd, settings.permissions.allowNetwork)) : null;
76432
76740
  const shell = new PersistentShell(settings.cwd, settings.bash.shell, {}, sandbox);
76433
76741
  const jobs = new BackgroundJobs(join(sessionOutputsDir(session.meta.sessionId), "jobs"));
@@ -76460,6 +76768,13 @@ function buildRuntime(parsed, resume = null) {
76460
76768
  `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
76769
  );
76462
76770
  }
76771
+ if (settings.bash.shell && posixShell(settings.bash.shell) !== settings.bash.shell)
76772
+ notices.push(
76773
+ `bash.shell is ${settings.bash.shell}, which does not speak POSIX sh; commands run in ${shell.shell}.`
76774
+ );
76775
+ const [mcpServers, waitingServers] = partition(loadConfigs(settings.cwd), settings.cwd);
76776
+ const waiting = waitingServers.filter((server) => server.config.enabled);
76777
+ for (const server of waiting) notices.push(waitingNotice(server));
76463
76778
  const agents = new Map(discover2(settings.cwd).map((agent) => [agent.name, agent]));
76464
76779
  const subagents = new SubagentRunner({
76465
76780
  definitions: agents,
@@ -76520,7 +76835,7 @@ function buildRuntime(parsed, resume = null) {
76520
76835
  tools,
76521
76836
  checkpoints,
76522
76837
  hooks,
76523
- new MCPManager(loadConfigs(settings.cwd)),
76838
+ new MCPManager(mcpServers, void 0, waiting),
76524
76839
  notices
76525
76840
  );
76526
76841
  }
@@ -76540,7 +76855,7 @@ ${listed2}`,
76540
76855
 
76541
76856
  // src/trace.ts
76542
76857
  import { spawn as spawn8 } from "node:child_process";
76543
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync9 } from "node:fs";
76858
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync10 } from "node:fs";
76544
76859
  import { pathToFileURL } from "node:url";
76545
76860
  init_messages();
76546
76861
  init_json();
@@ -76584,8 +76899,8 @@ function renderHtml(trace) {
76584
76899
  return trace_template_default.replace(/__HX_TRACE_(?:TITLE|DATA)__/g, (match4) => filled[match4]);
76585
76900
  }
76586
76901
  function writeTrace(trace, path) {
76587
- mkdirSync13(parent(path), { recursive: true });
76588
- writeFileSync9(path, renderHtml(trace), "utf8");
76902
+ mkdirSync14(parent(path), { recursive: true });
76903
+ writeFileSync10(path, renderHtml(trace), "utf8");
76589
76904
  return path;
76590
76905
  }
76591
76906
  var defaultTracePath = (session) => join(sessionDir(session.meta.sessionId), TRACE_FILENAME);
@@ -77883,8 +78198,8 @@ var ProcessTerminal = class {
77883
78198
  let buffer = Buffer.from(data);
77884
78199
  while (buffer.length) {
77885
78200
  try {
77886
- const written = writeSync5(1, buffer);
77887
- buffer = buffer.subarray(written);
78201
+ const written2 = writeSync5(1, buffer);
78202
+ buffer = buffer.subarray(written2);
77888
78203
  } catch (error) {
77889
78204
  if (error.code === "EAGAIN") continue;
77890
78205
  return;
@@ -78292,13 +78607,302 @@ var ShiftEnter = class {
78292
78607
  }
78293
78608
  };
78294
78609
 
78610
+ // src/term/component.ts
78611
+ var Widget = class {
78612
+ cache = [];
78613
+ cacheWidth = -1;
78614
+ isDirty = true;
78615
+ render(width) {
78616
+ if (this.isDirty || width !== this.cacheWidth) {
78617
+ this.cache = this.draw(width);
78618
+ this.cacheWidth = width;
78619
+ this.isDirty = false;
78620
+ }
78621
+ return this.cache;
78622
+ }
78623
+ invalidate() {
78624
+ this.isDirty = true;
78625
+ }
78626
+ get dirty() {
78627
+ return this.isDirty;
78628
+ }
78629
+ };
78630
+ var Container = class extends Widget {
78631
+ children;
78632
+ constructor(...children) {
78633
+ super();
78634
+ this.children = children;
78635
+ }
78636
+ add(child) {
78637
+ this.children.push(child);
78638
+ this.invalidate();
78639
+ return child;
78640
+ }
78641
+ extend(children) {
78642
+ this.children.push(...children);
78643
+ this.invalidate();
78644
+ }
78645
+ clear() {
78646
+ this.children = [];
78647
+ this.invalidate();
78648
+ }
78649
+ remove(child) {
78650
+ const index = this.children.indexOf(child);
78651
+ if (index >= 0) {
78652
+ this.children.splice(index, 1);
78653
+ this.invalidate();
78654
+ }
78655
+ }
78656
+ /** Concatenate the children, asking each: a clean child returns its cache,
78657
+ * so a frame costs what actually changed. */
78658
+ render(width) {
78659
+ const lines2 = [];
78660
+ for (const child of this.children) lines2.push(...child.render(width));
78661
+ this.isDirty = false;
78662
+ return lines2;
78663
+ }
78664
+ draw(width) {
78665
+ return this.render(width);
78666
+ }
78667
+ /** Offer the key to each child until one takes it. */
78668
+ handleInput(key2, data) {
78669
+ for (const child of this.children) {
78670
+ const handler = child.handleInput;
78671
+ if (handler?.call(child, key2, data)) return true;
78672
+ }
78673
+ return false;
78674
+ }
78675
+ };
78676
+
78677
+ // src/term/primitives.ts
78678
+ var Text = class extends Widget {
78679
+ constructor(content = "", paddingX = 1, paddingY = 0, tint2 = null) {
78680
+ super();
78681
+ this.content = content;
78682
+ this.paddingX = paddingX;
78683
+ this.paddingY = paddingY;
78684
+ this.tint = tint2;
78685
+ }
78686
+ content;
78687
+ paddingX;
78688
+ paddingY;
78689
+ tint;
78690
+ get text() {
78691
+ return this.content;
78692
+ }
78693
+ setText(text3) {
78694
+ if (text3 !== this.content) {
78695
+ this.content = text3;
78696
+ this.invalidate();
78697
+ }
78698
+ }
78699
+ draw(width) {
78700
+ const inner = Math.max(1, width - 2 * this.paddingX);
78701
+ const pad = " ".repeat(this.paddingX);
78702
+ const body = wrap2(this.content, inner).map((line) => `${pad}${line}`);
78703
+ const blank = Array(this.paddingY).fill("");
78704
+ return [...blank, ...body, ...blank].map(
78705
+ (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
78706
+ );
78707
+ }
78708
+ };
78709
+ var COLUMN = /^(\s*\S.*?[^\s.!?]\s{2,})\S/;
78710
+ function wrapAligned(paragraph2, width) {
78711
+ const lines2 = wrap2(paragraph2, width);
78712
+ if (lines2.length < 2) return lines2;
78713
+ const plain2 = stripAnsi(paragraph2);
78714
+ const column = COLUMN.exec(plain2);
78715
+ const lead = column ? cellWidth(column[1]) : plain2.length - plain2.replace(/^ +/, "").length;
78716
+ if (!(lead > 0 && lead <= Math.floor(width / 2))) return lines2;
78717
+ const [head4, body] = splitAtCell(paragraph2, lead);
78718
+ const rest = wrap2(body, width - lead);
78719
+ return [head4 + rest[0], ...rest.slice(1).map((line) => " ".repeat(lead) + line)];
78720
+ }
78721
+ function splitAtCell(text3, cells) {
78722
+ const tokens = tokenize(text3);
78723
+ let seen = 0;
78724
+ for (const [index, [isEscape, chunk]] of tokens.entries()) {
78725
+ if (isEscape) continue;
78726
+ if (seen >= cells) {
78727
+ const head4 = tokens.slice(0, index);
78728
+ const style = head4.filter(([isEscape2]) => isEscape2).map(([, c]) => c).join("");
78729
+ return [
78730
+ head4.map(([, c]) => c).join(""),
78731
+ style + tokens.slice(index).map(([, c]) => c).join("")
78732
+ ];
78733
+ }
78734
+ seen += cellWidth(chunk);
78735
+ }
78736
+ return [text3, ""];
78737
+ }
78738
+ var HangingText = class extends Widget {
78739
+ constructor(prefix, body, paddingX = 1, tint2 = null) {
78740
+ super();
78741
+ this.prefix = prefix;
78742
+ this.body = body;
78743
+ this.paddingX = paddingX;
78744
+ this.tint = tint2;
78745
+ this.prefixWidth = cellWidth(prefix);
78746
+ }
78747
+ prefix;
78748
+ body;
78749
+ paddingX;
78750
+ tint;
78751
+ prefixWidth;
78752
+ setBody(body) {
78753
+ if (body !== this.body) {
78754
+ this.body = body;
78755
+ this.invalidate();
78756
+ }
78757
+ }
78758
+ draw(width) {
78759
+ const pad = " ".repeat(this.paddingX);
78760
+ const inner = Math.max(1, width - 2 * this.paddingX - this.prefixWidth);
78761
+ const hang = " ".repeat(this.prefixWidth);
78762
+ const out = [];
78763
+ for (const paragraph2 of this.body.split("\n")) {
78764
+ wrapAligned(paragraph2, inner).forEach((line, index) => {
78765
+ out.push(`${pad}${out.length === 0 && index === 0 ? this.prefix : hang}${line}`);
78766
+ });
78767
+ }
78768
+ return out.map(
78769
+ (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
78770
+ );
78771
+ }
78772
+ };
78773
+ var Box = class extends Widget {
78774
+ constructor(paddingX = 1, paddingY = 1, tint2 = null, ...children) {
78775
+ super();
78776
+ this.paddingX = paddingX;
78777
+ this.paddingY = paddingY;
78778
+ this.tint = tint2;
78779
+ this.children = children;
78780
+ }
78781
+ paddingX;
78782
+ paddingY;
78783
+ tint;
78784
+ children;
78785
+ add(child) {
78786
+ this.children.push(child);
78787
+ this.invalidate();
78788
+ return child;
78789
+ }
78790
+ clear() {
78791
+ this.children = [];
78792
+ this.invalidate();
78793
+ }
78794
+ render(width) {
78795
+ const inner = Math.max(1, width - 2 * this.paddingX);
78796
+ const pad = " ".repeat(this.paddingX);
78797
+ const body = this.children.flatMap(
78798
+ (child) => child.render(inner).map((line) => `${pad}${stripTerminator(line)}`)
78799
+ );
78800
+ const blank = Array(this.paddingY).fill("");
78801
+ return [...blank, ...body, ...blank].map(
78802
+ (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
78803
+ );
78804
+ }
78805
+ draw(width) {
78806
+ return this.render(width);
78807
+ }
78808
+ };
78809
+ var Spacer = class extends Widget {
78810
+ constructor(lines2 = 1) {
78811
+ super();
78812
+ this.lines = lines2;
78813
+ }
78814
+ lines;
78815
+ draw() {
78816
+ return Array(this.lines).fill("");
78817
+ }
78818
+ };
78819
+ var Rule = class extends Widget {
78820
+ constructor(color2 = null, char = "\u2500") {
78821
+ super();
78822
+ this.color = color2;
78823
+ this.char = char;
78824
+ }
78825
+ color;
78826
+ char;
78827
+ /** What the rule is painted with. */
78828
+ get paint() {
78829
+ return this.color;
78830
+ }
78831
+ setColor(color2) {
78832
+ this.color = color2;
78833
+ this.invalidate();
78834
+ }
78835
+ draw(width) {
78836
+ const line = this.char.repeat(Math.max(1, width));
78837
+ return [terminate(this.color ? this.color(line) : line)];
78838
+ }
78839
+ };
78840
+ var LabelledRule = class extends Rule {
78841
+ constructor(color2 = null, label = "", align = "left", char = "\u2500") {
78842
+ super(color2, char);
78843
+ this.label = label;
78844
+ this.align = align;
78845
+ }
78846
+ label;
78847
+ align;
78848
+ setLabel(label) {
78849
+ if (label !== this.label) {
78850
+ this.label = label;
78851
+ this.invalidate();
78852
+ }
78853
+ }
78854
+ draw(width) {
78855
+ const w = Math.max(1, width);
78856
+ if (!this.label) return super.draw(w);
78857
+ const label = ` ${this.label} `;
78858
+ const labelWidth = cellWidth(label);
78859
+ if (labelWidth + 4 > w) return super.draw(w);
78860
+ const left = this.align === "center" ? Math.floor((w - labelWidth) / 2) : 2;
78861
+ const right = w - labelWidth - left;
78862
+ const paint = this.color ?? ((text3) => text3);
78863
+ return [terminate(paint(this.char.repeat(left)) + label + paint(this.char.repeat(right)))];
78864
+ }
78865
+ };
78866
+ var Lines = class extends Widget {
78867
+ constructor(lines2, paddingX = 1, tint2 = null) {
78868
+ super();
78869
+ this.lines = lines2;
78870
+ this.paddingX = paddingX;
78871
+ this.tint = tint2;
78872
+ }
78873
+ lines;
78874
+ paddingX;
78875
+ tint;
78876
+ setLines(lines2) {
78877
+ this.lines = lines2;
78878
+ this.invalidate();
78879
+ }
78880
+ draw(width) {
78881
+ const pad = " ".repeat(this.paddingX);
78882
+ const inner = Math.max(1, width - 2 * this.paddingX);
78883
+ return this.lines.map((line) => `${pad}${truncateToWidth(stripTerminator(line), inner)}`).map(
78884
+ (line) => this.tint === null ? fillLine(line, width) : terminate(this.tint(padTo(line, width)))
78885
+ );
78886
+ }
78887
+ };
78888
+ var stripTerminator = (line) => line.endsWith(SEGMENT_RESET) ? line.slice(0, -SEGMENT_RESET.length) : line;
78889
+ function padTo(line, width) {
78890
+ let fitted = line;
78891
+ let used2 = cellWidth(fitted);
78892
+ if (used2 > width) {
78893
+ fitted = truncateToWidth(fitted, width);
78894
+ used2 = cellWidth(fitted);
78895
+ }
78896
+ return fitted + " ".repeat(Math.max(0, width - used2));
78897
+ }
78898
+
78295
78899
  // src/tui/clipboard.ts
78296
78900
  init_images();
78297
78901
  init_pathlib();
78298
78902
  init_py();
78299
78903
  import { spawn as spawn9 } from "node:child_process";
78300
78904
  import { randomBytes as randomBytes8 } from "node:crypto";
78301
- import { readFileSync as readFileSync9, rmSync as rmSync5, statSync as statSync14, writeFileSync as writeFileSync10 } from "node:fs";
78905
+ import { readFileSync as readFileSync9, rmSync as rmSync5, statSync as statSync15, writeFileSync as writeFileSync11 } from "node:fs";
78302
78906
  import { tmpdir as tmpdir3 } from "node:os";
78303
78907
  var MAX_OSC52_ENCODED = 1e5;
78304
78908
  var TIMEOUT_MS = 5e3;
@@ -78403,7 +79007,7 @@ async function readImage2(platform = process.platform, env = process.env) {
78403
79007
  }
78404
79008
  async function withScratch(fn) {
78405
79009
  const target = join(tmpdir3(), `hx-clipboard-${randomBytes8(4).toString("hex")}.img`);
78406
- writeFileSync10(target, "");
79010
+ writeFileSync11(target, "");
78407
79011
  try {
78408
79012
  return await fn(target);
78409
79013
  } finally {
@@ -78465,7 +79069,7 @@ async function readLinux(env) {
78465
79069
  function copiedFile(path) {
78466
79070
  if (!isImagePath(path)) return null;
78467
79071
  try {
78468
- if (statSync14(path).size > MAX_SOURCE_BYTES)
79072
+ if (statSync15(path).size > MAX_SOURCE_BYTES)
78469
79073
  throw new ClipboardError(`${basename(path)} is too large to attach`);
78470
79074
  return { data: readFileSync9(path), name: basename(path) };
78471
79075
  } catch (error) {
@@ -78481,7 +79085,7 @@ init_resolve();
78481
79085
  init_store();
78482
79086
  init_config();
78483
79087
  init_context();
78484
- import { statSync as statSync15 } from "node:fs";
79088
+ import { statSync as statSync16 } from "node:fs";
78485
79089
  init_usage();
78486
79090
  init_json();
78487
79091
  init_net();
@@ -78944,295 +79548,6 @@ var THEME = new Theme();
78944
79548
  // src/tui/views/login.ts
78945
79549
  init_py();
78946
79550
 
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
79551
  // src/tui/format.ts
79237
79552
  init_py();
79238
79553
 
@@ -86255,7 +86570,7 @@ async function cmdAgents(ctx, _args) {
86255
86570
  }
86256
86571
  async function cmdMcp(ctx, args) {
86257
86572
  const manager = ctx.app.mcp;
86258
- if (manager === null || !manager.configs.length) {
86573
+ if (manager === null || !manager.configs.length && !manager.waiting.length) {
86259
86574
  ctx.app.notice("No MCP servers configured. Add one with `hx mcp add`.", "warning");
86260
86575
  return;
86261
86576
  }
@@ -86297,7 +86612,10 @@ async function cmdMcp(ctx, args) {
86297
86612
  lines2.push(` ${ljust(status3.name, 20)} ${status3.error} - /mcp login ${status3.name}`);
86298
86613
  else lines2.push(` ${ljust(status3.name, 20)} unavailable - ${status3.error}`);
86299
86614
  }
86300
- ctx.app.notice(lines2.join("\n"), manager.status().every((s) => s.connected) ? "info" : "warning");
86615
+ for (const { config: config2 } of manager.waiting)
86616
+ lines2.push(` ${ljust(config2.name, 20)} not approved - run \`hx mcp approve ${config2.name}\``);
86617
+ const healthy = manager.status().every((s) => s.connected) && !manager.waiting.length;
86618
+ ctx.app.notice(lines2.join("\n"), healthy ? "info" : "warning");
86301
86619
  }
86302
86620
  function onlyServerNeedingLogin(manager) {
86303
86621
  const waiting = manager.status().filter((s) => s.needsLogin);
@@ -86663,18 +86981,18 @@ async function cmdCopy(ctx, _args) {
86663
86981
  }
86664
86982
  async function cmdTrace(ctx, args) {
86665
86983
  const [target, openAfter] = traceArgs(args);
86666
- let written;
86984
+ let written2;
86667
86985
  try {
86668
- written = writeSessionTrace(ctx, target);
86986
+ written2 = writeSessionTrace(ctx, target);
86669
86987
  } catch (error) {
86670
86988
  if (!error.code) throw error;
86671
86989
  ctx.app.notice(`Could not write the trace: ${osErrorText(error)}`, "error");
86672
86990
  return;
86673
86991
  }
86674
- const opened = openAfter && await openInBrowser(written);
86992
+ const opened = openAfter && await openInBrowser(written2);
86675
86993
  const trailer = openAfter && !opened ? " No browser could be launched here; open it yourself." : "";
86676
86994
  ctx.app.notice(
86677
- `Trace written to ${written} (${formatBytes(statSync15(written).size)}).${trailer}`,
86995
+ `Trace written to ${written2} (${formatBytes(statSync16(written2).size)}).${trailer}`,
86678
86996
  "success"
86679
86997
  );
86680
86998
  }
@@ -87771,7 +88089,7 @@ var PermissionPrompt = class extends Framed {
87771
88089
 
87772
88090
  // src/tui/views/prompt.ts
87773
88091
  init_images();
87774
- import { statSync as statSync16 } from "node:fs";
88092
+ import { statSync as statSync17 } from "node:fs";
87775
88093
  init_pathlib();
87776
88094
  init_py();
87777
88095
 
@@ -88249,7 +88567,7 @@ var FileCompleter = class _FileCompleter {
88249
88567
  }
88250
88568
  const mtime3 = (path) => {
88251
88569
  try {
88252
- return statSync16(path).mtimeMs;
88570
+ return statSync17(path).mtimeMs;
88253
88571
  } catch {
88254
88572
  return 0;
88255
88573
  }
@@ -89042,6 +89360,13 @@ var HintsBar = class extends Widget {
89042
89360
  return [fillLine(` ${parts2.join(fg2("dim", SEPARATOR2))}`, width)];
89043
89361
  }
89044
89362
  };
89363
+ var LOGO = [
89364
+ " __ __ _ __",
89365
+ " / / / / | |/ /",
89366
+ " / /_/ / | /",
89367
+ " / __ / / |",
89368
+ "/_/ /_/ /_/|_|"
89369
+ ];
89045
89370
  var Header = class extends Widget {
89046
89371
  constructor(version = "", quiet = false) {
89047
89372
  super();
@@ -89051,6 +89376,7 @@ var Header = class extends Widget {
89051
89376
  quiet;
89052
89377
  versionText;
89053
89378
  isExpanded = false;
89379
+ showLogo = false;
89054
89380
  get version() {
89055
89381
  return this.versionText;
89056
89382
  }
@@ -89068,10 +89394,19 @@ var Header = class extends Widget {
89068
89394
  toggle() {
89069
89395
  this.expanded = !this.isExpanded;
89070
89396
  }
89397
+ /** The logo above the title: on the first screen of someone not signed in. */
89398
+ get logo() {
89399
+ return this.showLogo;
89400
+ }
89401
+ set logo(value) {
89402
+ this.showLogo = value;
89403
+ this.invalidate();
89404
+ }
89071
89405
  draw(width) {
89072
89406
  if (this.quiet) return [];
89073
89407
  const keys = keymap();
89074
- const lines2 = [fg2("accent", "hx", { bold: true }) + fg2("dim", ` v${this.versionText}`)];
89408
+ const lines2 = this.showLogo ? [...LOGO.map((row) => fg2("accent", row, { bold: true })), ""] : [];
89409
+ lines2.push(fg2("accent", "hx", { bold: true }) + fg2("dim", ` v${this.versionText}`));
89075
89410
  if (!this.isExpanded) {
89076
89411
  const inner = Math.max(1, width - 2);
89077
89412
  lines2.push(
@@ -89342,6 +89677,7 @@ var HXSession = class {
89342
89677
  this.view.dock.hints.setHints(HINTS);
89343
89678
  this.refreshStatus();
89344
89679
  if (this.loop.session.activeMessages().length) this.replayTranscript();
89680
+ if (this.extra.signIn) this.showSignIn(this.extra.signIn);
89345
89681
  if (this.loop.permissions !== null)
89346
89682
  this.loop.permissions.asker = (request2) => this.askPermission(request2);
89347
89683
  const events = this.consumeEvents();
@@ -89385,6 +89721,13 @@ var HXSession = class {
89385
89721
  this.runner.requestRender();
89386
89722
  }
89387
89723
  }
89724
+ /** Say how to sign in, a row below the header, under the logo - unless a
89725
+ * resumed conversation is already on screen. */
89726
+ showSignIn(hint2) {
89727
+ if (!this.view.transcript.blocks.length) this.view.header.logo = true;
89728
+ this.view.transcript.add(new Spacer(1));
89729
+ this.view.transcript.add(new Notice(hint2, "warning"));
89730
+ }
89388
89731
  // -- the surface the slash commands expect ---------------------------------
89389
89732
  get status() {
89390
89733
  return this.view.dock.status;
@@ -90352,7 +90695,6 @@ Usage:
90352
90695
  hx auth [set|clear] Show or change the OpenRouter API key
90353
90696
  hx docs [SECTION|--all] Print the manual, or one section of it
90354
90697
  hx changelog [VERSION] Print what shipped in each version
90355
- hx upgrade Update hx to the latest version
90356
90698
  hx --version Show version
90357
90699
  hx --help Show this message
90358
90700
 
@@ -90416,25 +90758,10 @@ ${USAGE}`, process.stderr);
90416
90758
  return runMcpCommand(parsed.rest);
90417
90759
  case "trace":
90418
90760
  return runTraceCommand(parsed);
90419
- case "upgrade":
90420
- return runUpgradeCommand();
90421
90761
  default:
90422
90762
  return runTuiCommand(parsed);
90423
90763
  }
90424
90764
  }
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
90765
  function parseArgs(args) {
90439
90766
  const parsed = {
90440
90767
  command: "tui",
@@ -90489,7 +90816,7 @@ function parseArgs(args) {
90489
90816
  }
90490
90817
  if (positional.length) {
90491
90818
  const [head4, ...tail3] = positional;
90492
- if (["resume", "prompt", "trace", "mcp", "upgrade", "auth"].includes(head4)) {
90819
+ if (["resume", "prompt", "trace", "mcp", "auth"].includes(head4)) {
90493
90820
  parsed.command = head4;
90494
90821
  parsed.rest = tail3;
90495
90822
  if (head4 === "resume" && tail3.length) parsed.sessionId = tail3[0];
@@ -90952,25 +91279,28 @@ function runTraceCommand(parsed) {
90952
91279
  destination = purePath(expandUser(target));
90953
91280
  if (isDir(destination)) destination = join(destination, TRACE_FILENAME);
90954
91281
  }
90955
- let written;
91282
+ let written2;
90956
91283
  try {
90957
- written = writeTrace(buildTrace(session), destination);
91284
+ written2 = writeTrace(buildTrace(session), destination);
90958
91285
  } catch (error) {
90959
91286
  if (!error.code) throw error;
90960
91287
  print(`error: could not write the trace: ${osErrorText(error)}`, process.stderr);
90961
91288
  return 1;
90962
91289
  }
90963
- print(written);
91290
+ print(written2);
90964
91291
  return 0;
90965
91292
  }
90966
91293
  var MCP_USAGE = `hx mcp list Show configured servers
90967
91294
  hx mcp add NAME COMMAND [ARGS...] Add a stdio server
90968
91295
  hx mcp add NAME --url URL [--header K:V]... Add an HTTP server
91296
+ hx mcp approve NAME Let a server from the project's .hx/mcp.json start
90969
91297
  hx mcp login NAME Sign in to an HTTP server in the browser
90970
91298
  hx mcp logout NAME Forget a server's sign-in
90971
91299
  hx mcp remove NAME Remove a server
90972
91300
 
90973
91301
  Add --user to write to ~/.hx/mcp.json instead of the project's .hx/mcp.json.
91302
+ A server in the project's .hx/mcp.json came with the project, so it starts only
91303
+ once approved: \`hx mcp add\` approves what it adds, and \`hx mcp approve\` the rest.
90974
91304
  An HTTP server that asks for sign-in gets it over OAuth unless a --header
90975
91305
  already sends Authorization.
90976
91306
  `;
@@ -90989,7 +91319,14 @@ async function runMcpCommand(given) {
90989
91319
  print(MCP_USAGE);
90990
91320
  return 0;
90991
91321
  }
90992
- for (const status3 of await probeServers(configs)) {
91322
+ const [startable, waiting] = partition(configs, cwd);
91323
+ for (const { config: config2, changed } of waiting) {
91324
+ const why = changed ? "changed since approved" : "not approved";
91325
+ print(
91326
+ `${ljust2(config2.name, 20)} ${why} - runs ${describeServer(config2)}; run \`hx mcp approve ${config2.name}\``
91327
+ );
91328
+ }
91329
+ for (const status3 of await probeServers(startable)) {
90993
91330
  if (status3.needsLogin) {
90994
91331
  print(`${ljust2(status3.name, 20)} ${status3.error} - run \`hx mcp login ${status3.name}\``);
90995
91332
  continue;
@@ -91029,9 +91366,10 @@ ${MCP_USAGE}`, process.stderr);
91029
91366
  });
91030
91367
  }
91031
91368
  print(`Added ${name} to ${saveConfig(config2, cwd, userLevel)}`);
91369
+ if (!userLevel) approve({ ...config2, source: "project" }, cwd);
91032
91370
  return 0;
91033
91371
  }
91034
- if (args[0] === "login" || args[0] === "logout") {
91372
+ if (args[0] === "approve") {
91035
91373
  if (args.length < 2) {
91036
91374
  print(MCP_USAGE, process.stderr);
91037
91375
  return 2;
@@ -91041,6 +91379,33 @@ ${MCP_USAGE}`, process.stderr);
91041
91379
  print(`No server named ${args[1]}.`, process.stderr);
91042
91380
  return 1;
91043
91381
  }
91382
+ if (server.source === "user") {
91383
+ print(`${server.name} is in your own ~/.hx/mcp.json; it needs no approval.`);
91384
+ return 0;
91385
+ }
91386
+ approve(server, cwd);
91387
+ const does = server.url ? "connects to" : "runs";
91388
+ print(`Approved ${server.name}. It ${does}: ${describeServer(server)}`);
91389
+ return 0;
91390
+ }
91391
+ if (args[0] === "login" || args[0] === "logout") {
91392
+ if (args.length < 2) {
91393
+ print(MCP_USAGE, process.stderr);
91394
+ return 2;
91395
+ }
91396
+ const configs = loadConfigs(cwd);
91397
+ const server = configs.find((c) => c.name === args[1]);
91398
+ if (server === void 0) {
91399
+ print(`No server named ${args[1]}.`, process.stderr);
91400
+ return 1;
91401
+ }
91402
+ if (args[0] === "login" && partition([server], cwd)[1].length) {
91403
+ print(
91404
+ `${server.name} has not been approved. Run \`hx mcp approve ${server.name}\` first.`,
91405
+ process.stderr
91406
+ );
91407
+ return 1;
91408
+ }
91044
91409
  return args[0] === "login" ? mcpLogin2(server) : mcpLogout(server);
91045
91410
  }
91046
91411
  if (args[0] === "remove") {
@@ -91049,6 +91414,7 @@ ${MCP_USAGE}`, process.stderr);
91049
91414
  return 2;
91050
91415
  }
91051
91416
  if (removeConfig(args[1], cwd, userLevel)) {
91417
+ if (!userLevel) revoke(args[1], cwd);
91052
91418
  print(`Removed ${args[1]}.`);
91053
91419
  return 0;
91054
91420
  }
@@ -91132,17 +91498,12 @@ async function runTuiCommand(parsed) {
91132
91498
  try {
91133
91499
  resume = resumeTarget(parsed);
91134
91500
  moved = adoptSessionDirectory(parsed, resume);
91135
- runtime2 = buildRuntime(parsed, resume);
91501
+ runtime2 = buildRuntime(parsed, resume, true);
91136
91502
  } 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
- }
91503
+ return report(error);
91144
91504
  }
91145
91505
  if (moved) runtime2.notices.push(moved);
91506
+ const signIn2 = runtime2.provider instanceof DeferredProvider ? signInHint(runtime2.loop.model, runtime2.auth) : null;
91146
91507
  let relaunch = null;
91147
91508
  try {
91148
91509
  await runtime2.connectMcp();
@@ -91157,7 +91518,8 @@ async function runTuiCommand(parsed) {
91157
91518
  mcp: runtime2.mcp,
91158
91519
  notices: runtime2.notices,
91159
91520
  checkpoints: runtime2.checkpoints,
91160
- renameLeft
91521
+ renameLeft,
91522
+ signIn: signIn2
91161
91523
  });
91162
91524
  } finally {
91163
91525
  runtime2.bus.close();