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