@oxbit/cli 0.10.1 → 0.11.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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from "node:util";
3
3
  import { createInterface } from "node:readline";
4
- import { mkdir, readFile as readFile$1, writeFile as writeFile$1, chmod, rename, stat, rm, readdir, realpath, unlink, open, access } from "node:fs/promises";
4
+ import { mkdir, readFile as readFile$1, writeFile as writeFile$1, chmod, rename, stat, rm, readdir, realpath, unlink, open, access, cp } from "node:fs/promises";
5
5
  import { join, dirname, isAbsolute, sep, basename, resolve } from "node:path";
6
6
  import { randomBytes, createHash, randomUUID } from "node:crypto";
7
7
  import { homedir } from "node:os";
@@ -24,15 +24,15 @@ async function listModels(models2, endpoint, signal) {
24
24
  }));
25
25
  }
26
26
  function modelError(error) {
27
- const { status, body } = error ?? {};
28
- if (typeof status !== "number") return error instanceof Error ? error : new Error(String(error));
27
+ const { status: status2, body } = error ?? {};
28
+ if (typeof status2 !== "number") return error instanceof Error ? error : new Error(String(error));
29
29
  let detail = typeof body === "string" ? body.trim().slice(0, 1e3) : "";
30
30
  try {
31
31
  const parsed = JSON.parse(detail);
32
32
  detail = parsed?.error?.message ?? parsed?.message ?? detail;
33
33
  } catch {
34
34
  }
35
- return new Error(`Model API error ${status}${detail ? `: ${detail}` : ""}`);
35
+ return new Error(`Model API error ${status2}${detail ? `: ${detail}` : ""}`);
36
36
  }
37
37
  async function* lines(chunks) {
38
38
  let buffer = "";
@@ -250,7 +250,7 @@ const updatePlan = {
250
250
  return "Plan updated";
251
251
  }
252
252
  };
253
- const terminalText = (result, status) => [(result?.output ?? "").replace(/\s+$/, ""), result?.truncated ? "[Output truncated to the last bytes]" : "", `[${status}]`].filter(Boolean).join("\n");
253
+ const terminalText = (result, status2) => [(result?.output ?? "").replace(/\s+$/, ""), result?.truncated ? "[Output truncated to the last bytes]" : "", `[${status2}]`].filter(Boolean).join("\n");
254
254
  const bash = {
255
255
  spec: {
256
256
  name: "bash",
@@ -296,8 +296,8 @@ const bash = {
296
296
  try {
297
297
  const exit = await Promise.race([context.request("terminal/wait_for_exit", terminal), interrupted]);
298
298
  const result = await context.request("terminal/output", terminal);
299
- const status = exit?.signal ? `Killed by ${exit.signal}` : `Exit code ${exit?.exitCode ?? "unknown"}`;
300
- const text = terminalText(result, status);
299
+ const status2 = exit?.signal ? `Killed by ${exit.signal}` : `Exit code ${exit?.exitCode ?? "unknown"}`;
300
+ const text = terminalText(result, status2);
301
301
  if (exit?.exitCode !== 0) throw new Error(text);
302
302
  return text;
303
303
  } catch (error) {
@@ -339,7 +339,7 @@ async function runTool(call, context, tools = TOOLS, gate = {}) {
339
339
  let args = typeof parsed === "string" ? {} : parsed;
340
340
  const tool = tools.find((item) => item.spec.name === call.name);
341
341
  const toolCallId = call.id;
342
- const update = (status, output2) => context.update({ sessionUpdate: "tool_call_update", toolCallId, status, rawOutput: output2.slice(0, 8e3) });
342
+ const update = (status2, output2) => context.update({ sessionUpdate: "tool_call_update", toolCallId, status: status2, rawOutput: output2.slice(0, 8e3) });
343
343
  const where = locations(context.root, args);
344
344
  context.update({
345
345
  sessionUpdate: "tool_call",
@@ -491,7 +491,7 @@ function replayable(items) {
491
491
  return kept.length === items.length ? items : kept.map((item) => item.type === "reasoning" ? item : Object.fromEntries(Object.entries(item).filter(([key]) => key !== "id")));
492
492
  }
493
493
  const openaiResponses = {
494
- request({ model, system: system2, turns, tools, effort }) {
494
+ request({ model, system, turns, tools, effort }) {
495
495
  const input2 = [];
496
496
  for (const turn of turns) {
497
497
  if (turn.role === "user") input2.push({ type: "message", role: "user", content: [{ type: "input_text", text: turn.text }] });
@@ -508,7 +508,7 @@ const openaiResponses = {
508
508
  path: "/v1/responses",
509
509
  body: {
510
510
  model,
511
- instructions: system2,
511
+ instructions: system,
512
512
  input: input2,
513
513
  stream: true,
514
514
  store: false,
@@ -566,8 +566,8 @@ function mergeDetails(details, fragments) {
566
566
  }
567
567
  }
568
568
  const chatCompletions = {
569
- request({ model, system: system2, turns, tools, effort }) {
570
- const messages2 = [{ role: "system", content: system2 }];
569
+ request({ model, system, turns, tools, effort }) {
570
+ const messages2 = [{ role: "system", content: system }];
571
571
  for (const turn of turns) {
572
572
  if (turn.role === "user") messages2.push({ role: "user", content: turn.text });
573
573
  else if (turn.role === "tool")
@@ -643,7 +643,7 @@ const STOP = {
643
643
  model_context_window_exceeded: "max_tokens",
644
644
  refusal: "refusal"
645
645
  };
646
- const input = (text) => {
646
+ const input$1 = (text) => {
647
647
  try {
648
648
  const value2 = JSON.parse(text || "{}");
649
649
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
@@ -652,7 +652,7 @@ const input = (text) => {
652
652
  }
653
653
  };
654
654
  const anthropicMessages = {
655
- request({ model, system: system2, turns, tools, effort, maxTokens }) {
655
+ request({ model, system, turns, tools, effort, maxTokens }) {
656
656
  const messages2 = [];
657
657
  const add = (role, content) => {
658
658
  const last = messages2.at(-1);
@@ -666,7 +666,7 @@ const anthropicMessages = {
666
666
  else if (turn.native?.api === "anthropic") add("assistant", structuredClone(turn.native.items));
667
667
  else add("assistant", [
668
668
  ...turn.text ? [{ type: "text", text: turn.text }] : [],
669
- ...turn.calls.map((call) => ({ type: "tool_use", id: call.id, name: call.name, input: input(call.arguments) }))
669
+ ...turn.calls.map((call) => ({ type: "tool_use", id: call.id, name: call.name, input: input$1(call.arguments) }))
670
670
  ]);
671
671
  }
672
672
  return {
@@ -674,7 +674,7 @@ const anthropicMessages = {
674
674
  path: "/v1/messages",
675
675
  body: {
676
676
  model,
677
- system: system2,
677
+ system,
678
678
  messages: messages2,
679
679
  stream: true,
680
680
  max_tokens: maxTokens ?? DEFAULT_MAX_TOKENS,
@@ -719,7 +719,7 @@ const anthropicMessages = {
719
719
  const block2 = blocks[event.index];
720
720
  if (block2?.type !== "tool_use") break;
721
721
  const text = json2.get(event.index) || "{}";
722
- block2.input = input(text);
722
+ block2.input = input$1(text);
723
723
  yield { type: "tool_call", call: { id: block2.id, name: block2.name, arguments: text } };
724
724
  break;
725
725
  }
@@ -798,6 +798,7 @@ Unfinished tasks and next steps, including the step in progress.
798
798
  Constraints and preferences the user stated.
799
799
  Keep exact file paths, commands, identifiers and error messages.`;
800
800
  const chars = (turn) => turn.role === "user" ? turn.text.length : turn.role === "assistant" ? turn.text.length + turn.calls.reduce((sum, call) => sum + call.name.length + call.arguments.length, 0) : turn.results.reduce((sum, result) => sum + result.output.length, 0);
801
+ const estimateTokens = (turns) => Math.ceil(turns.reduce((sum, turn) => sum + chars(turn), 0) / 4);
801
802
  const clip$1 = (text, max) => text.length > max ? `${text.slice(0, max)}
802
803
  [${text.length - max} more characters]` : text;
803
804
  function pruneToolOutputs(turns) {
@@ -860,10 +861,15 @@ class Compaction {
860
861
  /** Set when shouldCompact returns true; its absence in compact() means a manual run. */
861
862
  pending = /* @__PURE__ */ new Map();
862
863
  usable = /* @__PURE__ */ new Map();
863
- /** Tokens a request can use before compaction: the window minus min(reserve, maxOutput or 32000, window / 4). */
864
+ /** Tokens a request can use before compaction: `percent` of the window, else the window minus min(reserve, maxOutput or 32000, window / 4). */
864
865
  threshold(contextWindow, maxOutput) {
866
+ const percent = this.options.percent;
867
+ if (typeof percent === "number" && percent >= 50 && percent <= 95) return Math.floor(contextWindow * percent / 100);
865
868
  return contextWindow - Math.min(this.options.reserve ?? 2e4, maxOutput ?? DEFAULT_MAX_OUTPUT, Math.floor(contextWindow / 4));
866
869
  }
870
+ autoThreshold(contextWindow, maxOutput) {
871
+ return this.options.auto === false ? void 0 : this.threshold(contextWindow, maxOutput);
872
+ }
867
873
  /** Due when the last request's input plus output reaches the threshold. turn.ts does not pass `maxOutput` yet. */
868
874
  shouldCompact(session, usage, contextWindow, maxOutput) {
869
875
  if (!contextWindow) return false;
@@ -935,7 +941,7 @@ const ADAPTERS = {
935
941
  };
936
942
  const MAX_STEPS = 64;
937
943
  const CONTEXT_LIMIT$1 = 1e4;
938
- const system = (root, reviewed, shell) => [
944
+ const basePrompt = (root, reviewed, shell) => [
939
945
  `You are Oxbit Agent, a coding agent inside the Oxbit editor. You work in the workspace "${root.split("/").pop()}" through tools.`,
940
946
  "Paths are relative to the workspace root. Read a file before you edit it. edit_file replaces an exact string; write_file replaces a whole file.",
941
947
  ...reviewed ? ["The user reviews each write before it is saved; a rejected write returns an error, so ask what to change instead of retrying it."] : [],
@@ -991,7 +997,7 @@ ${text}
991
997
  }
992
998
  async system() {
993
999
  const sections = await Promise.all((this.host.extensions?.context ?? []).map((provider2) => provider2.system(this.session)));
994
- const base = system(this.session.cwd, this.host.capabilities.fs, this.host.capabilities.terminal);
1000
+ const base = basePrompt(this.session.cwd, this.host.capabilities.fs, this.host.capabilities.terminal);
995
1001
  return [base, ...this.session.context, ...sections].filter((section) => !!section).join("\n\n");
996
1002
  }
997
1003
  async run(models2, endpoint) {
@@ -1031,6 +1037,7 @@ ${text}
1031
1037
  }
1032
1038
  });
1033
1039
  usage = result.usage ?? usage;
1040
+ if (result.usage) session.usage = result.usage;
1034
1041
  const calls = result.turn.calls;
1035
1042
  const context = {
1036
1043
  root: session.cwd,
@@ -1108,6 +1115,7 @@ ${text}
1108
1115
  const before = await this.hook("PreCompact", { trigger, custom_instructions: typeof focus === "string" ? focus : "" });
1109
1116
  if (before.block || this.halt) return false;
1110
1117
  this.session.turns = await compactor.compact(this.session, summarize, this.signal);
1118
+ this.session.usage = void 0;
1111
1119
  await this.hook("PostCompact", { trigger });
1112
1120
  return true;
1113
1121
  }
@@ -1156,6 +1164,125 @@ async function generateTitle(host, session, signal) {
1156
1164
  return void 0;
1157
1165
  }
1158
1166
  }
1167
+ const estimate = (text) => Math.ceil(text.length / 4);
1168
+ const slug = (label) => label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "section";
1169
+ async function contextUsage(host, session, signal) {
1170
+ const providers = host.extensions?.context ?? [];
1171
+ const sections = await Promise.all(providers.map((provider2) => provider2.system(session)));
1172
+ const labeled = /* @__PURE__ */ new Map();
1173
+ let other = session.context.reduce((sum, text) => sum + estimate(text), 0);
1174
+ providers.forEach((provider2, index) => {
1175
+ const tokens = estimate(sections[index] ?? "");
1176
+ if (!provider2.label) {
1177
+ other += tokens;
1178
+ return;
1179
+ }
1180
+ const category = labeled.get(provider2.label) ?? { id: slug(provider2.label), label: provider2.label, tokens: 0 };
1181
+ category.tokens += tokens;
1182
+ labeled.set(provider2.label, category);
1183
+ });
1184
+ const builtin = new Set(builtinTools(host.capabilities).map((tool) => tool.spec.name));
1185
+ const tools = await new PromptTurn(host, session, signal).tools();
1186
+ const specs = (inside) => tools.filter((tool) => builtin.has(tool.spec.name) === inside).reduce((sum, tool) => sum + estimate(JSON.stringify(tool.spec)), 0);
1187
+ const categories = [
1188
+ { id: "system", label: "System prompt", tokens: estimate(basePrompt(session.cwd, host.capabilities.fs, host.capabilities.terminal)) },
1189
+ ...labeled.values(),
1190
+ { id: "context", label: "Other context", tokens: other },
1191
+ { id: "tools", label: "Built-in tools", tokens: specs(true) },
1192
+ { id: "mcp", label: "MCP and extension tools", tokens: specs(false) },
1193
+ { id: "messages", label: "Messages", tokens: estimateTokens(session.turns) }
1194
+ ];
1195
+ const used = categories.reduce((sum, category) => sum + category.tokens, 0);
1196
+ const info = session.catalog.find((model) => model.id === session.model);
1197
+ const window = info?.contextWindow;
1198
+ const threshold = window ? host.extensions?.compactor?.autoThreshold?.(window, info?.maxOutput) : void 0;
1199
+ return {
1200
+ model: session.model,
1201
+ estimated: true,
1202
+ categories,
1203
+ used,
1204
+ ...window ? { window, free: Math.max(0, (threshold ?? window) - used) } : {},
1205
+ ...info?.maxOutput ? { maxOutput: info.maxOutput } : {},
1206
+ ...window && threshold !== void 0 ? { threshold, buffer: Math.max(0, window - Math.max(threshold, used)) } : {},
1207
+ ...session.usage ? { reported: { ...session.usage } } : {}
1208
+ };
1209
+ }
1210
+ function contextGrid(usage, cells = 200) {
1211
+ if (!usage.window) return [];
1212
+ const scale = cells / Math.max(usage.window, usage.used);
1213
+ const grid = [];
1214
+ let sum = 0;
1215
+ for (const category of usage.categories) {
1216
+ sum += category.tokens;
1217
+ const end = Math.min(cells, Math.max(Math.round(sum * scale), category.tokens ? grid.length + 1 : grid.length));
1218
+ while (grid.length < end) grid.push(category.id);
1219
+ }
1220
+ const buffer = usage.threshold === void 0 ? cells : Math.max(grid.length, Math.round(usage.threshold * scale));
1221
+ while (grid.length < buffer) grid.push("free");
1222
+ while (grid.length < cells) grid.push("buffer");
1223
+ return grid;
1224
+ }
1225
+ const formatTokens = (tokens) => tokens < 1e3 ? String(tokens) : tokens < 1e6 ? `${+(tokens / 1e3).toFixed(1)}k` : `${+(tokens / 1e6).toFixed(2)}M`;
1226
+ const formatPercent = (tokens, window) => `${(tokens / window * 100).toFixed(1)}%`;
1227
+ const COLUMNS = 20;
1228
+ const GLYPHS = {
1229
+ system: "#",
1230
+ instructions: "=",
1231
+ skills: "+",
1232
+ context: "~",
1233
+ tools: "*",
1234
+ mcp: "%",
1235
+ messages: "@",
1236
+ free: ".",
1237
+ buffer: "x"
1238
+ };
1239
+ const SPARE = ["&", "$", "o", "^"];
1240
+ function contextLegend(usage) {
1241
+ let spare = 0;
1242
+ const rows = usage.categories.map((category) => ({
1243
+ id: category.id,
1244
+ glyph: GLYPHS[category.id] ?? SPARE[spare++ % SPARE.length],
1245
+ label: category.label,
1246
+ tokens: category.tokens
1247
+ }));
1248
+ if (usage.free !== void 0) rows.push({ id: "free", glyph: GLYPHS.free, label: "Free space", tokens: usage.free });
1249
+ if (usage.buffer !== void 0) rows.push({ id: "buffer", glyph: GLYPHS.buffer, label: "Auto-compact buffer", tokens: usage.buffer });
1250
+ return rows;
1251
+ }
1252
+ function contextNotes(usage) {
1253
+ const model = usage.model || "no model";
1254
+ const header = usage.window ? `Context usage for ${model}: ${formatTokens(usage.used)} of ${formatTokens(usage.window)} tokens (${formatPercent(usage.used, usage.window)}).` : `Context usage for ${model}: about ${formatTokens(usage.used)} tokens. The context window of this model is unknown.`;
1255
+ const footer = [
1256
+ ...usage.window ? [usage.threshold === void 0 ? "Auto compaction is off." : `Auto compaction runs at ${formatTokens(usage.threshold)} tokens (${formatPercent(usage.threshold, usage.window)}).`] : [],
1257
+ ...usage.reported ? [`Provider count for the last request: ${formatTokens(usage.reported.input + usage.reported.output)} tokens (${formatTokens(usage.reported.input)} input, ${formatTokens(usage.reported.output)} output).`] : [],
1258
+ "Category counts are estimates at 4 characters per token."
1259
+ ];
1260
+ return { header, footer };
1261
+ }
1262
+ function renderContextUsage(usage) {
1263
+ const legend = contextLegend(usage);
1264
+ const glyphs = new Map(legend.map((row) => [row.id, row.glyph]));
1265
+ const grid = contextGrid(usage);
1266
+ const rows = [];
1267
+ for (let index = 0; index < grid.length; index += COLUMNS) rows.push(grid.slice(index, index + COLUMNS).map((id2) => glyphs.get(id2)).join(" "));
1268
+ const width = Math.max(...legend.map((row) => row.label.length));
1269
+ const counts = legend.map((row) => formatTokens(row.tokens));
1270
+ const digits = Math.max(...counts.map((count2) => count2.length));
1271
+ const lines2 = legend.map((row, index) => `${row.glyph} ${row.label.padEnd(width)} ${counts[index].padStart(digits)}` + (usage.window ? ` ${formatPercent(row.tokens, usage.window).padStart(6)}` : ""));
1272
+ const { header, footer } = contextNotes(usage);
1273
+ return [header, "", "```text", ...rows.length ? [...rows, ""] : [], ...lines2, "```", "", footer.join(" ")].join("\n");
1274
+ }
1275
+ function contextCommand(host, session) {
1276
+ return {
1277
+ name: "context",
1278
+ description: "Show context window usage",
1279
+ run: async (_args, context) => {
1280
+ const usage = await contextUsage(host, session, context.signal);
1281
+ context.update({ sessionUpdate: "agent_message_chunk", content: { type: "text", text: renderContextUsage(usage) }, _meta: { oxbit: { context: usage } } });
1282
+ return { done: "end_turn" };
1283
+ }
1284
+ };
1285
+ }
1159
1286
  const EFFORTS = ["default", "low", "medium", "high"];
1160
1287
  const COMMAND = /^\/([\w:.-]+)(?:\s+([\s\S]*))?$/;
1161
1288
  const id = (params, key) => {
@@ -1245,7 +1372,7 @@ class Agent {
1245
1372
  currentValue: modeOf(session.meta),
1246
1373
  options: MODES.map(({ id: id2, name }) => ({ value: id2, name }))
1247
1374
  };
1248
- return session.catalog.length ? [models2, reasoning, mode] : [reasoning, mode];
1375
+ return [...session.catalog.length ? [models2] : [], reasoning, mode, ...this.host.configOptions?.(session) ?? []];
1249
1376
  }
1250
1377
  state(session) {
1251
1378
  return {
@@ -1371,13 +1498,14 @@ class Agent {
1371
1498
  this.host.update(sessionId, { sessionUpdate: "current_mode_update", currentModeId: modeId });
1372
1499
  return {};
1373
1500
  }
1374
- setConfigOption(sessionId, configId, value2) {
1501
+ async setConfigOption(sessionId, configId, value2) {
1375
1502
  const session = this.session(sessionId);
1376
1503
  if (configId === "model") return this.setModel(sessionId, value2);
1377
1504
  if (configId === "mode") {
1378
1505
  this.setMode(sessionId, value2);
1379
1506
  return { configOptions: this.options(session) };
1380
1507
  }
1508
+ if (configId !== "reasoning" && await this.host.setConfigOption?.(session, configId, value2)) return { configOptions: this.options(session) };
1381
1509
  if (configId !== "reasoning" || !EFFORTS.includes(value2)) throw new Error("Unknown agent setting");
1382
1510
  session.effort = value2;
1383
1511
  return { configOptions: this.options(session) };
@@ -1459,7 +1587,7 @@ ${submit.additionalContext.slice(0, CONTEXT_LIMIT$1)}`;
1459
1587
  }
1460
1588
  async commandList(session) {
1461
1589
  const lists = await Promise.all((this.host.extensions?.commands ?? []).map((provider2) => provider2.commands(session)));
1462
- return lists.flat();
1590
+ return [contextCommand(this.host, session), ...lists.flat()];
1463
1591
  }
1464
1592
  commandContext(session, turn) {
1465
1593
  return {
@@ -1529,12 +1657,12 @@ function hookSourcePaths(home, project, options = {}) {
1529
1657
  if (options.codex) paths.push({ dialect: "codex", path: `${home}/.codex/hooks.json` }, { dialect: "codex", path: `${project}/.codex/hooks.json` });
1530
1658
  return paths;
1531
1659
  }
1532
- const isObject = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
1660
+ const isObject$1 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
1533
1661
  const quote = (value2) => `'${value2.replace(/'/g, `'\\''`)}'`;
1534
1662
  function parse(source, warn) {
1535
- const hooks = isObject(source.config) ? source.config.hooks : void 0;
1663
+ const hooks = isObject$1(source.config) ? source.config.hooks : void 0;
1536
1664
  if (hooks === void 0) return [];
1537
- if (!isObject(hooks)) {
1665
+ if (!isObject$1(hooks)) {
1538
1666
  warn(`${source.path}: "hooks" must be an object`);
1539
1667
  return [];
1540
1668
  }
@@ -1542,19 +1670,22 @@ function parse(source, warn) {
1542
1670
  for (const [event, groups] of Object.entries(hooks)) {
1543
1671
  if (!EVENTS.has(event) || !Array.isArray(groups)) continue;
1544
1672
  for (const group of groups) {
1545
- for (const entry of isObject(group) && Array.isArray(group.hooks) ? group.hooks : []) {
1546
- if (!isObject(entry) || entry.type !== "command" || typeof entry.command !== "string" || !entry.command) {
1547
- warn(`${source.path}: skipped a ${isObject(entry) ? entry.type ?? "untyped" : "malformed"} ${event} hook; Oxbit runs command hooks only`);
1673
+ for (const entry of isObject$1(group) && Array.isArray(group.hooks) ? group.hooks : []) {
1674
+ if (!isObject$1(entry) || entry.type !== "command" || typeof entry.command !== "string" || !entry.command) {
1675
+ warn(`${source.path}: skipped a ${isObject$1(entry) ? entry.type ?? "untyped" : "malformed"} ${event} hook; Oxbit runs command hooks only`);
1548
1676
  continue;
1549
1677
  }
1550
1678
  const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : void 0;
1551
1679
  const seconds = typeof entry.timeout === "number" && entry.timeout > 0 ? entry.timeout : event === "UserPromptSubmit" ? 30 : 600;
1680
+ const matcher = typeof group.matcher === "string" ? group.matcher : "";
1681
+ const command = args ? [entry.command, ...args].map(quote).join(" ") : entry.command;
1552
1682
  handlers.push({
1683
+ id: [source.dialect, source.path, event, matcher, command].join("|"),
1553
1684
  dialect: source.dialect,
1554
1685
  path: source.path,
1555
1686
  event,
1556
- matcher: typeof group.matcher === "string" ? group.matcher : "",
1557
- command: args ? [entry.command, ...args].map(quote).join(" ") : entry.command,
1687
+ matcher,
1688
+ command,
1558
1689
  timeoutMs: seconds * 1e3,
1559
1690
  rule: typeof entry.if === "string" ? entry.if : void 0,
1560
1691
  async: entry.async === true,
@@ -1565,6 +1696,11 @@ function parse(source, warn) {
1565
1696
  }
1566
1697
  return handlers;
1567
1698
  }
1699
+ function listHooks(sources) {
1700
+ const warnings = [];
1701
+ const hooks = sources.flatMap((source) => parse(source, (message2) => warnings.push(message2))).map(({ id: id2, dialect, path: path2, event, matcher, command }) => ({ id: id2, dialect, path: path2, event, matcher, command }));
1702
+ return { hooks, warnings };
1703
+ }
1568
1704
  function matchesHook(matcher, values) {
1569
1705
  if (!matcher || matcher === "*") return true;
1570
1706
  if (/^[\w\- ,|]+$/.test(matcher)) return matcher.split(/[|,]/).map((name) => name.trim()).some((name) => values.includes(name));
@@ -1594,7 +1730,7 @@ function ruleAllows(rule, names, input2) {
1594
1730
  }
1595
1731
  function fromJson(event, json2) {
1596
1732
  const outcome = {};
1597
- const specific = isObject(json2.hookSpecificOutput) ? json2.hookSpecificOutput : {};
1733
+ const specific = isObject$1(json2.hookSpecificOutput) ? json2.hookSpecificOutput : {};
1598
1734
  if (json2.continue === false) outcome.continue = false;
1599
1735
  if (typeof json2.stopReason === "string") outcome.stopReason = json2.stopReason;
1600
1736
  if (typeof json2.systemMessage === "string") outcome.systemMessage = json2.systemMessage;
@@ -1608,7 +1744,7 @@ function fromJson(event, json2) {
1608
1744
  outcome.block = true;
1609
1745
  if (typeof json2.reason === "string") outcome.reason = json2.reason;
1610
1746
  }
1611
- if (isObject(specific.updatedInput)) outcome.updatedInput = specific.updatedInput;
1747
+ if (isObject$1(specific.updatedInput)) outcome.updatedInput = specific.updatedInput;
1612
1748
  if (typeof specific.additionalContext === "string") outcome.additionalContext = specific.additionalContext;
1613
1749
  return outcome;
1614
1750
  }
@@ -1650,14 +1786,14 @@ function createHookRunner(options) {
1650
1786
  warn(message2);
1651
1787
  };
1652
1788
  const handlersOf = (source) => {
1653
- if (!isObject(source.config)) return parse(source, warnOnce);
1789
+ if (!isObject$1(source.config)) return parse(source, warnOnce);
1654
1790
  let handlers = cache.get(source.config);
1655
1791
  if (!handlers) cache.set(source.config, handlers = parse(source, warnOnce));
1656
1792
  return handlers;
1657
1793
  };
1658
- async function runOne(handler, event, input2, signal) {
1794
+ async function runOne(handler2, event, input2, signal) {
1659
1795
  const name = typeof input2.tool_name === "string" ? input2.tool_name : void 0;
1660
- const alias = name && handler.dialect !== "oxbit" ? CLAUDE_TOOL_NAMES[name]?.[0] : void 0;
1796
+ const alias = name && handler2.dialect !== "oxbit" ? CLAUDE_TOOL_NAMES[name]?.[0] : void 0;
1661
1797
  const stdin = {
1662
1798
  transcript_path: options.transcriptPath?.(String(input2.session_id ?? "")) ?? "",
1663
1799
  ...input2,
@@ -1666,25 +1802,25 @@ function createHookRunner(options) {
1666
1802
  const cwd = String(input2.cwd ?? "");
1667
1803
  const controller = new AbortController();
1668
1804
  const abort = () => controller.abort();
1669
- const timer = setTimeout(abort, handler.timeoutMs);
1805
+ const timer = setTimeout(abort, handler2.timeoutMs);
1670
1806
  signal.addEventListener("abort", abort, { once: true });
1671
1807
  const killed = new Promise((resolve2) => controller.signal.addEventListener("abort", () => resolve2({ code: null, stdout: "", stderr: "" }), { once: true }));
1672
- const label = `${handler.path}: ${event} hook "${handler.command}"`;
1808
+ const label = `${handler2.path}: ${event} hook "${handler2.command}"`;
1673
1809
  const failed2 = (detail) => {
1674
1810
  warn(`${label} ${detail}`);
1675
- return handler.failBlocks && BLOCKING.has(event) && !signal.aborted ? { block: true, reason: `${label} ${detail}` } : {};
1811
+ return handler2.failBlocks && BLOCKING.has(event) && !signal.aborted ? { block: true, reason: `${label} ${detail}` } : {};
1676
1812
  };
1677
1813
  try {
1678
- const result = await Promise.race([options.exec(handler.command, {
1814
+ const result = await Promise.race([options.exec(handler2.command, {
1679
1815
  stdin: JSON.stringify(stdin),
1680
1816
  cwd,
1681
1817
  env: { CLAUDE_PROJECT_DIR: cwd, OXBIT_PROJECT_DIR: cwd },
1682
- timeoutMs: handler.timeoutMs,
1818
+ timeoutMs: handler2.timeoutMs,
1683
1819
  signal: controller.signal
1684
1820
  }), killed]);
1685
- if (result.code === null) return signal.aborted ? {} : failed2(`timed out after ${handler.timeoutMs / 1e3} s`);
1821
+ if (result.code === null) return signal.aborted ? {} : failed2(`timed out after ${handler2.timeoutMs / 1e3} s`);
1686
1822
  if (result.code === 2) {
1687
- const reason = result.stderr.trim() || `Blocked by ${handler.command}`;
1823
+ const reason = result.stderr.trim() || `Blocked by ${handler2.command}`;
1688
1824
  if (BLOCKING.has(event)) return { block: true, reason };
1689
1825
  warn(`${label} exited with 2: ${reason}`);
1690
1826
  return {};
@@ -1694,7 +1830,7 @@ function createHookRunner(options) {
1694
1830
  if (stdout.startsWith("{")) {
1695
1831
  try {
1696
1832
  const json2 = JSON.parse(stdout);
1697
- if (isObject(json2)) return fromJson(event, json2);
1833
+ if (isObject$1(json2)) return fromJson(event, json2);
1698
1834
  } catch {
1699
1835
  }
1700
1836
  }
@@ -1713,21 +1849,21 @@ function createHookRunner(options) {
1713
1849
  async run(event, input2, signal) {
1714
1850
  if (signal.aborted) return {};
1715
1851
  const sources = typeof options.sources === "function" ? await options.sources(String(input2.cwd ?? "")) : options.sources;
1716
- if (sources.some((source) => isObject(source.config) && source.config.disableAllHooks === true)) return {};
1852
+ if (sources.some((source) => isObject$1(source.config) && source.config.disableAllHooks === true)) return {};
1717
1853
  const tool = TOOL_EVENTS.has(event) && typeof input2.tool_name === "string" ? input2.tool_name : void 0;
1718
1854
  const names = tool ? [tool, ...CLAUDE_TOOL_NAMES[tool] ?? []] : void 0;
1719
1855
  const field = MATCH_FIELD[event];
1720
1856
  const values = names ?? (field && typeof input2[field] === "string" ? [input2[field]] : void 0);
1721
1857
  const selected = /* @__PURE__ */ new Map();
1722
- for (const handler of sources.flatMap(handlersOf)) {
1723
- if (handler.event !== event || values && !matchesHook(handler.matcher, values)) continue;
1724
- if (handler.rule !== void 0 && !(names && ruleAllows(handler.rule, names, isObject(input2.tool_input) ? input2.tool_input : {}))) continue;
1725
- const key = `${handler.command}\0${handler.rule ?? ""}`;
1726
- if (!selected.has(key)) selected.set(key, handler);
1858
+ for (const handler2 of sources.flatMap(handlersOf)) {
1859
+ if (handler2.event !== event || values && !matchesHook(handler2.matcher, values) || options.skip?.(handler2)) continue;
1860
+ if (handler2.rule !== void 0 && !(names && ruleAllows(handler2.rule, names, isObject$1(input2.tool_input) ? input2.tool_input : {}))) continue;
1861
+ const key = `${handler2.command}\0${handler2.rule ?? ""}`;
1862
+ if (!selected.has(key)) selected.set(key, handler2);
1727
1863
  }
1728
- const outcomes = await Promise.all([...selected.values()].map((handler) => {
1729
- if (!handler.async) return runOne(handler, event, input2, signal);
1730
- void runOne(handler, event, input2, signal);
1864
+ const outcomes = await Promise.all([...selected.values()].map((handler2) => {
1865
+ if (!handler2.async) return runOne(handler2, event, input2, signal);
1866
+ void runOne(handler2, event, input2, signal);
1731
1867
  return {};
1732
1868
  }));
1733
1869
  return mergeOutcomes(event, outcomes);
@@ -1771,7 +1907,7 @@ Before you decide the goal is achieved, treat completion as unproven:
1771
1907
  Blocked audit:
1772
1908
  - Call update_goal with status "blocked" only after the same blocker has repeated for three consecutive goal turns and you cannot progress without the user or an external change.
1773
1909
  - Do not mark the goal blocked because the work is hard, slow or uncertain. Use status "paused" only when the user asks for it.`;
1774
- const describe = (goal) => [
1910
+ const describe$1 = (goal) => [
1775
1911
  `Goal (${goal.status.replace("_", " ")}): ${goal.objective}`,
1776
1912
  `${goal.continuations} continuations. ${budgetLines(goal)}`,
1777
1913
  ...goal.note ? [`Note: ${goal.note}`] : []
@@ -1805,7 +1941,7 @@ function createGoal(options) {
1805
1941
  async run(args, context) {
1806
1942
  const { session } = context;
1807
1943
  const goal = goalOf(session.meta);
1808
- if (!args) return done(context, goal ? describe(goal) : "No goal is set. Start one with /goal <objective>.");
1944
+ if (!args) return done(context, goal ? describe$1(goal) : "No goal is set. Start one with /goal <objective>.");
1809
1945
  if (args === "clear") {
1810
1946
  delete session.meta.goal;
1811
1947
  publish(session);
@@ -1930,8 +2066,8 @@ function createLoop(options) {
1930
2066
  return loop?.status === "running" && owners.has(session.id) ? loop : void 0;
1931
2067
  };
1932
2068
  const publish = (session, loop) => statusUpdate(options, session.id, "loop", loop.status === "running" ? { status: loop.status, iteration: loop.iteration, max: loop.max, ...loop.intervalMs ? { interval: format(loop.intervalMs) } : {} } : null);
1933
- const finish = (session, loop, status, text) => {
1934
- loop.status = status;
2069
+ const finish = (session, loop, status2, text) => {
2070
+ loop.status = status2;
1935
2071
  delete loop.nextDelayMs;
1936
2072
  owners.delete(session.id);
1937
2073
  publish(session, loop);
@@ -2077,21 +2213,28 @@ class Instructions {
2077
2213
  this.options = options;
2078
2214
  }
2079
2215
  options;
2080
- /** Global files, then from the git root (or cwd) down to cwd the first non-empty AGENTS.override.md, AGENTS.md or CLAUDE.md. */
2081
- async load(cwd) {
2216
+ label = "Instructions";
2217
+ /** The global and Claude files whether or not they exist, then the project files that load, without repeated paths. */
2218
+ async candidates(cwd) {
2082
2219
  const { files, global, claude } = this.options;
2083
2220
  const dirs = ancestors(cwd, await findGitRoot(files, cwd) ?? cwd).reverse();
2221
+ const own = async (path2, scope) => path2 ? { path: path2, scope, text: (await readText(files, path2))?.text } : void 0;
2084
2222
  const found = [
2085
- ...await Promise.all([global, claude].map((path2) => path2 ? readText(files, path2) : void 0)),
2086
- ...await Promise.all(dirs.map((dir) => firstOf(files, dir)))
2223
+ ...await Promise.all([own(global, "global"), own(claude, "claude")]),
2224
+ ...(await Promise.all(dirs.map((dir) => firstOf(files, dir)))).map((file2) => file2 && { ...file2, scope: "project" })
2087
2225
  ];
2088
2226
  return found.filter((file2, index) => !!file2 && found.findIndex((other) => other?.path === file2.path) === index);
2089
2227
  }
2228
+ /** Global files, then from the git root (or cwd) down to cwd the first non-empty AGENTS.override.md, AGENTS.md or CLAUDE.md. */
2229
+ async load(cwd) {
2230
+ return (await this.candidates(cwd)).flatMap(({ path: path2, text }) => text === void 0 ? [] : [{ path: path2, text }]);
2231
+ }
2090
2232
  async system(session) {
2091
2233
  const sections = [];
2092
2234
  let used = 0;
2093
2235
  let skipped = 0;
2094
2236
  for (const file2 of await this.load(session.cwd)) {
2237
+ if (this.options.skip?.(file2.path)) continue;
2095
2238
  const room = Math.min(FILE_LIMIT, TOTAL_LIMIT - used);
2096
2239
  if (room <= 0) {
2097
2240
  skipped++;
@@ -2112,7 +2255,8 @@ ${clipped.text}${clipped.clipped ? `
2112
2255
  }
2113
2256
  }
2114
2257
  const KEY = /^([\w-]+):(?:[ \t]+(.*))?$/;
2115
- const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2258
+ const NAME$1 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2259
+ const isSkillName = (name) => name.length <= 64 && NAME$1.test(name);
2116
2260
  const OUTPUT_LIMIT = 1e5;
2117
2261
  const scalar = (text) => {
2118
2262
  const quoted = /^(["'])(.*)\1\s*(?:#.*)?$/s.exec(text);
@@ -2168,7 +2312,7 @@ function toSkill(text, path2, folder, warnings) {
2168
2312
  }
2169
2313
  const name = string2("name") || folder;
2170
2314
  if (!string2("name")) warnings.push(`${path2}: the frontmatter has no name, so the skill uses the directory name ${folder}`);
2171
- if (name.length > 64 || !NAME.test(name)) warnings.push(`${path2}: the name ${name} breaks the Agent Skills rules (a-z, 0-9 and single hyphens, 64 characters at most)`);
2315
+ if (!isSkillName(name)) warnings.push(`${path2}: the name ${name} breaks the Agent Skills rules (a-z, 0-9 and single hyphens, 64 characters at most)`);
2172
2316
  else if (name !== folder) warnings.push(`${path2}: the name ${name} differs from the directory name ${folder}`);
2173
2317
  if (description.length > 1024) warnings.push(`${path2}: the description is longer than 1024 characters`);
2174
2318
  const tools = data["allowed-tools"];
@@ -2215,6 +2359,7 @@ class Skills {
2215
2359
  this.options = options;
2216
2360
  }
2217
2361
  options;
2362
+ label = "Skills";
2218
2363
  sets = /* @__PURE__ */ new Map();
2219
2364
  /** Scans the user roots, then .agents/skills and .claude/skills from cwd up to the git root. The first skill with a name wins. */
2220
2365
  async discover(cwd) {
@@ -2243,8 +2388,11 @@ class Skills {
2243
2388
  this.sets.set(cwd, set);
2244
2389
  return set;
2245
2390
  }
2391
+ shown(skills) {
2392
+ return this.options.hidden ? skills.filter((skill) => !this.options.hidden(skill.name)) : skills;
2393
+ }
2246
2394
  async system(session) {
2247
- const visible = (await this.discover(session.cwd)).skills.filter((skill) => skill.model);
2395
+ const visible = this.shown((await this.discover(session.cwd)).skills).filter((skill) => skill.model);
2248
2396
  if (!visible.length) return void 0;
2249
2397
  return [
2250
2398
  "Skills hold instructions for specific tasks. When a task matches a skill's description, call the skill tool with its name before you act and follow the instructions it returns.",
@@ -2254,7 +2402,7 @@ class Skills {
2254
2402
  ].join("\n");
2255
2403
  }
2256
2404
  async tools(session) {
2257
- const { skills } = this.sets.get(session.cwd) ?? await this.discover(session.cwd);
2405
+ const skills = this.shown((this.sets.get(session.cwd) ?? await this.discover(session.cwd)).skills);
2258
2406
  return skills.length ? [this.tool(skills)] : [];
2259
2407
  }
2260
2408
  tool(skills) {
@@ -2303,7 +2451,7 @@ ${files.map((file2) => `- ${file2}`).join("\n")}` : ""));
2303
2451
  if (!set) void this.discover(session.cwd).catch(() => {
2304
2452
  });
2305
2453
  const list2 = { name: "skills", description: "List skills and skill warnings", run: (_args, context) => this.list(context) };
2306
- return [list2, ...(set?.skills ?? []).filter((skill) => skill.user && skill.name !== list2.name).map((skill) => ({
2454
+ return [list2, ...this.shown(set?.skills ?? []).filter((skill) => skill.user && skill.name !== list2.name).map((skill) => ({
2307
2455
  name: skill.name,
2308
2456
  description: skill.description.slice(0, 200),
2309
2457
  hint: skill.hint,
@@ -2354,7 +2502,7 @@ function parseMcpJson(text, scope, env = {}, source = ".mcp.json") {
2354
2502
  }
2355
2503
  const entries = body?.mcpServers;
2356
2504
  if (!entries || typeof entries !== "object" || Array.isArray(entries)) return { servers: [], warnings: [`${source} has no "mcpServers" object`] };
2357
- const servers = [];
2505
+ const servers2 = [];
2358
2506
  for (const [name, entry] of Object.entries(entries)) {
2359
2507
  if (!entry || typeof entry !== "object" || entry.enabled === false || entry.disabled === true) continue;
2360
2508
  const missing = /* @__PURE__ */ new Set();
@@ -2370,7 +2518,7 @@ function parseMcpJson(text, scope, env = {}, source = ".mcp.json") {
2370
2518
  continue;
2371
2519
  }
2372
2520
  const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : [];
2373
- servers.push({
2521
+ servers2.push({
2374
2522
  name,
2375
2523
  scope,
2376
2524
  type: "stdio",
@@ -2384,7 +2532,7 @@ function parseMcpJson(text, scope, env = {}, source = ".mcp.json") {
2384
2532
  warnings.push(`MCP server "${name}" in ${source} has no url`);
2385
2533
  continue;
2386
2534
  }
2387
- servers.push({
2535
+ servers2.push({
2388
2536
  name,
2389
2537
  scope,
2390
2538
  type: type === "sse" ? "sse" : "http",
@@ -2398,11 +2546,11 @@ function parseMcpJson(text, scope, env = {}, source = ".mcp.json") {
2398
2546
  }
2399
2547
  for (const variable of missing) warnings.push(`MCP server "${name}" references \${${variable}}, which is not set`);
2400
2548
  }
2401
- return { servers, warnings };
2549
+ return { servers: servers2, warnings };
2402
2550
  }
2403
2551
  const pairs = (value2) => Array.isArray(value2) ? Object.fromEntries(value2.filter((item) => typeof item?.name === "string" && typeof item?.value === "string").map((item) => [item.name, item.value])) : strings(value2);
2404
2552
  function parseAcpServers(list2) {
2405
- const servers = [];
2553
+ const servers2 = [];
2406
2554
  const warnings = [];
2407
2555
  for (const entry of Array.isArray(list2) ? list2 : []) {
2408
2556
  const name = typeof entry?.name === "string" && entry.name ? entry.name : void 0;
@@ -2411,11 +2559,11 @@ function parseAcpServers(list2) {
2411
2559
  continue;
2412
2560
  }
2413
2561
  if (entry.type === "http" || entry.type === "sse") {
2414
- if (typeof entry.url === "string" && entry.url) servers.push({ name, scope: "session", type: entry.type, url: entry.url, headers: pairs(entry.headers) });
2562
+ if (typeof entry.url === "string" && entry.url) servers2.push({ name, scope: "session", type: entry.type, url: entry.url, headers: pairs(entry.headers) });
2415
2563
  else warnings.push(`MCP server "${name}" from the client has no url`);
2416
2564
  } else if (entry.type === void 0 || entry.type === "stdio") {
2417
2565
  if (typeof entry.command === "string" && entry.command)
2418
- servers.push({
2566
+ servers2.push({
2419
2567
  name,
2420
2568
  scope: "session",
2421
2569
  type: "stdio",
@@ -2426,7 +2574,7 @@ function parseAcpServers(list2) {
2426
2574
  else warnings.push(`MCP server "${name}" from the client has no command`);
2427
2575
  } else warnings.push(`MCP server "${name}" from the client uses the "${entry.type}" transport, which Oxbit Agent does not support`);
2428
2576
  }
2429
- return { servers, warnings };
2577
+ return { servers: servers2, warnings };
2430
2578
  }
2431
2579
  function mergeServers(...lists) {
2432
2580
  const byName = /* @__PURE__ */ new Map();
@@ -2435,11 +2583,11 @@ function mergeServers(...lists) {
2435
2583
  }
2436
2584
  const MODERN_ERROR_CODES = /* @__PURE__ */ new Set([-32020, -32021, -32022]);
2437
2585
  class McpError extends Error {
2438
- constructor(message2, code, data, status) {
2586
+ constructor(message2, code, data, status2) {
2439
2587
  super(message2);
2440
2588
  this.code = code;
2441
2589
  this.data = data;
2442
- this.status = status;
2590
+ this.status = status2;
2443
2591
  }
2444
2592
  code;
2445
2593
  data;
@@ -2580,7 +2728,7 @@ async function bodyText(response, limit = ERROR_BODY_LIMIT) {
2580
2728
  }
2581
2729
  return text.slice(0, limit);
2582
2730
  }
2583
- const httpError = (status, body, message2 = `HTTP ${status}`) => Object.assign(new Error(message2), { status, body });
2731
+ const httpError = (status2, body, message2 = `HTTP ${status2}`) => Object.assign(new Error(message2), { status: status2, body });
2584
2732
  function base64url(bytes) {
2585
2733
  let binary = "";
2586
2734
  for (const byte of bytes) binary += String.fromCharCode(byte);
@@ -2639,14 +2787,14 @@ class HttpTransport {
2639
2787
  for (const [key, value2] of Object.entries(extra)) headers.set(key, value2);
2640
2788
  return headers;
2641
2789
  }
2642
- async exchange(init, signal, use) {
2790
+ async exchange(init, signal, use2) {
2643
2791
  const controller = new AbortController();
2644
2792
  const abort = () => controller.abort();
2645
2793
  signal?.addEventListener("abort", abort, { once: true });
2646
2794
  if (signal?.aborted) abort();
2647
2795
  this.controllers.add(controller);
2648
2796
  try {
2649
- return await use(await this.fetch(this.url, { ...init, signal: controller.signal }));
2797
+ return await use2(await this.fetch(this.url, { ...init, signal: controller.signal }));
2650
2798
  } catch (error) {
2651
2799
  if (controller.signal.aborted && !(error instanceof McpError)) throw new McpError("The MCP request was cancelled");
2652
2800
  throw error;
@@ -3124,8 +3272,6 @@ function renderResult(result) {
3124
3272
  }
3125
3273
  const CLIENT_INFO = { name: "oxbit-agent", version: "1" };
3126
3274
  const message$1 = (error) => error instanceof Error ? error.message : String(error);
3127
- const sessionKey = (session) => `${session.cwd}
3128
- ${JSON.stringify(session.meta.mcpServers ?? null)}`;
3129
3275
  class McpTools {
3130
3276
  constructor(options) {
3131
3277
  this.options = options;
@@ -3144,7 +3290,7 @@ class McpTools {
3144
3290
  const text = await this.options.read(path2).catch((error) => void warnings.push(`${path2}: ${message$1(error)}`));
3145
3291
  if (text === void 0) return;
3146
3292
  const parsed = parseMcpJson(text, scope, this.options.env ?? {}, path2);
3147
- lists.push(parsed.servers);
3293
+ lists.push(this.options.skip ? parsed.servers.filter((server) => !this.options.skip(server)) : parsed.servers);
3148
3294
  warnings.push(...parsed.warnings);
3149
3295
  };
3150
3296
  if (this.options.globalConfig) await load(this.options.globalConfig, "global");
@@ -3155,19 +3301,21 @@ class McpTools {
3155
3301
  const names = text === void 0 ? [] : parseMcpJson(text, "project", {}, project).servers.map((server) => server.name);
3156
3302
  if (names.length) warnings.push(`${project} is not trusted, so its servers are off: ${names.join(", ")}.${this.options.trustHint ? ` ${this.options.trustHint}` : ""}`);
3157
3303
  }
3158
- const acp2 = parseAcpServers(session.meta.mcpServers);
3159
- lists.push(acp2.servers);
3160
- warnings.push(...acp2.warnings);
3304
+ const acp = parseAcpServers(session.meta.mcpServers);
3305
+ lists.push(acp.servers);
3306
+ warnings.push(...acp.warnings);
3161
3307
  return { servers: mergeServers(...lists), warnings };
3162
3308
  }
3163
3309
  async state(session) {
3164
- const key = sessionKey(session);
3165
- const current = this.sessions.get(session.id);
3166
- if (current?.key === key) return current;
3167
3310
  const pending = this.loading.get(session.id) ?? (async () => {
3168
- const { servers, warnings } = await this.configure(session);
3169
- const state = { key, cwd: session.cwd, servers: servers.map((config) => ({ config, tools: 0, excluded: 0 })), warnings, closed: false, active: 0 };
3311
+ const { servers: servers2, warnings } = await this.configure(session);
3312
+ const key = JSON.stringify([session.cwd, servers2]);
3170
3313
  const previous = this.sessions.get(session.id);
3314
+ if (previous?.key === key) {
3315
+ previous.warnings = warnings;
3316
+ return previous;
3317
+ }
3318
+ const state = { key, cwd: session.cwd, servers: servers2.map((config) => ({ config, tools: 0, excluded: 0 })), warnings, closed: false, active: 0 };
3171
3319
  this.sessions.set(session.id, state);
3172
3320
  if (previous) await this.shut(previous);
3173
3321
  return state;
@@ -3308,10 +3456,11 @@ class McpTools {
3308
3456
  ...server.error ? { error: server.error } : {}
3309
3457
  })) };
3310
3458
  }
3459
+ /** The /mcp status text: one line per server of the last tools() call, then warnings. */
3311
3460
  report(sessionId) {
3312
- const { servers, warnings } = this.status(sessionId);
3313
- const lines2 = servers.length ? [`MCP servers (${servers.length}):`] : ["No MCP servers are configured for this session."];
3314
- for (const server of servers) {
3461
+ const { servers: servers2, warnings } = this.status(sessionId);
3462
+ const lines2 = servers2.length ? [`MCP servers (${servers2.length}):`] : ["No MCP servers are configured for this session."];
3463
+ for (const server of servers2) {
3315
3464
  const detail = server.state === "connected" ? `connected, ${server.tools} tools${server.excluded ? `, ${server.excluded} left out` : ""}` : server.state === "failed" ? `failed: ${server.error}` : server.state;
3316
3465
  lines2.push(`- ${server.name} (${server.scope}, ${server.type}${server.version ? `, ${server.version}` : ""}): ${detail}`);
3317
3466
  }
@@ -3594,7 +3743,7 @@ const PLAN_ERRORS = {
3594
3743
  subscription_sharing_invalid_user: "ChatGPT could not validate this account. Sign in with ChatGPT again.",
3595
3744
  subscription_sharing_route_not_supported: "ChatGPT plan usage does not support this request route."
3596
3745
  };
3597
- function describePlanError(status, body) {
3746
+ function describePlanError(status2, body) {
3598
3747
  let parsed;
3599
3748
  try {
3600
3749
  parsed = JSON.parse(body);
@@ -3603,7 +3752,7 @@ function describePlanError(status, body) {
3603
3752
  }
3604
3753
  const code = parsed?.error?.code;
3605
3754
  const known = typeof code === "string" ? PLAN_ERRORS[code] : void 0;
3606
- const text = known ?? (code === "subscription_sharing_unsupported_capability" ? `ChatGPT plan usage does not support ${parsed.error.param ?? "a field in this request"}.` : typeof parsed?.detail === "string" ? `ChatGPT plan request rejected (${status}): ${parsed.detail}` : void 0);
3755
+ const text = known ?? (code === "subscription_sharing_unsupported_capability" ? `ChatGPT plan usage does not support ${parsed.error.param ?? "a field in this request"}.` : typeof parsed?.detail === "string" ? `ChatGPT plan request rejected (${status2}): ${parsed.detail}` : void 0);
3607
3756
  return text ? JSON.stringify({ error: { message: text, code } }) : body;
3608
3757
  }
3609
3758
  const STORE_KEY = "chatgpt";
@@ -4066,6 +4215,7 @@ function serveStdio({ input: input2, output, factory, log }) {
4066
4215
  void server.peer.idle().then(() => server.close()).then(resolve2);
4067
4216
  }));
4068
4217
  }
4218
+ const stringList = (value2) => Array.isArray(value2) ? value2.filter((item) => typeof item === "string") : [];
4069
4219
  const agentHome = (env = process.env) => env.OXBIT_AGENT_HOME || join(homedir(), ".oxbit", "agent");
4070
4220
  function agentPaths(home) {
4071
4221
  return {
@@ -4113,8 +4263,14 @@ class AgentConfig {
4113
4263
  settings;
4114
4264
  paths;
4115
4265
  static async load(home = agentHome()) {
4116
- const settings = await readJson(agentPaths(home).settings, {});
4117
- return new AgentConfig(home, settings && typeof settings === "object" ? settings : {});
4266
+ const config = new AgentConfig(home, {});
4267
+ await config.reload();
4268
+ return config;
4269
+ }
4270
+ /** Reads settings.json again. */
4271
+ async reload() {
4272
+ const settings = await readJson(this.paths.settings, {});
4273
+ this.settings = settings && typeof settings === "object" ? settings : {};
4118
4274
  }
4119
4275
  /** Creates the home and skills directories. */
4120
4276
  async ensure() {
@@ -4217,7 +4373,7 @@ class FileCatalogCache {
4217
4373
  }
4218
4374
  }
4219
4375
  function agentVersion() {
4220
- return "0.10.1";
4376
+ return "0.11.0";
4221
4377
  }
4222
4378
  const clone = (message2) => JSON.parse(JSON.stringify(message2));
4223
4379
  class MemoryConnection {
@@ -4288,10 +4444,10 @@ class MemoryConnection {
4288
4444
  return () => void this.listeners.delete(listener);
4289
4445
  }
4290
4446
  /** One handler at a time. Without one, permission prompts resolve as "cancelled". */
4291
- onPermission(handler) {
4292
- this.permission = handler;
4447
+ onPermission(handler2) {
4448
+ this.permission = handler2;
4293
4449
  return () => {
4294
- if (this.permission === handler) this.permission = void 0;
4450
+ if (this.permission === handler2) this.permission = void 0;
4295
4451
  };
4296
4452
  }
4297
4453
  close() {
@@ -4299,14 +4455,14 @@ class MemoryConnection {
4299
4455
  }
4300
4456
  async serve(method, params) {
4301
4457
  if (method !== "session/request_permission") throw Object.assign(new Error(`Method not found: ${method}`), { code: -32601 });
4302
- const handler = this.permission;
4303
- if (!handler) return { outcome: { outcome: "cancelled" } };
4458
+ const handler2 = this.permission;
4459
+ if (!handler2) return { outcome: { outcome: "cancelled" } };
4304
4460
  const controller = new AbortController();
4305
4461
  const set = this.waiting.get(params.sessionId) ?? /* @__PURE__ */ new Set();
4306
4462
  this.waiting.set(params.sessionId, set.add(controller));
4307
4463
  const cancelled = new Promise((resolve2) => controller.signal.addEventListener("abort", () => resolve2({ outcome: "cancelled" }), { once: true }));
4308
4464
  try {
4309
- return { outcome: await Promise.race([handler(params, controller.signal), cancelled]) };
4465
+ return { outcome: await Promise.race([handler2(params, controller.signal), cancelled]) };
4310
4466
  } finally {
4311
4467
  set.delete(controller);
4312
4468
  if (!set.size) this.waiting.delete(params.sessionId);
@@ -4611,12 +4767,13 @@ function nodeMcp(config, log) {
4611
4767
  env: process.env,
4612
4768
  trustProject: (cwd) => projectTrusted(config.paths.settings, cwd),
4613
4769
  trustHint: `Add the project path to "trustedProjects" in ${config.paths.settings} to run them.`,
4770
+ skip: (server) => stringList(config.settings.disabledMcpServers).includes(server.name),
4614
4771
  clientInfo: { name: "oxbit-agent", version: agentVersion() }
4615
4772
  });
4616
4773
  }
4617
4774
  const PROVIDER_IDS = PROVIDERS.map((spec) => spec.id);
4618
4775
  const LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
4619
- const OLLAMA = "http://127.0.0.1:11434";
4776
+ const OLLAMA$1 = "http://127.0.0.1:11434";
4620
4777
  const DETECTED = ["anthropic", "openai", "openrouter"];
4621
4778
  function normalizeBase(input2) {
4622
4779
  let url;
@@ -4642,7 +4799,7 @@ async function chatgptSignedIn(auth) {
4642
4799
  const keyFor = async (spec, input2, baseUrl) => !!(await input2.auth.get(apiKeySlot(spec.id, baseUrl)) || spec.env && input2.env[spec.env]);
4643
4800
  function baseFor(spec, settings, env) {
4644
4801
  const own = settings.providers?.[spec.id]?.baseUrl;
4645
- const configured = spec.id === "ollama" ? own || env.OLLAMA_HOST || OLLAMA : spec.id === "openai-compatible" ? own || env.OXBIT_AGENT_BASE_URL : spec.id === "openrouter" ? own : void 0;
4802
+ const configured = spec.id === "ollama" ? own || env.OLLAMA_HOST || OLLAMA$1 : spec.id === "openai-compatible" ? own || env.OXBIT_AGENT_BASE_URL : spec.id === "openrouter" ? own : void 0;
4646
4803
  if (spec.id === "openai-compatible" && !configured) throw new Error('Set providers["openai-compatible"].baseUrl in settings.json or OXBIT_AGENT_BASE_URL');
4647
4804
  return configured ? normalizeBase(configured) : void 0;
4648
4805
  }
@@ -4661,8 +4818,10 @@ async function selectProvider(input2) {
4661
4818
  const { settings, env } = input2;
4662
4819
  const signedIn = await chatgptSignedIn(input2.auth);
4663
4820
  let detected;
4821
+ const disabled = stringList(settings.disabledProviders);
4664
4822
  if (!input2.provider && !settings.provider)
4665
4823
  for (const id2 of DETECTED) {
4824
+ if (disabled.includes(id2)) continue;
4666
4825
  if (await keyFor(providerSpec(id2), input2) || id2 === "openai" && signedIn) {
4667
4826
  detected = id2;
4668
4827
  break;
@@ -4690,7 +4849,7 @@ function missingKey(choice) {
4690
4849
  }
4691
4850
  const VALID_ID = /^[\w.-]{1,128}$/;
4692
4851
  const HEADER_LIMIT = 16 * 1024 * 1024;
4693
- async function firstLine$1(file2) {
4852
+ async function firstLine$2(file2) {
4694
4853
  const handle = await open(file2, "r");
4695
4854
  try {
4696
4855
  const chunks = [];
@@ -4770,7 +4929,7 @@ class JsonlSessionStore {
4770
4929
  const records = [];
4771
4930
  for (const name of names.filter((item) => item.endsWith(".jsonl"))) {
4772
4931
  try {
4773
- const head = summary(JSON.parse(await firstLine$1(join(dir, name))));
4932
+ const head = summary(JSON.parse(await firstLine$2(join(dir, name))));
4774
4933
  if (head && head.cwd === cwd) records.push(head);
4775
4934
  } catch {
4776
4935
  }
@@ -5007,6 +5166,9 @@ function undoCommands(store) {
5007
5166
  };
5008
5167
  return { commands: () => [undo, redo] };
5009
5168
  }
5169
+ const SETTINGS_COMMANDS = ["config", "skills", "hooks", "mcp", "instructions", "providers"];
5170
+ const settingsOf = (config) => config.settings;
5171
+ const compactAt$1 = (value2) => typeof value2 === "number" && value2 >= 50 && value2 <= 95 ? value2 : void 0;
5010
5172
  const nodeFiles = {
5011
5173
  async list(path2) {
5012
5174
  const entries = await readdir(path2, { withFileTypes: true }).catch(() => []);
@@ -5048,9 +5210,10 @@ const nodeHookExec = (command, { stdin, cwd, env, timeoutMs, signal }) => new Pr
5048
5210
  });
5049
5211
  child.on("close", finish);
5050
5212
  });
5051
- function hookSources(config, settings, home, log) {
5213
+ function hookSources(config, home, log) {
5052
5214
  const cache = /* @__PURE__ */ new Map();
5053
5215
  return async (cwd) => {
5216
+ const settings = settingsOf(config);
5054
5217
  const paths = hookSourcePaths(home, cwd, { claude: settings.claudeHooks === true, codex: settings.codexHooks === true });
5055
5218
  paths[0] = { ...paths[0], path: config.paths.settings };
5056
5219
  const sources = await Promise.all(paths.map(async ({ dialect, path: path2 }) => {
@@ -5071,151 +5234,344 @@ function hookSources(config, settings, home, log) {
5071
5234
  return sources.filter((source) => !!source);
5072
5235
  };
5073
5236
  }
5074
- function coreExtensions(config, update, options = {}) {
5075
- const settings = config.settings;
5076
- const { home = homedir(), log } = options;
5077
- const instructions = new Instructions({
5237
+ function instructionsFor(config, home) {
5238
+ return new Instructions({
5078
5239
  files: nodeFiles,
5079
5240
  global: config.paths.instructions,
5080
- claude: settings.claudeInstructions ? join(home, ".claude", "CLAUDE.md") : void 0,
5081
- append: typeof settings.appendSystemPrompt === "string" ? settings.appendSystemPrompt : void 0
5241
+ get claude() {
5242
+ return settingsOf(config).claudeInstructions ? join(home, ".claude", "CLAUDE.md") : void 0;
5243
+ },
5244
+ get append() {
5245
+ const append = settingsOf(config).appendSystemPrompt;
5246
+ return typeof append === "string" ? append : void 0;
5247
+ },
5248
+ skip: (path2) => stringList(settingsOf(config).disabledInstructions).includes(path2)
5249
+ });
5250
+ }
5251
+ function skillsFor(config, home) {
5252
+ return new Skills({
5253
+ files: nodeFiles,
5254
+ roots: [config.paths.skills, join(home, ".agents", "skills"), join(home, ".claude", "skills")],
5255
+ hidden: (name) => stringList(settingsOf(config).disabledSkills).includes(name)
5256
+ });
5257
+ }
5258
+ function coreExtensions(config, update, options = {}) {
5259
+ const { home = homedir(), log } = options;
5260
+ const instructions = instructionsFor(config, home);
5261
+ const skills = skillsFor(config, home);
5262
+ const reserved = new Set(SETTINGS_COMMANDS);
5263
+ const compaction = new Compaction({
5264
+ get auto() {
5265
+ return settingsOf(config).autoCompact !== false;
5266
+ },
5267
+ get percent() {
5268
+ return compactAt$1(settingsOf(config).autoCompactAt);
5269
+ }
5082
5270
  });
5083
- const skills = new Skills({ files: nodeFiles, roots: [config.paths.skills, join(home, ".agents", "skills"), join(home, ".claude", "skills")] });
5084
- const compaction = new Compaction({ auto: settings.autoCompact !== false });
5085
5271
  const control = controlExtensions({ update });
5086
5272
  return {
5087
5273
  context: [instructions, ...control.context, skills],
5088
5274
  tools: [...control.tools, skills],
5089
- commands: [compaction, ...control.commands, { commands: async (session) => (await skills.discover(session.cwd), skills.commands(session)) }],
5275
+ commands: [compaction, ...control.commands, { commands: async (session) => (await skills.discover(session.cwd), skills.commands(session).filter((command) => !reserved.has(command.name))) }],
5090
5276
  continuations: control.continuations,
5091
5277
  compactor: compaction,
5092
- hooks: createHookRunner({ sources: hookSources(config, settings, home, log), exec: nodeHookExec, onWarning: log })
5278
+ hooks: createHookRunner({
5279
+ sources: hookSources(config, home, log),
5280
+ exec: nodeHookExec,
5281
+ onWarning: log,
5282
+ skip: (hook) => stringList(settingsOf(config).disabledHooks).includes(hook.id)
5283
+ })
5093
5284
  };
5094
5285
  }
5095
- const MAX_BYTES = 1024 * 1024;
5096
- const WRITE_OPTIONS = [
5097
- { optionId: "allow", name: "Allow", kind: "allow_once" },
5098
- { optionId: "allow_always", name: "Allow edits for this session", kind: "allow_always" },
5099
- { optionId: "reject", name: "Reject", kind: "reject_once" }
5100
- ];
5101
- const absolutePath = (params) => {
5102
- const path2 = params.path;
5103
- if (typeof path2 !== "string" || !isAbsolute(path2) || path2.includes("\0")) throw new Error("ACP paths must be absolute");
5104
- return path2;
5286
+ const userHome = (deps) => deps.home ?? homedir();
5287
+ async function setListed(config, key, value2, listed) {
5288
+ const list2 = stringList(config.settings[key]).filter((item) => item !== value2);
5289
+ if (listed) list2.push(value2);
5290
+ await config.saveSettings({ [key]: list2.length ? list2 : void 0 });
5291
+ }
5292
+ const unknownRow = (rowId) => new Error(`Unknown setting ${rowId}`);
5293
+ const tilde = (path2, home) => path2 === home ? "~" : path2.startsWith(`${home}/`) ? `~${path2.slice(home.length)}` : path2;
5294
+ const firstLine$1 = (text, max = 80) => {
5295
+ const line = text.split("\n").map((part) => part.trim()).find((part) => part) ?? "";
5296
+ return line.length > max ? `${line.slice(0, max - 1)}…` : line;
5105
5297
  };
5106
- class LocalClient {
5107
- constructor(ask) {
5108
- this.ask = ask;
5109
- }
5110
- ask;
5111
- terminals = new Terminals();
5112
- trusted = /* @__PURE__ */ new Set();
5113
- async readTextFile(params) {
5114
- const text = await readFile$1(absolutePath(params), "utf8");
5115
- const start = (params.line ?? 1) - 1;
5116
- const content = params.line !== void 0 || params.limit !== void 0 ? text.split("\n").slice(start, params.limit ? start + params.limit : void 0).join("\n") : text;
5117
- if (Buffer.byteLength(content) > MAX_BYTES) throw new Error("File content exceeds 1 MiB; request a smaller line range");
5118
- return { content };
5298
+ const warningRows = (warnings) => warnings.map((warning, index) => ({ id: `warning:${index}`, section: "Warnings", label: warning }));
5299
+ const PERCENTS = [50, 60, 70, 80, 90];
5300
+ const MODE_OPTIONS = MODES.map(({ id: id2, name }) => ({ value: id2, name }));
5301
+ const ON_OFF = [{ value: "on", name: "On" }, { value: "off", name: "Off" }];
5302
+ const FLAGS = {
5303
+ "auto-compact": { key: "autoCompact", label: "Auto-compact", detail: "Summarize the conversation when the context window fills", on: true },
5304
+ "claude-instructions": { key: "claudeInstructions", label: "Load ~/.claude/CLAUDE.md", detail: "After the global AGENTS.md" },
5305
+ "claude-hooks": { key: "claudeHooks", label: "Claude hooks", detail: "~/.claude/settings.json and the project's .claude settings files" },
5306
+ "codex-hooks": { key: "codexHooks", label: "Codex hooks", detail: "~/.codex/hooks.json and the project's .codex/hooks.json" },
5307
+ "all-project-mcp": { key: "trustProjectMcp", label: "MCP servers in every project", detail: "Run .mcp.json servers without trusting each folder" }
5308
+ };
5309
+ const flagOn = (config, id2) => {
5310
+ const flag = FLAGS[id2];
5311
+ return flag.on ? config.settings[flag.key] !== false : config.settings[flag.key] === true;
5312
+ };
5313
+ const flagRow = (config, id2, section) => ({ id: id2, section, label: FLAGS[id2].label, detail: FLAGS[id2].detail, enabled: flagOn(config, id2) });
5314
+ async function setFlag(config, id2, enabled) {
5315
+ await config.saveSettings({ [FLAGS[id2].key]: enabled });
5316
+ return { message: `${FLAGS[id2].label} is ${enabled ? "on" : "off"}` };
5317
+ }
5318
+ function compactAt(config) {
5319
+ const value2 = config.settings.autoCompactAt;
5320
+ return typeof value2 === "number" && value2 >= 50 && value2 <= 95 ? String(value2) : "default";
5321
+ }
5322
+ function compactOptions(config) {
5323
+ const percents = [.../* @__PURE__ */ new Set([...PERCENTS, Number(compactAt(config)) || 50])].sort((a, b) => a - b);
5324
+ return [{ value: "default", name: "Default" }, ...percents.map((percent) => ({ value: String(percent), name: `${percent}%` }))];
5325
+ }
5326
+ async function setCompactAt(config, value2) {
5327
+ const percent = value2 === "default" ? void 0 : Number(value2.replace(/%$/, ""));
5328
+ if (percent !== void 0 && !(Number.isInteger(percent) && percent >= 50 && percent <= 95)) throw new Error("Compact at takes default or a percent from 50 to 95");
5329
+ await config.saveSettings({ autoCompactAt: percent });
5330
+ return { message: percent ? `Auto-compact starts at ${percent}% of the context window` : "Auto-compact uses the default threshold" };
5331
+ }
5332
+ const configPanel = {
5333
+ async panel({ config }, { cwd }) {
5334
+ const listed = stringList(config.settings.trustedProjects).includes(cwd);
5335
+ const trusted = await projectTrusted(config.paths.settings, cwd);
5336
+ const how = !trusted ? `Off for ${cwd}` : listed ? `Trusted: ${cwd}` : config.settings.trustProjectMcp === true ? "Trusted because MCP servers run in every project" : "Trusted through a parent folder";
5337
+ return { id: "config", title: "Config", rows: [
5338
+ {
5339
+ id: "mode",
5340
+ section: "Permissions",
5341
+ label: "Permission mode",
5342
+ detail: "Saved for new sessions and applied to this one",
5343
+ choice: { value: config.settings.mode ?? "ask", options: MODE_OPTIONS }
5344
+ },
5345
+ flagRow(config, "auto-compact", "Context"),
5346
+ { id: "compact-at", section: "Context", label: "Compact at", detail: "Share of the context window", choice: { value: compactAt(config), options: compactOptions(config) } },
5347
+ flagRow(config, "claude-instructions", "Instructions"),
5348
+ flagRow(config, "claude-hooks", "Hooks"),
5349
+ flagRow(config, "codex-hooks", "Hooks"),
5350
+ { id: "project-mcp", section: "MCP", label: "Project MCP servers for this folder", detail: how, enabled: trusted },
5351
+ flagRow(config, "all-project-mcp", "MCP")
5352
+ ] };
5353
+ },
5354
+ async toggle({ config }, rowId, enabled, { cwd }) {
5355
+ if (rowId in FLAGS) return setFlag(config, rowId, enabled);
5356
+ if (rowId !== "project-mcp") throw unknownRow(rowId);
5357
+ await setListed(config, "trustedProjects", cwd, enabled);
5358
+ const still = !enabled && await projectTrusted(config.paths.settings, cwd);
5359
+ return { message: enabled ? `MCP servers in ${cwd}/.mcp.json run from the next turn` : still ? `Removed ${cwd}; this folder stays trusted through another setting` : `MCP servers in ${cwd}/.mcp.json are off` };
5360
+ },
5361
+ async choose({ config }, rowId, value2) {
5362
+ if (rowId === "compact-at") return setCompactAt(config, value2);
5363
+ if (rowId !== "mode") throw unknownRow(rowId);
5364
+ const mode = MODES.find((item) => item.id === value2);
5365
+ if (!mode) throw new Error(`Unknown mode ${value2}. Use one of: ${MODES.map((item) => item.id).join(", ")}`);
5366
+ await config.saveSettings({ mode: mode.id });
5367
+ return { mode: mode.id, message: `Permission mode: ${mode.name}` };
5119
5368
  }
5120
- async writeTextFile(params) {
5121
- const path2 = absolutePath(params);
5122
- if (typeof params.content !== "string") throw new Error("Invalid content");
5123
- const sessionId = String(params.sessionId ?? "");
5124
- if (!params._meta?.oxbit?.mode && !this.trusted.has(sessionId)) {
5125
- const oldText = await readFile$1(path2, "utf8").catch(() => null);
5126
- const response = await this.ask({ sessionId, options: WRITE_OPTIONS, toolCall: {
5127
- toolCallId: params.toolCallId ?? `write-${Date.now()}`,
5128
- title: `Write ${path2}`,
5129
- kind: "edit",
5130
- status: "pending",
5131
- locations: [{ path: path2 }],
5132
- content: [{ type: "diff", path: path2, oldText, newText: params.content }]
5133
- } });
5134
- const choice = response?.outcome?.outcome === "selected" ? response.outcome.optionId : void 0;
5135
- if (choice === "allow_always") this.trusted.add(sessionId);
5136
- else if (choice !== "allow") throw new Error("The user rejected this write");
5369
+ };
5370
+ function compactConfigOptions(config) {
5371
+ return {
5372
+ configOptions: () => [
5373
+ { id: "auto-compact", name: "Auto-compact", type: "select", currentValue: flagOn(config, "auto-compact") ? "on" : "off", options: ON_OFF },
5374
+ { id: "compact-at", name: "Compact at", type: "select", currentValue: compactAt(config), options: compactOptions(config) }
5375
+ ],
5376
+ async setConfigOption(configId, value2) {
5377
+ if (configId === "auto-compact" && (value2 === "on" || value2 === "off")) await setFlag(config, "auto-compact", value2 === "on");
5378
+ else if (configId === "compact-at") await setCompactAt(config, value2);
5379
+ else return false;
5380
+ return true;
5137
5381
  }
5138
- await mkdir(dirname(path2), { recursive: true });
5139
- await writeFile$1(path2, params.content);
5140
- return {};
5141
- }
5142
- dispose() {
5143
- this.terminals.dispose();
5144
- }
5145
- }
5146
- function routeRequests(link, local) {
5147
- return (method, params) => {
5148
- const capabilities = link.capabilities();
5149
- if (method === "fs/read_text_file") return capabilities.fs?.readTextFile ? link.request(method, params) : local.readTextFile(params);
5150
- if (method === "fs/write_text_file") return capabilities.fs?.writeTextFile ? link.request(method, params) : local.writeTextFile(params);
5151
- if (method.startsWith("terminal/")) return capabilities.terminal ? link.request(method, params) : local.terminals.handle(method, params);
5152
- if (method === "oxbit/diagnostics" && !capabilities._meta?.oxbit?.diagnostics)
5153
- return Promise.reject(new Error("Language diagnostics are unavailable here. Run the project's type checker or linter with bash."));
5154
- return link.request(method, params);
5155
5382
  };
5156
5383
  }
5157
- function nodeHost(setup, link) {
5158
- const { root, config, provider: provider2, log } = setup;
5159
- const { runtime } = provider2;
5160
- const local = new LocalClient((params) => link.request("session/request_permission", params));
5161
- const mcp = nodeMcp(config, log);
5162
- let catalog;
5163
- const snapshots = snapshotStores(config.paths.snapshots, log);
5164
- const filesystems = /* @__PURE__ */ new Map();
5165
- const filesystemFor = (cwd) => {
5166
- let filesystem = filesystems.get(cwd);
5167
- if (!filesystem) filesystems.set(cwd, filesystem = nodeFilesystem(cwd, { ripgrep: setup.ripgrep }));
5168
- return filesystem;
5169
- };
5170
- const core = coreExtensions(config, (sessionId, update) => link.update(sessionId, update), { home: setup.userHome, log });
5171
- const extensions = {
5172
- sessions: new JsonlSessionStore(config.paths.sessions),
5173
- snapshots,
5174
- ...core,
5175
- ...setup.extensions,
5176
- context: [...core.context ?? [], ...setup.extensions?.context ?? []],
5177
- tools: [...core.tools ?? [], mcp, ...setup.extensions?.tools ?? []],
5178
- commands: [undoCommands(snapshots), ...core.commands ?? [], mcp, ...setup.extensions?.commands ?? []],
5179
- continuations: [...core.continuations ?? [], ...setup.extensions?.continuations ?? []],
5180
- hooks: mcp.hooks(setup.extensions?.hooks ?? core.hooks)
5181
- };
5182
- const host = {
5183
- models: () => {
5184
- if (setup.models) return setup.models;
5185
- if (!provider2.ready) throw missingKey(provider2);
5186
- return runtime.models();
5187
- },
5188
- endpoint: () => runtime.endpoint(),
5189
- ...setup.models ? {} : { catalog: () => catalog ??= runtime.catalog().catch((error) => {
5190
- catalog = void 0;
5191
- throw error;
5192
- }) },
5193
- model: () => provider2.model,
5194
- saveModel: (model) => {
5195
- provider2.model = model;
5196
- const providers = config.settings.providers ?? {};
5197
- config.saveSettings({ providers: { ...providers, [provider2.id]: { ...providers[provider2.id], model } } }).catch((error) => log?.(`Could not save the model: ${error.message}`));
5198
- },
5199
- get capabilities() {
5200
- return { fs: true, terminal: true, diagnostics: !!link.capabilities()._meta?.oxbit?.diagnostics, mcp: { http: true, sse: true } };
5201
- },
5202
- filesystem: filesystemFor(root),
5203
- filesystemFor,
5204
- root,
5205
- update: (sessionId, update) => link.update(sessionId, update),
5206
- request: routeRequests(link, local),
5207
- generateTitles: setup.generateTitles,
5208
- extensions
5209
- };
5210
- const dispose = async () => {
5211
- local.dispose();
5212
- await mcp.closeAll();
5213
- };
5214
- return { host, local, mcp, dispose };
5215
- }
5216
- const agentFactory = (setup) => (link) => {
5217
- const { host, dispose } = nodeHost(setup, link);
5218
- return { agent: new Agent(host), host, dispose };
5384
+ async function parsedHooks(deps, cwd) {
5385
+ const sources = await hookSources(deps.config, userHome(deps))(cwd);
5386
+ const off = sources.find((source) => source.config?.disableAllHooks === true);
5387
+ return { ...listHooks(sources), off: off?.path };
5388
+ }
5389
+ const hooksPanel = {
5390
+ async panel(deps, { cwd }) {
5391
+ const home = userHome(deps);
5392
+ const { hooks, warnings, off } = await parsedHooks(deps, cwd);
5393
+ const disabled = new Set(stringList(deps.config.settings.disabledHooks));
5394
+ const rows = [
5395
+ flagRow(deps.config, "claude-hooks", "Sources"),
5396
+ flagRow(deps.config, "codex-hooks", "Sources"),
5397
+ ...hooks.map((hook) => ({
5398
+ id: hook.id,
5399
+ section: "Hooks",
5400
+ label: `${hook.event} ${hook.matcher}`.trim(),
5401
+ detail: `${hook.command} · ${tilde(hook.path, home)}`,
5402
+ enabled: !disabled.has(hook.id)
5403
+ }))
5404
+ ];
5405
+ if (!hooks.length) rows.push({ id: "none", section: "Hooks", label: "No command hooks in the loaded files" });
5406
+ rows.push(...warningRows(warnings));
5407
+ return {
5408
+ id: "hooks",
5409
+ title: "Hooks",
5410
+ rows,
5411
+ footer: off ? `disableAllHooks in ${tilde(off, home)} turns every hook off.` : `Hooks come from ${tilde(deps.config.paths.settings, home)} and the project's .oxbit/settings.json.`
5412
+ };
5413
+ },
5414
+ async toggle(deps, rowId, enabled, { cwd }) {
5415
+ if (rowId === "claude-hooks" || rowId === "codex-hooks") return setFlag(deps.config, rowId, enabled);
5416
+ const hook = (await parsedHooks(deps, cwd)).hooks.find((item) => item.id === rowId);
5417
+ if (!hook && !stringList(deps.config.settings.disabledHooks).includes(rowId)) throw unknownRow(rowId);
5418
+ await setListed(deps.config, "disabledHooks", rowId, !enabled);
5419
+ return { message: `${hook ? `${hook.event} hook "${hook.command}"` : "The hook"} is ${enabled ? "on" : "off"}` };
5420
+ }
5421
+ };
5422
+ const SECTIONS = { global: "Global files", claude: "Global files", project: "Project files" };
5423
+ const instructionsPanel = {
5424
+ async panel(deps, { cwd }) {
5425
+ const home = userHome(deps);
5426
+ const append = deps.config.settings.appendSystemPrompt;
5427
+ const prompt = typeof append === "string" ? append : "";
5428
+ const disabled = new Set(stringList(deps.config.settings.disabledInstructions));
5429
+ const files = await instructionsFor(deps.config, home).candidates(cwd);
5430
+ const rows = [
5431
+ {
5432
+ id: "prompt",
5433
+ section: "Global prompt",
5434
+ label: "Global prompt",
5435
+ detail: firstLine$1(prompt) || "none",
5436
+ actions: [{ id: "edit", label: "Edit", inputs: [{ label: "Text added after the instruction files (empty clears it)", value: prompt }] }]
5437
+ },
5438
+ ...files.map((file2) => ({
5439
+ id: file2.path,
5440
+ section: SECTIONS[file2.scope],
5441
+ label: tilde(file2.path, home),
5442
+ ...file2.text === void 0 ? { detail: "not found" } : { detail: firstLine$1(file2.text), enabled: !disabled.has(file2.path) }
5443
+ }))
5444
+ ];
5445
+ return { id: "instructions", title: "Instructions", rows, footer: "Changes apply to the next turn's system prompt." };
5446
+ },
5447
+ async toggle(deps, rowId, enabled, { cwd }) {
5448
+ const files = await instructionsFor(deps.config, userHome(deps)).candidates(cwd);
5449
+ const known = files.some((file2) => file2.path === rowId && file2.text !== void 0) || stringList(deps.config.settings.disabledInstructions).includes(rowId);
5450
+ if (!known) throw unknownRow(rowId);
5451
+ await setListed(deps.config, "disabledInstructions", rowId, !enabled);
5452
+ return { message: `${tilde(rowId, userHome(deps))} is ${enabled ? "on" : "off"}` };
5453
+ },
5454
+ async act({ config }, rowId, actionId, inputs) {
5455
+ if (rowId !== "prompt" || actionId !== "edit") throw unknownRow(`${rowId} ${actionId}`);
5456
+ const text = inputs[0] ?? "";
5457
+ await config.saveSettings({ appendSystemPrompt: text.trim() ? text : void 0 });
5458
+ return { message: text.trim() ? "Saved the global prompt" : "Cleared the global prompt" };
5459
+ }
5460
+ };
5461
+ const NAME = /^[\w-]+$/;
5462
+ const isObject = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
5463
+ function splitCommandLine(line) {
5464
+ const words = [];
5465
+ let word = "", quote2 = "", started = false;
5466
+ for (let index = 0; index < line.length; index++) {
5467
+ const char = line[index];
5468
+ if (quote2) {
5469
+ if (char === quote2) quote2 = "";
5470
+ else if (char === "\\" && quote2 === '"' && index + 1 < line.length) word += line[++index];
5471
+ else word += char;
5472
+ } else if (char === '"' || char === "'") [quote2, started] = [char, true];
5473
+ else if (char === "\\" && index + 1 < line.length) [word, started] = [word + line[++index], true];
5474
+ else if (/\s/.test(char)) {
5475
+ if (started) words.push(word);
5476
+ [word, started] = ["", false];
5477
+ } else [word, started] = [word + char, true];
5478
+ }
5479
+ if (quote2) throw new Error("The command line has an unclosed quote");
5480
+ if (started) words.push(word);
5481
+ return words;
5482
+ }
5483
+ function serverEntry(input2) {
5484
+ const text = input2.trim();
5485
+ if (/^https?:\/\//i.test(text)) {
5486
+ if (!URL.canParse(text)) throw new Error(`Invalid URL: ${text}`);
5487
+ return { type: "http", url: text };
5488
+ }
5489
+ const [command, ...args] = splitCommandLine(text);
5490
+ if (!command) throw new Error("Enter a URL or a command line");
5491
+ return { command, args };
5492
+ }
5493
+ const describe = (entry) => {
5494
+ if (!isObject(entry)) return "invalid entry";
5495
+ const off = entry.enabled === false || entry.disabled === true ? ", off in the file" : "";
5496
+ if (typeof entry.url === "string") return `${typeof entry.type === "string" ? entry.type : "http"} ${entry.url}${off}`;
5497
+ const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : [];
5498
+ return `stdio ${[entry.command, ...args].join(" ")}${off}`;
5499
+ };
5500
+ async function servers(path2) {
5501
+ try {
5502
+ const body = await readJson(path2, {});
5503
+ const entries = isObject(body) && isObject(body.mcpServers) ? body.mcpServers : {};
5504
+ return { body: isObject(body) ? body : {}, entries };
5505
+ } catch (error) {
5506
+ return { body: {}, entries: {}, error: error.message };
5507
+ }
5508
+ }
5509
+ async function writeServers(config, change) {
5510
+ const { body, entries, error } = await servers(config.paths.mcp);
5511
+ if (error) throw new Error(error);
5512
+ const next = { ...entries };
5513
+ change(next);
5514
+ await writeJson(config.paths.mcp, { ...body, mcpServers: next }, 384);
5515
+ }
5516
+ const mcpPanel = {
5517
+ async panel(deps, { cwd }) {
5518
+ const { config } = deps;
5519
+ const home = userHome(deps);
5520
+ const disabled = new Set(stringList(config.settings.disabledMcpServers));
5521
+ const user = await servers(config.paths.mcp);
5522
+ const projectPath = join(cwd, ".mcp.json");
5523
+ const project = await servers(projectPath);
5524
+ const trust = await projectTrusted(config.paths.settings, cwd) ? "this folder is trusted" : "this folder is not trusted; turn on Project MCP servers in /config";
5525
+ const rows = [
5526
+ ...Object.entries(user.entries).map(([name, entry]) => ({
5527
+ id: `user:${name}`,
5528
+ section: `User · ${tilde(config.paths.mcp, home)}`,
5529
+ label: name,
5530
+ detail: describe(entry),
5531
+ enabled: !disabled.has(name),
5532
+ actions: [{ id: "remove", label: "Remove" }]
5533
+ })),
5534
+ ...Object.entries(project.entries).map(([name, entry]) => ({
5535
+ id: `project:${name}`,
5536
+ section: `Project · ${tilde(projectPath, home)}`,
5537
+ label: name,
5538
+ detail: `${describe(entry)} · ${trust}`,
5539
+ enabled: !disabled.has(name)
5540
+ })),
5541
+ { id: "add", section: "Add", label: "Add server", detail: `Saved in ${tilde(config.paths.mcp, home)}`, actions: [{
5542
+ id: "add",
5543
+ label: "Add server",
5544
+ inputs: [{ label: "Name", placeholder: "github" }, { label: "URL or command line", placeholder: "https://example.com/mcp or npx -y @scope/server" }]
5545
+ }] },
5546
+ ...[user.error, project.error].filter((error) => !!error).map((error, index) => ({ id: `warning:${index}`, section: "Warnings", label: error }))
5547
+ ];
5548
+ return { id: "mcp", title: "MCP servers", rows, footer: "Changes apply on the next turn." };
5549
+ },
5550
+ async toggle({ config }, rowId, enabled) {
5551
+ const name = /^(?:user|project):(.+)$/.exec(rowId)?.[1];
5552
+ if (!name) throw unknownRow(rowId);
5553
+ await setListed(config, "disabledMcpServers", name, !enabled);
5554
+ return { message: `${name} is ${enabled ? "on" : "off"} from the next turn` };
5555
+ },
5556
+ async act({ config }, rowId, actionId, inputs) {
5557
+ if (rowId === "add" && actionId === "add") {
5558
+ const name2 = inputs[0]?.trim() ?? "";
5559
+ if (!NAME.test(name2)) throw new Error("Use a server name with letters, digits, - and _");
5560
+ const entry = serverEntry(inputs[1] ?? "");
5561
+ await writeServers(config, (entries) => {
5562
+ if (name2 in entries) throw new Error(`${config.paths.mcp} already has a server named ${name2}`);
5563
+ entries[name2] = entry;
5564
+ });
5565
+ return { message: `Added ${name2}; it connects on the next turn` };
5566
+ }
5567
+ const name = rowId.startsWith("user:") ? rowId.slice("user:".length) : void 0;
5568
+ if (!name || actionId !== "remove") throw unknownRow(`${rowId} ${actionId}`);
5569
+ await writeServers(config, (entries) => {
5570
+ if (!(name in entries)) throw new Error(`${config.paths.mcp} has no server named ${name}`);
5571
+ delete entries[name];
5572
+ });
5573
+ return { message: `Removed ${name}` };
5574
+ }
5219
5575
  };
5220
5576
  const nodeLoopback = {
5221
5577
  listen: (port, path2) => new Promise((resolve2, reject) => {
@@ -5230,10 +5586,10 @@ const nodeLoopback = {
5230
5586
  let answered = false;
5231
5587
  const request = {
5232
5588
  query: Object.fromEntries(url.searchParams),
5233
- respond(status, html) {
5589
+ respond(status2, html) {
5234
5590
  if (answered) return;
5235
5591
  answered = true;
5236
- res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(html);
5592
+ res.writeHead(status2, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(html);
5237
5593
  }
5238
5594
  };
5239
5595
  const [waiter] = waiting;
@@ -5275,7 +5631,51 @@ const nodeLoopback = {
5275
5631
  class UsageError extends Error {
5276
5632
  }
5277
5633
  const SIGN_IN_TIMEOUT_MS = 10 * 6e4;
5278
- const fetchOf = (io) => io.fetch ?? ((url, init) => fetch(url, init));
5634
+ const fetchOr = (custom) => custom ?? ((url, init) => fetch(url, init));
5635
+ async function apiKeyChoice(input2, id2) {
5636
+ const spec = providerSpec(id2);
5637
+ if (!spec) throw new UsageError(`Unknown provider ${id2}`);
5638
+ if (!spec.methods.some((method) => method.id === "api-key")) throw new UsageError(`${spec.name} does not use an API key`);
5639
+ return selectProvider({ ...input2, provider: spec.id, method: "api-key" });
5640
+ }
5641
+ async function saveApiKey(config, choice, key) {
5642
+ await choice.runtime.models().keys.save(choice.runtime.endpoint(), key);
5643
+ const own = config.settings.providers?.[choice.id];
5644
+ if (own?.method && own.method !== "api-key")
5645
+ await config.saveSettings({ providers: { ...config.settings.providers, [choice.id]: { ...own, method: "api-key" } } });
5646
+ }
5647
+ async function removeApiKeys(auth, input2, id2) {
5648
+ const removed = [];
5649
+ const drop = async (slot) => {
5650
+ if (!await auth.get(slot)) return;
5651
+ await auth.delete(slot);
5652
+ removed.push(slot.slice("apikey:".length));
5653
+ };
5654
+ const spec = id2 === void 0 ? void 0 : providerSpec(id2);
5655
+ if (id2 !== void 0 && !spec) throw new UsageError(`Unknown provider ${id2}`);
5656
+ const base = spec?.id === "openai-compatible" ? input2.settings.providers?.[spec.id]?.baseUrl || input2.env.OXBIT_AGENT_BASE_URL : void 0;
5657
+ if (spec && spec.id !== "openai-compatible") await drop(apiKeySlot(spec.id));
5658
+ else if (base) await drop(apiKeySlot("openai-compatible", normalizeBase(base)));
5659
+ else for (const slot of await auth.keys()) if (slot.startsWith(spec ? `apikey:${spec.id} ` : "apikey:")) await drop(slot);
5660
+ return removed;
5661
+ }
5662
+ async function signOutChatGPT(auth, fetch2) {
5663
+ if (!await chatgptSignedIn(auth)) return void 0;
5664
+ return (await new ChatGPTAuth({ fetch: fetch2, store: auth }).signOut()).revoked;
5665
+ }
5666
+ async function signInChatGPT({ config, auth, fetch: fetch2, signal, open: open2, paste }) {
5667
+ const chatgpt = new ChatGPTAuth({ fetch: fetch2, store: auth });
5668
+ const record = await chatgpt.record();
5669
+ const account = await chatgpt.signIn({ loopback: nodeLoopback, account: record.active, paste, signal, open: (address) => {
5670
+ const url = new URL(address);
5671
+ url.searchParams.delete("id_token_hint");
5672
+ return open2(url.toString());
5673
+ } });
5674
+ if (!planEnabled(account)) return { account, enabled: false };
5675
+ const providers = config.settings.providers ?? {};
5676
+ await config.saveSettings({ ...config.settings.provider ? {} : { provider: "openai" }, providers: { ...providers, openai: { ...providers.openai, method: "chatgpt" } } });
5677
+ return { account, enabled: true };
5678
+ }
5279
5679
  async function readAll(stream) {
5280
5680
  let text = "";
5281
5681
  for await (const chunk of stream) text += chunk;
@@ -5345,48 +5745,39 @@ async function login({ config, auth, io, input: input2 }, rest) {
5345
5745
  if (!input2.provider) return void io.stdout.write(methodList()), 0;
5346
5746
  const spec = providerSpec(input2.provider);
5347
5747
  if (!spec) throw new UsageError(`Unknown provider ${input2.provider}`);
5348
- if (!spec.methods.some((method) => method.id === "api-key")) throw new UsageError(`${spec.name} does not use an API key`);
5349
- const choice = await selectProvider({ ...input2, method: "api-key" });
5748
+ const choice = await apiKeyChoice(input2, spec.id);
5350
5749
  const key = (await readSecret(io, `${spec.methods.find((method) => method.id === "api-key").label}: `)).trim();
5351
5750
  if (!key) throw new UsageError("No API key on stdin");
5352
- await choice.runtime.models().keys.save(choice.runtime.endpoint(), key);
5353
- const own = config.settings.providers?.[spec.id];
5354
- if (own?.method && own.method !== "api-key")
5355
- await config.saveSettings({ providers: { ...config.settings.providers, [spec.id]: { ...own, method: "api-key" } } });
5751
+ await saveApiKey(config, choice, key);
5356
5752
  io.stdout.write(`Saved the ${spec.name} API key in ${auth.path}
5357
5753
  `);
5358
5754
  return 0;
5359
5755
  }
5360
5756
  async function loginChatGPT({ config, auth, io }) {
5361
- const chatgpt = new ChatGPTAuth({ fetch: fetchOf(io), store: auth });
5362
- const record = await chatgpt.record();
5363
5757
  let noBrowser;
5364
5758
  const failed2 = new Promise((resolve2) => noBrowser = resolve2);
5365
5759
  io.stderr.write(`${CHATGPT_LABELS.signIn}
5366
5760
  `);
5367
- const account = await chatgpt.signIn({
5368
- loopback: nodeLoopback,
5369
- account: record.active,
5370
- open: async (address) => {
5371
- const url = new URL(address);
5372
- url.searchParams.delete("id_token_hint");
5761
+ const { account, enabled } = await signInChatGPT({
5762
+ config,
5763
+ auth,
5764
+ fetch: fetchOr(io.fetch),
5765
+ open: async (url) => {
5373
5766
  io.stderr.write(`Open this page to sign in:
5374
5767
  ${url}
5375
5768
  `);
5376
- if (await (io.open ?? ((target) => openBrowser(target, io.env)))(url.toString())) return;
5377
- io.stderr.write(`No browser opened. After you sign in, paste the address the browser lands on (it starts with ${url.searchParams.get("redirect_uri")}):
5769
+ if (await (io.open ?? ((target) => openBrowser(target, io.env)))(url)) return;
5770
+ io.stderr.write(`No browser opened. After you sign in, paste the address the browser lands on (it starts with ${new URL(url).searchParams.get("redirect_uri")}):
5378
5771
  `);
5379
5772
  noBrowser();
5380
5773
  },
5381
5774
  paste: (signal) => failed2.then(() => pastedLine(io.stdin, signal)),
5382
5775
  signal: AbortSignal.timeout(SIGN_IN_TIMEOUT_MS)
5383
5776
  });
5384
- if (!planEnabled(account)) {
5777
+ if (!enabled) {
5385
5778
  io.stderr.write("You are signed in, but ChatGPT plan use was not allowed. Run oxbit-agent login chatgpt again and allow it, or use an OpenAI API key.\n");
5386
5779
  return 1;
5387
5780
  }
5388
- const providers = config.settings.providers ?? {};
5389
- await config.saveSettings({ ...config.settings.provider ? {} : { provider: "openai" }, providers: { ...providers, openai: { ...providers.openai, method: "chatgpt" } } });
5390
5781
  io.stdout.write(`${CHATGPT_LABELS.welcome}${account.email ? ` (${account.email})` : ""}.
5391
5782
  ${CHATGPT_LABELS.manage}: ${CHATGPT.usageUrl}
5392
5783
  `);
@@ -5397,28 +5788,10 @@ ${CHATGPT_LABELS.manage}: ${CHATGPT.usageUrl}
5397
5788
  async function logout({ auth, io, input: input2 }, rest) {
5398
5789
  if (rest.length > 1) throw new UsageError(`Unexpected argument ${rest[1]}`);
5399
5790
  const target = rest[0] ?? input2.provider;
5400
- const removed = [];
5401
- const signOut = async () => {
5402
- if (!await chatgptSignedIn(auth)) return;
5403
- const { revoked } = await new ChatGPTAuth({ fetch: fetchOf(io), store: auth }).signOut();
5404
- removed.push(revoked ? "ChatGPT" : "ChatGPT (signed out here; OpenAI did not confirm the revocation)");
5405
- };
5406
- const drop = async (slot) => {
5407
- if (!await auth.get(slot)) return;
5408
- await auth.delete(slot);
5409
- removed.push(slot.slice("apikey:".length));
5410
- };
5411
- if (target === "chatgpt") await signOut();
5412
- else if (target) {
5413
- const spec = providerSpec(target);
5414
- if (!spec) throw new UsageError(`Unknown provider ${target}`);
5415
- const base = spec.id === "openai-compatible" ? input2.settings.providers?.[spec.id]?.baseUrl || input2.env.OXBIT_AGENT_BASE_URL : void 0;
5416
- if (spec.id !== "openai-compatible") await drop(apiKeySlot(spec.id));
5417
- else if (base) await drop(apiKeySlot(spec.id, normalizeBase(base)));
5418
- else for (const slot of await auth.keys()) if (slot.startsWith(`apikey:${spec.id} `)) await drop(slot);
5419
- } else {
5420
- for (const slot of await auth.keys()) if (slot.startsWith("apikey:")) await drop(slot);
5421
- await signOut();
5791
+ const removed = target === "chatgpt" ? [] : await removeApiKeys(auth, input2, target);
5792
+ if (!target || target === "chatgpt") {
5793
+ const revoked = await signOutChatGPT(auth, fetchOr(io.fetch));
5794
+ if (revoked !== void 0) removed.push(revoked ? "ChatGPT" : "ChatGPT (signed out here; OpenAI did not confirm the revocation)");
5422
5795
  }
5423
5796
  io.stdout.write(removed.length ? `Removed ${removed.join(", ")}
5424
5797
  ` : "Nothing to remove\n");
@@ -5438,6 +5811,509 @@ async function models({ io, input: input2 }) {
5438
5811
  }
5439
5812
  return 0;
5440
5813
  }
5814
+ const OLLAMA = "http://127.0.0.1:11434";
5815
+ const input = (deps) => ({ settings: deps.config.settings, env: deps.env, auth: deps.auth, fetch: deps.fetch });
5816
+ const has = (spec, method) => spec.methods.some((item) => item.id === method);
5817
+ function baseOf(spec, deps) {
5818
+ const own = deps.config.settings.providers?.[spec.id]?.baseUrl;
5819
+ const base = spec.id === "openai-compatible" ? own || deps.env.OXBIT_AGENT_BASE_URL : spec.id === "ollama" ? own || deps.env.OLLAMA_HOST || OLLAMA : own;
5820
+ try {
5821
+ return base ? normalizeBase(base) : void 0;
5822
+ } catch {
5823
+ return void 0;
5824
+ }
5825
+ }
5826
+ async function status(spec, deps, signedIn) {
5827
+ const parts = [];
5828
+ const current = deps.provider();
5829
+ if (current.id === spec.id) parts.push(current.runtime.method === "chatgpt" ? "in use with your ChatGPT plan" : "in use");
5830
+ if (spec.id === "openai" && signedIn) parts.push("signed in with ChatGPT");
5831
+ const base = baseOf(spec, deps);
5832
+ let saved = false;
5833
+ if (spec.id === "openai-compatible" && !base) parts.push("no base URL");
5834
+ else if (has(spec, "api-key")) {
5835
+ saved = !!await deps.auth.get(apiKeySlot(spec.id, base));
5836
+ const env = spec.env && deps.env[spec.env] ? spec.env : void 0;
5837
+ parts.push(saved ? "API key saved" : env ? `key from ${env}` : has(spec, "none") ? "no key needed" : "no key");
5838
+ } else parts.push("no key needed");
5839
+ if (base && spec.id !== "openrouter") parts.push(base);
5840
+ if (spec.id === "anthropic") parts.push("Claude Console API key");
5841
+ if (stringList(deps.config.settings.disabledProviders).includes(spec.id)) parts.push("disabled");
5842
+ return { text: parts.join(", "), saved };
5843
+ }
5844
+ async function apply(deps, id2, message2) {
5845
+ if (deps.rebuild) {
5846
+ await deps.rebuild(id2);
5847
+ return { message: message2, reloaded: true };
5848
+ }
5849
+ return { message: `${message2}. It applies after the agent restarts.` };
5850
+ }
5851
+ async function use(deps, spec) {
5852
+ const { config } = deps;
5853
+ await selectProvider({ ...input(deps), provider: spec.id });
5854
+ const previous = config.settings.provider ?? deps.provider().id;
5855
+ const providers = { ...config.settings.providers };
5856
+ if (config.settings.model && !providers[previous]?.model) providers[previous] = { ...providers[previous], model: config.settings.model };
5857
+ await config.saveSettings({ provider: spec.id, model: void 0, providers });
5858
+ return apply(deps, spec.id, `Using ${spec.name}`);
5859
+ }
5860
+ async function signIn(deps, context) {
5861
+ const signal = AbortSignal.any([context.signal, AbortSignal.timeout(SIGN_IN_TIMEOUT_MS)]);
5862
+ const { account, enabled } = await signInChatGPT({ config: deps.config, auth: deps.auth, fetch: fetchOr(deps.fetch), signal, open: async (url) => {
5863
+ context.progress(`Open this page to sign in: ${url}`);
5864
+ if (!await (deps.open ?? ((target) => openBrowser(target, deps.env)))(url)) context.progress(`No browser opened. Open this page to sign in: ${url}`);
5865
+ } });
5866
+ if (!enabled) throw new Error("You are signed in, but ChatGPT plan use was not allowed. Sign in again and allow it, or use an OpenAI API key.");
5867
+ return `${CHATGPT_LABELS.welcome}${account.email ? ` (${account.email})` : ""}`;
5868
+ }
5869
+ const providersPanel = {
5870
+ async panel(deps) {
5871
+ const signedIn = await chatgptSignedIn(deps.auth);
5872
+ const disabled = stringList(deps.config.settings.disabledProviders);
5873
+ const rows = await Promise.all(PROVIDERS.map(async (spec) => {
5874
+ const inUse = deps.provider().id === spec.id;
5875
+ const { text, saved } = await status(spec, deps, signedIn);
5876
+ const reloads = inUse || void 0;
5877
+ const actions = inUse ? [] : [{ id: "use", label: "Use this provider", reloads: true }];
5878
+ if (has(spec, "api-key")) actions.push({ id: "key", label: "Set API key", inputs: [{ label: spec.methods.find((item) => item.id === "api-key").label, secret: true }], reloads });
5879
+ if (saved) actions.push({ id: "remove-key", label: "Remove API key", reloads });
5880
+ if (has(spec, "chatgpt")) actions.push(signedIn ? { id: "signout", label: "Sign out of ChatGPT", reloads } : { id: "chatgpt", label: "Sign in with ChatGPT", reloads });
5881
+ if (spec.id === "openai-compatible") actions.push({
5882
+ id: "base-url",
5883
+ label: "Base URL",
5884
+ reloads,
5885
+ inputs: [{ label: "Base URL (empty clears it)", placeholder: "https://llm.example.com", value: deps.config.settings.providers?.[spec.id]?.baseUrl ?? "" }]
5886
+ });
5887
+ return { id: spec.id, section: "Providers", label: spec.name, detail: text, enabled: !disabled.includes(spec.id), actions };
5888
+ }));
5889
+ return { id: "providers", title: "Providers", rows, footer: "Unchecked providers are skipped when settings name no provider; --provider and Use still pick them." };
5890
+ },
5891
+ async toggle({ config }, rowId, enabled) {
5892
+ const spec = providerSpec(rowId);
5893
+ if (!spec) throw unknownRow(rowId);
5894
+ await setListed(config, "disabledProviders", spec.id, !enabled);
5895
+ return { message: `${spec.name} is ${enabled ? "on" : "off"} for automatic selection` };
5896
+ },
5897
+ async act(deps, rowId, actionId, inputs, context) {
5898
+ const spec = providerSpec(rowId);
5899
+ if (!spec) throw unknownRow(rowId);
5900
+ const inUse = deps.provider().id === spec.id;
5901
+ const done = (message2) => inUse ? apply(deps, spec.id, message2) : Promise.resolve({ message: message2 });
5902
+ switch (actionId) {
5903
+ case "use":
5904
+ return use(deps, spec);
5905
+ case "key": {
5906
+ const key = inputs[0]?.trim();
5907
+ if (!key) throw new Error("The API key is empty");
5908
+ await saveApiKey(deps.config, await apiKeyChoice(input(deps), spec.id), key);
5909
+ return done(`Saved the ${spec.name} API key in ${deps.auth.path}`);
5910
+ }
5911
+ case "remove-key": {
5912
+ const removed = await removeApiKeys(deps.auth, input(deps), spec.id);
5913
+ return done(removed.length ? `Removed the ${spec.name} API key` : "No saved key to remove");
5914
+ }
5915
+ case "chatgpt": {
5916
+ if (spec.id !== "openai") break;
5917
+ return done(await signIn(deps, context));
5918
+ }
5919
+ case "signout": {
5920
+ const revoked = await signOutChatGPT(deps.auth, fetchOr(deps.fetch));
5921
+ if (revoked === void 0) return { message: "Not signed in with ChatGPT" };
5922
+ return done(revoked ? "Signed out of ChatGPT" : "Signed out here; OpenAI did not confirm the revocation");
5923
+ }
5924
+ case "base-url": {
5925
+ if (spec.id !== "openai-compatible") break;
5926
+ const value2 = inputs[0]?.trim();
5927
+ const baseUrl = value2 ? normalizeBase(value2) : void 0;
5928
+ const providers = deps.config.settings.providers ?? {};
5929
+ await deps.config.saveSettings({ providers: { ...providers, [spec.id]: { ...providers[spec.id], baseUrl } } });
5930
+ return done(baseUrl ? `Base URL: ${baseUrl}` : "Cleared the base URL");
5931
+ }
5932
+ }
5933
+ throw unknownRow(`${rowId} ${actionId}`);
5934
+ }
5935
+ };
5936
+ const exists = (path2) => stat(path2).then(() => true, () => false);
5937
+ const skillName = (rowId) => rowId.startsWith("skill:") ? rowId.slice("skill:".length) : void 0;
5938
+ const expandPath = (input2, cwd, home) => resolve(cwd, input2 === "~" ? home : input2.startsWith("~/") ? join(home, input2.slice(2)) : input2);
5939
+ async function newSkill({ config }, name, description) {
5940
+ const summary2 = description.replace(/\s+/g, " ").trim();
5941
+ if (!isSkillName(name)) throw new Error("Use a skill name with a-z, 0-9 and single hyphens, 64 characters at most");
5942
+ if (!summary2) throw new Error("Add a description: what the skill does and when to use it");
5943
+ if (summary2.length > 1024) throw new Error("The description is longer than 1024 characters");
5944
+ const dir = join(config.paths.skills, name);
5945
+ if (await exists(dir)) throw new Error(`${dir} already exists`);
5946
+ await mkdir(dir, { recursive: true });
5947
+ const path2 = join(dir, "SKILL.md");
5948
+ await writeFile$1(path2, `---
5949
+ name: ${name}
5950
+ description: ${JSON.stringify(summary2)}
5951
+ ---
5952
+
5953
+ # ${name}
5954
+
5955
+ Write the steps for this skill here.
5956
+ `, { flag: "wx" });
5957
+ return { message: `Created ${path2}` };
5958
+ }
5959
+ async function importSkill(deps, input2, cwd) {
5960
+ if (!input2.trim()) throw new Error("Enter the path of a folder that contains SKILL.md");
5961
+ const source = expandPath(input2.trim(), cwd, userHome(deps));
5962
+ const text = await readOptional(join(source, "SKILL.md"));
5963
+ if (text === void 0) throw new Error(`${source} has no SKILL.md`);
5964
+ const description = parseFrontmatter(text).data.description;
5965
+ if (typeof description !== "string" || !description.trim()) throw new Error(`${join(source, "SKILL.md")} has no description in its frontmatter`);
5966
+ const target = join(deps.config.paths.skills, basename(source));
5967
+ if (await exists(target)) throw new Error(`${target} already exists`);
5968
+ await mkdir(deps.config.paths.skills, { recursive: true });
5969
+ await cp(source, target, { recursive: true, errorOnExist: true, force: false });
5970
+ return { message: `Imported ${basename(source)} into ${target}` };
5971
+ }
5972
+ const skillsPanel = {
5973
+ async panel(deps, { cwd }) {
5974
+ const home = userHome(deps);
5975
+ const { skills, warnings } = await skillsFor(deps.config, home).discover(cwd);
5976
+ const hidden = new Set(stringList(deps.config.settings.disabledSkills));
5977
+ const rows = skills.map((skill) => {
5978
+ const enabled = !hidden.has(skill.name);
5979
+ return {
5980
+ id: `skill:${skill.name}`,
5981
+ section: "Skills",
5982
+ label: skill.name,
5983
+ detail: firstLine$1(skill.description, 120) || tilde(skill.path, home),
5984
+ enabled,
5985
+ ...enabled && skill.user ? { actions: [{ id: "use", label: `Use /${skill.name}` }] } : {}
5986
+ };
5987
+ });
5988
+ if (!rows.length) rows.push({ id: "none", section: "Skills", label: "No skills found" });
5989
+ const folder = tilde(deps.config.paths.skills, home);
5990
+ rows.push(
5991
+ { id: "new", section: "Add", label: "New skill", detail: `Creates ${folder}/<name>/SKILL.md`, actions: [{
5992
+ id: "new",
5993
+ label: "New skill",
5994
+ inputs: [{ label: "Name", placeholder: "release-notes" }, { label: "Description", placeholder: "What the skill does and when to use it" }]
5995
+ }] },
5996
+ { id: "import", section: "Add", label: "Import a skill folder", detail: `Copies a folder with SKILL.md into ${folder}`, actions: [{
5997
+ id: "import",
5998
+ label: "Import",
5999
+ inputs: [{ label: "Folder with SKILL.md", placeholder: "~/Downloads/review" }]
6000
+ }] },
6001
+ ...warningRows(warnings)
6002
+ );
6003
+ return { id: "skills", title: "Skills", rows, footer: "Unchecked skills leave the catalog, the skill tool and the slash commands." };
6004
+ },
6005
+ async toggle({ config }, rowId, enabled) {
6006
+ const name = skillName(rowId);
6007
+ if (!name) throw unknownRow(rowId);
6008
+ await setListed(config, "disabledSkills", name, !enabled);
6009
+ return { message: `${name} is ${enabled ? "on" : "off"}` };
6010
+ },
6011
+ async act(deps, rowId, actionId, inputs, { cwd }) {
6012
+ if (rowId === "new" && actionId === "new") return newSkill(deps, inputs[0]?.trim() ?? "", inputs[1] ?? "");
6013
+ if (rowId === "import" && actionId === "import") return importSkill(deps, inputs[0] ?? "", cwd);
6014
+ const name = skillName(rowId);
6015
+ if (name && actionId === "use") return { draft: `/${name} ` };
6016
+ throw unknownRow(`${rowId} ${actionId}`);
6017
+ }
6018
+ };
6019
+ const HELP = {
6020
+ config: { description: "Show or change agent settings", hint: "<setting> <value>", usage: [
6021
+ "`/config <setting> <value>`, for example `/config auto-compact off`, `/config compact-at 80` or `/config mode full-auto`"
6022
+ ] },
6023
+ skills: { description: "List skills; turn one on or off, create one or import a folder", hint: "enable|disable|new|import ...", usage: [
6024
+ "`/skills enable <name|number>`, `/skills disable <name|number>`",
6025
+ "`/skills new <name> <description>`",
6026
+ "`/skills import <folder with SKILL.md>`"
6027
+ ] },
6028
+ hooks: { description: "List command hooks and turn them on or off", hint: "enable|disable <number>", usage: [
6029
+ "`/hooks enable <number>`, `/hooks disable <number>`",
6030
+ "`/config claude-hooks on`, `/config codex-hooks on` load Claude Code and Codex hook files"
6031
+ ] },
6032
+ mcp: { description: "Show MCP servers; add, remove, turn on or off, or reconnect them", hint: "add|remove|enable|disable|reload ...", usage: [
6033
+ "`/mcp add <name> <url or command line>`, `/mcp remove <name>`",
6034
+ "`/mcp enable <name>`, `/mcp disable <name>`",
6035
+ "`/mcp reload` reconnects the servers"
6036
+ ] },
6037
+ instructions: { description: "List instruction files and the global prompt", hint: "enable|disable|prompt ...", usage: [
6038
+ "`/instructions enable <number|path>`, `/instructions disable <number|path>`",
6039
+ "`/instructions prompt <text>` sets the global prompt; without text it clears it"
6040
+ ] },
6041
+ providers: { description: "List model providers; pick one, or sign in or out of ChatGPT", hint: "use|enable|disable|login|signout ...", usage: [
6042
+ "`/providers use <id>` saves the provider for the next start",
6043
+ "`/providers enable <id>`, `/providers disable <id>` for automatic selection",
6044
+ "`/providers login chatgpt`, `/providers signout`",
6045
+ "API keys: run `oxbit-agent login --provider <id>` in a terminal, or use /providers in the oxbit-agent terminal UI"
6046
+ ] }
6047
+ };
6048
+ const ON = /* @__PURE__ */ new Set(["on", "true", "yes", "enable", "enabled", "1"]);
6049
+ const OFF = /* @__PURE__ */ new Set(["off", "false", "no", "disable", "disabled", "0"]);
6050
+ const split = (text) => {
6051
+ const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(text.trim());
6052
+ return match ? [match[1], match[2]?.trim() ?? ""] : ["", ""];
6053
+ };
6054
+ function rowText(row, keys) {
6055
+ const box = row.enabled === void 0 ? "" : row.enabled ? "[x] " : "[ ] ";
6056
+ const choice = row.choice ? `: ${row.choice.value} (${row.choice.options.map((option) => option.value).join(", ")})` : "";
6057
+ return `${box}${row.label}${keys ? ` \`${row.id}\`` : ""}${choice}${row.detail ? ` · ${row.detail}` : ""}`;
6058
+ }
6059
+ function panelText(panel, usage, keys = false) {
6060
+ const lines2 = [`## ${panel.title}`];
6061
+ let section;
6062
+ panel.rows.forEach((row, index) => {
6063
+ if (row.section !== section && (section = row.section)) lines2.push("", `**${section}**`);
6064
+ lines2.push(`${index + 1}. ${rowText(row, keys)}`);
6065
+ });
6066
+ if (panel.footer) lines2.push("", panel.footer);
6067
+ lines2.push("", "**Usage**", ...usage.map((line) => `- ${line}`));
6068
+ return lines2.join("\n");
6069
+ }
6070
+ function findRow(panel, key, cwd) {
6071
+ const rows = panel.rows;
6072
+ const found = /^\d+$/.test(key) ? rows[Number(key) - 1] : rows.find((row) => row.id === key) ?? rows.find((row) => row.id.endsWith(`:${key}`)) ?? rows.find((row) => row.label.toLowerCase() === key.toLowerCase()) ?? rows.find((row) => row.id === resolve(cwd, key));
6073
+ if (!found) throw new Error(`No ${key} in ${panel.title}. Run /${panel.id} to list them.`);
6074
+ return found;
6075
+ }
6076
+ const switchOf = (value2) => {
6077
+ const word = value2.toLowerCase();
6078
+ if (ON.has(word)) return true;
6079
+ if (OFF.has(word)) return false;
6080
+ throw new Error(`Use on or off, not ${value2 || "nothing"}`);
6081
+ };
6082
+ function settingsCommands(manager, mcp) {
6083
+ const say = (context, text) => context.update({ sessionUpdate: "agent_message_chunk", content: { type: "text", text } });
6084
+ const panelContext = (context) => ({ cwd: context.session.cwd, sessionId: context.session.id, signal: context.signal, progress: (text) => say(context, `${text}
6085
+
6086
+ `) });
6087
+ const applied = (change, context) => {
6088
+ if (change.mode) {
6089
+ context.session.meta.mode = change.mode;
6090
+ context.update({ sessionUpdate: "current_mode_update", currentModeId: change.mode });
6091
+ }
6092
+ return [change.message, change.draft && `Type ${change.draft.trim()} to use it.`].filter((part) => !!part).join("\n") || "Done";
6093
+ };
6094
+ const status2 = async (context, reload) => {
6095
+ if (!mcp) return "";
6096
+ if (reload) await mcp.close(context.session.id);
6097
+ await mcp.tools(context.session);
6098
+ return mcp.report(context.session.id);
6099
+ };
6100
+ async function run2(id2, args, context) {
6101
+ const at = panelContext(context);
6102
+ const panel = () => manager.panel(id2, at);
6103
+ const toggle = async (key, enabled) => applied(await manager.toggle(id2, findRow(await panel(), key, at.cwd).id, enabled, at), context);
6104
+ const act = async (rowId, actionId, inputs = []) => applied(await manager.act(id2, rowId, actionId, inputs, at), context);
6105
+ const [verb, rest] = split(args);
6106
+ const word = verb.toLowerCase();
6107
+ if (!verb) return [id2 === "mcp" ? await status2(context, false) : "", panelText(await panel(), HELP[id2].usage, id2 === "config" || id2 === "providers")].filter((part) => !!part).join("\n\n");
6108
+ if (word === "enable" || word === "disable") return toggle(rest, word === "enable");
6109
+ if (id2 === "config") {
6110
+ const row = findRow(await panel(), verb, at.cwd);
6111
+ if (row.choice) {
6112
+ const value2 = row.choice.options.find((option) => [option.value, option.name].some((name) => name.toLowerCase() === rest.toLowerCase().replace(/%$/, "")));
6113
+ return applied(await manager.choose(id2, row.id, value2?.value ?? rest, at), context);
6114
+ }
6115
+ if (row.enabled === void 0) throw new Error(`${row.label} has no on or off setting`);
6116
+ return applied(await manager.toggle(id2, row.id, switchOf(rest), at), context);
6117
+ }
6118
+ if (id2 === "skills" && word === "new") return act("new", "new", split(rest));
6119
+ if (id2 === "skills" && word === "import") return act("import", "import", [rest]);
6120
+ if (id2 === "instructions" && word === "prompt") return act("prompt", "edit", [rest]);
6121
+ if (id2 === "mcp" && word === "add") return act("add", "add", split(rest));
6122
+ if (id2 === "mcp" && word === "remove") return act(`user:${rest}`, "remove");
6123
+ if (id2 === "mcp" && word === "reload") return status2(context, true);
6124
+ if (id2 === "providers" && word === "use") return act(findRow(await panel(), rest, at.cwd).id, "use");
6125
+ if (id2 === "providers" && word === "signout") return act("openai", "signout");
6126
+ if (id2 === "providers" && word === "login" && rest.toLowerCase() === "chatgpt") return act("openai", "chatgpt");
6127
+ throw new Error(`Unknown /${id2} command. Usage:
6128
+ ${HELP[id2].usage.map((line) => `- ${line}`).join("\n")}`);
6129
+ }
6130
+ const commands = SETTINGS_COMMANDS.map((id2) => ({
6131
+ name: id2,
6132
+ description: HELP[id2].description,
6133
+ hint: HELP[id2].hint,
6134
+ run: async (args, context) => {
6135
+ let text;
6136
+ try {
6137
+ text = await run2(id2, args, context);
6138
+ } catch (error) {
6139
+ if (context.signal.aborted) return { done: "cancelled" };
6140
+ text = error instanceof Error ? error.message : String(error);
6141
+ }
6142
+ say(context, text);
6143
+ return { done: "end_turn" };
6144
+ }
6145
+ }));
6146
+ return { commands: () => commands };
6147
+ }
6148
+ const PANELS = {
6149
+ config: configPanel,
6150
+ skills: skillsPanel,
6151
+ hooks: hooksPanel,
6152
+ mcp: mcpPanel,
6153
+ instructions: instructionsPanel,
6154
+ providers: providersPanel
6155
+ };
6156
+ const handler = (id2) => {
6157
+ const panel = Object.hasOwn(PANELS, id2) ? PANELS[id2] : void 0;
6158
+ if (!panel) throw new Error(`Unknown settings panel ${id2}`);
6159
+ return panel;
6160
+ };
6161
+ const unsupported = (id2, what) => new Error(`The ${id2} panel has nothing to ${what}`);
6162
+ function createSettingsManager(deps) {
6163
+ return {
6164
+ panel: (id2, context) => handler(id2).panel(deps, context),
6165
+ async toggle(id2, rowId, enabled, context) {
6166
+ const panel = handler(id2);
6167
+ if (!panel.toggle) throw unsupported(id2, "turn on or off");
6168
+ return panel.toggle(deps, rowId, enabled, context);
6169
+ },
6170
+ async choose(id2, rowId, value2, context) {
6171
+ const panel = handler(id2);
6172
+ if (!panel.choose) throw unsupported(id2, "choose");
6173
+ return panel.choose(deps, rowId, value2, context);
6174
+ },
6175
+ async act(id2, rowId, actionId, inputs, context) {
6176
+ const panel = handler(id2);
6177
+ if (!panel.act) throw unsupported(id2, "run");
6178
+ return panel.act(deps, rowId, actionId, inputs, context);
6179
+ }
6180
+ };
6181
+ }
6182
+ const MAX_BYTES = 1024 * 1024;
6183
+ const WRITE_OPTIONS = [
6184
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
6185
+ { optionId: "allow_always", name: "Allow edits for this session", kind: "allow_always" },
6186
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
6187
+ ];
6188
+ const absolutePath = (params) => {
6189
+ const path2 = params.path;
6190
+ if (typeof path2 !== "string" || !isAbsolute(path2) || path2.includes("\0")) throw new Error("ACP paths must be absolute");
6191
+ return path2;
6192
+ };
6193
+ class LocalClient {
6194
+ constructor(ask) {
6195
+ this.ask = ask;
6196
+ }
6197
+ ask;
6198
+ terminals = new Terminals();
6199
+ trusted = /* @__PURE__ */ new Set();
6200
+ async readTextFile(params) {
6201
+ const text = await readFile$1(absolutePath(params), "utf8");
6202
+ const start = (params.line ?? 1) - 1;
6203
+ const content = params.line !== void 0 || params.limit !== void 0 ? text.split("\n").slice(start, params.limit ? start + params.limit : void 0).join("\n") : text;
6204
+ if (Buffer.byteLength(content) > MAX_BYTES) throw new Error("File content exceeds 1 MiB; request a smaller line range");
6205
+ return { content };
6206
+ }
6207
+ async writeTextFile(params) {
6208
+ const path2 = absolutePath(params);
6209
+ if (typeof params.content !== "string") throw new Error("Invalid content");
6210
+ const sessionId = String(params.sessionId ?? "");
6211
+ if (!params._meta?.oxbit?.mode && !this.trusted.has(sessionId)) {
6212
+ const oldText = await readFile$1(path2, "utf8").catch(() => null);
6213
+ const response = await this.ask({ sessionId, options: WRITE_OPTIONS, toolCall: {
6214
+ toolCallId: params.toolCallId ?? `write-${Date.now()}`,
6215
+ title: `Write ${path2}`,
6216
+ kind: "edit",
6217
+ status: "pending",
6218
+ locations: [{ path: path2 }],
6219
+ content: [{ type: "diff", path: path2, oldText, newText: params.content }]
6220
+ } });
6221
+ const choice = response?.outcome?.outcome === "selected" ? response.outcome.optionId : void 0;
6222
+ if (choice === "allow_always") this.trusted.add(sessionId);
6223
+ else if (choice !== "allow") throw new Error("The user rejected this write");
6224
+ }
6225
+ await mkdir(dirname(path2), { recursive: true });
6226
+ await writeFile$1(path2, params.content);
6227
+ return {};
6228
+ }
6229
+ dispose() {
6230
+ this.terminals.dispose();
6231
+ }
6232
+ }
6233
+ function routeRequests(link, local) {
6234
+ return (method, params) => {
6235
+ const capabilities = link.capabilities();
6236
+ if (method === "fs/read_text_file") return capabilities.fs?.readTextFile ? link.request(method, params) : local.readTextFile(params);
6237
+ if (method === "fs/write_text_file") return capabilities.fs?.writeTextFile ? link.request(method, params) : local.writeTextFile(params);
6238
+ if (method.startsWith("terminal/")) return capabilities.terminal ? link.request(method, params) : local.terminals.handle(method, params);
6239
+ if (method === "oxbit/diagnostics" && !capabilities._meta?.oxbit?.diagnostics)
6240
+ return Promise.reject(new Error("Language diagnostics are unavailable here. Run the project's type checker or linter with bash."));
6241
+ return link.request(method, params);
6242
+ };
6243
+ }
6244
+ function nodeHost(setup, link) {
6245
+ const { root, config, provider: provider2, log } = setup;
6246
+ const { runtime } = provider2;
6247
+ const local = new LocalClient((params) => link.request("session/request_permission", params));
6248
+ const mcp = nodeMcp(config, log);
6249
+ let catalog;
6250
+ const snapshots = snapshotStores(config.paths.snapshots, log);
6251
+ const filesystems = /* @__PURE__ */ new Map();
6252
+ const filesystemFor = (cwd) => {
6253
+ let filesystem = filesystems.get(cwd);
6254
+ if (!filesystem) filesystems.set(cwd, filesystem = nodeFilesystem(cwd, { ripgrep: setup.ripgrep }));
6255
+ return filesystem;
6256
+ };
6257
+ const core = coreExtensions(config, (sessionId, update) => link.update(sessionId, update), { home: setup.userHome, log });
6258
+ const settings = setup.settings ?? createSettingsManager({
6259
+ config,
6260
+ auth: new FileAuthStore(config.paths.auth),
6261
+ env: process.env,
6262
+ home: setup.userHome ?? homedir(),
6263
+ provider: () => provider2
6264
+ });
6265
+ const compactOptions2 = compactConfigOptions(config);
6266
+ const extensions = {
6267
+ sessions: new JsonlSessionStore(config.paths.sessions),
6268
+ snapshots,
6269
+ ...core,
6270
+ ...setup.extensions,
6271
+ context: [...core.context ?? [], ...setup.extensions?.context ?? []],
6272
+ tools: [...core.tools ?? [], mcp, ...setup.extensions?.tools ?? []],
6273
+ commands: [undoCommands(snapshots), settingsCommands(settings, mcp), ...core.commands ?? [], ...setup.extensions?.commands ?? []],
6274
+ continuations: [...core.continuations ?? [], ...setup.extensions?.continuations ?? []],
6275
+ hooks: mcp.hooks(setup.extensions?.hooks ?? core.hooks)
6276
+ };
6277
+ const host = {
6278
+ models: () => {
6279
+ if (setup.models) return setup.models;
6280
+ if (!provider2.ready) throw missingKey(provider2);
6281
+ return runtime.models();
6282
+ },
6283
+ endpoint: () => runtime.endpoint(),
6284
+ ...setup.models ? {} : { catalog: () => catalog ??= runtime.catalog().catch((error) => {
6285
+ catalog = void 0;
6286
+ throw error;
6287
+ }) },
6288
+ model: () => provider2.model,
6289
+ saveModel: (model) => {
6290
+ provider2.model = model;
6291
+ const providers = config.settings.providers ?? {};
6292
+ config.saveSettings({ providers: { ...providers, [provider2.id]: { ...providers[provider2.id], model } } }).catch((error) => log?.(`Could not save the model: ${error.message}`));
6293
+ },
6294
+ get capabilities() {
6295
+ return { fs: true, terminal: true, diagnostics: !!link.capabilities()._meta?.oxbit?.diagnostics, mcp: { http: true, sse: true } };
6296
+ },
6297
+ filesystem: filesystemFor(root),
6298
+ filesystemFor,
6299
+ root,
6300
+ update: (sessionId, update) => link.update(sessionId, update),
6301
+ request: routeRequests(link, local),
6302
+ generateTitles: setup.generateTitles,
6303
+ configOptions: compactOptions2.configOptions,
6304
+ setConfigOption: (_session, configId, value2) => compactOptions2.setConfigOption(configId, value2),
6305
+ extensions
6306
+ };
6307
+ const dispose = async () => {
6308
+ local.dispose();
6309
+ await mcp.closeAll();
6310
+ };
6311
+ return { host, local, mcp, dispose };
6312
+ }
6313
+ const agentFactory = (setup) => (link) => {
6314
+ const { host, dispose } = nodeHost(setup, link);
6315
+ return { agent: new Agent(host), host, dispose };
6316
+ };
5441
6317
  const firstLine = (text) => String(text ?? "").trim().split("\n")[0].slice(0, 200);
5442
6318
  async function runPrint(options) {
5443
6319
  const { connection, stdout, stderr } = options;
@@ -5559,8 +6435,6 @@ ${USAGE}`);
5559
6435
  const commandContext = { config, auth, io, input: input2 };
5560
6436
  const [command, ...rest] = positionals;
5561
6437
  switch (command) {
5562
- case "acp":
5563
- return await acp(config, await selectProvider(input2), io, log);
5564
6438
  case "models":
5565
6439
  return await models(commandContext);
5566
6440
  case "login":
@@ -5568,43 +6442,55 @@ ${USAGE}`);
5568
6442
  case "logout":
5569
6443
  return await logout(commandContext, rest);
5570
6444
  }
5571
- const provider2 = await selectProvider(input2);
5572
- const setup = (generateTitles, quiet = false) => ({ root: io.cwd, config, generateTitles, log: quiet ? void 0 : log, provider: provider2 });
6445
+ let provider2 = await selectProvider(input2);
6446
+ const deps = { config, auth, env: io.env, fetch: io.fetch, open: io.open, provider: () => provider2 };
6447
+ const settings = createSettingsManager(deps);
6448
+ const setup = (generateTitles, quiet = false) => ({ root: io.cwd, config, generateTitles, log: quiet ? void 0 : log, provider: provider2, settings });
6449
+ if (command === "acp") {
6450
+ await serveStdio({ input: io.stdin, output: io.stdout, factory: agentFactory(setup(true)), log });
6451
+ return 0;
6452
+ }
5573
6453
  const sessionId = await resumeId(config, values, io.cwd);
5574
6454
  const mode = values.mode ?? config.settings.mode;
5575
6455
  if (values.print !== void 0) {
5576
6456
  if (positionals.length) throw new UsageError(`Unexpected argument ${positionals[0]}`);
5577
6457
  const prompt = values.print === "-" ? (await readAll(io.stdin)).trim() : values.print;
5578
6458
  if (!prompt) throw new UsageError("The prompt is empty");
5579
- const connection2 = await MemoryConnection.open(agentFactory(setup(false)));
6459
+ const connection = await MemoryConnection.open(agentFactory(setup(false)));
5580
6460
  const abort = new AbortController();
5581
6461
  const interrupt = () => abort.abort();
5582
6462
  process.once("SIGINT", interrupt);
5583
6463
  try {
5584
- return await runPrint({ connection: connection2, prompt, sessionId, mode, model: values.model, stdout: io.stdout, stderr: io.stderr, signal: abort.signal });
6464
+ return await runPrint({ connection, prompt, sessionId, mode, model: values.model, stdout: io.stdout, stderr: io.stderr, signal: abort.signal });
5585
6465
  } finally {
5586
6466
  process.off("SIGINT", interrupt);
5587
- await connection2.close();
6467
+ await connection.close();
5588
6468
  }
5589
6469
  }
5590
6470
  if (!io.stdout.isTTY) throw new UsageError("stdout is not a terminal. Use -p <prompt> for one reply or `oxbit-agent acp` for ACP");
5591
- const tui = await import("./assets/index-BrBnidfI.js");
5592
- const connected = setup(true, true);
5593
- const connection = await MemoryConnection.open(agentFactory(connected));
6471
+ const [tui, memory] = await Promise.all([import("./assets/index-T_6HaJx-.js"), import("./assets/memory-Bdy8hvTl.js")]);
6472
+ const label = () => ({ provider: provider2.runtime.label, usingChatGPTPlan: provider2.runtime.method === "chatgpt" });
6473
+ const panels = createSettingsManager({ ...deps, rebuild: async (id2) => {
6474
+ await config.reload();
6475
+ const next = await selectProvider({ ...input2, settings: config.settings, provider: id2, model: id2 === provider2.id ? provider2.model : void 0 });
6476
+ const connection = await MemoryConnection.open(agentFactory({ ...setup(true, true), provider: next }));
6477
+ provider2 = next;
6478
+ await adapter.replace(connection, label());
6479
+ } });
6480
+ const adapter = memory.memoryTuiConnection(await MemoryConnection.open(agentFactory(setup(true, true))), { ...label(), mode, settings: panels });
5594
6481
  try {
5595
- return await tui.runTui({
5596
- connection,
6482
+ const { sessionId: last } = await tui.run(adapter, {
5597
6483
  cwd: io.cwd,
5598
6484
  sessionId,
5599
6485
  prompt: positionals.join(" ") || void 0,
5600
- mode,
5601
6486
  version: agentVersion(),
5602
- providerLabel: connected.provider.runtime.label,
5603
- usingChatGPTPlan: connected.provider.runtime.method === "chatgpt",
5604
- historyPath: config.paths.history
6487
+ history: memory.fileHistory(config.paths.history)
5605
6488
  });
6489
+ if (last) io.stderr.write(`Resume this session with: oxbit-agent -r ${last}
6490
+ `);
6491
+ return 0;
5606
6492
  } finally {
5607
- await connection.close();
6493
+ await adapter.connection.close();
5608
6494
  }
5609
6495
  } catch (error) {
5610
6496
  io.stderr.write(`oxbit-agent: ${error instanceof Error ? error.message : String(error)}
@@ -5623,13 +6509,14 @@ async function resumeId(config, values, cwd) {
5623
6509
  if (values.resume && !await store.load(values.resume)) throw new Error(`No saved session ${values.resume}`);
5624
6510
  return values.resume;
5625
6511
  }
5626
- async function acp(config, provider2, io, log) {
5627
- await serveStdio({ input: io.stdin, output: io.stdout, factory: agentFactory({ root: io.cwd, config, provider: provider2, log }), log });
5628
- return 0;
5629
- }
5630
6512
  void main(process.argv.slice(2), { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr, env: process.env, cwd: process.cwd() }).then((code) => process.stdout.write("", () => process.exit(code)));
5631
6513
  export {
5632
6514
  MODES as M,
6515
+ contextGrid as a,
6516
+ contextNotes as b,
6517
+ contextLegend as c,
6518
+ formatPercent as d,
6519
+ formatTokens as f,
5633
6520
  walkFiles as w
5634
6521
  };
5635
6522
  //# sourceMappingURL=index.js.map