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