agent-yes 1.262.0 → 1.263.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{SUPPORTED_CLIS-BFLGbQmm.js → SUPPORTED_CLIS--5E9wO4L.js} +3 -3
- package/dist/{SUPPORTED_CLIS-CjKxvhlh.js → SUPPORTED_CLIS-DjFlWHpg.js} +2 -2
- package/dist/{agentShare-LEETLOm1.js → agentShare-BP6wiu_5.js} +2 -2
- package/dist/{callback-lqCCgSy1.js → callback-B0PQUZcK.js} +2 -2
- package/dist/{callback-CB3UjryF.js → callback-DFUFLIZQ.js} +3 -3
- package/dist/cli.js +5 -5
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-P6Q7Eo_B.js → notifyDaemon-BskLhHdg.js} +2 -2
- package/dist/{rustBinary-_qRGhAa5.js → rustBinary-Borvwmya.js} +2 -2
- package/dist/{schedule-ChAdx2h6.js → schedule-BSEIVKQ0.js} +4 -4
- package/dist/{serve-DhtgWGZB.js → serve-BEdLsSpv.js} +159 -17
- package/dist/{setup-BlJRaODn.js → setup-DFRvZ_ku.js} +2 -2
- package/dist/subcommands-B1YlmRaZ.js +10 -0
- package/dist/{subcommands-Zk3brdKt.js → subcommands-Bo1MSwuC.js} +78 -17
- package/dist/{terminal-BVWhnQUf.js → terminal-BMQWojIp.js} +2 -2
- package/dist/{trayApp-DtThjl0_.js → trayApp-CjB5QhdV.js} +2 -2
- package/dist/trayApp-DrdktFnW.js +5 -0
- package/dist/{ts-BqaOPFCQ.js → ts-BSHR_dga.js} +2 -2
- package/dist/{versionChecker-D-WLKAN6.js → versionChecker-BTLZ3PAb.js} +2 -2
- package/dist/{widget-CJyd9t8k.js → widget-BVss0Z9Y.js} +3 -3
- package/dist/{ws-mG4yeZu8.js → ws-DMudFezi.js} +2 -2
- package/dist/{ws-Vy3mBjNm.js → ws-DxNNwnrc.js} +2 -2
- package/lab/ui/index.html +101 -36
- package/package.json +2 -1
- package/scripts/deepseek-codex.ts +580 -0
- package/ts/serve.spec.ts +36 -0
- package/ts/serve.ts +144 -1
- package/ts/subcommands.ts +109 -1
- package/dist/subcommands-D6O9uRvN.js +0 -10
- package/dist/trayApp-C6U7Hghi.js +0 -5
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
|
|
8
|
+
type Json = Record<string, any>;
|
|
9
|
+
|
|
10
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
|
|
12
|
+
// Load .env.local ourselves (bun only auto-loads it from the *current working
|
|
13
|
+
// directory*, so `ay ds` from an unrelated cwd would miss the token). Sources,
|
|
14
|
+
// in ascending precedence: ~/.agent-yes/.env.local (global) → <repo-root>/.env.local
|
|
15
|
+
// (local checkout) → the ambient environment, which always wins.
|
|
16
|
+
function loadLocalEnv(): void {
|
|
17
|
+
const paths = [join(homedir(), ".agent-yes", ".env.local"), join(root, ".env.local")];
|
|
18
|
+
for (const path of paths) {
|
|
19
|
+
let raw: string;
|
|
20
|
+
try {
|
|
21
|
+
raw = readFileSync(path, "utf-8");
|
|
22
|
+
} catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
for (const line of raw.split("\n")) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
28
|
+
const eq = trimmed.indexOf("=");
|
|
29
|
+
if (eq <= 0) continue;
|
|
30
|
+
const key = trimmed.slice(0, eq).trim();
|
|
31
|
+
const value = trimmed
|
|
32
|
+
.slice(eq + 1)
|
|
33
|
+
.trim()
|
|
34
|
+
.replace(/^["']|["']$/g, "");
|
|
35
|
+
if (key && process.env[key] === undefined) process.env[key] = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
loadLocalEnv();
|
|
40
|
+
|
|
41
|
+
const apiKey = process.env.DEEPSEEK_API_KEY;
|
|
42
|
+
const upstream = (process.env.DEEPSEEK_BASE_URL || "https://api.deepseek.com").replace(/\/$/, "");
|
|
43
|
+
const model = process.env.DEEPSEEK_MODEL || "deepseek-v4-pro";
|
|
44
|
+
|
|
45
|
+
if (!apiKey) {
|
|
46
|
+
console.error("DEEPSEEK_API_KEY is missing from .env.local");
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function textContent(content: unknown): string {
|
|
51
|
+
if (typeof content === "string") return content;
|
|
52
|
+
if (!Array.isArray(content)) return "";
|
|
53
|
+
return content
|
|
54
|
+
.map((part) => {
|
|
55
|
+
if (typeof part === "string") return part;
|
|
56
|
+
if (part?.type === "input_text" || part?.type === "output_text" || part?.type === "text") {
|
|
57
|
+
return part.text || "";
|
|
58
|
+
}
|
|
59
|
+
return "";
|
|
60
|
+
})
|
|
61
|
+
.join("");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// DeepSeek reasoning models require that any reasoning_content emitted in a
|
|
65
|
+
// prior assistant turn be echoed back verbatim on the next request, but the
|
|
66
|
+
// Responses<->Chat translation here rebuilds assistant turns from Codex's
|
|
67
|
+
// history and has nowhere to carry that field. Stash it out-of-band, keyed
|
|
68
|
+
// by the item/call id we handed back to Codex, and reattach on replay.
|
|
69
|
+
const reasoningStore = new Map<string, string>();
|
|
70
|
+
|
|
71
|
+
function toChat(body: Json) {
|
|
72
|
+
const messages: Json[] = [];
|
|
73
|
+
if (body.instructions) messages.push({ role: "system", content: String(body.instructions) });
|
|
74
|
+
const input =
|
|
75
|
+
typeof body.input === "string" ? [{ role: "user", content: body.input }] : body.input || [];
|
|
76
|
+
|
|
77
|
+
// Codex can fire several tool calls in one turn (parallel shell commands).
|
|
78
|
+
// OpenAI-compatible endpoints (incl. DeepSeek) require all concurrent calls
|
|
79
|
+
// to live in ONE assistant message, with every tool_call_id answered by the
|
|
80
|
+
// immediately following tool messages. So buffer consecutive call items and
|
|
81
|
+
// flush them as a single assistant message before any non-call item.
|
|
82
|
+
let pendingCalls: Array<{
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
arguments: string;
|
|
86
|
+
reasoning_content?: string;
|
|
87
|
+
}> = [];
|
|
88
|
+
const flushCalls = () => {
|
|
89
|
+
if (!pendingCalls.length) return;
|
|
90
|
+
const reasoning_content = pendingCalls.find((c) => c.reasoning_content)?.reasoning_content;
|
|
91
|
+
messages.push({
|
|
92
|
+
role: "assistant",
|
|
93
|
+
content: null,
|
|
94
|
+
...(reasoning_content ? { reasoning_content } : {}),
|
|
95
|
+
tool_calls: pendingCalls.map(({ id, name, arguments: args }) => ({
|
|
96
|
+
id,
|
|
97
|
+
type: "function",
|
|
98
|
+
function: { name, arguments: args },
|
|
99
|
+
})),
|
|
100
|
+
});
|
|
101
|
+
pendingCalls = [];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (const item of input) {
|
|
105
|
+
if (item.type === "message" || item.role) {
|
|
106
|
+
flushCalls();
|
|
107
|
+
const content = textContent(item.content);
|
|
108
|
+
const role = item.role === "developer" ? "system" : item.role || "user";
|
|
109
|
+
if (content) {
|
|
110
|
+
const msg: Json = { role, content };
|
|
111
|
+
const reasoning_content = item.id && reasoningStore.get(item.id);
|
|
112
|
+
if (reasoning_content) msg.reasoning_content = reasoning_content;
|
|
113
|
+
messages.push(msg);
|
|
114
|
+
}
|
|
115
|
+
} else if (item.type === "function_call") {
|
|
116
|
+
const reasoning_content = reasoningStore.get(item.call_id || item.id);
|
|
117
|
+
pendingCalls.push({
|
|
118
|
+
id: item.call_id || item.id,
|
|
119
|
+
name: item.name,
|
|
120
|
+
arguments: item.arguments || "{}",
|
|
121
|
+
...(reasoning_content ? { reasoning_content } : {}),
|
|
122
|
+
});
|
|
123
|
+
} else if (item.type === "function_call_output") {
|
|
124
|
+
flushCalls();
|
|
125
|
+
messages.push({
|
|
126
|
+
role: "tool",
|
|
127
|
+
tool_call_id: item.call_id,
|
|
128
|
+
content: textContent(item.output) || String(item.output || ""),
|
|
129
|
+
});
|
|
130
|
+
} else if (item.type === "custom_tool_call") {
|
|
131
|
+
const reasoning_content = reasoningStore.get(item.call_id || item.id);
|
|
132
|
+
pendingCalls.push({
|
|
133
|
+
id: item.call_id || item.id,
|
|
134
|
+
name: item.name,
|
|
135
|
+
arguments: JSON.stringify({ input: item.input || "" }),
|
|
136
|
+
...(reasoning_content ? { reasoning_content } : {}),
|
|
137
|
+
});
|
|
138
|
+
} else if (item.type === "custom_tool_call_output") {
|
|
139
|
+
flushCalls();
|
|
140
|
+
messages.push({
|
|
141
|
+
role: "tool",
|
|
142
|
+
tool_call_id: item.call_id,
|
|
143
|
+
content: textContent(item.output) || String(item.output || ""),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
flushCalls();
|
|
148
|
+
|
|
149
|
+
const custom = new Set<string>();
|
|
150
|
+
const tools = (body.tools || []).flatMap((tool: Json) => {
|
|
151
|
+
if (tool.type === "function") {
|
|
152
|
+
const fn = tool.function || tool;
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
type: "function",
|
|
156
|
+
function: {
|
|
157
|
+
name: fn.name,
|
|
158
|
+
description: fn.description || "",
|
|
159
|
+
parameters: fn.parameters || { type: "object", properties: {} },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
if (tool.type === "custom" && tool.name) {
|
|
165
|
+
custom.add(tool.name);
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
type: "function",
|
|
169
|
+
function: {
|
|
170
|
+
name: tool.name,
|
|
171
|
+
description: `${tool.description || ""}\nReturn the custom tool input in the input field.`,
|
|
172
|
+
parameters: {
|
|
173
|
+
type: "object",
|
|
174
|
+
properties: { input: { type: "string" } },
|
|
175
|
+
required: ["input"],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
return [];
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
request: {
|
|
186
|
+
model,
|
|
187
|
+
messages,
|
|
188
|
+
...(tools.length ? { tools } : {}),
|
|
189
|
+
stream: true,
|
|
190
|
+
stream_options: { include_usage: true },
|
|
191
|
+
max_tokens: body.max_output_tokens,
|
|
192
|
+
},
|
|
193
|
+
custom,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sse(data: Json | "[DONE]"): string {
|
|
198
|
+
return `data: ${data === "[DONE]" ? data : JSON.stringify(data)}\n\n`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function responses(request: Request): Promise<Response> {
|
|
202
|
+
let body: Json;
|
|
203
|
+
try {
|
|
204
|
+
body = await request.json();
|
|
205
|
+
} catch {
|
|
206
|
+
return Response.json({ error: { message: "invalid JSON" } }, { status: 400 });
|
|
207
|
+
}
|
|
208
|
+
const { request: chat, custom } = toChat(body);
|
|
209
|
+
const requestBody = JSON.stringify(chat);
|
|
210
|
+
const upstreamResponse = await fetch(`${upstream}/chat/completions`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
213
|
+
body: requestBody,
|
|
214
|
+
});
|
|
215
|
+
if (process.env.DEEPSEEK_DEBUG) {
|
|
216
|
+
console.error(
|
|
217
|
+
`[deepseek-adapter] upstream ${upstreamResponse.status} ${upstreamResponse.statusText}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (!upstreamResponse.ok || !upstreamResponse.body) {
|
|
221
|
+
const upstreamBody = await upstreamResponse.text();
|
|
222
|
+
if (process.env.DEEPSEEK_DEBUG) {
|
|
223
|
+
console.error(`[deepseek-adapter] request: ${requestBody.slice(0, 3000)}`);
|
|
224
|
+
console.error(`[deepseek-adapter] upstream error body: ${upstreamBody.slice(0, 3000)}`);
|
|
225
|
+
}
|
|
226
|
+
return new Response(upstreamBody, {
|
|
227
|
+
status: upstreamResponse.status,
|
|
228
|
+
headers: {
|
|
229
|
+
"Content-Type": upstreamResponse.headers.get("content-type") || "application/json",
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const responseId = `resp_deepseek_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
235
|
+
const encoder = new TextEncoder();
|
|
236
|
+
const decoder = new TextDecoder();
|
|
237
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
238
|
+
async start(controller) {
|
|
239
|
+
const emit = (event: Json | "[DONE]") => controller.enqueue(encoder.encode(sse(event)));
|
|
240
|
+
const responseBase = {
|
|
241
|
+
id: responseId,
|
|
242
|
+
object: "response",
|
|
243
|
+
model,
|
|
244
|
+
status: "in_progress",
|
|
245
|
+
output: [],
|
|
246
|
+
};
|
|
247
|
+
emit({ type: "response.created", response: responseBase });
|
|
248
|
+
emit({ type: "response.in_progress", response: responseBase });
|
|
249
|
+
let buffer = "";
|
|
250
|
+
let textStarted = false;
|
|
251
|
+
let text = "";
|
|
252
|
+
let reasoning = "";
|
|
253
|
+
let usage: Json = {};
|
|
254
|
+
const toolCalls = new Map<number, { id: string; name: string; arguments: string }>();
|
|
255
|
+
const reader = upstreamResponse.body!.getReader();
|
|
256
|
+
try {
|
|
257
|
+
while (true) {
|
|
258
|
+
const { done, value } = await reader.read();
|
|
259
|
+
if (done) break;
|
|
260
|
+
buffer += decoder.decode(value, { stream: true });
|
|
261
|
+
const lines = buffer.split("\n");
|
|
262
|
+
buffer = lines.pop() || "";
|
|
263
|
+
for (const raw of lines) {
|
|
264
|
+
const line = raw.trim();
|
|
265
|
+
if (!line.startsWith("data:")) continue;
|
|
266
|
+
const payload = line.slice(5).trim();
|
|
267
|
+
if (!payload || payload === "[DONE]") continue;
|
|
268
|
+
const chunk = JSON.parse(payload);
|
|
269
|
+
if (chunk.usage) usage = chunk.usage;
|
|
270
|
+
const delta = chunk.choices?.[0]?.delta || {};
|
|
271
|
+
if (delta.reasoning_content) reasoning += delta.reasoning_content;
|
|
272
|
+
if (delta.content) {
|
|
273
|
+
if (!textStarted) {
|
|
274
|
+
textStarted = true;
|
|
275
|
+
emit({
|
|
276
|
+
type: "response.output_item.added",
|
|
277
|
+
output_index: 0,
|
|
278
|
+
item: {
|
|
279
|
+
id: `${responseId}_msg`,
|
|
280
|
+
type: "message",
|
|
281
|
+
role: "assistant",
|
|
282
|
+
status: "in_progress",
|
|
283
|
+
content: [],
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
emit({
|
|
287
|
+
type: "response.content_part.added",
|
|
288
|
+
item_id: `${responseId}_msg`,
|
|
289
|
+
output_index: 0,
|
|
290
|
+
content_index: 0,
|
|
291
|
+
part: { type: "output_text", text: "", annotations: [] },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
text += delta.content;
|
|
295
|
+
emit({
|
|
296
|
+
type: "response.output_text.delta",
|
|
297
|
+
item_id: `${responseId}_msg`,
|
|
298
|
+
output_index: 0,
|
|
299
|
+
content_index: 0,
|
|
300
|
+
delta: delta.content,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
for (const call of delta.tool_calls || []) {
|
|
304
|
+
const index = call.index || 0;
|
|
305
|
+
const current = toolCalls.get(index) || {
|
|
306
|
+
id: call.id || `call_${crypto.randomUUID()}`,
|
|
307
|
+
name: "",
|
|
308
|
+
arguments: "",
|
|
309
|
+
};
|
|
310
|
+
if (call.id) current.id = call.id;
|
|
311
|
+
if (call.function?.name) current.name += call.function.name;
|
|
312
|
+
if (call.function?.arguments) current.arguments += call.function.arguments;
|
|
313
|
+
toolCalls.set(index, current);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const output: Json[] = [];
|
|
318
|
+
if (textStarted) {
|
|
319
|
+
const item = {
|
|
320
|
+
id: `${responseId}_msg`,
|
|
321
|
+
type: "message",
|
|
322
|
+
role: "assistant",
|
|
323
|
+
status: "completed",
|
|
324
|
+
content: [{ type: "output_text", text, annotations: [] }],
|
|
325
|
+
};
|
|
326
|
+
emit({
|
|
327
|
+
type: "response.output_text.done",
|
|
328
|
+
item_id: item.id,
|
|
329
|
+
output_index: 0,
|
|
330
|
+
content_index: 0,
|
|
331
|
+
text,
|
|
332
|
+
});
|
|
333
|
+
emit({
|
|
334
|
+
type: "response.content_part.done",
|
|
335
|
+
item_id: item.id,
|
|
336
|
+
output_index: 0,
|
|
337
|
+
content_index: 0,
|
|
338
|
+
part: item.content[0],
|
|
339
|
+
});
|
|
340
|
+
emit({ type: "response.output_item.done", output_index: 0, item });
|
|
341
|
+
output.push(item);
|
|
342
|
+
if (reasoning) reasoningStore.set(item.id, reasoning);
|
|
343
|
+
}
|
|
344
|
+
for (const [, call] of [...toolCalls].sort(([a], [b]) => a - b)) {
|
|
345
|
+
if (reasoning) reasoningStore.set(call.id, reasoning);
|
|
346
|
+
const outputIndex = output.length;
|
|
347
|
+
if (custom.has(call.name)) {
|
|
348
|
+
let input = call.arguments;
|
|
349
|
+
try {
|
|
350
|
+
input = JSON.parse(call.arguments).input ?? call.arguments;
|
|
351
|
+
} catch {}
|
|
352
|
+
const item = {
|
|
353
|
+
id: `${responseId}_tool_${outputIndex}`,
|
|
354
|
+
type: "custom_tool_call",
|
|
355
|
+
status: "completed",
|
|
356
|
+
call_id: call.id,
|
|
357
|
+
name: call.name,
|
|
358
|
+
input,
|
|
359
|
+
};
|
|
360
|
+
emit({
|
|
361
|
+
type: "response.output_item.added",
|
|
362
|
+
output_index: outputIndex,
|
|
363
|
+
item: { ...item, status: "in_progress", input: "" },
|
|
364
|
+
});
|
|
365
|
+
emit({
|
|
366
|
+
type: "response.custom_tool_call_input.delta",
|
|
367
|
+
item_id: item.id,
|
|
368
|
+
output_index: outputIndex,
|
|
369
|
+
delta: input,
|
|
370
|
+
});
|
|
371
|
+
emit({
|
|
372
|
+
type: "response.custom_tool_call_input.done",
|
|
373
|
+
item_id: item.id,
|
|
374
|
+
output_index: outputIndex,
|
|
375
|
+
input,
|
|
376
|
+
});
|
|
377
|
+
emit({ type: "response.output_item.done", output_index: outputIndex, item });
|
|
378
|
+
output.push(item);
|
|
379
|
+
} else {
|
|
380
|
+
const item = {
|
|
381
|
+
id: `${responseId}_tool_${outputIndex}`,
|
|
382
|
+
type: "function_call",
|
|
383
|
+
status: "completed",
|
|
384
|
+
call_id: call.id,
|
|
385
|
+
name: call.name,
|
|
386
|
+
arguments: call.arguments,
|
|
387
|
+
};
|
|
388
|
+
emit({
|
|
389
|
+
type: "response.output_item.added",
|
|
390
|
+
output_index: outputIndex,
|
|
391
|
+
item: { ...item, status: "in_progress", arguments: "" },
|
|
392
|
+
});
|
|
393
|
+
emit({
|
|
394
|
+
type: "response.function_call_arguments.delta",
|
|
395
|
+
item_id: item.id,
|
|
396
|
+
output_index: outputIndex,
|
|
397
|
+
delta: call.arguments,
|
|
398
|
+
});
|
|
399
|
+
emit({
|
|
400
|
+
type: "response.function_call_arguments.done",
|
|
401
|
+
item_id: item.id,
|
|
402
|
+
output_index: outputIndex,
|
|
403
|
+
arguments: call.arguments,
|
|
404
|
+
});
|
|
405
|
+
emit({ type: "response.output_item.done", output_index: outputIndex, item });
|
|
406
|
+
output.push(item);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
emit({
|
|
410
|
+
type: "response.completed",
|
|
411
|
+
response: {
|
|
412
|
+
...responseBase,
|
|
413
|
+
status: "completed",
|
|
414
|
+
output,
|
|
415
|
+
usage: {
|
|
416
|
+
input_tokens: usage.prompt_tokens || 0,
|
|
417
|
+
output_tokens: usage.completion_tokens || 0,
|
|
418
|
+
total_tokens: usage.total_tokens || 0,
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
emit("[DONE]");
|
|
423
|
+
controller.close();
|
|
424
|
+
} catch (error) {
|
|
425
|
+
controller.error(error);
|
|
426
|
+
} finally {
|
|
427
|
+
reader.releaseLock();
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
return new Response(stream, {
|
|
432
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" },
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const server = Bun.serve({
|
|
437
|
+
hostname: "127.0.0.1",
|
|
438
|
+
port: 0,
|
|
439
|
+
fetch(request) {
|
|
440
|
+
const url = new URL(request.url);
|
|
441
|
+
if (
|
|
442
|
+
request.method === "POST" &&
|
|
443
|
+
(url.pathname === "/v1/responses" || url.pathname === "/responses")
|
|
444
|
+
)
|
|
445
|
+
return responses(request);
|
|
446
|
+
if (url.pathname === "/health") return Response.json({ ok: true, model });
|
|
447
|
+
return Response.json({ error: { message: "not found" } }, { status: 404 });
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const codexHome = join(root, ".codex-deepseek");
|
|
452
|
+
mkdirSync(codexHome, { recursive: true });
|
|
453
|
+
|
|
454
|
+
// The DeepSeek provider config that used to be injected as codex `-c` flags.
|
|
455
|
+
// Now written to CODEX_HOME/config.toml instead, because codex is spawned
|
|
456
|
+
// through the agent-yes wrapper (`ay codex`) whose yargs parser would swallow
|
|
457
|
+
// `-c` (alias of --continue) before it ever reached codex.
|
|
458
|
+
const configToml = [
|
|
459
|
+
`model_provider = "deepseek"`,
|
|
460
|
+
`model = "${model}"`,
|
|
461
|
+
`model_supports_reasoning_summaries = false`,
|
|
462
|
+
`model_context_window = 1000000`,
|
|
463
|
+
``,
|
|
464
|
+
`[model_providers.deepseek]`,
|
|
465
|
+
`name = "DeepSeek local adapter"`,
|
|
466
|
+
`base_url = "http://127.0.0.1:${server.port}/v1"`,
|
|
467
|
+
`env_key = "DEEPSEEK_PROXY_KEY"`,
|
|
468
|
+
`wire_api = "responses"`,
|
|
469
|
+
// Let codex absorb transient upstream 5xx (e.g. DeepSeek gateway 502s) with
|
|
470
|
+
// its built-in exponential backoff instead of surfacing them to the user.
|
|
471
|
+
`request_max_retries = 3`,
|
|
472
|
+
`stream_max_retries = 3`,
|
|
473
|
+
].join("\n");
|
|
474
|
+
writeFileSync(join(codexHome, "config.toml"), configToml);
|
|
475
|
+
|
|
476
|
+
if (process.env.DEEPSEEK_SERVER_ONLY) {
|
|
477
|
+
// Debug mode: run the adapter without spawning codex so the upstream request
|
|
478
|
+
// can be driven by an external codex/curl invocation. Serve until signalled.
|
|
479
|
+
await new Promise<void>((resolve) => {
|
|
480
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) process.once(signal, resolve);
|
|
481
|
+
});
|
|
482
|
+
server.stop(true);
|
|
483
|
+
process.exit(0);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Spawn codex THROUGH the agent-yes wrapper so the session is a first-class
|
|
487
|
+
// agent: registered in `ay ls`, tail-able, resumable, auto-yes, etc. AGENT_YES_BIN
|
|
488
|
+
// is injected by `ay ds`; fall back to `ay` on PATH for a bare `bun run deepseek`.
|
|
489
|
+
// Exception: `exec` subcommand, whose non-interactive one-shot runs don't need
|
|
490
|
+
// agent registration, and which the wrapper's codex `defaultArgs` (`--search`)
|
|
491
|
+
// would break (`codex --search exec` is an unknown-argument error).
|
|
492
|
+
const args = process.argv.slice(2);
|
|
493
|
+
const ayBin = process.env.AGENT_YES_BIN || "ay";
|
|
494
|
+
const isExec = args[0] === "exec";
|
|
495
|
+
const spawnedAt = Date.now();
|
|
496
|
+
const child = Bun.spawn(isExec ? ["codex", ...args] : [ayBin, "codex", ...args], {
|
|
497
|
+
cwd: process.cwd(),
|
|
498
|
+
env: { ...process.env, CODEX_HOME: codexHome, DEEPSEEK_PROXY_KEY: "local-adapter" },
|
|
499
|
+
stdin: "inherit",
|
|
500
|
+
stdout: "inherit",
|
|
501
|
+
stderr: "inherit",
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
505
|
+
process.on(signal, () => child.kill(signal));
|
|
506
|
+
}
|
|
507
|
+
const exitCode = await child.exited;
|
|
508
|
+
|
|
509
|
+
// When `ay codex` is launched from a nested non-TTY context (AGENT_YES_PID set
|
|
510
|
+
// + stdout piped) the wrapper detaches the codex agent and returns immediately
|
|
511
|
+
// (see cli.ts shouldForkNested). That orphaned agent still talks to THIS
|
|
512
|
+
// adapter, so tearing the server down here would break it mid-stream. Keep
|
|
513
|
+
// serving until every codex agent this invocation spawned has exited.
|
|
514
|
+
await waitForOrphanedCodex(spawnedAt);
|
|
515
|
+
|
|
516
|
+
server.stop(true);
|
|
517
|
+
process.exit(exitCode);
|
|
518
|
+
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// orphan-aware server teardown
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
function isPidAlive(pid: number): boolean {
|
|
524
|
+
try {
|
|
525
|
+
process.kill(pid, 0);
|
|
526
|
+
return true;
|
|
527
|
+
} catch {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Last-wins-per-pid view of the agent registry (~/.agent-yes/pids.jsonl). */
|
|
533
|
+
function readAgentRegistry(): Array<{
|
|
534
|
+
pid: number;
|
|
535
|
+
cli?: string;
|
|
536
|
+
cwd?: string;
|
|
537
|
+
status?: string;
|
|
538
|
+
started_at?: number;
|
|
539
|
+
}> {
|
|
540
|
+
const pidsPath = join(homedir(), ".agent-yes", "pids.jsonl");
|
|
541
|
+
let raw: string;
|
|
542
|
+
try {
|
|
543
|
+
raw = readFileSync(pidsPath, "utf-8");
|
|
544
|
+
} catch {
|
|
545
|
+
return [];
|
|
546
|
+
}
|
|
547
|
+
const merged = new Map<number, any>();
|
|
548
|
+
for (const line of raw.split("\n")) {
|
|
549
|
+
const trimmed = line.trim();
|
|
550
|
+
if (!trimmed) continue;
|
|
551
|
+
try {
|
|
552
|
+
const doc = JSON.parse(trimmed);
|
|
553
|
+
if (typeof doc.pid === "number") {
|
|
554
|
+
const prev = merged.get(doc.pid);
|
|
555
|
+
merged.set(doc.pid, prev ? { ...prev, ...doc } : doc);
|
|
556
|
+
}
|
|
557
|
+
} catch {
|
|
558
|
+
/* skip corrupt */
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return [...merged.values()];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function waitForOrphanedCodex(spawnedAt: number): Promise<void> {
|
|
565
|
+
const MAX_WAIT_MS = 24 * 60 * 60 * 1000;
|
|
566
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
567
|
+
while (Date.now() < deadline) {
|
|
568
|
+
const orphan = readAgentRegistry().find(
|
|
569
|
+
(r) =>
|
|
570
|
+
r.cli === "codex" &&
|
|
571
|
+
r.cwd === process.cwd() &&
|
|
572
|
+
typeof r.started_at === "number" &&
|
|
573
|
+
r.started_at >= spawnedAt - 5000 &&
|
|
574
|
+
r.status !== "exited" &&
|
|
575
|
+
isPidAlive(r.pid),
|
|
576
|
+
);
|
|
577
|
+
if (!orphan) return;
|
|
578
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
579
|
+
}
|
|
580
|
+
}
|
package/ts/serve.spec.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
installerArgv,
|
|
5
5
|
isNoNodeExecError,
|
|
6
6
|
oxmgrVersionHasWindowsFix,
|
|
7
|
+
parseGitUrl,
|
|
7
8
|
portlessConsoleUrl,
|
|
8
9
|
} from "./serve.ts";
|
|
9
10
|
|
|
@@ -128,3 +129,38 @@ describe("installerArgv", () => {
|
|
|
128
129
|
expect(installerArgv("pm2", null, null)).toBeNull();
|
|
129
130
|
});
|
|
130
131
|
});
|
|
132
|
+
|
|
133
|
+
// The raw-clone spawn path only handles NON-github git sources; github (and
|
|
134
|
+
// bare owner/repo, which never reaches this parser) goes through the provision
|
|
135
|
+
// module. Owner/repo come from the URL's last two path segments so the checkout
|
|
136
|
+
// lands in the standard <wsRoot>/<owner>/<repo>/tree/<branch> layout.
|
|
137
|
+
describe("parseGitUrl", () => {
|
|
138
|
+
it("parses scp-style, ssh and https non-github URLs", () => {
|
|
139
|
+
expect(parseGitUrl("git@gitlab.com:acme/tools.git")).toEqual({
|
|
140
|
+
url: "git@gitlab.com:acme/tools.git",
|
|
141
|
+
owner: "acme",
|
|
142
|
+
repo: "tools",
|
|
143
|
+
});
|
|
144
|
+
expect(parseGitUrl("ssh://git@git.corp.io/team/app.git")).toEqual({
|
|
145
|
+
url: "ssh://git@git.corp.io/team/app.git",
|
|
146
|
+
owner: "team",
|
|
147
|
+
repo: "app",
|
|
148
|
+
});
|
|
149
|
+
expect(parseGitUrl("https://gitlab.com/acme/tools")).toEqual({
|
|
150
|
+
url: "https://gitlab.com/acme/tools",
|
|
151
|
+
owner: "acme",
|
|
152
|
+
repo: "tools",
|
|
153
|
+
});
|
|
154
|
+
// subgrouped gitlab paths use the LAST two segments
|
|
155
|
+
expect(parseGitUrl("https://gitlab.com/org/group/proj.git")?.owner).toBe("group");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("rejects github, bare specs, and traversal-looking segments", () => {
|
|
159
|
+
expect(parseGitUrl("https://github.com/acme/tools")).toBeNull();
|
|
160
|
+
expect(parseGitUrl("git@github.com:acme/tools.git")).toBeNull();
|
|
161
|
+
expect(parseGitUrl("acme/tools")).toBeNull();
|
|
162
|
+
expect(parseGitUrl("https://gitlab.com/tools")).toBeNull();
|
|
163
|
+
expect(parseGitUrl("https://gitlab.com/../evil")).toBeNull();
|
|
164
|
+
expect(parseGitUrl("file:///etc/passwd")).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
});
|