@zhongqian97-code/ecode 0.5.45 → 0.5.47

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.
@@ -0,0 +1,727 @@
1
+ #!/usr/bin/env node
2
+ const _ew=process.emitWarning.bind(process);process.emitWarning=function(w,...a){if((w?.message??w)?.includes?.('punycode'))return;_ew(w,...a);};
3
+
4
+ // src/config.ts
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ function expandTilde(p) {
9
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
10
+ return join(homedir(), p.slice(1));
11
+ }
12
+ return p;
13
+ }
14
+ var MODEL_CONTEXT_LIMITS = {
15
+ // OpenAI GPT 系列
16
+ "gpt-4o": 128e3,
17
+ "gpt-4o-mini": 128e3,
18
+ "gpt-4-turbo": 128e3,
19
+ "gpt-4": 8192,
20
+ // 老版 GPT-4 上下文极小,需特别注意
21
+ "gpt-3.5-turbo": 16385,
22
+ // OpenAI o 系列推理模型
23
+ "o1": 2e5,
24
+ "o1-mini": 128e3,
25
+ "o1-preview": 128e3,
26
+ "o3": 2e5,
27
+ "o3-mini": 2e5,
28
+ // Anthropic Claude 系列(全线支持 200K)
29
+ "claude-3-5-sonnet-20241022": 2e5,
30
+ "claude-3-5-haiku-20241022": 2e5,
31
+ "claude-3-opus-20240229": 2e5,
32
+ "claude-3-sonnet-20240229": 2e5,
33
+ "claude-3-haiku-20240307": 2e5,
34
+ "claude-sonnet-4-6": 2e5,
35
+ "claude-opus-4-7": 2e5,
36
+ "claude-haiku-4-5-20251001": 2e5,
37
+ // DeepSeek 系列(上下文较小,截断策略需更激进)
38
+ "deepseek-chat": 65536,
39
+ "deepseek-reasoner": 65536,
40
+ // MiniMax 系列(baseUrl: https://api.minimax.chat/v1)
41
+ "MiniMax-M2.5": 1e6,
42
+ "MiniMax-M2.5-highspeed": 192e3,
43
+ "MiniMax-Text-01": 1e6
44
+ };
45
+ var DEFAULT_CONTEXT_LIMIT = 128e3;
46
+ function getContextLimit(model, override) {
47
+ if (override !== void 0) return override;
48
+ return MODEL_CONTEXT_LIMITS[model] ?? DEFAULT_CONTEXT_LIMIT;
49
+ }
50
+ var DEFAULTS = {
51
+ baseUrl: "https://api.openai.com/v1",
52
+ apiKey: "",
53
+ model: "gpt-4o",
54
+ dangerousPatterns: [
55
+ "rm -rf",
56
+ "sudo",
57
+ "chmod",
58
+ "chown",
59
+ "mkfs",
60
+ "dd",
61
+ "fdisk",
62
+ "kill",
63
+ "pkill",
64
+ "killall",
65
+ "reboot",
66
+ "shutdown",
67
+ "halt",
68
+ "curl -X DELETE",
69
+ "wget --delete-after"
70
+ ],
71
+ logDir: void 0
72
+ };
73
+ function loadConfig() {
74
+ const configPath = join(homedir(), ".ecode", "config.json");
75
+ let fileConfig = {};
76
+ if (existsSync(configPath)) {
77
+ try {
78
+ const raw = readFileSync(configPath, "utf-8");
79
+ fileConfig = JSON.parse(raw);
80
+ } catch (err) {
81
+ const msg = err instanceof Error ? err.message : String(err);
82
+ process.stderr.write(`[ecode] warning: ${configPath} parse failed (${msg}), falling back to defaults/env
83
+ `);
84
+ }
85
+ }
86
+ return {
87
+ // ?? 运算符:仅在左侧为 null / undefined 时才取右侧值,
88
+ // 与 || 的区别在于不会跳过空字符串,保证显式设置 "" 也能生效
89
+ baseUrl: process.env.ECODE_BASE_URL ?? fileConfig.baseUrl ?? DEFAULTS.baseUrl,
90
+ apiKey: process.env.ECODE_API_KEY ?? fileConfig.apiKey ?? DEFAULTS.apiKey,
91
+ model: process.env.ECODE_MODEL ?? fileConfig.model ?? DEFAULTS.model,
92
+ // dangerousPatterns 不支持环境变量注入:命令数组通过单个环境变量传递需要转义,
93
+ // 容易引入歧义,因此仅支持配置文件覆盖
94
+ dangerousPatterns: fileConfig.dangerousPatterns ?? DEFAULTS.dangerousPatterns,
95
+ // logDir: ECODE_LOG_DIR 环境变量 > 配置文件 > undefined(禁用日志)
96
+ // expandTilde 将 ~ 替换为 homedir(),Node.js 不执行 shell 的 ~ 展开
97
+ logDir: (() => {
98
+ const raw = process.env.ECODE_LOG_DIR ?? fileConfig.logDir ?? DEFAULTS.logDir;
99
+ return raw ? expandTilde(raw) : raw;
100
+ })(),
101
+ // contextLimit 仅支持配置文件配置:数值类型在文件中更直观,
102
+ // 环境变量还需要 parseInt 转换,增加出错风险
103
+ contextLimit: fileConfig.contextLimit,
104
+ // ECODE_SYSTEM_PROMPT 环境变量优先;未设时从配置文件读取;
105
+ // 两者均未设时保持 undefined(由 resolveSystemPrompt 决定使用内置默认值)。
106
+ // 注意:空字符串 "" 是有效值(表示禁用),?? 不会跳过空字符串。
107
+ systemPrompt: process.env.ECODE_SYSTEM_PROMPT ?? fileConfig.systemPrompt,
108
+ // providers/defaultProvider 仅支持配置文件配置,不支持环境变量注入
109
+ providers: fileConfig.providers,
110
+ defaultProvider: fileConfig.defaultProvider,
111
+ // ECODE_WEB_TOKEN 环境变量优先;未设时从配置文件读取;
112
+ // 两者均未设时保持 undefined,运行时由 generateAccessToken() 生成随机令牌
113
+ webToken: process.env.ECODE_WEB_TOKEN ?? fileConfig.webToken
114
+ };
115
+ }
116
+ function saveConfig(partial) {
117
+ const configPath = join(homedir(), ".ecode", "config.json");
118
+ const configDir = join(homedir(), ".ecode");
119
+ mkdirSync(configDir, { recursive: true });
120
+ let existing = {};
121
+ if (existsSync(configPath)) {
122
+ try {
123
+ const raw = readFileSync(configPath, "utf-8");
124
+ existing = JSON.parse(raw);
125
+ } catch (err) {
126
+ const msg = err instanceof Error ? err.message : String(err);
127
+ process.stderr.write(`[ecode] warning: ${configPath} parse failed (${msg}), overwriting with new config
128
+ `);
129
+ }
130
+ }
131
+ const merged = { ...existing, ...partial };
132
+ writeFileSync(configPath, JSON.stringify(merged, null, 2), "utf-8");
133
+ }
134
+
135
+ // src/providers/openai.ts
136
+ import OpenAI from "openai";
137
+ function createOpenAIProvider(profile) {
138
+ const THINK_END = "</think>";
139
+ const openai = new OpenAI({
140
+ baseURL: profile.baseUrl,
141
+ apiKey: profile.apiKey
142
+ });
143
+ const capabilities = Object.freeze(
144
+ Object.defineProperties({}, {
145
+ supportsTools: { value: true, writable: false, enumerable: true, configurable: false },
146
+ supportsReasoningStream: { value: true, writable: false, enumerable: true, configurable: false },
147
+ supportsImages: { value: false, writable: false, enumerable: true, configurable: false },
148
+ supportsJsonSchema: { value: true, writable: false, enumerable: true, configurable: false }
149
+ })
150
+ );
151
+ return {
152
+ capabilities,
153
+ /**
154
+ * stream 方法向 LLM 发起一次流式对话请求,返回异步可迭代的 chunk 序列。
155
+ *
156
+ * 实现细节:
157
+ * - 使用 `Symbol.asyncIterator` + async generator 实现懒执行:
158
+ * 只有调用方执行 `for await` 时才真正发起 HTTP 请求,避免浪费连接
159
+ * - 内部维护两个累加器:
160
+ * 1. `tcAccumulator`:按 index 聚合工具调用的分片参数(JSON 字符串)
161
+ * 2. `reasoningAccumulator`:拼接思考链的所有分片文本
162
+ * - 只在最终 chunk(`finish_reason !== null`)中 yield done=true 的完整信息
163
+ *
164
+ * @param messages 完整的对话历史,包含 user/assistant/tool 所有轮次
165
+ * @param tools 可选的工具列表,不传或传空数组时不附加 tools 字段
166
+ * @param signal 可选的 AbortSignal,用于取消请求
167
+ */
168
+ stream(messages, tools, signal) {
169
+ return {
170
+ [Symbol.asyncIterator]: async function* () {
171
+ var _a, _b, _c;
172
+ let thinkPhase = "pre";
173
+ let scanningForClose = false;
174
+ let closeSearchBuffer = "";
175
+ let visibleTextTail = "";
176
+ function rememberVisibleText(text) {
177
+ if (!text) return;
178
+ visibleTextTail = (visibleTextTail + text).slice(-256);
179
+ }
180
+ function hasNearbyVisibleOpenThink(maxDistance = 4) {
181
+ const openIdx = visibleTextTail.lastIndexOf("<think>");
182
+ const closeIdx = visibleTextTail.lastIndexOf(THINK_END);
183
+ if (openIdx === -1 || openIdx < closeIdx) return false;
184
+ return visibleTextTail.length - (openIdx + "<think>".length) <= maxDistance;
185
+ }
186
+ function processContent(raw) {
187
+ if (!raw) return { text: "", thinking: "" };
188
+ if (thinkPhase === "post") return { text: raw, thinking: "" };
189
+ if (thinkPhase === "pre") {
190
+ if (raw.startsWith("<think>")) {
191
+ thinkPhase = "in";
192
+ raw = raw.slice(7);
193
+ } else {
194
+ thinkPhase = "post";
195
+ return { text: raw, thinking: "" };
196
+ }
197
+ }
198
+ const endIdx = raw.indexOf(THINK_END);
199
+ if (endIdx === -1) return { text: "", thinking: raw };
200
+ const thinking = raw.slice(0, endIdx);
201
+ thinkPhase = "post";
202
+ return { text: raw.slice(endIdx + THINK_END.length), thinking };
203
+ }
204
+ const requestParams = {
205
+ model: profile.model,
206
+ messages,
207
+ stream: true,
208
+ stream_options: { include_usage: true }
209
+ };
210
+ if (tools && tools.length > 0) {
211
+ requestParams.tools = tools;
212
+ }
213
+ const response = await openai.chat.completions.create(
214
+ requestParams,
215
+ signal ? { signal } : void 0
216
+ );
217
+ const tcAccumulator = /* @__PURE__ */ new Map();
218
+ let reasoningAccumulator = "";
219
+ const reasoningDetailsAcc = /* @__PURE__ */ new Map();
220
+ for await (const chunk of response) {
221
+ const choice = chunk.choices[0];
222
+ if (!choice) continue;
223
+ const delta = choice.delta;
224
+ if (delta.reasoning_content) {
225
+ reasoningAccumulator += delta.reasoning_content;
226
+ }
227
+ if (delta.reasoning_details && delta.reasoning_details.length > 0) {
228
+ scanningForClose = true;
229
+ for (const rd of delta.reasoning_details) {
230
+ const id = rd.id ?? "";
231
+ const text = rd.text ?? "";
232
+ if (!reasoningDetailsAcc.has(id)) {
233
+ reasoningDetailsAcc.set(id, {
234
+ type: rd.type ?? "reasoning.text",
235
+ id,
236
+ format: rd.format ?? "",
237
+ index: rd.index ?? 0,
238
+ text: ""
239
+ });
240
+ }
241
+ const existing = reasoningDetailsAcc.get(id);
242
+ existing.text += text;
243
+ if (!delta.reasoning_content && text) {
244
+ reasoningAccumulator += text;
245
+ }
246
+ }
247
+ }
248
+ if (delta.tool_calls) {
249
+ for (const tc of delta.tool_calls) {
250
+ if (!tcAccumulator.has(tc.index)) {
251
+ tcAccumulator.set(tc.index, {
252
+ id: tc.id ?? "",
253
+ name: ((_a = tc.function) == null ? void 0 : _a.name) ?? "",
254
+ arguments: ""
255
+ });
256
+ }
257
+ const existing = tcAccumulator.get(tc.index);
258
+ if (tc.id) existing.id = tc.id;
259
+ if ((_b = tc.function) == null ? void 0 : _b.name) existing.name = tc.function.name;
260
+ existing.arguments += ((_c = tc.function) == null ? void 0 : _c.arguments) ?? "";
261
+ }
262
+ }
263
+ const isLast = choice.finish_reason != null;
264
+ let rawToProcess;
265
+ if (delta.reasoning_details && delta.reasoning_details.length > 0) {
266
+ rawToProcess = "";
267
+ } else if (scanningForClose) {
268
+ closeSearchBuffer += delta.content ?? "";
269
+ const closeIdx = closeSearchBuffer.indexOf(THINK_END);
270
+ if (closeIdx !== -1) {
271
+ const openIdx = closeSearchBuffer.lastIndexOf("<think>", closeIdx);
272
+ const hasNearbyLiteralPair = openIdx !== -1 && closeIdx - (openIdx + "<think>".length) <= 8;
273
+ const afterClose = closeSearchBuffer.slice(closeIdx + THINK_END.length);
274
+ rawToProcess = hasNearbyLiteralPair ? closeSearchBuffer : afterClose.length > 0 ? afterClose : closeSearchBuffer.slice(0, closeIdx);
275
+ scanningForClose = false;
276
+ closeSearchBuffer = "";
277
+ thinkPhase = "post";
278
+ } else if (isLast) {
279
+ rawToProcess = closeSearchBuffer;
280
+ scanningForClose = false;
281
+ closeSearchBuffer = "";
282
+ thinkPhase = "post";
283
+ } else {
284
+ rawToProcess = "";
285
+ }
286
+ } else {
287
+ rawToProcess = delta.content ?? "";
288
+ }
289
+ if (thinkPhase === "post" && rawToProcess.endsWith(THINK_END) && !rawToProcess.includes("<think>") && !hasNearbyVisibleOpenThink()) {
290
+ rawToProcess = rawToProcess.slice(0, -THINK_END.length);
291
+ }
292
+ const { text: filteredText, thinking: thinkContent } = processContent(rawToProcess);
293
+ rememberVisibleText(filteredText);
294
+ if (thinkContent) {
295
+ reasoningAccumulator += thinkContent;
296
+ }
297
+ if (isLast) {
298
+ const rawUsage = chunk.usage;
299
+ yield {
300
+ text: filteredText,
301
+ done: true,
302
+ finishReason: choice.finish_reason,
303
+ // tcAccumulator 为空说明本轮没有工具调用,传 undefined 而非空数组,
304
+ // 让调用方用 if (chunk.toolCalls) 做简洁判断
305
+ toolCalls: tcAccumulator.size > 0 ? Array.from(tcAccumulator.values()) : void 0,
306
+ reasoning: reasoningAccumulator || void 0,
307
+ // reasoningDetails 仅在流中出现过结构化推理时返回(MiniMax 兼容)
308
+ reasoningDetails: reasoningDetailsAcc.size > 0 ? Array.from(reasoningDetailsAcc.values()) : void 0,
309
+ // 将 snake_case 的原始字段映射为 camelCase,对外接口保持一致
310
+ usage: rawUsage ? {
311
+ promptTokens: rawUsage.prompt_tokens,
312
+ completionTokens: rawUsage.completion_tokens,
313
+ totalTokens: rawUsage.total_tokens
314
+ } : void 0
315
+ };
316
+ } else {
317
+ const incrementalReasoning = delta.reasoning_content || thinkContent || (delta.reasoning_details && delta.reasoning_details.length > 0 ? delta.reasoning_details.map((rd) => rd.text ?? "").join("") : void 0) || void 0;
318
+ yield {
319
+ text: filteredText,
320
+ reasoning: incrementalReasoning || void 0,
321
+ done: false
322
+ };
323
+ }
324
+ }
325
+ }
326
+ };
327
+ }
328
+ };
329
+ }
330
+
331
+ // src/providers/anthropic.ts
332
+ import https from "https";
333
+ import { URL } from "url";
334
+ function convertMessagesToAnthropic(messages) {
335
+ let system = "";
336
+ const anthropicMessages = [];
337
+ for (let i = 0; i < messages.length; i++) {
338
+ const msg = messages[i];
339
+ if (msg.role === "system") {
340
+ system = msg.content;
341
+ continue;
342
+ }
343
+ if (msg.role === "user") {
344
+ anthropicMessages.push({ role: "user", content: msg.content });
345
+ continue;
346
+ }
347
+ if (msg.role === "tool") {
348
+ const toolResults = [];
349
+ while (i < messages.length && messages[i].role === "tool") {
350
+ const toolMsg = messages[i];
351
+ toolResults.push({
352
+ type: "tool_result",
353
+ tool_use_id: toolMsg.tool_call_id,
354
+ content: toolMsg.content
355
+ });
356
+ i++;
357
+ }
358
+ i--;
359
+ anthropicMessages.push({ role: "user", content: toolResults });
360
+ continue;
361
+ }
362
+ if (msg.role === "assistant") {
363
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
364
+ const content = [];
365
+ if (msg.content) content.push({ type: "text", text: msg.content });
366
+ for (const tc of msg.tool_calls) {
367
+ let input = {};
368
+ try {
369
+ input = JSON.parse(tc.function.arguments);
370
+ } catch {
371
+ input = {};
372
+ }
373
+ content.push({ type: "tool_use", id: tc.id, name: tc.function.name, input });
374
+ }
375
+ anthropicMessages.push({ role: "assistant", content });
376
+ } else {
377
+ anthropicMessages.push({ role: "assistant", content: msg.content ?? "" });
378
+ }
379
+ }
380
+ }
381
+ return { system, anthropicMessages };
382
+ }
383
+ function convertToolsToAnthropic(tools) {
384
+ return tools.map((tool) => ({
385
+ name: tool.function.name,
386
+ description: tool.function.description,
387
+ input_schema: tool.function.parameters
388
+ }));
389
+ }
390
+ function createAnthropicProvider(profile) {
391
+ const capabilities = Object.freeze(
392
+ Object.defineProperties({}, {
393
+ supportsTools: { value: true, writable: false, enumerable: true },
394
+ supportsReasoningStream: { value: false, writable: false, enumerable: true },
395
+ supportsImages: { value: true, writable: false, enumerable: true },
396
+ supportsJsonSchema: { value: true, writable: false, enumerable: true }
397
+ })
398
+ );
399
+ return {
400
+ capabilities,
401
+ stream(messages, tools, signal) {
402
+ return streamAnthropicResponse(profile, messages, tools, signal);
403
+ }
404
+ };
405
+ }
406
+ async function* streamAnthropicResponse(profile, messages, tools, signal) {
407
+ const { system, anthropicMessages } = convertMessagesToAnthropic(messages);
408
+ const endpointUrl = new URL(`${profile.baseUrl.replace(/\/v1\/?$/, "").replace(/\/$/, "")}/v1/messages`);
409
+ const requestBody = {
410
+ model: profile.model,
411
+ max_tokens: 8192,
412
+ messages: anthropicMessages,
413
+ stream: true
414
+ };
415
+ if (system) requestBody.system = system;
416
+ if (tools && tools.length > 0) requestBody.tools = convertToolsToAnthropic(tools);
417
+ const bodyStr = JSON.stringify(requestBody);
418
+ yield* makeHttpsStream(endpointUrl, profile.apiKey, bodyStr, signal);
419
+ }
420
+ async function* makeHttpsStream(url, apiKey, body, signal) {
421
+ var _a;
422
+ const queue = [];
423
+ let notify = null;
424
+ function push(item) {
425
+ queue.push(item);
426
+ if (notify) {
427
+ const fn = notify;
428
+ notify = null;
429
+ fn();
430
+ }
431
+ }
432
+ const port = url.port ? parseInt(url.port, 10) : 443;
433
+ const req = https.request(
434
+ {
435
+ hostname: url.hostname,
436
+ port,
437
+ path: url.pathname + url.search,
438
+ method: "POST",
439
+ headers: {
440
+ "x-api-key": apiKey,
441
+ "anthropic-version": "2023-06-01",
442
+ "content-type": "application/json",
443
+ accept: "text/event-stream",
444
+ "content-length": String(Buffer.byteLength(body, "utf8"))
445
+ }
446
+ },
447
+ (res) => {
448
+ const statusCode = res.statusCode;
449
+ if (statusCode !== 200) {
450
+ push(new Error(`Anthropic API error: HTTP ${statusCode}`));
451
+ return;
452
+ }
453
+ const r = res;
454
+ r.on("data", (chunk) => push(chunk));
455
+ r.on("end", () => push(null));
456
+ r.on("error", (err) => push(err));
457
+ }
458
+ );
459
+ req.on("error", (err) => push(err));
460
+ if (signal) {
461
+ signal.addEventListener(
462
+ "abort",
463
+ () => {
464
+ req.destroy();
465
+ push(null);
466
+ },
467
+ { once: true }
468
+ );
469
+ }
470
+ req.write(body);
471
+ req.end();
472
+ let buffer = "";
473
+ const toolBlocks = /* @__PURE__ */ new Map();
474
+ let stopReason = null;
475
+ let inputTokens = 0;
476
+ let outputTokens = 0;
477
+ while (true) {
478
+ let item;
479
+ if (queue.length > 0) {
480
+ item = queue.shift();
481
+ } else {
482
+ await new Promise((resolve) => {
483
+ notify = resolve;
484
+ });
485
+ item = queue.shift();
486
+ }
487
+ if (item === null) break;
488
+ if (item instanceof Error) throw item;
489
+ buffer += item.toString("utf8");
490
+ const lines = buffer.split("\n");
491
+ buffer = lines.pop() ?? "";
492
+ for (const line of lines) {
493
+ if (!line.startsWith("data: ")) continue;
494
+ const data = line.slice(6).trim();
495
+ if (data === "[DONE]") break;
496
+ let event;
497
+ try {
498
+ event = JSON.parse(data);
499
+ } catch {
500
+ continue;
501
+ }
502
+ const eventType = event.type;
503
+ if (eventType === "message_start") {
504
+ const msg = event.message;
505
+ if ((_a = msg == null ? void 0 : msg.usage) == null ? void 0 : _a.input_tokens) inputTokens = msg.usage.input_tokens;
506
+ } else if (eventType === "content_block_start") {
507
+ const block = event.content_block;
508
+ const index = event.index;
509
+ if ((block == null ? void 0 : block.type) === "tool_use" && block.id && block.name) {
510
+ toolBlocks.set(index, { id: block.id, name: block.name, jsonAccum: "" });
511
+ }
512
+ } else if (eventType === "content_block_delta") {
513
+ const delta = event.delta;
514
+ const index = event.index;
515
+ if ((delta == null ? void 0 : delta.type) === "text_delta" && delta.text) {
516
+ yield { text: delta.text, done: false };
517
+ } else if ((delta == null ? void 0 : delta.type) === "input_json_delta" && delta.partial_json) {
518
+ const tool = toolBlocks.get(index);
519
+ if (tool) tool.jsonAccum += delta.partial_json;
520
+ }
521
+ } else if (eventType === "message_delta") {
522
+ const delta = event.delta;
523
+ stopReason = (delta == null ? void 0 : delta.stop_reason) ?? null;
524
+ const usage = event.usage;
525
+ if (usage == null ? void 0 : usage.output_tokens) outputTokens = usage.output_tokens;
526
+ } else if (eventType === "message_stop") {
527
+ const toolCalls = [...toolBlocks.values()].map((t) => ({
528
+ id: t.id,
529
+ name: t.name,
530
+ arguments: t.jsonAccum
531
+ }));
532
+ yield {
533
+ done: true,
534
+ text: "",
535
+ finishReason: stopReason,
536
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
537
+ usage: {
538
+ promptTokens: inputTokens,
539
+ completionTokens: outputTokens,
540
+ totalTokens: inputTokens + outputTokens
541
+ }
542
+ };
543
+ return;
544
+ }
545
+ }
546
+ }
547
+ yield { done: true, text: "", finishReason: stopReason };
548
+ }
549
+
550
+ // src/providers/index.ts
551
+ function createProvider(profile) {
552
+ const useAnthropic = profile.apiFormat === "anthropic" || profile.baseUrl.includes("anthropic");
553
+ return useAnthropic ? createAnthropicProvider(profile) : createOpenAIProvider(profile);
554
+ }
555
+ function resolveActiveProfile(config, providerName) {
556
+ var _a, _b;
557
+ if (providerName !== void 0) {
558
+ const profile = (_a = config.providers) == null ? void 0 : _a[providerName];
559
+ if (!profile) {
560
+ throw new Error(`Provider '${providerName}' not found`);
561
+ }
562
+ return profile;
563
+ }
564
+ if (config.defaultProvider) {
565
+ const profile = (_b = config.providers) == null ? void 0 : _b[config.defaultProvider];
566
+ if (profile) return profile;
567
+ }
568
+ if (config.providers) {
569
+ if (config.providers["default"]) {
570
+ return config.providers["default"];
571
+ }
572
+ const first = Object.values(config.providers)[0];
573
+ if (first) return first;
574
+ }
575
+ return {
576
+ baseUrl: config.baseUrl,
577
+ apiKey: config.apiKey,
578
+ model: config.model
579
+ };
580
+ }
581
+
582
+ // src/sessions/metadata.ts
583
+ import * as crypto from "crypto";
584
+ import * as fs from "fs";
585
+ import * as path from "path";
586
+ function metadataPathFromLogFile(logFilePath) {
587
+ const base = path.basename(logFilePath, ".jsonl");
588
+ const dir = path.dirname(logFilePath);
589
+ return path.join(dir, `${base}-session.json`);
590
+ }
591
+ function createSessionMetadata(logFilePath, model, id) {
592
+ const now = (/* @__PURE__ */ new Date()).toISOString();
593
+ return {
594
+ id: id ?? crypto.randomUUID(),
595
+ startTime: now,
596
+ lastActivity: now,
597
+ cwd: process.cwd(),
598
+ model,
599
+ title: "",
600
+ turnCount: 0,
601
+ totalTokens: 0,
602
+ logFile: path.basename(logFilePath)
603
+ };
604
+ }
605
+ function writeSessionMetadata(logFilePath, metadata) {
606
+ const metaPath = metadataPathFromLogFile(logFilePath);
607
+ try {
608
+ fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2) + "\n");
609
+ } catch (err) {
610
+ process.stderr.write(`[sessions] Failed to write metadata: ${err}
611
+ `);
612
+ }
613
+ }
614
+ function readSessionMetadata(metaFilePath) {
615
+ try {
616
+ const raw = fs.readFileSync(metaFilePath, "utf-8");
617
+ return JSON.parse(raw);
618
+ } catch {
619
+ return null;
620
+ }
621
+ }
622
+ function updateSessionMetadata(logFilePath, partial) {
623
+ const metaPath = metadataPathFromLogFile(logFilePath);
624
+ let existing = null;
625
+ try {
626
+ const raw = fs.readFileSync(metaPath, "utf-8");
627
+ existing = JSON.parse(raw);
628
+ } catch {
629
+ return;
630
+ }
631
+ writeSessionMetadata(logFilePath, { ...existing, ...partial });
632
+ }
633
+ function listSessions(logDir) {
634
+ try {
635
+ const files = fs.readdirSync(logDir);
636
+ const metaFiles = files.filter((f) => f.endsWith("-session.json"));
637
+ const sessions = [];
638
+ for (const file of metaFiles) {
639
+ const meta = readSessionMetadata(path.join(logDir, file));
640
+ if (meta) sessions.push(meta);
641
+ }
642
+ return sessions.sort(
643
+ (a, b) => b.lastActivity.localeCompare(a.lastActivity)
644
+ );
645
+ } catch {
646
+ return [];
647
+ }
648
+ }
649
+ function findSession(logDir, idOrPrefix) {
650
+ const sessions = listSessions(logDir);
651
+ return sessions.find(
652
+ (s) => s.id === idOrPrefix || s.id.startsWith(idOrPrefix)
653
+ ) ?? null;
654
+ }
655
+ function generateTitle(firstUserMessage) {
656
+ const oneLine = firstUserMessage.replace(/\n+/g, " ").trim();
657
+ return oneLine.length > 50 ? oneLine.slice(0, 47) + "..." : oneLine;
658
+ }
659
+ function deleteSessionFiles(logDir, id) {
660
+ const session = findSession(logDir, id);
661
+ if (!session) return false;
662
+ const logFilePath = path.join(logDir, session.logFile);
663
+ const metaFilePath = metadataPathFromLogFile(logFilePath);
664
+ let deleted = false;
665
+ try {
666
+ fs.unlinkSync(logFilePath);
667
+ deleted = true;
668
+ } catch {
669
+ }
670
+ try {
671
+ fs.unlinkSync(metaFilePath);
672
+ deleted = true;
673
+ } catch {
674
+ }
675
+ return deleted;
676
+ }
677
+ function loadMessagesFromJsonl(logFilePath) {
678
+ try {
679
+ const content = fs.readFileSync(logFilePath, "utf-8");
680
+ const lines = content.split("\n").filter((l) => l.trim());
681
+ const messages = [];
682
+ for (const line of lines) {
683
+ try {
684
+ const entry = JSON.parse(line);
685
+ if (!entry.role || entry.role === "system") continue;
686
+ if (entry.role === "user") {
687
+ messages.push({ role: "user", content: entry.content ?? "" });
688
+ } else if (entry.role === "assistant") {
689
+ const msg = {
690
+ role: "assistant",
691
+ content: entry.content ?? null
692
+ };
693
+ if (entry.tool_calls) {
694
+ msg.tool_calls = entry.tool_calls;
695
+ }
696
+ messages.push(msg);
697
+ } else if (entry.role === "tool") {
698
+ messages.push({
699
+ role: "tool",
700
+ tool_call_id: entry.tool_call_id ?? "",
701
+ content: entry.content ?? ""
702
+ });
703
+ }
704
+ } catch {
705
+ }
706
+ }
707
+ return messages;
708
+ } catch {
709
+ return [];
710
+ }
711
+ }
712
+
713
+ export {
714
+ getContextLimit,
715
+ loadConfig,
716
+ saveConfig,
717
+ createProvider,
718
+ resolveActiveProfile,
719
+ createSessionMetadata,
720
+ writeSessionMetadata,
721
+ updateSessionMetadata,
722
+ listSessions,
723
+ findSession,
724
+ generateTitle,
725
+ deleteSessionFiles,
726
+ loadMessagesFromJsonl
727
+ };