@oxbit/cli 0.10.1 → 0.11.1

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,39 +1,12 @@
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";
8
8
  import { spawn } from "node:child_process";
9
9
  import { createServer } from "node:http";
10
- const count = (...values) => values.find((value2) => Number.isSafeInteger(value2) && value2 > 0);
11
- async function readText$1(models2, request, signal) {
12
- let text = "";
13
- for await (const chunk of models2.stream(request, signal)) text += chunk;
14
- return text;
15
- }
16
- async function listModels(models2, endpoint, signal) {
17
- const body = JSON.parse(await readText$1(models2, { ...endpoint, method: "GET", path: "/v1/models" }, signal));
18
- if (!Array.isArray(body?.data)) throw new Error("The model list has no data array");
19
- return body.data.filter((item) => typeof item?.id === "string" && item.id).slice(0, 1e3).map((item) => ({
20
- id: item.id,
21
- name: typeof item.display_name === "string" ? item.display_name : typeof item.name === "string" ? item.name : item.id,
22
- contextWindow: count(item.max_input_tokens, item.context_length),
23
- maxOutput: count(item.max_tokens, item.top_provider?.max_completion_tokens)
24
- }));
25
- }
26
- function modelError(error) {
27
- const { status, body } = error ?? {};
28
- if (typeof status !== "number") return error instanceof Error ? error : new Error(String(error));
29
- let detail = typeof body === "string" ? body.trim().slice(0, 1e3) : "";
30
- try {
31
- const parsed = JSON.parse(detail);
32
- detail = parsed?.error?.message ?? parsed?.message ?? detail;
33
- } catch {
34
- }
35
- return new Error(`Model API error ${status}${detail ? `: ${detail}` : ""}`);
36
- }
37
10
  async function* lines(chunks) {
38
11
  let buffer = "";
39
12
  for await (const chunk of chunks) {
@@ -76,6 +49,137 @@ async function* jsonEvents(chunks) {
76
49
  if (value2 && typeof value2 === "object") yield { ...event && !value2.type ? { type: event } : {}, ...value2 };
77
50
  }
78
51
  }
52
+ function replayable(items) {
53
+ const kept = items.filter((item) => item.type !== "reasoning" || item.encrypted_content);
54
+ return kept.length === items.length ? items : kept.map((item) => item.type === "reasoning" ? item : Object.fromEntries(Object.entries(item).filter(([key]) => key !== "id")));
55
+ }
56
+ function responsesBody({ model, system, turns, tools, effort }, replay2) {
57
+ const input2 = [];
58
+ for (const turn of turns) {
59
+ if (turn.role === "user") input2.push({ type: "message", role: "user", content: [{ type: "input_text", text: turn.text }] });
60
+ else if (turn.role === "tool")
61
+ for (const result of turn.results) input2.push({ type: "function_call_output", call_id: result.id, output: result.output });
62
+ else if (turn.native && replay2.includes(turn.native.api)) input2.push(...replayable(turn.native.items));
63
+ else {
64
+ if (turn.text) input2.push({ type: "message", role: "assistant", content: turn.text });
65
+ for (const call of turn.calls) input2.push({ type: "function_call", call_id: call.id, name: call.name, arguments: call.arguments });
66
+ }
67
+ }
68
+ return {
69
+ model,
70
+ instructions: system,
71
+ input: input2,
72
+ stream: true,
73
+ store: false,
74
+ ...tools.length ? { tools: tools.map((tool) => ({ type: "function", ...tool, strict: false })) } : {},
75
+ ...effort ? { reasoning: { effort, summary: "auto" } } : {}
76
+ };
77
+ }
78
+ const openaiResponses = {
79
+ request: (input2) => ({ method: "POST", path: "/v1/responses", body: responsesBody(input2, ["openai"]) }),
80
+ async *events(chunks) {
81
+ const items = [];
82
+ for await (const event of jsonEvents(chunks)) {
83
+ switch (event.type) {
84
+ case "response.output_text.delta":
85
+ if (event.delta) yield { type: "text", text: event.delta };
86
+ break;
87
+ case "response.reasoning_summary_text.delta":
88
+ case "response.reasoning_text.delta":
89
+ if (event.delta) yield { type: "reasoning", text: event.delta };
90
+ break;
91
+ case "response.reasoning_summary_part.added":
92
+ if (event.summary_index > 0) yield { type: "reasoning", text: "\n\n" };
93
+ break;
94
+ case "response.output_item.done":
95
+ items[event.output_index ?? items.length] = event.item;
96
+ if (event.item?.type === "function_call")
97
+ yield { type: "tool_call", call: { id: event.item.call_id, name: event.item.name, arguments: event.item.arguments ?? "" } };
98
+ break;
99
+ case "response.completed":
100
+ case "response.incomplete": {
101
+ const { usage, incomplete_details: incomplete } = event.response ?? {};
102
+ if (usage) yield { type: "usage", input: usage.input_tokens ?? 0, output: usage.output_tokens ?? 0 };
103
+ const native = items.filter(Boolean);
104
+ const reason = native.some((item) => item.type === "function_call") ? "tool_use" : event.type === "response.completed" ? "end_turn" : incomplete?.reason === "content_filter" ? "refusal" : "max_tokens";
105
+ yield { type: "stop", reason, native };
106
+ return;
107
+ }
108
+ case "response.failed":
109
+ throw new Error(event.response?.error?.message ?? "The model response failed");
110
+ case "error":
111
+ throw new Error(event.message ?? "The model stream failed");
112
+ }
113
+ }
114
+ }
115
+ };
116
+ const CHATGPT_CLIENT_VERSION = "0.162.1";
117
+ const CHATGPT_MODELS_PATH = `/codex/models?client_version=${CHATGPT_CLIENT_VERSION}`;
118
+ const WINDOW = { contextWindow: 272e3 };
119
+ const CHATGPT_MODELS = [
120
+ { id: "gpt-6.1-sol", name: "GPT-6.1-Sol", ...WINDOW },
121
+ { id: "gpt-6-astra", name: "GPT-6-Astra", ...WINDOW },
122
+ { id: "gpt-6-sol", name: "GPT-6-Sol", ...WINDOW },
123
+ { id: "gpt-6-luna", name: "GPT-6-Luna", ...WINDOW },
124
+ { id: "gpt-5.6-sol", name: "GPT-5.6-Sol", ...WINDOW },
125
+ { id: "gpt-5.6-terra", name: "GPT-5.6-Terra", ...WINDOW },
126
+ { id: "gpt-5.6-luna", name: "GPT-5.6-Luna", ...WINDOW },
127
+ { id: "gpt-5.5", name: "GPT-5.5", ...WINDOW }
128
+ ];
129
+ const chatgptResponses = {
130
+ request: (input2) => ({
131
+ method: "POST",
132
+ path: "/codex/responses",
133
+ body: {
134
+ ...responsesBody({ ...input2, system: input2.system || "You are a helpful assistant." }, ["openai", "chatgpt"]),
135
+ include: ["reasoning.encrypted_content"],
136
+ tool_choice: "auto"
137
+ }
138
+ }),
139
+ events: openaiResponses.events
140
+ };
141
+ const count = (...values) => values.find((value2) => Number.isSafeInteger(value2) && value2 > 0);
142
+ async function readText$1(models2, request, signal) {
143
+ let text = "";
144
+ for await (const chunk of models2.stream(request, signal)) text += chunk;
145
+ return text;
146
+ }
147
+ async function listModels(models2, endpoint, signal) {
148
+ if (endpoint.api === "chatgpt") return listChatGPTModels(models2, signal);
149
+ const body = JSON.parse(await readText$1(models2, { ...endpoint, method: "GET", path: "/v1/models" }, signal));
150
+ if (!Array.isArray(body?.data)) throw new Error("The model list has no data array");
151
+ return body.data.filter((item) => typeof item?.id === "string" && item.id).slice(0, 1e3).map((item) => ({
152
+ id: item.id,
153
+ name: typeof item.display_name === "string" ? item.display_name : typeof item.name === "string" ? item.name : item.id,
154
+ contextWindow: count(item.max_input_tokens, item.context_length),
155
+ maxOutput: count(item.max_tokens, item.top_provider?.max_completion_tokens)
156
+ }));
157
+ }
158
+ async function listChatGPTModels(models2, signal) {
159
+ try {
160
+ const body = JSON.parse(await readText$1(models2, { api: "chatgpt", method: "GET", path: CHATGPT_MODELS_PATH }, signal));
161
+ const listed = (Array.isArray(body?.models) ? body.models : []).filter((item) => typeof item?.slug === "string" && item.slug && item.visibility === "list").sort((a, b) => (Number(a.priority) || 0) - (Number(b.priority) || 0)).map((item) => ({
162
+ id: item.slug,
163
+ name: typeof item.display_name === "string" && item.display_name ? item.display_name : item.slug,
164
+ contextWindow: count(item.context_window)
165
+ }));
166
+ if (listed.length) return listed;
167
+ } catch (error) {
168
+ if (signal?.aborted) throw error;
169
+ }
170
+ return CHATGPT_MODELS.map((model) => ({ ...model }));
171
+ }
172
+ function modelError(error) {
173
+ const { status: status2, body } = error ?? {};
174
+ if (typeof status2 !== "number") return error instanceof Error ? error : new Error(String(error));
175
+ let detail = typeof body === "string" ? body.trim().slice(0, 1e3) : "";
176
+ try {
177
+ const parsed = JSON.parse(detail);
178
+ detail = parsed?.error?.message ?? parsed?.message ?? detail;
179
+ } catch {
180
+ }
181
+ return new Error(`Model API error ${status2}${detail ? `: ${detail}` : ""}`);
182
+ }
79
183
  const OUTPUT_LIMIT$1 = 1e5;
80
184
  const BASH_TIMEOUT = 12e4;
81
185
  const BASH_MAX_TIMEOUT = 6e5;
@@ -250,7 +354,7 @@ const updatePlan = {
250
354
  return "Plan updated";
251
355
  }
252
356
  };
253
- const terminalText = (result, status) => [(result?.output ?? "").replace(/\s+$/, ""), result?.truncated ? "[Output truncated to the last bytes]" : "", `[${status}]`].filter(Boolean).join("\n");
357
+ const terminalText = (result, status2) => [(result?.output ?? "").replace(/\s+$/, ""), result?.truncated ? "[Output truncated to the last bytes]" : "", `[${status2}]`].filter(Boolean).join("\n");
254
358
  const bash = {
255
359
  spec: {
256
360
  name: "bash",
@@ -296,8 +400,8 @@ const bash = {
296
400
  try {
297
401
  const exit = await Promise.race([context.request("terminal/wait_for_exit", terminal), interrupted]);
298
402
  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);
403
+ const status2 = exit?.signal ? `Killed by ${exit.signal}` : `Exit code ${exit?.exitCode ?? "unknown"}`;
404
+ const text = terminalText(result, status2);
301
405
  if (exit?.exitCode !== 0) throw new Error(text);
302
406
  return text;
303
407
  } catch (error) {
@@ -339,7 +443,7 @@ async function runTool(call, context, tools = TOOLS, gate = {}) {
339
443
  let args = typeof parsed === "string" ? {} : parsed;
340
444
  const tool = tools.find((item) => item.spec.name === call.name);
341
445
  const toolCallId = call.id;
342
- const update = (status, output2) => context.update({ sessionUpdate: "tool_call_update", toolCallId, status, rawOutput: output2.slice(0, 8e3) });
446
+ const update = (status2, output2) => context.update({ sessionUpdate: "tool_call_update", toolCallId, status: status2, rawOutput: output2.slice(0, 8e3) });
343
447
  const where = locations(context.root, args);
344
448
  context.update({
345
449
  sessionUpdate: "tool_call",
@@ -486,73 +590,6 @@ async function streamStep({ adapter, models: models2, endpoint, input: input2, s
486
590
  }
487
591
  return result;
488
592
  }
489
- function replayable(items) {
490
- const kept = items.filter((item) => item.type !== "reasoning" || item.encrypted_content);
491
- return kept.length === items.length ? items : kept.map((item) => item.type === "reasoning" ? item : Object.fromEntries(Object.entries(item).filter(([key]) => key !== "id")));
492
- }
493
- const openaiResponses = {
494
- request({ model, system: system2, turns, tools, effort }) {
495
- const input2 = [];
496
- for (const turn of turns) {
497
- if (turn.role === "user") input2.push({ type: "message", role: "user", content: [{ type: "input_text", text: turn.text }] });
498
- else if (turn.role === "tool")
499
- for (const result of turn.results) input2.push({ type: "function_call_output", call_id: result.id, output: result.output });
500
- else if (turn.native?.api === "openai") input2.push(...replayable(turn.native.items));
501
- else {
502
- if (turn.text) input2.push({ type: "message", role: "assistant", content: turn.text });
503
- for (const call of turn.calls) input2.push({ type: "function_call", call_id: call.id, name: call.name, arguments: call.arguments });
504
- }
505
- }
506
- return {
507
- method: "POST",
508
- path: "/v1/responses",
509
- body: {
510
- model,
511
- instructions: system2,
512
- input: input2,
513
- stream: true,
514
- store: false,
515
- ...tools.length ? { tools: tools.map((tool) => ({ type: "function", ...tool, strict: false })) } : {},
516
- ...effort ? { reasoning: { effort, summary: "auto" } } : {}
517
- }
518
- };
519
- },
520
- async *events(chunks) {
521
- const items = [];
522
- for await (const event of jsonEvents(chunks)) {
523
- switch (event.type) {
524
- case "response.output_text.delta":
525
- if (event.delta) yield { type: "text", text: event.delta };
526
- break;
527
- case "response.reasoning_summary_text.delta":
528
- case "response.reasoning_text.delta":
529
- if (event.delta) yield { type: "reasoning", text: event.delta };
530
- break;
531
- case "response.reasoning_summary_part.added":
532
- if (event.summary_index > 0) yield { type: "reasoning", text: "\n\n" };
533
- break;
534
- case "response.output_item.done":
535
- items[event.output_index ?? items.length] = event.item;
536
- if (event.item?.type === "function_call")
537
- yield { type: "tool_call", call: { id: event.item.call_id, name: event.item.name, arguments: event.item.arguments ?? "" } };
538
- break;
539
- case "response.completed":
540
- case "response.incomplete": {
541
- const { usage, incomplete_details: incomplete } = event.response ?? {};
542
- if (usage) yield { type: "usage", input: usage.input_tokens ?? 0, output: usage.output_tokens ?? 0 };
543
- const native = items.filter(Boolean);
544
- const reason = native.some((item) => item.type === "function_call") ? "tool_use" : event.type === "response.completed" ? "end_turn" : incomplete?.reason === "content_filter" ? "refusal" : "max_tokens";
545
- yield { type: "stop", reason, native };
546
- return;
547
- }
548
- case "response.failed":
549
- throw new Error(event.response?.error?.message ?? "The model response failed");
550
- case "error":
551
- throw new Error(event.message ?? "The model stream failed");
552
- }
553
- }
554
- }
555
- };
556
593
  const STOP$1 = { stop: "end_turn", tool_calls: "tool_use", function_call: "tool_use", length: "max_tokens", content_filter: "refusal" };
557
594
  function mergeDetails(details, fragments) {
558
595
  if (!Array.isArray(fragments)) return;
@@ -566,8 +603,8 @@ function mergeDetails(details, fragments) {
566
603
  }
567
604
  }
568
605
  const chatCompletions = {
569
- request({ model, system: system2, turns, tools, effort }) {
570
- const messages2 = [{ role: "system", content: system2 }];
606
+ request({ model, system, turns, tools, effort }) {
607
+ const messages2 = [{ role: "system", content: system }];
571
608
  for (const turn of turns) {
572
609
  if (turn.role === "user") messages2.push({ role: "user", content: turn.text });
573
610
  else if (turn.role === "tool")
@@ -643,7 +680,7 @@ const STOP = {
643
680
  model_context_window_exceeded: "max_tokens",
644
681
  refusal: "refusal"
645
682
  };
646
- const input = (text) => {
683
+ const input$1 = (text) => {
647
684
  try {
648
685
  const value2 = JSON.parse(text || "{}");
649
686
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
@@ -652,7 +689,7 @@ const input = (text) => {
652
689
  }
653
690
  };
654
691
  const anthropicMessages = {
655
- request({ model, system: system2, turns, tools, effort, maxTokens }) {
692
+ request({ model, system, turns, tools, effort, maxTokens }) {
656
693
  const messages2 = [];
657
694
  const add = (role, content) => {
658
695
  const last = messages2.at(-1);
@@ -666,7 +703,7 @@ const anthropicMessages = {
666
703
  else if (turn.native?.api === "anthropic") add("assistant", structuredClone(turn.native.items));
667
704
  else add("assistant", [
668
705
  ...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) }))
706
+ ...turn.calls.map((call) => ({ type: "tool_use", id: call.id, name: call.name, input: input$1(call.arguments) }))
670
707
  ]);
671
708
  }
672
709
  return {
@@ -674,7 +711,7 @@ const anthropicMessages = {
674
711
  path: "/v1/messages",
675
712
  body: {
676
713
  model,
677
- system: system2,
714
+ system,
678
715
  messages: messages2,
679
716
  stream: true,
680
717
  max_tokens: maxTokens ?? DEFAULT_MAX_TOKENS,
@@ -719,7 +756,7 @@ const anthropicMessages = {
719
756
  const block2 = blocks[event.index];
720
757
  if (block2?.type !== "tool_use") break;
721
758
  const text = json2.get(event.index) || "{}";
722
- block2.input = input(text);
759
+ block2.input = input$1(text);
723
760
  yield { type: "tool_call", call: { id: block2.id, name: block2.name, arguments: text } };
724
761
  break;
725
762
  }
@@ -798,6 +835,7 @@ Unfinished tasks and next steps, including the step in progress.
798
835
  Constraints and preferences the user stated.
799
836
  Keep exact file paths, commands, identifiers and error messages.`;
800
837
  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);
838
+ const estimateTokens = (turns) => Math.ceil(turns.reduce((sum, turn) => sum + chars(turn), 0) / 4);
801
839
  const clip$1 = (text, max) => text.length > max ? `${text.slice(0, max)}
802
840
  [${text.length - max} more characters]` : text;
803
841
  function pruneToolOutputs(turns) {
@@ -860,10 +898,15 @@ class Compaction {
860
898
  /** Set when shouldCompact returns true; its absence in compact() means a manual run. */
861
899
  pending = /* @__PURE__ */ new Map();
862
900
  usable = /* @__PURE__ */ new Map();
863
- /** Tokens a request can use before compaction: the window minus min(reserve, maxOutput or 32000, window / 4). */
901
+ /** Tokens a request can use before compaction: `percent` of the window, else the window minus min(reserve, maxOutput or 32000, window / 4). */
864
902
  threshold(contextWindow, maxOutput) {
903
+ const percent = this.options.percent;
904
+ if (typeof percent === "number" && percent >= 50 && percent <= 95) return Math.floor(contextWindow * percent / 100);
865
905
  return contextWindow - Math.min(this.options.reserve ?? 2e4, maxOutput ?? DEFAULT_MAX_OUTPUT, Math.floor(contextWindow / 4));
866
906
  }
907
+ autoThreshold(contextWindow, maxOutput) {
908
+ return this.options.auto === false ? void 0 : this.threshold(contextWindow, maxOutput);
909
+ }
867
910
  /** Due when the last request's input plus output reaches the threshold. turn.ts does not pass `maxOutput` yet. */
868
911
  shouldCompact(session, usage, contextWindow, maxOutput) {
869
912
  if (!contextWindow) return false;
@@ -931,11 +974,12 @@ Additional focus: ${focus}` : "");
931
974
  const ADAPTERS = {
932
975
  openai: openaiResponses,
933
976
  "openai-compatible": chatCompletions,
934
- anthropic: anthropicMessages
977
+ anthropic: anthropicMessages,
978
+ chatgpt: chatgptResponses
935
979
  };
936
980
  const MAX_STEPS = 64;
937
981
  const CONTEXT_LIMIT$1 = 1e4;
938
- const system = (root, reviewed, shell) => [
982
+ const basePrompt = (root, reviewed, shell) => [
939
983
  `You are Oxbit Agent, a coding agent inside the Oxbit editor. You work in the workspace "${root.split("/").pop()}" through tools.`,
940
984
  "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
985
  ...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 +1035,7 @@ ${text}
991
1035
  }
992
1036
  async system() {
993
1037
  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);
1038
+ const base = basePrompt(this.session.cwd, this.host.capabilities.fs, this.host.capabilities.terminal);
995
1039
  return [base, ...this.session.context, ...sections].filter((section) => !!section).join("\n\n");
996
1040
  }
997
1041
  async run(models2, endpoint) {
@@ -1031,6 +1075,7 @@ ${text}
1031
1075
  }
1032
1076
  });
1033
1077
  usage = result.usage ?? usage;
1078
+ if (result.usage) session.usage = result.usage;
1034
1079
  const calls = result.turn.calls;
1035
1080
  const context = {
1036
1081
  root: session.cwd,
@@ -1108,6 +1153,7 @@ ${text}
1108
1153
  const before = await this.hook("PreCompact", { trigger, custom_instructions: typeof focus === "string" ? focus : "" });
1109
1154
  if (before.block || this.halt) return false;
1110
1155
  this.session.turns = await compactor.compact(this.session, summarize, this.signal);
1156
+ this.session.usage = void 0;
1111
1157
  await this.hook("PostCompact", { trigger });
1112
1158
  return true;
1113
1159
  }
@@ -1156,6 +1202,125 @@ async function generateTitle(host, session, signal) {
1156
1202
  return void 0;
1157
1203
  }
1158
1204
  }
1205
+ const estimate = (text) => Math.ceil(text.length / 4);
1206
+ const slug = (label) => label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "section";
1207
+ async function contextUsage(host, session, signal) {
1208
+ const providers = host.extensions?.context ?? [];
1209
+ const sections = await Promise.all(providers.map((provider2) => provider2.system(session)));
1210
+ const labeled = /* @__PURE__ */ new Map();
1211
+ let other = session.context.reduce((sum, text) => sum + estimate(text), 0);
1212
+ providers.forEach((provider2, index) => {
1213
+ const tokens = estimate(sections[index] ?? "");
1214
+ if (!provider2.label) {
1215
+ other += tokens;
1216
+ return;
1217
+ }
1218
+ const category = labeled.get(provider2.label) ?? { id: slug(provider2.label), label: provider2.label, tokens: 0 };
1219
+ category.tokens += tokens;
1220
+ labeled.set(provider2.label, category);
1221
+ });
1222
+ const builtin = new Set(builtinTools(host.capabilities).map((tool) => tool.spec.name));
1223
+ const tools = await new PromptTurn(host, session, signal).tools();
1224
+ const specs = (inside) => tools.filter((tool) => builtin.has(tool.spec.name) === inside).reduce((sum, tool) => sum + estimate(JSON.stringify(tool.spec)), 0);
1225
+ const categories = [
1226
+ { id: "system", label: "System prompt", tokens: estimate(basePrompt(session.cwd, host.capabilities.fs, host.capabilities.terminal)) },
1227
+ ...labeled.values(),
1228
+ { id: "context", label: "Other context", tokens: other },
1229
+ { id: "tools", label: "Built-in tools", tokens: specs(true) },
1230
+ { id: "mcp", label: "MCP and extension tools", tokens: specs(false) },
1231
+ { id: "messages", label: "Messages", tokens: estimateTokens(session.turns) }
1232
+ ];
1233
+ const used = categories.reduce((sum, category) => sum + category.tokens, 0);
1234
+ const info = session.catalog.find((model) => model.id === session.model);
1235
+ const window = info?.contextWindow;
1236
+ const threshold = window ? host.extensions?.compactor?.autoThreshold?.(window, info?.maxOutput) : void 0;
1237
+ return {
1238
+ model: session.model,
1239
+ estimated: true,
1240
+ categories,
1241
+ used,
1242
+ ...window ? { window, free: Math.max(0, (threshold ?? window) - used) } : {},
1243
+ ...info?.maxOutput ? { maxOutput: info.maxOutput } : {},
1244
+ ...window && threshold !== void 0 ? { threshold, buffer: Math.max(0, window - Math.max(threshold, used)) } : {},
1245
+ ...session.usage ? { reported: { ...session.usage } } : {}
1246
+ };
1247
+ }
1248
+ function contextGrid(usage, cells = 200) {
1249
+ if (!usage.window) return [];
1250
+ const scale = cells / Math.max(usage.window, usage.used);
1251
+ const grid = [];
1252
+ let sum = 0;
1253
+ for (const category of usage.categories) {
1254
+ sum += category.tokens;
1255
+ const end = Math.min(cells, Math.max(Math.round(sum * scale), category.tokens ? grid.length + 1 : grid.length));
1256
+ while (grid.length < end) grid.push(category.id);
1257
+ }
1258
+ const buffer = usage.threshold === void 0 ? cells : Math.max(grid.length, Math.round(usage.threshold * scale));
1259
+ while (grid.length < buffer) grid.push("free");
1260
+ while (grid.length < cells) grid.push("buffer");
1261
+ return grid;
1262
+ }
1263
+ const formatTokens = (tokens) => tokens < 1e3 ? String(tokens) : tokens < 1e6 ? `${+(tokens / 1e3).toFixed(1)}k` : `${+(tokens / 1e6).toFixed(2)}M`;
1264
+ const formatPercent = (tokens, window) => `${(tokens / window * 100).toFixed(1)}%`;
1265
+ const COLUMNS = 20;
1266
+ const GLYPHS = {
1267
+ system: "#",
1268
+ instructions: "=",
1269
+ skills: "+",
1270
+ context: "~",
1271
+ tools: "*",
1272
+ mcp: "%",
1273
+ messages: "@",
1274
+ free: ".",
1275
+ buffer: "x"
1276
+ };
1277
+ const SPARE = ["&", "$", "o", "^"];
1278
+ function contextLegend(usage) {
1279
+ let spare = 0;
1280
+ const rows = usage.categories.map((category) => ({
1281
+ id: category.id,
1282
+ glyph: GLYPHS[category.id] ?? SPARE[spare++ % SPARE.length],
1283
+ label: category.label,
1284
+ tokens: category.tokens
1285
+ }));
1286
+ if (usage.free !== void 0) rows.push({ id: "free", glyph: GLYPHS.free, label: "Free space", tokens: usage.free });
1287
+ if (usage.buffer !== void 0) rows.push({ id: "buffer", glyph: GLYPHS.buffer, label: "Auto-compact buffer", tokens: usage.buffer });
1288
+ return rows;
1289
+ }
1290
+ function contextNotes(usage) {
1291
+ const model = usage.model || "no model";
1292
+ 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.`;
1293
+ const footer = [
1294
+ ...usage.window ? [usage.threshold === void 0 ? "Auto compaction is off." : `Auto compaction runs at ${formatTokens(usage.threshold)} tokens (${formatPercent(usage.threshold, usage.window)}).`] : [],
1295
+ ...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).`] : [],
1296
+ "Category counts are estimates at 4 characters per token."
1297
+ ];
1298
+ return { header, footer };
1299
+ }
1300
+ function renderContextUsage(usage) {
1301
+ const legend = contextLegend(usage);
1302
+ const glyphs = new Map(legend.map((row) => [row.id, row.glyph]));
1303
+ const grid = contextGrid(usage);
1304
+ const rows = [];
1305
+ for (let index = 0; index < grid.length; index += COLUMNS) rows.push(grid.slice(index, index + COLUMNS).map((id2) => glyphs.get(id2)).join(" "));
1306
+ const width = Math.max(...legend.map((row) => row.label.length));
1307
+ const counts = legend.map((row) => formatTokens(row.tokens));
1308
+ const digits = Math.max(...counts.map((count2) => count2.length));
1309
+ 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)}` : ""));
1310
+ const { header, footer } = contextNotes(usage);
1311
+ return [header, "", "```text", ...rows.length ? [...rows, ""] : [], ...lines2, "```", "", footer.join(" ")].join("\n");
1312
+ }
1313
+ function contextCommand(host, session) {
1314
+ return {
1315
+ name: "context",
1316
+ description: "Show context window usage",
1317
+ run: async (_args, context) => {
1318
+ const usage = await contextUsage(host, session, context.signal);
1319
+ context.update({ sessionUpdate: "agent_message_chunk", content: { type: "text", text: renderContextUsage(usage) }, _meta: { oxbit: { context: usage } } });
1320
+ return { done: "end_turn" };
1321
+ }
1322
+ };
1323
+ }
1159
1324
  const EFFORTS = ["default", "low", "medium", "high"];
1160
1325
  const COMMAND = /^\/([\w:.-]+)(?:\s+([\s\S]*))?$/;
1161
1326
  const id = (params, key) => {
@@ -1245,7 +1410,7 @@ class Agent {
1245
1410
  currentValue: modeOf(session.meta),
1246
1411
  options: MODES.map(({ id: id2, name }) => ({ value: id2, name }))
1247
1412
  };
1248
- return session.catalog.length ? [models2, reasoning, mode] : [reasoning, mode];
1413
+ return [...session.catalog.length ? [models2] : [], reasoning, mode, ...this.host.configOptions?.(session) ?? []];
1249
1414
  }
1250
1415
  state(session) {
1251
1416
  return {
@@ -1265,7 +1430,7 @@ class Agent {
1265
1430
  if (cwd !== normalize(this.host.root) && !this.host.filesystemFor) throw new Error(`This agent serves ${this.host.root} only`);
1266
1431
  return cwd;
1267
1432
  }
1268
- /** The host catalog or `GET /v1/models`, with `model` first when the list lacks it. */
1433
+ /** The host catalog or `listModels`, with `model` first when the list lacks it. */
1269
1434
  async models(model) {
1270
1435
  const listed = this.host.catalog ? Promise.resolve().then(() => this.host.catalog()) : this.catalog ??= listModels(this.service(), this.host.endpoint());
1271
1436
  const catalog = await listed.catch(() => {
@@ -1371,13 +1536,14 @@ class Agent {
1371
1536
  this.host.update(sessionId, { sessionUpdate: "current_mode_update", currentModeId: modeId });
1372
1537
  return {};
1373
1538
  }
1374
- setConfigOption(sessionId, configId, value2) {
1539
+ async setConfigOption(sessionId, configId, value2) {
1375
1540
  const session = this.session(sessionId);
1376
1541
  if (configId === "model") return this.setModel(sessionId, value2);
1377
1542
  if (configId === "mode") {
1378
1543
  this.setMode(sessionId, value2);
1379
1544
  return { configOptions: this.options(session) };
1380
1545
  }
1546
+ if (configId !== "reasoning" && await this.host.setConfigOption?.(session, configId, value2)) return { configOptions: this.options(session) };
1381
1547
  if (configId !== "reasoning" || !EFFORTS.includes(value2)) throw new Error("Unknown agent setting");
1382
1548
  session.effort = value2;
1383
1549
  return { configOptions: this.options(session) };
@@ -1459,7 +1625,7 @@ ${submit.additionalContext.slice(0, CONTEXT_LIMIT$1)}`;
1459
1625
  }
1460
1626
  async commandList(session) {
1461
1627
  const lists = await Promise.all((this.host.extensions?.commands ?? []).map((provider2) => provider2.commands(session)));
1462
- return lists.flat();
1628
+ return [contextCommand(this.host, session), ...lists.flat()];
1463
1629
  }
1464
1630
  commandContext(session, turn) {
1465
1631
  return {
@@ -1529,12 +1695,12 @@ function hookSourcePaths(home, project, options = {}) {
1529
1695
  if (options.codex) paths.push({ dialect: "codex", path: `${home}/.codex/hooks.json` }, { dialect: "codex", path: `${project}/.codex/hooks.json` });
1530
1696
  return paths;
1531
1697
  }
1532
- const isObject = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
1698
+ const isObject$1 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
1533
1699
  const quote = (value2) => `'${value2.replace(/'/g, `'\\''`)}'`;
1534
1700
  function parse(source, warn) {
1535
- const hooks = isObject(source.config) ? source.config.hooks : void 0;
1701
+ const hooks = isObject$1(source.config) ? source.config.hooks : void 0;
1536
1702
  if (hooks === void 0) return [];
1537
- if (!isObject(hooks)) {
1703
+ if (!isObject$1(hooks)) {
1538
1704
  warn(`${source.path}: "hooks" must be an object`);
1539
1705
  return [];
1540
1706
  }
@@ -1542,19 +1708,22 @@ function parse(source, warn) {
1542
1708
  for (const [event, groups] of Object.entries(hooks)) {
1543
1709
  if (!EVENTS.has(event) || !Array.isArray(groups)) continue;
1544
1710
  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`);
1711
+ for (const entry of isObject$1(group) && Array.isArray(group.hooks) ? group.hooks : []) {
1712
+ if (!isObject$1(entry) || entry.type !== "command" || typeof entry.command !== "string" || !entry.command) {
1713
+ warn(`${source.path}: skipped a ${isObject$1(entry) ? entry.type ?? "untyped" : "malformed"} ${event} hook; Oxbit runs command hooks only`);
1548
1714
  continue;
1549
1715
  }
1550
1716
  const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : void 0;
1551
1717
  const seconds = typeof entry.timeout === "number" && entry.timeout > 0 ? entry.timeout : event === "UserPromptSubmit" ? 30 : 600;
1718
+ const matcher = typeof group.matcher === "string" ? group.matcher : "";
1719
+ const command = args ? [entry.command, ...args].map(quote).join(" ") : entry.command;
1552
1720
  handlers.push({
1721
+ id: [source.dialect, source.path, event, matcher, command].join("|"),
1553
1722
  dialect: source.dialect,
1554
1723
  path: source.path,
1555
1724
  event,
1556
- matcher: typeof group.matcher === "string" ? group.matcher : "",
1557
- command: args ? [entry.command, ...args].map(quote).join(" ") : entry.command,
1725
+ matcher,
1726
+ command,
1558
1727
  timeoutMs: seconds * 1e3,
1559
1728
  rule: typeof entry.if === "string" ? entry.if : void 0,
1560
1729
  async: entry.async === true,
@@ -1565,6 +1734,11 @@ function parse(source, warn) {
1565
1734
  }
1566
1735
  return handlers;
1567
1736
  }
1737
+ function listHooks(sources) {
1738
+ const warnings = [];
1739
+ 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 }));
1740
+ return { hooks, warnings };
1741
+ }
1568
1742
  function matchesHook(matcher, values) {
1569
1743
  if (!matcher || matcher === "*") return true;
1570
1744
  if (/^[\w\- ,|]+$/.test(matcher)) return matcher.split(/[|,]/).map((name) => name.trim()).some((name) => values.includes(name));
@@ -1594,7 +1768,7 @@ function ruleAllows(rule, names, input2) {
1594
1768
  }
1595
1769
  function fromJson(event, json2) {
1596
1770
  const outcome = {};
1597
- const specific = isObject(json2.hookSpecificOutput) ? json2.hookSpecificOutput : {};
1771
+ const specific = isObject$1(json2.hookSpecificOutput) ? json2.hookSpecificOutput : {};
1598
1772
  if (json2.continue === false) outcome.continue = false;
1599
1773
  if (typeof json2.stopReason === "string") outcome.stopReason = json2.stopReason;
1600
1774
  if (typeof json2.systemMessage === "string") outcome.systemMessage = json2.systemMessage;
@@ -1608,7 +1782,7 @@ function fromJson(event, json2) {
1608
1782
  outcome.block = true;
1609
1783
  if (typeof json2.reason === "string") outcome.reason = json2.reason;
1610
1784
  }
1611
- if (isObject(specific.updatedInput)) outcome.updatedInput = specific.updatedInput;
1785
+ if (isObject$1(specific.updatedInput)) outcome.updatedInput = specific.updatedInput;
1612
1786
  if (typeof specific.additionalContext === "string") outcome.additionalContext = specific.additionalContext;
1613
1787
  return outcome;
1614
1788
  }
@@ -1650,14 +1824,14 @@ function createHookRunner(options) {
1650
1824
  warn(message2);
1651
1825
  };
1652
1826
  const handlersOf = (source) => {
1653
- if (!isObject(source.config)) return parse(source, warnOnce);
1827
+ if (!isObject$1(source.config)) return parse(source, warnOnce);
1654
1828
  let handlers = cache.get(source.config);
1655
1829
  if (!handlers) cache.set(source.config, handlers = parse(source, warnOnce));
1656
1830
  return handlers;
1657
1831
  };
1658
- async function runOne(handler, event, input2, signal) {
1832
+ async function runOne(handler2, event, input2, signal) {
1659
1833
  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;
1834
+ const alias = name && handler2.dialect !== "oxbit" ? CLAUDE_TOOL_NAMES[name]?.[0] : void 0;
1661
1835
  const stdin = {
1662
1836
  transcript_path: options.transcriptPath?.(String(input2.session_id ?? "")) ?? "",
1663
1837
  ...input2,
@@ -1666,25 +1840,25 @@ function createHookRunner(options) {
1666
1840
  const cwd = String(input2.cwd ?? "");
1667
1841
  const controller = new AbortController();
1668
1842
  const abort = () => controller.abort();
1669
- const timer = setTimeout(abort, handler.timeoutMs);
1843
+ const timer = setTimeout(abort, handler2.timeoutMs);
1670
1844
  signal.addEventListener("abort", abort, { once: true });
1671
1845
  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}"`;
1846
+ const label = `${handler2.path}: ${event} hook "${handler2.command}"`;
1673
1847
  const failed2 = (detail) => {
1674
1848
  warn(`${label} ${detail}`);
1675
- return handler.failBlocks && BLOCKING.has(event) && !signal.aborted ? { block: true, reason: `${label} ${detail}` } : {};
1849
+ return handler2.failBlocks && BLOCKING.has(event) && !signal.aborted ? { block: true, reason: `${label} ${detail}` } : {};
1676
1850
  };
1677
1851
  try {
1678
- const result = await Promise.race([options.exec(handler.command, {
1852
+ const result = await Promise.race([options.exec(handler2.command, {
1679
1853
  stdin: JSON.stringify(stdin),
1680
1854
  cwd,
1681
1855
  env: { CLAUDE_PROJECT_DIR: cwd, OXBIT_PROJECT_DIR: cwd },
1682
- timeoutMs: handler.timeoutMs,
1856
+ timeoutMs: handler2.timeoutMs,
1683
1857
  signal: controller.signal
1684
1858
  }), killed]);
1685
- if (result.code === null) return signal.aborted ? {} : failed2(`timed out after ${handler.timeoutMs / 1e3} s`);
1859
+ if (result.code === null) return signal.aborted ? {} : failed2(`timed out after ${handler2.timeoutMs / 1e3} s`);
1686
1860
  if (result.code === 2) {
1687
- const reason = result.stderr.trim() || `Blocked by ${handler.command}`;
1861
+ const reason = result.stderr.trim() || `Blocked by ${handler2.command}`;
1688
1862
  if (BLOCKING.has(event)) return { block: true, reason };
1689
1863
  warn(`${label} exited with 2: ${reason}`);
1690
1864
  return {};
@@ -1694,7 +1868,7 @@ function createHookRunner(options) {
1694
1868
  if (stdout.startsWith("{")) {
1695
1869
  try {
1696
1870
  const json2 = JSON.parse(stdout);
1697
- if (isObject(json2)) return fromJson(event, json2);
1871
+ if (isObject$1(json2)) return fromJson(event, json2);
1698
1872
  } catch {
1699
1873
  }
1700
1874
  }
@@ -1713,21 +1887,21 @@ function createHookRunner(options) {
1713
1887
  async run(event, input2, signal) {
1714
1888
  if (signal.aborted) return {};
1715
1889
  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 {};
1890
+ if (sources.some((source) => isObject$1(source.config) && source.config.disableAllHooks === true)) return {};
1717
1891
  const tool = TOOL_EVENTS.has(event) && typeof input2.tool_name === "string" ? input2.tool_name : void 0;
1718
1892
  const names = tool ? [tool, ...CLAUDE_TOOL_NAMES[tool] ?? []] : void 0;
1719
1893
  const field = MATCH_FIELD[event];
1720
1894
  const values = names ?? (field && typeof input2[field] === "string" ? [input2[field]] : void 0);
1721
1895
  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);
1896
+ for (const handler2 of sources.flatMap(handlersOf)) {
1897
+ if (handler2.event !== event || values && !matchesHook(handler2.matcher, values) || options.skip?.(handler2)) continue;
1898
+ if (handler2.rule !== void 0 && !(names && ruleAllows(handler2.rule, names, isObject$1(input2.tool_input) ? input2.tool_input : {}))) continue;
1899
+ const key = `${handler2.command}\0${handler2.rule ?? ""}`;
1900
+ if (!selected.has(key)) selected.set(key, handler2);
1727
1901
  }
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);
1902
+ const outcomes = await Promise.all([...selected.values()].map((handler2) => {
1903
+ if (!handler2.async) return runOne(handler2, event, input2, signal);
1904
+ void runOne(handler2, event, input2, signal);
1731
1905
  return {};
1732
1906
  }));
1733
1907
  return mergeOutcomes(event, outcomes);
@@ -1771,7 +1945,7 @@ Before you decide the goal is achieved, treat completion as unproven:
1771
1945
  Blocked audit:
1772
1946
  - 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
1947
  - 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) => [
1948
+ const describe$1 = (goal) => [
1775
1949
  `Goal (${goal.status.replace("_", " ")}): ${goal.objective}`,
1776
1950
  `${goal.continuations} continuations. ${budgetLines(goal)}`,
1777
1951
  ...goal.note ? [`Note: ${goal.note}`] : []
@@ -1805,7 +1979,7 @@ function createGoal(options) {
1805
1979
  async run(args, context) {
1806
1980
  const { session } = context;
1807
1981
  const goal = goalOf(session.meta);
1808
- if (!args) return done(context, goal ? describe(goal) : "No goal is set. Start one with /goal <objective>.");
1982
+ if (!args) return done(context, goal ? describe$1(goal) : "No goal is set. Start one with /goal <objective>.");
1809
1983
  if (args === "clear") {
1810
1984
  delete session.meta.goal;
1811
1985
  publish(session);
@@ -1930,8 +2104,8 @@ function createLoop(options) {
1930
2104
  return loop?.status === "running" && owners.has(session.id) ? loop : void 0;
1931
2105
  };
1932
2106
  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;
2107
+ const finish = (session, loop, status2, text) => {
2108
+ loop.status = status2;
1935
2109
  delete loop.nextDelayMs;
1936
2110
  owners.delete(session.id);
1937
2111
  publish(session, loop);
@@ -2077,21 +2251,28 @@ class Instructions {
2077
2251
  this.options = options;
2078
2252
  }
2079
2253
  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) {
2254
+ label = "Instructions";
2255
+ /** The global and Claude files whether or not they exist, then the project files that load, without repeated paths. */
2256
+ async candidates(cwd) {
2082
2257
  const { files, global, claude } = this.options;
2083
2258
  const dirs = ancestors(cwd, await findGitRoot(files, cwd) ?? cwd).reverse();
2259
+ const own = async (path2, scope) => path2 ? { path: path2, scope, text: (await readText(files, path2))?.text } : void 0;
2084
2260
  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)))
2261
+ ...await Promise.all([own(global, "global"), own(claude, "claude")]),
2262
+ ...(await Promise.all(dirs.map((dir) => firstOf(files, dir)))).map((file2) => file2 && { ...file2, scope: "project" })
2087
2263
  ];
2088
2264
  return found.filter((file2, index) => !!file2 && found.findIndex((other) => other?.path === file2.path) === index);
2089
2265
  }
2266
+ /** Global files, then from the git root (or cwd) down to cwd the first non-empty AGENTS.override.md, AGENTS.md or CLAUDE.md. */
2267
+ async load(cwd) {
2268
+ return (await this.candidates(cwd)).flatMap(({ path: path2, text }) => text === void 0 ? [] : [{ path: path2, text }]);
2269
+ }
2090
2270
  async system(session) {
2091
2271
  const sections = [];
2092
2272
  let used = 0;
2093
2273
  let skipped = 0;
2094
2274
  for (const file2 of await this.load(session.cwd)) {
2275
+ if (this.options.skip?.(file2.path)) continue;
2095
2276
  const room = Math.min(FILE_LIMIT, TOTAL_LIMIT - used);
2096
2277
  if (room <= 0) {
2097
2278
  skipped++;
@@ -2112,7 +2293,8 @@ ${clipped.text}${clipped.clipped ? `
2112
2293
  }
2113
2294
  }
2114
2295
  const KEY = /^([\w-]+):(?:[ \t]+(.*))?$/;
2115
- const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2296
+ const NAME$1 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2297
+ const isSkillName = (name) => name.length <= 64 && NAME$1.test(name);
2116
2298
  const OUTPUT_LIMIT = 1e5;
2117
2299
  const scalar = (text) => {
2118
2300
  const quoted = /^(["'])(.*)\1\s*(?:#.*)?$/s.exec(text);
@@ -2168,7 +2350,7 @@ function toSkill(text, path2, folder, warnings) {
2168
2350
  }
2169
2351
  const name = string2("name") || folder;
2170
2352
  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)`);
2353
+ 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
2354
  else if (name !== folder) warnings.push(`${path2}: the name ${name} differs from the directory name ${folder}`);
2173
2355
  if (description.length > 1024) warnings.push(`${path2}: the description is longer than 1024 characters`);
2174
2356
  const tools = data["allowed-tools"];
@@ -2215,6 +2397,7 @@ class Skills {
2215
2397
  this.options = options;
2216
2398
  }
2217
2399
  options;
2400
+ label = "Skills";
2218
2401
  sets = /* @__PURE__ */ new Map();
2219
2402
  /** 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
2403
  async discover(cwd) {
@@ -2243,8 +2426,11 @@ class Skills {
2243
2426
  this.sets.set(cwd, set);
2244
2427
  return set;
2245
2428
  }
2429
+ shown(skills) {
2430
+ return this.options.hidden ? skills.filter((skill) => !this.options.hidden(skill.name)) : skills;
2431
+ }
2246
2432
  async system(session) {
2247
- const visible = (await this.discover(session.cwd)).skills.filter((skill) => skill.model);
2433
+ const visible = this.shown((await this.discover(session.cwd)).skills).filter((skill) => skill.model);
2248
2434
  if (!visible.length) return void 0;
2249
2435
  return [
2250
2436
  "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 +2440,7 @@ class Skills {
2254
2440
  ].join("\n");
2255
2441
  }
2256
2442
  async tools(session) {
2257
- const { skills } = this.sets.get(session.cwd) ?? await this.discover(session.cwd);
2443
+ const skills = this.shown((this.sets.get(session.cwd) ?? await this.discover(session.cwd)).skills);
2258
2444
  return skills.length ? [this.tool(skills)] : [];
2259
2445
  }
2260
2446
  tool(skills) {
@@ -2303,7 +2489,7 @@ ${files.map((file2) => `- ${file2}`).join("\n")}` : ""));
2303
2489
  if (!set) void this.discover(session.cwd).catch(() => {
2304
2490
  });
2305
2491
  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) => ({
2492
+ return [list2, ...this.shown(set?.skills ?? []).filter((skill) => skill.user && skill.name !== list2.name).map((skill) => ({
2307
2493
  name: skill.name,
2308
2494
  description: skill.description.slice(0, 200),
2309
2495
  hint: skill.hint,
@@ -2354,7 +2540,7 @@ function parseMcpJson(text, scope, env = {}, source = ".mcp.json") {
2354
2540
  }
2355
2541
  const entries = body?.mcpServers;
2356
2542
  if (!entries || typeof entries !== "object" || Array.isArray(entries)) return { servers: [], warnings: [`${source} has no "mcpServers" object`] };
2357
- const servers = [];
2543
+ const servers2 = [];
2358
2544
  for (const [name, entry] of Object.entries(entries)) {
2359
2545
  if (!entry || typeof entry !== "object" || entry.enabled === false || entry.disabled === true) continue;
2360
2546
  const missing = /* @__PURE__ */ new Set();
@@ -2370,7 +2556,7 @@ function parseMcpJson(text, scope, env = {}, source = ".mcp.json") {
2370
2556
  continue;
2371
2557
  }
2372
2558
  const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : [];
2373
- servers.push({
2559
+ servers2.push({
2374
2560
  name,
2375
2561
  scope,
2376
2562
  type: "stdio",
@@ -2384,7 +2570,7 @@ function parseMcpJson(text, scope, env = {}, source = ".mcp.json") {
2384
2570
  warnings.push(`MCP server "${name}" in ${source} has no url`);
2385
2571
  continue;
2386
2572
  }
2387
- servers.push({
2573
+ servers2.push({
2388
2574
  name,
2389
2575
  scope,
2390
2576
  type: type === "sse" ? "sse" : "http",
@@ -2398,11 +2584,11 @@ function parseMcpJson(text, scope, env = {}, source = ".mcp.json") {
2398
2584
  }
2399
2585
  for (const variable of missing) warnings.push(`MCP server "${name}" references \${${variable}}, which is not set`);
2400
2586
  }
2401
- return { servers, warnings };
2587
+ return { servers: servers2, warnings };
2402
2588
  }
2403
2589
  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
2590
  function parseAcpServers(list2) {
2405
- const servers = [];
2591
+ const servers2 = [];
2406
2592
  const warnings = [];
2407
2593
  for (const entry of Array.isArray(list2) ? list2 : []) {
2408
2594
  const name = typeof entry?.name === "string" && entry.name ? entry.name : void 0;
@@ -2411,11 +2597,11 @@ function parseAcpServers(list2) {
2411
2597
  continue;
2412
2598
  }
2413
2599
  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) });
2600
+ if (typeof entry.url === "string" && entry.url) servers2.push({ name, scope: "session", type: entry.type, url: entry.url, headers: pairs(entry.headers) });
2415
2601
  else warnings.push(`MCP server "${name}" from the client has no url`);
2416
2602
  } else if (entry.type === void 0 || entry.type === "stdio") {
2417
2603
  if (typeof entry.command === "string" && entry.command)
2418
- servers.push({
2604
+ servers2.push({
2419
2605
  name,
2420
2606
  scope: "session",
2421
2607
  type: "stdio",
@@ -2426,7 +2612,7 @@ function parseAcpServers(list2) {
2426
2612
  else warnings.push(`MCP server "${name}" from the client has no command`);
2427
2613
  } else warnings.push(`MCP server "${name}" from the client uses the "${entry.type}" transport, which Oxbit Agent does not support`);
2428
2614
  }
2429
- return { servers, warnings };
2615
+ return { servers: servers2, warnings };
2430
2616
  }
2431
2617
  function mergeServers(...lists) {
2432
2618
  const byName = /* @__PURE__ */ new Map();
@@ -2435,11 +2621,11 @@ function mergeServers(...lists) {
2435
2621
  }
2436
2622
  const MODERN_ERROR_CODES = /* @__PURE__ */ new Set([-32020, -32021, -32022]);
2437
2623
  class McpError extends Error {
2438
- constructor(message2, code, data, status) {
2624
+ constructor(message2, code, data, status2) {
2439
2625
  super(message2);
2440
2626
  this.code = code;
2441
2627
  this.data = data;
2442
- this.status = status;
2628
+ this.status = status2;
2443
2629
  }
2444
2630
  code;
2445
2631
  data;
@@ -2580,7 +2766,7 @@ async function bodyText(response, limit = ERROR_BODY_LIMIT) {
2580
2766
  }
2581
2767
  return text.slice(0, limit);
2582
2768
  }
2583
- const httpError = (status, body, message2 = `HTTP ${status}`) => Object.assign(new Error(message2), { status, body });
2769
+ const httpError = (status2, body, message2 = `HTTP ${status2}`) => Object.assign(new Error(message2), { status: status2, body });
2584
2770
  function base64url(bytes) {
2585
2771
  let binary = "";
2586
2772
  for (const byte of bytes) binary += String.fromCharCode(byte);
@@ -2639,14 +2825,14 @@ class HttpTransport {
2639
2825
  for (const [key, value2] of Object.entries(extra)) headers.set(key, value2);
2640
2826
  return headers;
2641
2827
  }
2642
- async exchange(init, signal, use) {
2828
+ async exchange(init, signal, use2) {
2643
2829
  const controller = new AbortController();
2644
2830
  const abort = () => controller.abort();
2645
2831
  signal?.addEventListener("abort", abort, { once: true });
2646
2832
  if (signal?.aborted) abort();
2647
2833
  this.controllers.add(controller);
2648
2834
  try {
2649
- return await use(await this.fetch(this.url, { ...init, signal: controller.signal }));
2835
+ return await use2(await this.fetch(this.url, { ...init, signal: controller.signal }));
2650
2836
  } catch (error) {
2651
2837
  if (controller.signal.aborted && !(error instanceof McpError)) throw new McpError("The MCP request was cancelled");
2652
2838
  throw error;
@@ -3124,8 +3310,6 @@ function renderResult(result) {
3124
3310
  }
3125
3311
  const CLIENT_INFO = { name: "oxbit-agent", version: "1" };
3126
3312
  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
3313
  class McpTools {
3130
3314
  constructor(options) {
3131
3315
  this.options = options;
@@ -3144,7 +3328,7 @@ class McpTools {
3144
3328
  const text = await this.options.read(path2).catch((error) => void warnings.push(`${path2}: ${message$1(error)}`));
3145
3329
  if (text === void 0) return;
3146
3330
  const parsed = parseMcpJson(text, scope, this.options.env ?? {}, path2);
3147
- lists.push(parsed.servers);
3331
+ lists.push(this.options.skip ? parsed.servers.filter((server) => !this.options.skip(server)) : parsed.servers);
3148
3332
  warnings.push(...parsed.warnings);
3149
3333
  };
3150
3334
  if (this.options.globalConfig) await load(this.options.globalConfig, "global");
@@ -3155,19 +3339,21 @@ class McpTools {
3155
3339
  const names = text === void 0 ? [] : parseMcpJson(text, "project", {}, project).servers.map((server) => server.name);
3156
3340
  if (names.length) warnings.push(`${project} is not trusted, so its servers are off: ${names.join(", ")}.${this.options.trustHint ? ` ${this.options.trustHint}` : ""}`);
3157
3341
  }
3158
- const acp2 = parseAcpServers(session.meta.mcpServers);
3159
- lists.push(acp2.servers);
3160
- warnings.push(...acp2.warnings);
3342
+ const acp = parseAcpServers(session.meta.mcpServers);
3343
+ lists.push(acp.servers);
3344
+ warnings.push(...acp.warnings);
3161
3345
  return { servers: mergeServers(...lists), warnings };
3162
3346
  }
3163
3347
  async state(session) {
3164
- const key = sessionKey(session);
3165
- const current = this.sessions.get(session.id);
3166
- if (current?.key === key) return current;
3167
3348
  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 };
3349
+ const { servers: servers2, warnings } = await this.configure(session);
3350
+ const key = JSON.stringify([session.cwd, servers2]);
3170
3351
  const previous = this.sessions.get(session.id);
3352
+ if (previous?.key === key) {
3353
+ previous.warnings = warnings;
3354
+ return previous;
3355
+ }
3356
+ const state = { key, cwd: session.cwd, servers: servers2.map((config) => ({ config, tools: 0, excluded: 0 })), warnings, closed: false, active: 0 };
3171
3357
  this.sessions.set(session.id, state);
3172
3358
  if (previous) await this.shut(previous);
3173
3359
  return state;
@@ -3308,10 +3494,11 @@ class McpTools {
3308
3494
  ...server.error ? { error: server.error } : {}
3309
3495
  })) };
3310
3496
  }
3497
+ /** The /mcp status text: one line per server of the last tools() call, then warnings. */
3311
3498
  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) {
3499
+ const { servers: servers2, warnings } = this.status(sessionId);
3500
+ const lines2 = servers2.length ? [`MCP servers (${servers2.length}):`] : ["No MCP servers are configured for this session."];
3501
+ for (const server of servers2) {
3315
3502
  const detail = server.state === "connected" ? `connected, ${server.tools} tools${server.excluded ? `, ${server.excluded} left out` : ""}` : server.state === "failed" ? `failed: ${server.error}` : server.state;
3316
3503
  lines2.push(`- ${server.name} (${server.scope}, ${server.type}${server.version ? `, ${server.version}` : ""}): ${detail}`);
3317
3504
  }
@@ -3594,7 +3781,7 @@ const PLAN_ERRORS = {
3594
3781
  subscription_sharing_invalid_user: "ChatGPT could not validate this account. Sign in with ChatGPT again.",
3595
3782
  subscription_sharing_route_not_supported: "ChatGPT plan usage does not support this request route."
3596
3783
  };
3597
- function describePlanError(status, body) {
3784
+ function describePlanError(status2, body) {
3598
3785
  let parsed;
3599
3786
  try {
3600
3787
  parsed = JSON.parse(body);
@@ -3603,7 +3790,7 @@ function describePlanError(status, body) {
3603
3790
  }
3604
3791
  const code = parsed?.error?.code;
3605
3792
  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);
3793
+ 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
3794
  return text ? JSON.stringify({ error: { message: text, code } }) : body;
3608
3795
  }
3609
3796
  const STORE_KEY = "chatgpt";
@@ -4066,6 +4253,7 @@ function serveStdio({ input: input2, output, factory, log }) {
4066
4253
  void server.peer.idle().then(() => server.close()).then(resolve2);
4067
4254
  }));
4068
4255
  }
4256
+ const stringList = (value2) => Array.isArray(value2) ? value2.filter((item) => typeof item === "string") : [];
4069
4257
  const agentHome = (env = process.env) => env.OXBIT_AGENT_HOME || join(homedir(), ".oxbit", "agent");
4070
4258
  function agentPaths(home) {
4071
4259
  return {
@@ -4113,8 +4301,14 @@ class AgentConfig {
4113
4301
  settings;
4114
4302
  paths;
4115
4303
  static async load(home = agentHome()) {
4116
- const settings = await readJson(agentPaths(home).settings, {});
4117
- return new AgentConfig(home, settings && typeof settings === "object" ? settings : {});
4304
+ const config = new AgentConfig(home, {});
4305
+ await config.reload();
4306
+ return config;
4307
+ }
4308
+ /** Reads settings.json again. */
4309
+ async reload() {
4310
+ const settings = await readJson(this.paths.settings, {});
4311
+ this.settings = settings && typeof settings === "object" ? settings : {};
4118
4312
  }
4119
4313
  /** Creates the home and skills directories. */
4120
4314
  async ensure() {
@@ -4217,7 +4411,7 @@ class FileCatalogCache {
4217
4411
  }
4218
4412
  }
4219
4413
  function agentVersion() {
4220
- return "0.10.1";
4414
+ return "0.11.1";
4221
4415
  }
4222
4416
  const clone = (message2) => JSON.parse(JSON.stringify(message2));
4223
4417
  class MemoryConnection {
@@ -4288,10 +4482,10 @@ class MemoryConnection {
4288
4482
  return () => void this.listeners.delete(listener);
4289
4483
  }
4290
4484
  /** One handler at a time. Without one, permission prompts resolve as "cancelled". */
4291
- onPermission(handler) {
4292
- this.permission = handler;
4485
+ onPermission(handler2) {
4486
+ this.permission = handler2;
4293
4487
  return () => {
4294
- if (this.permission === handler) this.permission = void 0;
4488
+ if (this.permission === handler2) this.permission = void 0;
4295
4489
  };
4296
4490
  }
4297
4491
  close() {
@@ -4299,14 +4493,14 @@ class MemoryConnection {
4299
4493
  }
4300
4494
  async serve(method, params) {
4301
4495
  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" } };
4496
+ const handler2 = this.permission;
4497
+ if (!handler2) return { outcome: { outcome: "cancelled" } };
4304
4498
  const controller = new AbortController();
4305
4499
  const set = this.waiting.get(params.sessionId) ?? /* @__PURE__ */ new Set();
4306
4500
  this.waiting.set(params.sessionId, set.add(controller));
4307
4501
  const cancelled = new Promise((resolve2) => controller.signal.addEventListener("abort", () => resolve2({ outcome: "cancelled" }), { once: true }));
4308
4502
  try {
4309
- return { outcome: await Promise.race([handler(params, controller.signal), cancelled]) };
4503
+ return { outcome: await Promise.race([handler2(params, controller.signal), cancelled]) };
4310
4504
  } finally {
4311
4505
  set.delete(controller);
4312
4506
  if (!set.size) this.waiting.delete(params.sessionId);
@@ -4611,12 +4805,13 @@ function nodeMcp(config, log) {
4611
4805
  env: process.env,
4612
4806
  trustProject: (cwd) => projectTrusted(config.paths.settings, cwd),
4613
4807
  trustHint: `Add the project path to "trustedProjects" in ${config.paths.settings} to run them.`,
4808
+ skip: (server) => stringList(config.settings.disabledMcpServers).includes(server.name),
4614
4809
  clientInfo: { name: "oxbit-agent", version: agentVersion() }
4615
4810
  });
4616
4811
  }
4617
4812
  const PROVIDER_IDS = PROVIDERS.map((spec) => spec.id);
4618
4813
  const LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
4619
- const OLLAMA = "http://127.0.0.1:11434";
4814
+ const OLLAMA$1 = "http://127.0.0.1:11434";
4620
4815
  const DETECTED = ["anthropic", "openai", "openrouter"];
4621
4816
  function normalizeBase(input2) {
4622
4817
  let url;
@@ -4642,7 +4837,7 @@ async function chatgptSignedIn(auth) {
4642
4837
  const keyFor = async (spec, input2, baseUrl) => !!(await input2.auth.get(apiKeySlot(spec.id, baseUrl)) || spec.env && input2.env[spec.env]);
4643
4838
  function baseFor(spec, settings, env) {
4644
4839
  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;
4840
+ 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
4841
  if (spec.id === "openai-compatible" && !configured) throw new Error('Set providers["openai-compatible"].baseUrl in settings.json or OXBIT_AGENT_BASE_URL');
4647
4842
  return configured ? normalizeBase(configured) : void 0;
4648
4843
  }
@@ -4661,8 +4856,10 @@ async function selectProvider(input2) {
4661
4856
  const { settings, env } = input2;
4662
4857
  const signedIn = await chatgptSignedIn(input2.auth);
4663
4858
  let detected;
4859
+ const disabled = stringList(settings.disabledProviders);
4664
4860
  if (!input2.provider && !settings.provider)
4665
4861
  for (const id2 of DETECTED) {
4862
+ if (disabled.includes(id2)) continue;
4666
4863
  if (await keyFor(providerSpec(id2), input2) || id2 === "openai" && signedIn) {
4667
4864
  detected = id2;
4668
4865
  break;
@@ -4690,7 +4887,7 @@ function missingKey(choice) {
4690
4887
  }
4691
4888
  const VALID_ID = /^[\w.-]{1,128}$/;
4692
4889
  const HEADER_LIMIT = 16 * 1024 * 1024;
4693
- async function firstLine$1(file2) {
4890
+ async function firstLine$2(file2) {
4694
4891
  const handle = await open(file2, "r");
4695
4892
  try {
4696
4893
  const chunks = [];
@@ -4770,7 +4967,7 @@ class JsonlSessionStore {
4770
4967
  const records = [];
4771
4968
  for (const name of names.filter((item) => item.endsWith(".jsonl"))) {
4772
4969
  try {
4773
- const head = summary(JSON.parse(await firstLine$1(join(dir, name))));
4970
+ const head = summary(JSON.parse(await firstLine$2(join(dir, name))));
4774
4971
  if (head && head.cwd === cwd) records.push(head);
4775
4972
  } catch {
4776
4973
  }
@@ -5007,6 +5204,9 @@ function undoCommands(store) {
5007
5204
  };
5008
5205
  return { commands: () => [undo, redo] };
5009
5206
  }
5207
+ const SETTINGS_COMMANDS = ["config", "skills", "hooks", "mcp", "instructions", "providers"];
5208
+ const settingsOf = (config) => config.settings;
5209
+ const compactAt$1 = (value2) => typeof value2 === "number" && value2 >= 50 && value2 <= 95 ? value2 : void 0;
5010
5210
  const nodeFiles = {
5011
5211
  async list(path2) {
5012
5212
  const entries = await readdir(path2, { withFileTypes: true }).catch(() => []);
@@ -5048,9 +5248,10 @@ const nodeHookExec = (command, { stdin, cwd, env, timeoutMs, signal }) => new Pr
5048
5248
  });
5049
5249
  child.on("close", finish);
5050
5250
  });
5051
- function hookSources(config, settings, home, log) {
5251
+ function hookSources(config, home, log) {
5052
5252
  const cache = /* @__PURE__ */ new Map();
5053
5253
  return async (cwd) => {
5254
+ const settings = settingsOf(config);
5054
5255
  const paths = hookSourcePaths(home, cwd, { claude: settings.claudeHooks === true, codex: settings.codexHooks === true });
5055
5256
  paths[0] = { ...paths[0], path: config.paths.settings };
5056
5257
  const sources = await Promise.all(paths.map(async ({ dialect, path: path2 }) => {
@@ -5071,151 +5272,344 @@ function hookSources(config, settings, home, log) {
5071
5272
  return sources.filter((source) => !!source);
5072
5273
  };
5073
5274
  }
5074
- function coreExtensions(config, update, options = {}) {
5075
- const settings = config.settings;
5076
- const { home = homedir(), log } = options;
5077
- const instructions = new Instructions({
5275
+ function instructionsFor(config, home) {
5276
+ return new Instructions({
5078
5277
  files: nodeFiles,
5079
5278
  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
5279
+ get claude() {
5280
+ return settingsOf(config).claudeInstructions ? join(home, ".claude", "CLAUDE.md") : void 0;
5281
+ },
5282
+ get append() {
5283
+ const append = settingsOf(config).appendSystemPrompt;
5284
+ return typeof append === "string" ? append : void 0;
5285
+ },
5286
+ skip: (path2) => stringList(settingsOf(config).disabledInstructions).includes(path2)
5287
+ });
5288
+ }
5289
+ function skillsFor(config, home) {
5290
+ return new Skills({
5291
+ files: nodeFiles,
5292
+ roots: [config.paths.skills, join(home, ".agents", "skills"), join(home, ".claude", "skills")],
5293
+ hidden: (name) => stringList(settingsOf(config).disabledSkills).includes(name)
5294
+ });
5295
+ }
5296
+ function coreExtensions(config, update, options = {}) {
5297
+ const { home = homedir(), log } = options;
5298
+ const instructions = instructionsFor(config, home);
5299
+ const skills = skillsFor(config, home);
5300
+ const reserved = new Set(SETTINGS_COMMANDS);
5301
+ const compaction = new Compaction({
5302
+ get auto() {
5303
+ return settingsOf(config).autoCompact !== false;
5304
+ },
5305
+ get percent() {
5306
+ return compactAt$1(settingsOf(config).autoCompactAt);
5307
+ }
5082
5308
  });
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
5309
  const control = controlExtensions({ update });
5086
5310
  return {
5087
5311
  context: [instructions, ...control.context, skills],
5088
5312
  tools: [...control.tools, skills],
5089
- commands: [compaction, ...control.commands, { commands: async (session) => (await skills.discover(session.cwd), skills.commands(session)) }],
5313
+ commands: [compaction, ...control.commands, { commands: async (session) => (await skills.discover(session.cwd), skills.commands(session).filter((command) => !reserved.has(command.name))) }],
5090
5314
  continuations: control.continuations,
5091
5315
  compactor: compaction,
5092
- hooks: createHookRunner({ sources: hookSources(config, settings, home, log), exec: nodeHookExec, onWarning: log })
5316
+ hooks: createHookRunner({
5317
+ sources: hookSources(config, home, log),
5318
+ exec: nodeHookExec,
5319
+ onWarning: log,
5320
+ skip: (hook) => stringList(settingsOf(config).disabledHooks).includes(hook.id)
5321
+ })
5093
5322
  };
5094
5323
  }
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;
5324
+ const userHome = (deps) => deps.home ?? homedir();
5325
+ async function setListed(config, key, value2, listed) {
5326
+ const list2 = stringList(config.settings[key]).filter((item) => item !== value2);
5327
+ if (listed) list2.push(value2);
5328
+ await config.saveSettings({ [key]: list2.length ? list2 : void 0 });
5329
+ }
5330
+ const unknownRow = (rowId) => new Error(`Unknown setting ${rowId}`);
5331
+ const tilde = (path2, home) => path2 === home ? "~" : path2.startsWith(`${home}/`) ? `~${path2.slice(home.length)}` : path2;
5332
+ const firstLine$1 = (text, max = 80) => {
5333
+ const line = text.split("\n").map((part) => part.trim()).find((part) => part) ?? "";
5334
+ return line.length > max ? `${line.slice(0, max - 1)}…` : line;
5105
5335
  };
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 };
5336
+ const warningRows = (warnings) => warnings.map((warning, index) => ({ id: `warning:${index}`, section: "Warnings", label: warning }));
5337
+ const PERCENTS = [50, 60, 70, 80, 90];
5338
+ const MODE_OPTIONS = MODES.map(({ id: id2, name }) => ({ value: id2, name }));
5339
+ const ON_OFF = [{ value: "on", name: "On" }, { value: "off", name: "Off" }];
5340
+ const FLAGS = {
5341
+ "auto-compact": { key: "autoCompact", label: "Auto-compact", detail: "Summarize the conversation when the context window fills", on: true },
5342
+ "claude-instructions": { key: "claudeInstructions", label: "Load ~/.claude/CLAUDE.md", detail: "After the global AGENTS.md" },
5343
+ "claude-hooks": { key: "claudeHooks", label: "Claude hooks", detail: "~/.claude/settings.json and the project's .claude settings files" },
5344
+ "codex-hooks": { key: "codexHooks", label: "Codex hooks", detail: "~/.codex/hooks.json and the project's .codex/hooks.json" },
5345
+ "all-project-mcp": { key: "trustProjectMcp", label: "MCP servers in every project", detail: "Run .mcp.json servers without trusting each folder" }
5346
+ };
5347
+ const flagOn = (config, id2) => {
5348
+ const flag = FLAGS[id2];
5349
+ return flag.on ? config.settings[flag.key] !== false : config.settings[flag.key] === true;
5350
+ };
5351
+ const flagRow = (config, id2, section) => ({ id: id2, section, label: FLAGS[id2].label, detail: FLAGS[id2].detail, enabled: flagOn(config, id2) });
5352
+ async function setFlag(config, id2, enabled) {
5353
+ await config.saveSettings({ [FLAGS[id2].key]: enabled });
5354
+ return { message: `${FLAGS[id2].label} is ${enabled ? "on" : "off"}` };
5355
+ }
5356
+ function compactAt(config) {
5357
+ const value2 = config.settings.autoCompactAt;
5358
+ return typeof value2 === "number" && value2 >= 50 && value2 <= 95 ? String(value2) : "default";
5359
+ }
5360
+ function compactOptions(config) {
5361
+ const percents = [.../* @__PURE__ */ new Set([...PERCENTS, Number(compactAt(config)) || 50])].sort((a, b) => a - b);
5362
+ return [{ value: "default", name: "Default" }, ...percents.map((percent) => ({ value: String(percent), name: `${percent}%` }))];
5363
+ }
5364
+ async function setCompactAt(config, value2) {
5365
+ const percent = value2 === "default" ? void 0 : Number(value2.replace(/%$/, ""));
5366
+ 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");
5367
+ await config.saveSettings({ autoCompactAt: percent });
5368
+ return { message: percent ? `Auto-compact starts at ${percent}% of the context window` : "Auto-compact uses the default threshold" };
5369
+ }
5370
+ const configPanel = {
5371
+ async panel({ config }, { cwd }) {
5372
+ const listed = stringList(config.settings.trustedProjects).includes(cwd);
5373
+ const trusted = await projectTrusted(config.paths.settings, cwd);
5374
+ 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";
5375
+ return { id: "config", title: "Config", rows: [
5376
+ {
5377
+ id: "mode",
5378
+ section: "Permissions",
5379
+ label: "Permission mode",
5380
+ detail: "Saved for new sessions and applied to this one",
5381
+ choice: { value: config.settings.mode ?? "ask", options: MODE_OPTIONS }
5382
+ },
5383
+ flagRow(config, "auto-compact", "Context"),
5384
+ { id: "compact-at", section: "Context", label: "Compact at", detail: "Share of the context window", choice: { value: compactAt(config), options: compactOptions(config) } },
5385
+ flagRow(config, "claude-instructions", "Instructions"),
5386
+ flagRow(config, "claude-hooks", "Hooks"),
5387
+ flagRow(config, "codex-hooks", "Hooks"),
5388
+ { id: "project-mcp", section: "MCP", label: "Project MCP servers for this folder", detail: how, enabled: trusted },
5389
+ flagRow(config, "all-project-mcp", "MCP")
5390
+ ] };
5391
+ },
5392
+ async toggle({ config }, rowId, enabled, { cwd }) {
5393
+ if (rowId in FLAGS) return setFlag(config, rowId, enabled);
5394
+ if (rowId !== "project-mcp") throw unknownRow(rowId);
5395
+ await setListed(config, "trustedProjects", cwd, enabled);
5396
+ const still = !enabled && await projectTrusted(config.paths.settings, cwd);
5397
+ 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` };
5398
+ },
5399
+ async choose({ config }, rowId, value2) {
5400
+ if (rowId === "compact-at") return setCompactAt(config, value2);
5401
+ if (rowId !== "mode") throw unknownRow(rowId);
5402
+ const mode = MODES.find((item) => item.id === value2);
5403
+ if (!mode) throw new Error(`Unknown mode ${value2}. Use one of: ${MODES.map((item) => item.id).join(", ")}`);
5404
+ await config.saveSettings({ mode: mode.id });
5405
+ return { mode: mode.id, message: `Permission mode: ${mode.name}` };
5119
5406
  }
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");
5407
+ };
5408
+ function compactConfigOptions(config) {
5409
+ return {
5410
+ configOptions: () => [
5411
+ { id: "auto-compact", name: "Auto-compact", type: "select", currentValue: flagOn(config, "auto-compact") ? "on" : "off", options: ON_OFF },
5412
+ { id: "compact-at", name: "Compact at", type: "select", currentValue: compactAt(config), options: compactOptions(config) }
5413
+ ],
5414
+ async setConfigOption(configId, value2) {
5415
+ if (configId === "auto-compact" && (value2 === "on" || value2 === "off")) await setFlag(config, "auto-compact", value2 === "on");
5416
+ else if (configId === "compact-at") await setCompactAt(config, value2);
5417
+ else return false;
5418
+ return true;
5137
5419
  }
5138
- await mkdir(dirname(path2), { recursive: true });
5139
- await writeFile$1(path2, params.content);
5140
- return {};
5141
- }
5142
- dispose() {
5143
- this.terminals.dispose();
5144
- }
5420
+ };
5145
5421
  }
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
- };
5156
- }
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 };
5422
+ async function parsedHooks(deps, cwd) {
5423
+ const sources = await hookSources(deps.config, userHome(deps))(cwd);
5424
+ const off = sources.find((source) => source.config?.disableAllHooks === true);
5425
+ return { ...listHooks(sources), off: off?.path };
5426
+ }
5427
+ const hooksPanel = {
5428
+ async panel(deps, { cwd }) {
5429
+ const home = userHome(deps);
5430
+ const { hooks, warnings, off } = await parsedHooks(deps, cwd);
5431
+ const disabled = new Set(stringList(deps.config.settings.disabledHooks));
5432
+ const rows = [
5433
+ flagRow(deps.config, "claude-hooks", "Sources"),
5434
+ flagRow(deps.config, "codex-hooks", "Sources"),
5435
+ ...hooks.map((hook) => ({
5436
+ id: hook.id,
5437
+ section: "Hooks",
5438
+ label: `${hook.event} ${hook.matcher}`.trim(),
5439
+ detail: `${hook.command} · ${tilde(hook.path, home)}`,
5440
+ enabled: !disabled.has(hook.id)
5441
+ }))
5442
+ ];
5443
+ if (!hooks.length) rows.push({ id: "none", section: "Hooks", label: "No command hooks in the loaded files" });
5444
+ rows.push(...warningRows(warnings));
5445
+ return {
5446
+ id: "hooks",
5447
+ title: "Hooks",
5448
+ rows,
5449
+ 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.`
5450
+ };
5451
+ },
5452
+ async toggle(deps, rowId, enabled, { cwd }) {
5453
+ if (rowId === "claude-hooks" || rowId === "codex-hooks") return setFlag(deps.config, rowId, enabled);
5454
+ const hook = (await parsedHooks(deps, cwd)).hooks.find((item) => item.id === rowId);
5455
+ if (!hook && !stringList(deps.config.settings.disabledHooks).includes(rowId)) throw unknownRow(rowId);
5456
+ await setListed(deps.config, "disabledHooks", rowId, !enabled);
5457
+ return { message: `${hook ? `${hook.event} hook "${hook.command}"` : "The hook"} is ${enabled ? "on" : "off"}` };
5458
+ }
5459
+ };
5460
+ const SECTIONS = { global: "Global files", claude: "Global files", project: "Project files" };
5461
+ const instructionsPanel = {
5462
+ async panel(deps, { cwd }) {
5463
+ const home = userHome(deps);
5464
+ const append = deps.config.settings.appendSystemPrompt;
5465
+ const prompt = typeof append === "string" ? append : "";
5466
+ const disabled = new Set(stringList(deps.config.settings.disabledInstructions));
5467
+ const files = await instructionsFor(deps.config, home).candidates(cwd);
5468
+ const rows = [
5469
+ {
5470
+ id: "prompt",
5471
+ section: "Global prompt",
5472
+ label: "Global prompt",
5473
+ detail: firstLine$1(prompt) || "none",
5474
+ actions: [{ id: "edit", label: "Edit", inputs: [{ label: "Text added after the instruction files (empty clears it)", value: prompt }] }]
5475
+ },
5476
+ ...files.map((file2) => ({
5477
+ id: file2.path,
5478
+ section: SECTIONS[file2.scope],
5479
+ label: tilde(file2.path, home),
5480
+ ...file2.text === void 0 ? { detail: "not found" } : { detail: firstLine$1(file2.text), enabled: !disabled.has(file2.path) }
5481
+ }))
5482
+ ];
5483
+ return { id: "instructions", title: "Instructions", rows, footer: "Changes apply to the next turn's system prompt." };
5484
+ },
5485
+ async toggle(deps, rowId, enabled, { cwd }) {
5486
+ const files = await instructionsFor(deps.config, userHome(deps)).candidates(cwd);
5487
+ const known = files.some((file2) => file2.path === rowId && file2.text !== void 0) || stringList(deps.config.settings.disabledInstructions).includes(rowId);
5488
+ if (!known) throw unknownRow(rowId);
5489
+ await setListed(deps.config, "disabledInstructions", rowId, !enabled);
5490
+ return { message: `${tilde(rowId, userHome(deps))} is ${enabled ? "on" : "off"}` };
5491
+ },
5492
+ async act({ config }, rowId, actionId, inputs) {
5493
+ if (rowId !== "prompt" || actionId !== "edit") throw unknownRow(`${rowId} ${actionId}`);
5494
+ const text = inputs[0] ?? "";
5495
+ await config.saveSettings({ appendSystemPrompt: text.trim() ? text : void 0 });
5496
+ return { message: text.trim() ? "Saved the global prompt" : "Cleared the global prompt" };
5497
+ }
5498
+ };
5499
+ const NAME = /^[\w-]+$/;
5500
+ const isObject = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
5501
+ function splitCommandLine(line) {
5502
+ const words = [];
5503
+ let word = "", quote2 = "", started = false;
5504
+ for (let index = 0; index < line.length; index++) {
5505
+ const char = line[index];
5506
+ if (quote2) {
5507
+ if (char === quote2) quote2 = "";
5508
+ else if (char === "\\" && quote2 === '"' && index + 1 < line.length) word += line[++index];
5509
+ else word += char;
5510
+ } else if (char === '"' || char === "'") [quote2, started] = [char, true];
5511
+ else if (char === "\\" && index + 1 < line.length) [word, started] = [word + line[++index], true];
5512
+ else if (/\s/.test(char)) {
5513
+ if (started) words.push(word);
5514
+ [word, started] = ["", false];
5515
+ } else [word, started] = [word + char, true];
5516
+ }
5517
+ if (quote2) throw new Error("The command line has an unclosed quote");
5518
+ if (started) words.push(word);
5519
+ return words;
5520
+ }
5521
+ function serverEntry(input2) {
5522
+ const text = input2.trim();
5523
+ if (/^https?:\/\//i.test(text)) {
5524
+ if (!URL.canParse(text)) throw new Error(`Invalid URL: ${text}`);
5525
+ return { type: "http", url: text };
5526
+ }
5527
+ const [command, ...args] = splitCommandLine(text);
5528
+ if (!command) throw new Error("Enter a URL or a command line");
5529
+ return { command, args };
5530
+ }
5531
+ const describe = (entry) => {
5532
+ if (!isObject(entry)) return "invalid entry";
5533
+ const off = entry.enabled === false || entry.disabled === true ? ", off in the file" : "";
5534
+ if (typeof entry.url === "string") return `${typeof entry.type === "string" ? entry.type : "http"} ${entry.url}${off}`;
5535
+ const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : [];
5536
+ return `stdio ${[entry.command, ...args].join(" ")}${off}`;
5537
+ };
5538
+ async function servers(path2) {
5539
+ try {
5540
+ const body = await readJson(path2, {});
5541
+ const entries = isObject(body) && isObject(body.mcpServers) ? body.mcpServers : {};
5542
+ return { body: isObject(body) ? body : {}, entries };
5543
+ } catch (error) {
5544
+ return { body: {}, entries: {}, error: error.message };
5545
+ }
5546
+ }
5547
+ async function writeServers(config, change) {
5548
+ const { body, entries, error } = await servers(config.paths.mcp);
5549
+ if (error) throw new Error(error);
5550
+ const next = { ...entries };
5551
+ change(next);
5552
+ await writeJson(config.paths.mcp, { ...body, mcpServers: next }, 384);
5553
+ }
5554
+ const mcpPanel = {
5555
+ async panel(deps, { cwd }) {
5556
+ const { config } = deps;
5557
+ const home = userHome(deps);
5558
+ const disabled = new Set(stringList(config.settings.disabledMcpServers));
5559
+ const user = await servers(config.paths.mcp);
5560
+ const projectPath = join(cwd, ".mcp.json");
5561
+ const project = await servers(projectPath);
5562
+ const trust = await projectTrusted(config.paths.settings, cwd) ? "this folder is trusted" : "this folder is not trusted; turn on Project MCP servers in /config";
5563
+ const rows = [
5564
+ ...Object.entries(user.entries).map(([name, entry]) => ({
5565
+ id: `user:${name}`,
5566
+ section: `User · ${tilde(config.paths.mcp, home)}`,
5567
+ label: name,
5568
+ detail: describe(entry),
5569
+ enabled: !disabled.has(name),
5570
+ actions: [{ id: "remove", label: "Remove" }]
5571
+ })),
5572
+ ...Object.entries(project.entries).map(([name, entry]) => ({
5573
+ id: `project:${name}`,
5574
+ section: `Project · ${tilde(projectPath, home)}`,
5575
+ label: name,
5576
+ detail: `${describe(entry)} · ${trust}`,
5577
+ enabled: !disabled.has(name)
5578
+ })),
5579
+ { id: "add", section: "Add", label: "Add server", detail: `Saved in ${tilde(config.paths.mcp, home)}`, actions: [{
5580
+ id: "add",
5581
+ label: "Add server",
5582
+ inputs: [{ label: "Name", placeholder: "github" }, { label: "URL or command line", placeholder: "https://example.com/mcp or npx -y @scope/server" }]
5583
+ }] },
5584
+ ...[user.error, project.error].filter((error) => !!error).map((error, index) => ({ id: `warning:${index}`, section: "Warnings", label: error }))
5585
+ ];
5586
+ return { id: "mcp", title: "MCP servers", rows, footer: "Changes apply on the next turn." };
5587
+ },
5588
+ async toggle({ config }, rowId, enabled) {
5589
+ const name = /^(?:user|project):(.+)$/.exec(rowId)?.[1];
5590
+ if (!name) throw unknownRow(rowId);
5591
+ await setListed(config, "disabledMcpServers", name, !enabled);
5592
+ return { message: `${name} is ${enabled ? "on" : "off"} from the next turn` };
5593
+ },
5594
+ async act({ config }, rowId, actionId, inputs) {
5595
+ if (rowId === "add" && actionId === "add") {
5596
+ const name2 = inputs[0]?.trim() ?? "";
5597
+ if (!NAME.test(name2)) throw new Error("Use a server name with letters, digits, - and _");
5598
+ const entry = serverEntry(inputs[1] ?? "");
5599
+ await writeServers(config, (entries) => {
5600
+ if (name2 in entries) throw new Error(`${config.paths.mcp} already has a server named ${name2}`);
5601
+ entries[name2] = entry;
5602
+ });
5603
+ return { message: `Added ${name2}; it connects on the next turn` };
5604
+ }
5605
+ const name = rowId.startsWith("user:") ? rowId.slice("user:".length) : void 0;
5606
+ if (!name || actionId !== "remove") throw unknownRow(`${rowId} ${actionId}`);
5607
+ await writeServers(config, (entries) => {
5608
+ if (!(name in entries)) throw new Error(`${config.paths.mcp} has no server named ${name}`);
5609
+ delete entries[name];
5610
+ });
5611
+ return { message: `Removed ${name}` };
5612
+ }
5219
5613
  };
5220
5614
  const nodeLoopback = {
5221
5615
  listen: (port, path2) => new Promise((resolve2, reject) => {
@@ -5230,10 +5624,10 @@ const nodeLoopback = {
5230
5624
  let answered = false;
5231
5625
  const request = {
5232
5626
  query: Object.fromEntries(url.searchParams),
5233
- respond(status, html) {
5627
+ respond(status2, html) {
5234
5628
  if (answered) return;
5235
5629
  answered = true;
5236
- res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(html);
5630
+ res.writeHead(status2, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(html);
5237
5631
  }
5238
5632
  };
5239
5633
  const [waiter] = waiting;
@@ -5275,7 +5669,51 @@ const nodeLoopback = {
5275
5669
  class UsageError extends Error {
5276
5670
  }
5277
5671
  const SIGN_IN_TIMEOUT_MS = 10 * 6e4;
5278
- const fetchOf = (io) => io.fetch ?? ((url, init) => fetch(url, init));
5672
+ const fetchOr = (custom) => custom ?? ((url, init) => fetch(url, init));
5673
+ async function apiKeyChoice(input2, id2) {
5674
+ const spec = providerSpec(id2);
5675
+ if (!spec) throw new UsageError(`Unknown provider ${id2}`);
5676
+ if (!spec.methods.some((method) => method.id === "api-key")) throw new UsageError(`${spec.name} does not use an API key`);
5677
+ return selectProvider({ ...input2, provider: spec.id, method: "api-key" });
5678
+ }
5679
+ async function saveApiKey(config, choice, key) {
5680
+ await choice.runtime.models().keys.save(choice.runtime.endpoint(), key);
5681
+ const own = config.settings.providers?.[choice.id];
5682
+ if (own?.method && own.method !== "api-key")
5683
+ await config.saveSettings({ providers: { ...config.settings.providers, [choice.id]: { ...own, method: "api-key" } } });
5684
+ }
5685
+ async function removeApiKeys(auth, input2, id2) {
5686
+ const removed = [];
5687
+ const drop = async (slot) => {
5688
+ if (!await auth.get(slot)) return;
5689
+ await auth.delete(slot);
5690
+ removed.push(slot.slice("apikey:".length));
5691
+ };
5692
+ const spec = id2 === void 0 ? void 0 : providerSpec(id2);
5693
+ if (id2 !== void 0 && !spec) throw new UsageError(`Unknown provider ${id2}`);
5694
+ const base = spec?.id === "openai-compatible" ? input2.settings.providers?.[spec.id]?.baseUrl || input2.env.OXBIT_AGENT_BASE_URL : void 0;
5695
+ if (spec && spec.id !== "openai-compatible") await drop(apiKeySlot(spec.id));
5696
+ else if (base) await drop(apiKeySlot("openai-compatible", normalizeBase(base)));
5697
+ else for (const slot of await auth.keys()) if (slot.startsWith(spec ? `apikey:${spec.id} ` : "apikey:")) await drop(slot);
5698
+ return removed;
5699
+ }
5700
+ async function signOutChatGPT(auth, fetch2) {
5701
+ if (!await chatgptSignedIn(auth)) return void 0;
5702
+ return (await new ChatGPTAuth({ fetch: fetch2, store: auth }).signOut()).revoked;
5703
+ }
5704
+ async function signInChatGPT({ config, auth, fetch: fetch2, signal, open: open2, paste }) {
5705
+ const chatgpt = new ChatGPTAuth({ fetch: fetch2, store: auth });
5706
+ const record = await chatgpt.record();
5707
+ const account = await chatgpt.signIn({ loopback: nodeLoopback, account: record.active, paste, signal, open: (address) => {
5708
+ const url = new URL(address);
5709
+ url.searchParams.delete("id_token_hint");
5710
+ return open2(url.toString());
5711
+ } });
5712
+ if (!planEnabled(account)) return { account, enabled: false };
5713
+ const providers = config.settings.providers ?? {};
5714
+ await config.saveSettings({ ...config.settings.provider ? {} : { provider: "openai" }, providers: { ...providers, openai: { ...providers.openai, method: "chatgpt" } } });
5715
+ return { account, enabled: true };
5716
+ }
5279
5717
  async function readAll(stream) {
5280
5718
  let text = "";
5281
5719
  for await (const chunk of stream) text += chunk;
@@ -5345,48 +5783,39 @@ async function login({ config, auth, io, input: input2 }, rest) {
5345
5783
  if (!input2.provider) return void io.stdout.write(methodList()), 0;
5346
5784
  const spec = providerSpec(input2.provider);
5347
5785
  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" });
5786
+ const choice = await apiKeyChoice(input2, spec.id);
5350
5787
  const key = (await readSecret(io, `${spec.methods.find((method) => method.id === "api-key").label}: `)).trim();
5351
5788
  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" } } });
5789
+ await saveApiKey(config, choice, key);
5356
5790
  io.stdout.write(`Saved the ${spec.name} API key in ${auth.path}
5357
5791
  `);
5358
5792
  return 0;
5359
5793
  }
5360
5794
  async function loginChatGPT({ config, auth, io }) {
5361
- const chatgpt = new ChatGPTAuth({ fetch: fetchOf(io), store: auth });
5362
- const record = await chatgpt.record();
5363
5795
  let noBrowser;
5364
5796
  const failed2 = new Promise((resolve2) => noBrowser = resolve2);
5365
5797
  io.stderr.write(`${CHATGPT_LABELS.signIn}
5366
5798
  `);
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");
5799
+ const { account, enabled } = await signInChatGPT({
5800
+ config,
5801
+ auth,
5802
+ fetch: fetchOr(io.fetch),
5803
+ open: async (url) => {
5373
5804
  io.stderr.write(`Open this page to sign in:
5374
5805
  ${url}
5375
5806
  `);
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")}):
5807
+ if (await (io.open ?? ((target) => openBrowser(target, io.env)))(url)) return;
5808
+ 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
5809
  `);
5379
5810
  noBrowser();
5380
5811
  },
5381
5812
  paste: (signal) => failed2.then(() => pastedLine(io.stdin, signal)),
5382
5813
  signal: AbortSignal.timeout(SIGN_IN_TIMEOUT_MS)
5383
5814
  });
5384
- if (!planEnabled(account)) {
5815
+ if (!enabled) {
5385
5816
  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
5817
  return 1;
5387
5818
  }
5388
- const providers = config.settings.providers ?? {};
5389
- await config.saveSettings({ ...config.settings.provider ? {} : { provider: "openai" }, providers: { ...providers, openai: { ...providers.openai, method: "chatgpt" } } });
5390
5819
  io.stdout.write(`${CHATGPT_LABELS.welcome}${account.email ? ` (${account.email})` : ""}.
5391
5820
  ${CHATGPT_LABELS.manage}: ${CHATGPT.usageUrl}
5392
5821
  `);
@@ -5397,28 +5826,10 @@ ${CHATGPT_LABELS.manage}: ${CHATGPT.usageUrl}
5397
5826
  async function logout({ auth, io, input: input2 }, rest) {
5398
5827
  if (rest.length > 1) throw new UsageError(`Unexpected argument ${rest[1]}`);
5399
5828
  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();
5829
+ const removed = target === "chatgpt" ? [] : await removeApiKeys(auth, input2, target);
5830
+ if (!target || target === "chatgpt") {
5831
+ const revoked = await signOutChatGPT(auth, fetchOr(io.fetch));
5832
+ if (revoked !== void 0) removed.push(revoked ? "ChatGPT" : "ChatGPT (signed out here; OpenAI did not confirm the revocation)");
5422
5833
  }
5423
5834
  io.stdout.write(removed.length ? `Removed ${removed.join(", ")}
5424
5835
  ` : "Nothing to remove\n");
@@ -5438,6 +5849,509 @@ async function models({ io, input: input2 }) {
5438
5849
  }
5439
5850
  return 0;
5440
5851
  }
5852
+ const OLLAMA = "http://127.0.0.1:11434";
5853
+ const input = (deps) => ({ settings: deps.config.settings, env: deps.env, auth: deps.auth, fetch: deps.fetch });
5854
+ const has = (spec, method) => spec.methods.some((item) => item.id === method);
5855
+ function baseOf(spec, deps) {
5856
+ const own = deps.config.settings.providers?.[spec.id]?.baseUrl;
5857
+ const base = spec.id === "openai-compatible" ? own || deps.env.OXBIT_AGENT_BASE_URL : spec.id === "ollama" ? own || deps.env.OLLAMA_HOST || OLLAMA : own;
5858
+ try {
5859
+ return base ? normalizeBase(base) : void 0;
5860
+ } catch {
5861
+ return void 0;
5862
+ }
5863
+ }
5864
+ async function status(spec, deps, signedIn) {
5865
+ const parts = [];
5866
+ const current = deps.provider();
5867
+ if (current.id === spec.id) parts.push(current.runtime.method === "chatgpt" ? "in use with your ChatGPT plan" : "in use");
5868
+ if (spec.id === "openai" && signedIn) parts.push("signed in with ChatGPT");
5869
+ const base = baseOf(spec, deps);
5870
+ let saved = false;
5871
+ if (spec.id === "openai-compatible" && !base) parts.push("no base URL");
5872
+ else if (has(spec, "api-key")) {
5873
+ saved = !!await deps.auth.get(apiKeySlot(spec.id, base));
5874
+ const env = spec.env && deps.env[spec.env] ? spec.env : void 0;
5875
+ parts.push(saved ? "API key saved" : env ? `key from ${env}` : has(spec, "none") ? "no key needed" : "no key");
5876
+ } else parts.push("no key needed");
5877
+ if (base && spec.id !== "openrouter") parts.push(base);
5878
+ if (spec.id === "anthropic") parts.push("Claude Console API key");
5879
+ if (stringList(deps.config.settings.disabledProviders).includes(spec.id)) parts.push("disabled");
5880
+ return { text: parts.join(", "), saved };
5881
+ }
5882
+ async function apply(deps, id2, message2) {
5883
+ if (deps.rebuild) {
5884
+ await deps.rebuild(id2);
5885
+ return { message: message2, reloaded: true };
5886
+ }
5887
+ return { message: `${message2}. It applies after the agent restarts.` };
5888
+ }
5889
+ async function use(deps, spec) {
5890
+ const { config } = deps;
5891
+ await selectProvider({ ...input(deps), provider: spec.id });
5892
+ const previous = config.settings.provider ?? deps.provider().id;
5893
+ const providers = { ...config.settings.providers };
5894
+ if (config.settings.model && !providers[previous]?.model) providers[previous] = { ...providers[previous], model: config.settings.model };
5895
+ await config.saveSettings({ provider: spec.id, model: void 0, providers });
5896
+ return apply(deps, spec.id, `Using ${spec.name}`);
5897
+ }
5898
+ async function signIn(deps, context) {
5899
+ const signal = AbortSignal.any([context.signal, AbortSignal.timeout(SIGN_IN_TIMEOUT_MS)]);
5900
+ const { account, enabled } = await signInChatGPT({ config: deps.config, auth: deps.auth, fetch: fetchOr(deps.fetch), signal, open: async (url) => {
5901
+ context.progress(`Open this page to sign in: ${url}`);
5902
+ if (!await (deps.open ?? ((target) => openBrowser(target, deps.env)))(url)) context.progress(`No browser opened. Open this page to sign in: ${url}`);
5903
+ } });
5904
+ 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.");
5905
+ return `${CHATGPT_LABELS.welcome}${account.email ? ` (${account.email})` : ""}`;
5906
+ }
5907
+ const providersPanel = {
5908
+ async panel(deps) {
5909
+ const signedIn = await chatgptSignedIn(deps.auth);
5910
+ const disabled = stringList(deps.config.settings.disabledProviders);
5911
+ const rows = await Promise.all(PROVIDERS.map(async (spec) => {
5912
+ const inUse = deps.provider().id === spec.id;
5913
+ const { text, saved } = await status(spec, deps, signedIn);
5914
+ const reloads = inUse || void 0;
5915
+ const actions = inUse ? [] : [{ id: "use", label: "Use this provider", reloads: true }];
5916
+ 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 });
5917
+ if (saved) actions.push({ id: "remove-key", label: "Remove API key", reloads });
5918
+ if (has(spec, "chatgpt")) actions.push(signedIn ? { id: "signout", label: "Sign out of ChatGPT", reloads } : { id: "chatgpt", label: "Sign in with ChatGPT", reloads });
5919
+ if (spec.id === "openai-compatible") actions.push({
5920
+ id: "base-url",
5921
+ label: "Base URL",
5922
+ reloads,
5923
+ inputs: [{ label: "Base URL (empty clears it)", placeholder: "https://llm.example.com", value: deps.config.settings.providers?.[spec.id]?.baseUrl ?? "" }]
5924
+ });
5925
+ return { id: spec.id, section: "Providers", label: spec.name, detail: text, enabled: !disabled.includes(spec.id), actions };
5926
+ }));
5927
+ return { id: "providers", title: "Providers", rows, footer: "Unchecked providers are skipped when settings name no provider; --provider and Use still pick them." };
5928
+ },
5929
+ async toggle({ config }, rowId, enabled) {
5930
+ const spec = providerSpec(rowId);
5931
+ if (!spec) throw unknownRow(rowId);
5932
+ await setListed(config, "disabledProviders", spec.id, !enabled);
5933
+ return { message: `${spec.name} is ${enabled ? "on" : "off"} for automatic selection` };
5934
+ },
5935
+ async act(deps, rowId, actionId, inputs, context) {
5936
+ const spec = providerSpec(rowId);
5937
+ if (!spec) throw unknownRow(rowId);
5938
+ const inUse = deps.provider().id === spec.id;
5939
+ const done = (message2) => inUse ? apply(deps, spec.id, message2) : Promise.resolve({ message: message2 });
5940
+ switch (actionId) {
5941
+ case "use":
5942
+ return use(deps, spec);
5943
+ case "key": {
5944
+ const key = inputs[0]?.trim();
5945
+ if (!key) throw new Error("The API key is empty");
5946
+ await saveApiKey(deps.config, await apiKeyChoice(input(deps), spec.id), key);
5947
+ return done(`Saved the ${spec.name} API key in ${deps.auth.path}`);
5948
+ }
5949
+ case "remove-key": {
5950
+ const removed = await removeApiKeys(deps.auth, input(deps), spec.id);
5951
+ return done(removed.length ? `Removed the ${spec.name} API key` : "No saved key to remove");
5952
+ }
5953
+ case "chatgpt": {
5954
+ if (spec.id !== "openai") break;
5955
+ return done(await signIn(deps, context));
5956
+ }
5957
+ case "signout": {
5958
+ const revoked = await signOutChatGPT(deps.auth, fetchOr(deps.fetch));
5959
+ if (revoked === void 0) return { message: "Not signed in with ChatGPT" };
5960
+ return done(revoked ? "Signed out of ChatGPT" : "Signed out here; OpenAI did not confirm the revocation");
5961
+ }
5962
+ case "base-url": {
5963
+ if (spec.id !== "openai-compatible") break;
5964
+ const value2 = inputs[0]?.trim();
5965
+ const baseUrl = value2 ? normalizeBase(value2) : void 0;
5966
+ const providers = deps.config.settings.providers ?? {};
5967
+ await deps.config.saveSettings({ providers: { ...providers, [spec.id]: { ...providers[spec.id], baseUrl } } });
5968
+ return done(baseUrl ? `Base URL: ${baseUrl}` : "Cleared the base URL");
5969
+ }
5970
+ }
5971
+ throw unknownRow(`${rowId} ${actionId}`);
5972
+ }
5973
+ };
5974
+ const exists = (path2) => stat(path2).then(() => true, () => false);
5975
+ const skillName = (rowId) => rowId.startsWith("skill:") ? rowId.slice("skill:".length) : void 0;
5976
+ const expandPath = (input2, cwd, home) => resolve(cwd, input2 === "~" ? home : input2.startsWith("~/") ? join(home, input2.slice(2)) : input2);
5977
+ async function newSkill({ config }, name, description) {
5978
+ const summary2 = description.replace(/\s+/g, " ").trim();
5979
+ if (!isSkillName(name)) throw new Error("Use a skill name with a-z, 0-9 and single hyphens, 64 characters at most");
5980
+ if (!summary2) throw new Error("Add a description: what the skill does and when to use it");
5981
+ if (summary2.length > 1024) throw new Error("The description is longer than 1024 characters");
5982
+ const dir = join(config.paths.skills, name);
5983
+ if (await exists(dir)) throw new Error(`${dir} already exists`);
5984
+ await mkdir(dir, { recursive: true });
5985
+ const path2 = join(dir, "SKILL.md");
5986
+ await writeFile$1(path2, `---
5987
+ name: ${name}
5988
+ description: ${JSON.stringify(summary2)}
5989
+ ---
5990
+
5991
+ # ${name}
5992
+
5993
+ Write the steps for this skill here.
5994
+ `, { flag: "wx" });
5995
+ return { message: `Created ${path2}` };
5996
+ }
5997
+ async function importSkill(deps, input2, cwd) {
5998
+ if (!input2.trim()) throw new Error("Enter the path of a folder that contains SKILL.md");
5999
+ const source = expandPath(input2.trim(), cwd, userHome(deps));
6000
+ const text = await readOptional(join(source, "SKILL.md"));
6001
+ if (text === void 0) throw new Error(`${source} has no SKILL.md`);
6002
+ const description = parseFrontmatter(text).data.description;
6003
+ if (typeof description !== "string" || !description.trim()) throw new Error(`${join(source, "SKILL.md")} has no description in its frontmatter`);
6004
+ const target = join(deps.config.paths.skills, basename(source));
6005
+ if (await exists(target)) throw new Error(`${target} already exists`);
6006
+ await mkdir(deps.config.paths.skills, { recursive: true });
6007
+ await cp(source, target, { recursive: true, errorOnExist: true, force: false });
6008
+ return { message: `Imported ${basename(source)} into ${target}` };
6009
+ }
6010
+ const skillsPanel = {
6011
+ async panel(deps, { cwd }) {
6012
+ const home = userHome(deps);
6013
+ const { skills, warnings } = await skillsFor(deps.config, home).discover(cwd);
6014
+ const hidden = new Set(stringList(deps.config.settings.disabledSkills));
6015
+ const rows = skills.map((skill) => {
6016
+ const enabled = !hidden.has(skill.name);
6017
+ return {
6018
+ id: `skill:${skill.name}`,
6019
+ section: "Skills",
6020
+ label: skill.name,
6021
+ detail: firstLine$1(skill.description, 120) || tilde(skill.path, home),
6022
+ enabled,
6023
+ ...enabled && skill.user ? { actions: [{ id: "use", label: `Use /${skill.name}` }] } : {}
6024
+ };
6025
+ });
6026
+ if (!rows.length) rows.push({ id: "none", section: "Skills", label: "No skills found" });
6027
+ const folder = tilde(deps.config.paths.skills, home);
6028
+ rows.push(
6029
+ { id: "new", section: "Add", label: "New skill", detail: `Creates ${folder}/<name>/SKILL.md`, actions: [{
6030
+ id: "new",
6031
+ label: "New skill",
6032
+ inputs: [{ label: "Name", placeholder: "release-notes" }, { label: "Description", placeholder: "What the skill does and when to use it" }]
6033
+ }] },
6034
+ { id: "import", section: "Add", label: "Import a skill folder", detail: `Copies a folder with SKILL.md into ${folder}`, actions: [{
6035
+ id: "import",
6036
+ label: "Import",
6037
+ inputs: [{ label: "Folder with SKILL.md", placeholder: "~/Downloads/review" }]
6038
+ }] },
6039
+ ...warningRows(warnings)
6040
+ );
6041
+ return { id: "skills", title: "Skills", rows, footer: "Unchecked skills leave the catalog, the skill tool and the slash commands." };
6042
+ },
6043
+ async toggle({ config }, rowId, enabled) {
6044
+ const name = skillName(rowId);
6045
+ if (!name) throw unknownRow(rowId);
6046
+ await setListed(config, "disabledSkills", name, !enabled);
6047
+ return { message: `${name} is ${enabled ? "on" : "off"}` };
6048
+ },
6049
+ async act(deps, rowId, actionId, inputs, { cwd }) {
6050
+ if (rowId === "new" && actionId === "new") return newSkill(deps, inputs[0]?.trim() ?? "", inputs[1] ?? "");
6051
+ if (rowId === "import" && actionId === "import") return importSkill(deps, inputs[0] ?? "", cwd);
6052
+ const name = skillName(rowId);
6053
+ if (name && actionId === "use") return { draft: `/${name} ` };
6054
+ throw unknownRow(`${rowId} ${actionId}`);
6055
+ }
6056
+ };
6057
+ const HELP = {
6058
+ config: { description: "Show or change agent settings", hint: "<setting> <value>", usage: [
6059
+ "`/config <setting> <value>`, for example `/config auto-compact off`, `/config compact-at 80` or `/config mode full-auto`"
6060
+ ] },
6061
+ skills: { description: "List skills; turn one on or off, create one or import a folder", hint: "enable|disable|new|import ...", usage: [
6062
+ "`/skills enable <name|number>`, `/skills disable <name|number>`",
6063
+ "`/skills new <name> <description>`",
6064
+ "`/skills import <folder with SKILL.md>`"
6065
+ ] },
6066
+ hooks: { description: "List command hooks and turn them on or off", hint: "enable|disable <number>", usage: [
6067
+ "`/hooks enable <number>`, `/hooks disable <number>`",
6068
+ "`/config claude-hooks on`, `/config codex-hooks on` load Claude Code and Codex hook files"
6069
+ ] },
6070
+ mcp: { description: "Show MCP servers; add, remove, turn on or off, or reconnect them", hint: "add|remove|enable|disable|reload ...", usage: [
6071
+ "`/mcp add <name> <url or command line>`, `/mcp remove <name>`",
6072
+ "`/mcp enable <name>`, `/mcp disable <name>`",
6073
+ "`/mcp reload` reconnects the servers"
6074
+ ] },
6075
+ instructions: { description: "List instruction files and the global prompt", hint: "enable|disable|prompt ...", usage: [
6076
+ "`/instructions enable <number|path>`, `/instructions disable <number|path>`",
6077
+ "`/instructions prompt <text>` sets the global prompt; without text it clears it"
6078
+ ] },
6079
+ providers: { description: "List model providers; pick one, or sign in or out of ChatGPT", hint: "use|enable|disable|login|signout ...", usage: [
6080
+ "`/providers use <id>` saves the provider for the next start",
6081
+ "`/providers enable <id>`, `/providers disable <id>` for automatic selection",
6082
+ "`/providers login chatgpt`, `/providers signout`",
6083
+ "API keys: run `oxbit-agent login --provider <id>` in a terminal, or use /providers in the oxbit-agent terminal UI"
6084
+ ] }
6085
+ };
6086
+ const ON = /* @__PURE__ */ new Set(["on", "true", "yes", "enable", "enabled", "1"]);
6087
+ const OFF = /* @__PURE__ */ new Set(["off", "false", "no", "disable", "disabled", "0"]);
6088
+ const split = (text) => {
6089
+ const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(text.trim());
6090
+ return match ? [match[1], match[2]?.trim() ?? ""] : ["", ""];
6091
+ };
6092
+ function rowText(row, keys) {
6093
+ const box = row.enabled === void 0 ? "" : row.enabled ? "[x] " : "[ ] ";
6094
+ const choice = row.choice ? `: ${row.choice.value} (${row.choice.options.map((option) => option.value).join(", ")})` : "";
6095
+ return `${box}${row.label}${keys ? ` \`${row.id}\`` : ""}${choice}${row.detail ? ` · ${row.detail}` : ""}`;
6096
+ }
6097
+ function panelText(panel, usage, keys = false) {
6098
+ const lines2 = [`## ${panel.title}`];
6099
+ let section;
6100
+ panel.rows.forEach((row, index) => {
6101
+ if (row.section !== section && (section = row.section)) lines2.push("", `**${section}**`);
6102
+ lines2.push(`${index + 1}. ${rowText(row, keys)}`);
6103
+ });
6104
+ if (panel.footer) lines2.push("", panel.footer);
6105
+ lines2.push("", "**Usage**", ...usage.map((line) => `- ${line}`));
6106
+ return lines2.join("\n");
6107
+ }
6108
+ function findRow(panel, key, cwd) {
6109
+ const rows = panel.rows;
6110
+ 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));
6111
+ if (!found) throw new Error(`No ${key} in ${panel.title}. Run /${panel.id} to list them.`);
6112
+ return found;
6113
+ }
6114
+ const switchOf = (value2) => {
6115
+ const word = value2.toLowerCase();
6116
+ if (ON.has(word)) return true;
6117
+ if (OFF.has(word)) return false;
6118
+ throw new Error(`Use on or off, not ${value2 || "nothing"}`);
6119
+ };
6120
+ function settingsCommands(manager, mcp) {
6121
+ const say = (context, text) => context.update({ sessionUpdate: "agent_message_chunk", content: { type: "text", text } });
6122
+ const panelContext = (context) => ({ cwd: context.session.cwd, sessionId: context.session.id, signal: context.signal, progress: (text) => say(context, `${text}
6123
+
6124
+ `) });
6125
+ const applied = (change, context) => {
6126
+ if (change.mode) {
6127
+ context.session.meta.mode = change.mode;
6128
+ context.update({ sessionUpdate: "current_mode_update", currentModeId: change.mode });
6129
+ }
6130
+ return [change.message, change.draft && `Type ${change.draft.trim()} to use it.`].filter((part) => !!part).join("\n") || "Done";
6131
+ };
6132
+ const status2 = async (context, reload) => {
6133
+ if (!mcp) return "";
6134
+ if (reload) await mcp.close(context.session.id);
6135
+ await mcp.tools(context.session);
6136
+ return mcp.report(context.session.id);
6137
+ };
6138
+ async function run2(id2, args, context) {
6139
+ const at = panelContext(context);
6140
+ const panel = () => manager.panel(id2, at);
6141
+ const toggle = async (key, enabled) => applied(await manager.toggle(id2, findRow(await panel(), key, at.cwd).id, enabled, at), context);
6142
+ const act = async (rowId, actionId, inputs = []) => applied(await manager.act(id2, rowId, actionId, inputs, at), context);
6143
+ const [verb, rest] = split(args);
6144
+ const word = verb.toLowerCase();
6145
+ 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");
6146
+ if (word === "enable" || word === "disable") return toggle(rest, word === "enable");
6147
+ if (id2 === "config") {
6148
+ const row = findRow(await panel(), verb, at.cwd);
6149
+ if (row.choice) {
6150
+ const value2 = row.choice.options.find((option) => [option.value, option.name].some((name) => name.toLowerCase() === rest.toLowerCase().replace(/%$/, "")));
6151
+ return applied(await manager.choose(id2, row.id, value2?.value ?? rest, at), context);
6152
+ }
6153
+ if (row.enabled === void 0) throw new Error(`${row.label} has no on or off setting`);
6154
+ return applied(await manager.toggle(id2, row.id, switchOf(rest), at), context);
6155
+ }
6156
+ if (id2 === "skills" && word === "new") return act("new", "new", split(rest));
6157
+ if (id2 === "skills" && word === "import") return act("import", "import", [rest]);
6158
+ if (id2 === "instructions" && word === "prompt") return act("prompt", "edit", [rest]);
6159
+ if (id2 === "mcp" && word === "add") return act("add", "add", split(rest));
6160
+ if (id2 === "mcp" && word === "remove") return act(`user:${rest}`, "remove");
6161
+ if (id2 === "mcp" && word === "reload") return status2(context, true);
6162
+ if (id2 === "providers" && word === "use") return act(findRow(await panel(), rest, at.cwd).id, "use");
6163
+ if (id2 === "providers" && word === "signout") return act("openai", "signout");
6164
+ if (id2 === "providers" && word === "login" && rest.toLowerCase() === "chatgpt") return act("openai", "chatgpt");
6165
+ throw new Error(`Unknown /${id2} command. Usage:
6166
+ ${HELP[id2].usage.map((line) => `- ${line}`).join("\n")}`);
6167
+ }
6168
+ const commands = SETTINGS_COMMANDS.map((id2) => ({
6169
+ name: id2,
6170
+ description: HELP[id2].description,
6171
+ hint: HELP[id2].hint,
6172
+ run: async (args, context) => {
6173
+ let text;
6174
+ try {
6175
+ text = await run2(id2, args, context);
6176
+ } catch (error) {
6177
+ if (context.signal.aborted) return { done: "cancelled" };
6178
+ text = error instanceof Error ? error.message : String(error);
6179
+ }
6180
+ say(context, text);
6181
+ return { done: "end_turn" };
6182
+ }
6183
+ }));
6184
+ return { commands: () => commands };
6185
+ }
6186
+ const PANELS = {
6187
+ config: configPanel,
6188
+ skills: skillsPanel,
6189
+ hooks: hooksPanel,
6190
+ mcp: mcpPanel,
6191
+ instructions: instructionsPanel,
6192
+ providers: providersPanel
6193
+ };
6194
+ const handler = (id2) => {
6195
+ const panel = Object.hasOwn(PANELS, id2) ? PANELS[id2] : void 0;
6196
+ if (!panel) throw new Error(`Unknown settings panel ${id2}`);
6197
+ return panel;
6198
+ };
6199
+ const unsupported = (id2, what) => new Error(`The ${id2} panel has nothing to ${what}`);
6200
+ function createSettingsManager(deps) {
6201
+ return {
6202
+ panel: (id2, context) => handler(id2).panel(deps, context),
6203
+ async toggle(id2, rowId, enabled, context) {
6204
+ const panel = handler(id2);
6205
+ if (!panel.toggle) throw unsupported(id2, "turn on or off");
6206
+ return panel.toggle(deps, rowId, enabled, context);
6207
+ },
6208
+ async choose(id2, rowId, value2, context) {
6209
+ const panel = handler(id2);
6210
+ if (!panel.choose) throw unsupported(id2, "choose");
6211
+ return panel.choose(deps, rowId, value2, context);
6212
+ },
6213
+ async act(id2, rowId, actionId, inputs, context) {
6214
+ const panel = handler(id2);
6215
+ if (!panel.act) throw unsupported(id2, "run");
6216
+ return panel.act(deps, rowId, actionId, inputs, context);
6217
+ }
6218
+ };
6219
+ }
6220
+ const MAX_BYTES = 1024 * 1024;
6221
+ const WRITE_OPTIONS = [
6222
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
6223
+ { optionId: "allow_always", name: "Allow edits for this session", kind: "allow_always" },
6224
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
6225
+ ];
6226
+ const absolutePath = (params) => {
6227
+ const path2 = params.path;
6228
+ if (typeof path2 !== "string" || !isAbsolute(path2) || path2.includes("\0")) throw new Error("ACP paths must be absolute");
6229
+ return path2;
6230
+ };
6231
+ class LocalClient {
6232
+ constructor(ask) {
6233
+ this.ask = ask;
6234
+ }
6235
+ ask;
6236
+ terminals = new Terminals();
6237
+ trusted = /* @__PURE__ */ new Set();
6238
+ async readTextFile(params) {
6239
+ const text = await readFile$1(absolutePath(params), "utf8");
6240
+ const start = (params.line ?? 1) - 1;
6241
+ 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;
6242
+ if (Buffer.byteLength(content) > MAX_BYTES) throw new Error("File content exceeds 1 MiB; request a smaller line range");
6243
+ return { content };
6244
+ }
6245
+ async writeTextFile(params) {
6246
+ const path2 = absolutePath(params);
6247
+ if (typeof params.content !== "string") throw new Error("Invalid content");
6248
+ const sessionId = String(params.sessionId ?? "");
6249
+ if (!params._meta?.oxbit?.mode && !this.trusted.has(sessionId)) {
6250
+ const oldText = await readFile$1(path2, "utf8").catch(() => null);
6251
+ const response = await this.ask({ sessionId, options: WRITE_OPTIONS, toolCall: {
6252
+ toolCallId: params.toolCallId ?? `write-${Date.now()}`,
6253
+ title: `Write ${path2}`,
6254
+ kind: "edit",
6255
+ status: "pending",
6256
+ locations: [{ path: path2 }],
6257
+ content: [{ type: "diff", path: path2, oldText, newText: params.content }]
6258
+ } });
6259
+ const choice = response?.outcome?.outcome === "selected" ? response.outcome.optionId : void 0;
6260
+ if (choice === "allow_always") this.trusted.add(sessionId);
6261
+ else if (choice !== "allow") throw new Error("The user rejected this write");
6262
+ }
6263
+ await mkdir(dirname(path2), { recursive: true });
6264
+ await writeFile$1(path2, params.content);
6265
+ return {};
6266
+ }
6267
+ dispose() {
6268
+ this.terminals.dispose();
6269
+ }
6270
+ }
6271
+ function routeRequests(link, local) {
6272
+ return (method, params) => {
6273
+ const capabilities = link.capabilities();
6274
+ if (method === "fs/read_text_file") return capabilities.fs?.readTextFile ? link.request(method, params) : local.readTextFile(params);
6275
+ if (method === "fs/write_text_file") return capabilities.fs?.writeTextFile ? link.request(method, params) : local.writeTextFile(params);
6276
+ if (method.startsWith("terminal/")) return capabilities.terminal ? link.request(method, params) : local.terminals.handle(method, params);
6277
+ if (method === "oxbit/diagnostics" && !capabilities._meta?.oxbit?.diagnostics)
6278
+ return Promise.reject(new Error("Language diagnostics are unavailable here. Run the project's type checker or linter with bash."));
6279
+ return link.request(method, params);
6280
+ };
6281
+ }
6282
+ function nodeHost(setup, link) {
6283
+ const { root, config, provider: provider2, log } = setup;
6284
+ const { runtime } = provider2;
6285
+ const local = new LocalClient((params) => link.request("session/request_permission", params));
6286
+ const mcp = nodeMcp(config, log);
6287
+ let catalog;
6288
+ const snapshots = snapshotStores(config.paths.snapshots, log);
6289
+ const filesystems = /* @__PURE__ */ new Map();
6290
+ const filesystemFor = (cwd) => {
6291
+ let filesystem = filesystems.get(cwd);
6292
+ if (!filesystem) filesystems.set(cwd, filesystem = nodeFilesystem(cwd, { ripgrep: setup.ripgrep }));
6293
+ return filesystem;
6294
+ };
6295
+ const core = coreExtensions(config, (sessionId, update) => link.update(sessionId, update), { home: setup.userHome, log });
6296
+ const settings = setup.settings ?? createSettingsManager({
6297
+ config,
6298
+ auth: new FileAuthStore(config.paths.auth),
6299
+ env: process.env,
6300
+ home: setup.userHome ?? homedir(),
6301
+ provider: () => provider2
6302
+ });
6303
+ const compactOptions2 = compactConfigOptions(config);
6304
+ const extensions = {
6305
+ sessions: new JsonlSessionStore(config.paths.sessions),
6306
+ snapshots,
6307
+ ...core,
6308
+ ...setup.extensions,
6309
+ context: [...core.context ?? [], ...setup.extensions?.context ?? []],
6310
+ tools: [...core.tools ?? [], mcp, ...setup.extensions?.tools ?? []],
6311
+ commands: [undoCommands(snapshots), settingsCommands(settings, mcp), ...core.commands ?? [], ...setup.extensions?.commands ?? []],
6312
+ continuations: [...core.continuations ?? [], ...setup.extensions?.continuations ?? []],
6313
+ hooks: mcp.hooks(setup.extensions?.hooks ?? core.hooks)
6314
+ };
6315
+ const host = {
6316
+ models: () => {
6317
+ if (setup.models) return setup.models;
6318
+ if (!provider2.ready) throw missingKey(provider2);
6319
+ return runtime.models();
6320
+ },
6321
+ endpoint: () => runtime.endpoint(),
6322
+ ...setup.models ? {} : { catalog: () => catalog ??= runtime.catalog().catch((error) => {
6323
+ catalog = void 0;
6324
+ throw error;
6325
+ }) },
6326
+ model: () => provider2.model,
6327
+ saveModel: (model) => {
6328
+ provider2.model = model;
6329
+ const providers = config.settings.providers ?? {};
6330
+ config.saveSettings({ providers: { ...providers, [provider2.id]: { ...providers[provider2.id], model } } }).catch((error) => log?.(`Could not save the model: ${error.message}`));
6331
+ },
6332
+ get capabilities() {
6333
+ return { fs: true, terminal: true, diagnostics: !!link.capabilities()._meta?.oxbit?.diagnostics, mcp: { http: true, sse: true } };
6334
+ },
6335
+ filesystem: filesystemFor(root),
6336
+ filesystemFor,
6337
+ root,
6338
+ update: (sessionId, update) => link.update(sessionId, update),
6339
+ request: routeRequests(link, local),
6340
+ generateTitles: setup.generateTitles,
6341
+ configOptions: compactOptions2.configOptions,
6342
+ setConfigOption: (_session, configId, value2) => compactOptions2.setConfigOption(configId, value2),
6343
+ extensions
6344
+ };
6345
+ const dispose = async () => {
6346
+ local.dispose();
6347
+ await mcp.closeAll();
6348
+ };
6349
+ return { host, local, mcp, dispose };
6350
+ }
6351
+ const agentFactory = (setup) => (link) => {
6352
+ const { host, dispose } = nodeHost(setup, link);
6353
+ return { agent: new Agent(host), host, dispose };
6354
+ };
5441
6355
  const firstLine = (text) => String(text ?? "").trim().split("\n")[0].slice(0, 200);
5442
6356
  async function runPrint(options) {
5443
6357
  const { connection, stdout, stderr } = options;
@@ -5559,8 +6473,6 @@ ${USAGE}`);
5559
6473
  const commandContext = { config, auth, io, input: input2 };
5560
6474
  const [command, ...rest] = positionals;
5561
6475
  switch (command) {
5562
- case "acp":
5563
- return await acp(config, await selectProvider(input2), io, log);
5564
6476
  case "models":
5565
6477
  return await models(commandContext);
5566
6478
  case "login":
@@ -5568,43 +6480,55 @@ ${USAGE}`);
5568
6480
  case "logout":
5569
6481
  return await logout(commandContext, rest);
5570
6482
  }
5571
- const provider2 = await selectProvider(input2);
5572
- const setup = (generateTitles, quiet = false) => ({ root: io.cwd, config, generateTitles, log: quiet ? void 0 : log, provider: provider2 });
6483
+ let provider2 = await selectProvider(input2);
6484
+ const deps = { config, auth, env: io.env, fetch: io.fetch, open: io.open, provider: () => provider2 };
6485
+ const settings = createSettingsManager(deps);
6486
+ const setup = (generateTitles, quiet = false) => ({ root: io.cwd, config, generateTitles, log: quiet ? void 0 : log, provider: provider2, settings });
6487
+ if (command === "acp") {
6488
+ await serveStdio({ input: io.stdin, output: io.stdout, factory: agentFactory(setup(true)), log });
6489
+ return 0;
6490
+ }
5573
6491
  const sessionId = await resumeId(config, values, io.cwd);
5574
6492
  const mode = values.mode ?? config.settings.mode;
5575
6493
  if (values.print !== void 0) {
5576
6494
  if (positionals.length) throw new UsageError(`Unexpected argument ${positionals[0]}`);
5577
6495
  const prompt = values.print === "-" ? (await readAll(io.stdin)).trim() : values.print;
5578
6496
  if (!prompt) throw new UsageError("The prompt is empty");
5579
- const connection2 = await MemoryConnection.open(agentFactory(setup(false)));
6497
+ const connection = await MemoryConnection.open(agentFactory(setup(false)));
5580
6498
  const abort = new AbortController();
5581
6499
  const interrupt = () => abort.abort();
5582
6500
  process.once("SIGINT", interrupt);
5583
6501
  try {
5584
- return await runPrint({ connection: connection2, prompt, sessionId, mode, model: values.model, stdout: io.stdout, stderr: io.stderr, signal: abort.signal });
6502
+ return await runPrint({ connection, prompt, sessionId, mode, model: values.model, stdout: io.stdout, stderr: io.stderr, signal: abort.signal });
5585
6503
  } finally {
5586
6504
  process.off("SIGINT", interrupt);
5587
- await connection2.close();
6505
+ await connection.close();
5588
6506
  }
5589
6507
  }
5590
6508
  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));
6509
+ const [tui, memory] = await Promise.all([import("./assets/index-T_6HaJx-.js"), import("./assets/memory-Bdy8hvTl.js")]);
6510
+ const label = () => ({ provider: provider2.runtime.label, usingChatGPTPlan: provider2.runtime.method === "chatgpt" });
6511
+ const panels = createSettingsManager({ ...deps, rebuild: async (id2) => {
6512
+ await config.reload();
6513
+ const next = await selectProvider({ ...input2, settings: config.settings, provider: id2, model: id2 === provider2.id ? provider2.model : void 0 });
6514
+ const connection = await MemoryConnection.open(agentFactory({ ...setup(true, true), provider: next }));
6515
+ provider2 = next;
6516
+ await adapter.replace(connection, label());
6517
+ } });
6518
+ const adapter = memory.memoryTuiConnection(await MemoryConnection.open(agentFactory(setup(true, true))), { ...label(), mode, settings: panels });
5594
6519
  try {
5595
- return await tui.runTui({
5596
- connection,
6520
+ const { sessionId: last } = await tui.run(adapter, {
5597
6521
  cwd: io.cwd,
5598
6522
  sessionId,
5599
6523
  prompt: positionals.join(" ") || void 0,
5600
- mode,
5601
6524
  version: agentVersion(),
5602
- providerLabel: connected.provider.runtime.label,
5603
- usingChatGPTPlan: connected.provider.runtime.method === "chatgpt",
5604
- historyPath: config.paths.history
6525
+ history: memory.fileHistory(config.paths.history)
5605
6526
  });
6527
+ if (last) io.stderr.write(`Resume this session with: oxbit-agent -r ${last}
6528
+ `);
6529
+ return 0;
5606
6530
  } finally {
5607
- await connection.close();
6531
+ await adapter.connection.close();
5608
6532
  }
5609
6533
  } catch (error) {
5610
6534
  io.stderr.write(`oxbit-agent: ${error instanceof Error ? error.message : String(error)}
@@ -5623,13 +6547,14 @@ async function resumeId(config, values, cwd) {
5623
6547
  if (values.resume && !await store.load(values.resume)) throw new Error(`No saved session ${values.resume}`);
5624
6548
  return values.resume;
5625
6549
  }
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
6550
  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
6551
  export {
5632
6552
  MODES as M,
6553
+ contextGrid as a,
6554
+ contextNotes as b,
6555
+ contextLegend as c,
6556
+ formatPercent as d,
6557
+ formatTokens as f,
5633
6558
  walkFiles as w
5634
6559
  };
5635
6560
  //# sourceMappingURL=index.js.map