@claude-code-kit/agent 0.2.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/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/index.d.mts +722 -0
- package/dist/index.d.ts +722 -0
- package/dist/index.js +1722 -0
- package/dist/index.mjs +1670 -0
- package/package.json +71 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1670 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/compaction/interface.ts
|
|
9
|
+
var NoopCompaction = class {
|
|
10
|
+
compact(messages, _maxTokens) {
|
|
11
|
+
return messages;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/context-manager.ts
|
|
16
|
+
function estimateTokens(message) {
|
|
17
|
+
let text;
|
|
18
|
+
if (typeof message.content === "string") {
|
|
19
|
+
text = message.content;
|
|
20
|
+
} else {
|
|
21
|
+
text = message.content.map((part) => part.type === "text" ? part.text : "[image]").join("");
|
|
22
|
+
}
|
|
23
|
+
if (message.role === "assistant" && message.toolCalls) {
|
|
24
|
+
for (const tc of message.toolCalls) {
|
|
25
|
+
text += tc.name + JSON.stringify(tc.input);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return Math.ceil(text.length / 4);
|
|
29
|
+
}
|
|
30
|
+
function estimateTotalTokens(messages) {
|
|
31
|
+
let total = 0;
|
|
32
|
+
for (const msg of messages) {
|
|
33
|
+
total += estimateTokens(msg);
|
|
34
|
+
}
|
|
35
|
+
return total;
|
|
36
|
+
}
|
|
37
|
+
var ContextManager = class {
|
|
38
|
+
contextLimit;
|
|
39
|
+
compactionStrategy;
|
|
40
|
+
provider;
|
|
41
|
+
/** Compact when usage exceeds this fraction of the context limit */
|
|
42
|
+
compactionThreshold = 0.85;
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this.contextLimit = options.contextLimit ?? 1e5;
|
|
45
|
+
this.compactionStrategy = options.compactionStrategy ?? new NoopCompaction();
|
|
46
|
+
this.provider = options.provider ?? null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Count tokens for the messages. Uses provider.countTokens if available,
|
|
50
|
+
* otherwise falls back to heuristic estimation.
|
|
51
|
+
*/
|
|
52
|
+
async countTokens(messages) {
|
|
53
|
+
if (this.provider?.countTokens) {
|
|
54
|
+
return this.provider.countTokens(messages);
|
|
55
|
+
}
|
|
56
|
+
return estimateTotalTokens(messages);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if compaction is needed and apply it if so.
|
|
60
|
+
* Returns the (possibly compacted) messages array.
|
|
61
|
+
*/
|
|
62
|
+
async maybeCompact(messages) {
|
|
63
|
+
const tokenCount = await this.countTokens(messages);
|
|
64
|
+
const threshold = this.contextLimit * this.compactionThreshold;
|
|
65
|
+
if (tokenCount > threshold) {
|
|
66
|
+
const targetTokens = Math.floor(this.contextLimit * 0.6);
|
|
67
|
+
return await this.compactionStrategy.compact(messages, targetTokens);
|
|
68
|
+
}
|
|
69
|
+
return messages;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Force compaction (e.g. after receiving a "context too long" error from the API).
|
|
73
|
+
*/
|
|
74
|
+
async forceCompact(messages) {
|
|
75
|
+
const targetTokens = Math.floor(this.contextLimit * 0.5);
|
|
76
|
+
return await this.compactionStrategy.compact(messages, targetTokens);
|
|
77
|
+
}
|
|
78
|
+
setContextLimit(limit) {
|
|
79
|
+
this.contextLimit = limit;
|
|
80
|
+
}
|
|
81
|
+
setCompactionStrategy(strategy) {
|
|
82
|
+
this.compactionStrategy = strategy;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/permission.ts
|
|
87
|
+
function createPermissionHandler(config) {
|
|
88
|
+
return async (request) => {
|
|
89
|
+
if (config.alwaysAllow?.includes(request.tool)) {
|
|
90
|
+
return { decision: "allow" };
|
|
91
|
+
}
|
|
92
|
+
if (config.alwaysDeny?.includes(request.tool)) {
|
|
93
|
+
return { decision: "deny", reason: `Tool "${request.tool}" is in the deny list` };
|
|
94
|
+
}
|
|
95
|
+
if (config.sessionApproved?.has(request.tool)) {
|
|
96
|
+
return { decision: "allow" };
|
|
97
|
+
}
|
|
98
|
+
if (config.autoApproveReadOnly && request.isReadOnly) {
|
|
99
|
+
return { decision: "allow" };
|
|
100
|
+
}
|
|
101
|
+
if (config.onPermission) {
|
|
102
|
+
return config.onPermission(request);
|
|
103
|
+
}
|
|
104
|
+
return { decision: "allow" };
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
var allowAll = async () => ({ decision: "allow" });
|
|
108
|
+
var allowReadOnly = async (request) => {
|
|
109
|
+
if (request.isReadOnly) {
|
|
110
|
+
return { decision: "allow" };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
decision: "deny",
|
|
114
|
+
reason: "No permission handler configured. Set permissionHandler in AgentConfig."
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
var denyAll = async () => ({
|
|
118
|
+
decision: "deny",
|
|
119
|
+
reason: "All tool executions are denied"
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// src/session/memory.ts
|
|
123
|
+
var InMemorySession = class {
|
|
124
|
+
messages = [];
|
|
125
|
+
getMessages() {
|
|
126
|
+
return [...this.messages];
|
|
127
|
+
}
|
|
128
|
+
setMessages(messages) {
|
|
129
|
+
this.messages = [...messages];
|
|
130
|
+
}
|
|
131
|
+
clear() {
|
|
132
|
+
this.messages = [];
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/tool-formatter.ts
|
|
137
|
+
import { z } from "zod";
|
|
138
|
+
function zodToInputSchema(schema) {
|
|
139
|
+
if (typeof z.toJSONSchema === "function") {
|
|
140
|
+
const jsonSchema = z.toJSONSchema(schema);
|
|
141
|
+
const { $schema: _, ...rest } = jsonSchema;
|
|
142
|
+
return rest;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const mod = __require("zod-to-json-schema");
|
|
146
|
+
const jsonSchema = mod.zodToJsonSchema(schema, { target: "openApi3" });
|
|
147
|
+
const { $schema: _, ...rest } = jsonSchema;
|
|
148
|
+
return rest;
|
|
149
|
+
} catch {
|
|
150
|
+
return { type: "object" };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function toolToProviderFormat(tool) {
|
|
154
|
+
return {
|
|
155
|
+
name: tool.name,
|
|
156
|
+
description: tool.description,
|
|
157
|
+
inputSchema: zodToInputSchema(tool.inputSchema)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/tool-registry.ts
|
|
162
|
+
var ToolRegistry = class {
|
|
163
|
+
tools = /* @__PURE__ */ new Map();
|
|
164
|
+
register(tool) {
|
|
165
|
+
if (this.tools.has(tool.name)) {
|
|
166
|
+
throw new Error(`Tool "${tool.name}" is already registered`);
|
|
167
|
+
}
|
|
168
|
+
this.tools.set(tool.name, tool);
|
|
169
|
+
}
|
|
170
|
+
unregister(name) {
|
|
171
|
+
return this.tools.delete(name);
|
|
172
|
+
}
|
|
173
|
+
get(name) {
|
|
174
|
+
return this.tools.get(name);
|
|
175
|
+
}
|
|
176
|
+
has(name) {
|
|
177
|
+
return this.tools.has(name);
|
|
178
|
+
}
|
|
179
|
+
list() {
|
|
180
|
+
return Array.from(this.tools.values());
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Returns all tools in the provider-ready format (name, description, JSON Schema).
|
|
184
|
+
*/
|
|
185
|
+
toProviderFormat() {
|
|
186
|
+
return this.list().map(toolToProviderFormat);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Execute a tool by name with the given input.
|
|
190
|
+
*/
|
|
191
|
+
async execute(name, input, context) {
|
|
192
|
+
const tool = this.tools.get(name);
|
|
193
|
+
if (!tool) {
|
|
194
|
+
return { content: `Unknown tool: ${name}`, isError: true };
|
|
195
|
+
}
|
|
196
|
+
const parsed = tool.inputSchema.safeParse(input);
|
|
197
|
+
if (!parsed.success) {
|
|
198
|
+
return {
|
|
199
|
+
content: `Invalid input for tool "${name}": ${parsed.error.message}`,
|
|
200
|
+
isError: true
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const timeout = tool.timeout ?? 12e4;
|
|
204
|
+
const timeoutSignal = AbortSignal.timeout(timeout);
|
|
205
|
+
const combinedSignal = AbortSignal.any([context.abortSignal, timeoutSignal]);
|
|
206
|
+
const toolContext = { ...context, abortSignal: combinedSignal };
|
|
207
|
+
try {
|
|
208
|
+
return await tool.execute(parsed.data, toolContext);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
211
|
+
return { content: `Tool "${name}" failed: ${message}`, isError: true };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
clear() {
|
|
215
|
+
this.tools.clear();
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/agent.ts
|
|
220
|
+
var DEFAULT_MAX_TURNS = 50;
|
|
221
|
+
var Agent = class {
|
|
222
|
+
provider;
|
|
223
|
+
model;
|
|
224
|
+
systemPrompt;
|
|
225
|
+
maxTokens;
|
|
226
|
+
temperature;
|
|
227
|
+
maxTurns;
|
|
228
|
+
session;
|
|
229
|
+
toolRegistry;
|
|
230
|
+
contextManager;
|
|
231
|
+
permissionHandler;
|
|
232
|
+
workingDirectory;
|
|
233
|
+
abortController = null;
|
|
234
|
+
constructor(config) {
|
|
235
|
+
this.provider = config.provider;
|
|
236
|
+
this.model = config.model;
|
|
237
|
+
this.systemPrompt = config.systemPrompt;
|
|
238
|
+
this.maxTokens = config.maxTokens;
|
|
239
|
+
this.temperature = config.temperature;
|
|
240
|
+
this.maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
241
|
+
this.session = config.session ?? new InMemorySession();
|
|
242
|
+
this.permissionHandler = config.permissionHandler ?? allowReadOnly;
|
|
243
|
+
this.workingDirectory = config.workingDirectory ?? process.cwd();
|
|
244
|
+
this.toolRegistry = new ToolRegistry();
|
|
245
|
+
if (config.tools) {
|
|
246
|
+
for (const tool of config.tools) {
|
|
247
|
+
this.toolRegistry.register(tool);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
this.contextManager = new ContextManager({
|
|
251
|
+
contextLimit: config.contextLimit,
|
|
252
|
+
compactionStrategy: config.compactionStrategy,
|
|
253
|
+
provider: config.provider
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Run the agent loop. Yields events as the agent processes the input.
|
|
258
|
+
*
|
|
259
|
+
* The loop:
|
|
260
|
+
* 1. Add user message(s) to conversation
|
|
261
|
+
* 2. Check compaction
|
|
262
|
+
* 3. Call provider.chat() with messages + tools
|
|
263
|
+
* 4. Stream chunks, accumulate text + tool calls
|
|
264
|
+
* 5. If tool_use: check permission -> execute -> add results -> loop to step 2
|
|
265
|
+
* 6. If end_turn: yield done event with full message history
|
|
266
|
+
*/
|
|
267
|
+
async *run(input) {
|
|
268
|
+
this.abortController = new AbortController();
|
|
269
|
+
const messages = this.session.getMessages();
|
|
270
|
+
if (typeof input === "string") {
|
|
271
|
+
messages.push({ role: "user", content: input });
|
|
272
|
+
} else {
|
|
273
|
+
messages.push(...input);
|
|
274
|
+
}
|
|
275
|
+
this.session.setMessages(messages);
|
|
276
|
+
let turns = 0;
|
|
277
|
+
while (turns < this.maxTurns) {
|
|
278
|
+
turns++;
|
|
279
|
+
const currentMessages = await this.contextManager.maybeCompact(this.session.getMessages());
|
|
280
|
+
this.session.setMessages(currentMessages);
|
|
281
|
+
const providerTools = this.toolRegistry.toProviderFormat();
|
|
282
|
+
let accumulatedText = "";
|
|
283
|
+
const accumulatedToolCalls = [];
|
|
284
|
+
const toolParseErrors = /* @__PURE__ */ new Map();
|
|
285
|
+
let currentToolId;
|
|
286
|
+
let currentToolName;
|
|
287
|
+
let currentToolArgs = "";
|
|
288
|
+
try {
|
|
289
|
+
const stream = this.provider.chat({
|
|
290
|
+
model: this.model,
|
|
291
|
+
messages: this.session.getMessages(),
|
|
292
|
+
tools: providerTools.length > 0 ? providerTools : void 0,
|
|
293
|
+
systemPrompt: this.systemPrompt,
|
|
294
|
+
maxTokens: this.maxTokens,
|
|
295
|
+
temperature: this.temperature,
|
|
296
|
+
signal: this.abortController.signal
|
|
297
|
+
});
|
|
298
|
+
for await (const chunk of stream) {
|
|
299
|
+
switch (chunk.type) {
|
|
300
|
+
case "text":
|
|
301
|
+
if (chunk.text) {
|
|
302
|
+
accumulatedText += chunk.text;
|
|
303
|
+
yield { type: "text", text: chunk.text };
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
case "tool_use_start":
|
|
307
|
+
if (chunk.toolCall) {
|
|
308
|
+
currentToolId = chunk.toolCall.id;
|
|
309
|
+
currentToolName = chunk.toolCall.name;
|
|
310
|
+
currentToolArgs = "";
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
case "tool_use_delta":
|
|
314
|
+
if (chunk.text) {
|
|
315
|
+
currentToolArgs += chunk.text;
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
case "tool_use_end": {
|
|
319
|
+
if (currentToolId && currentToolName) {
|
|
320
|
+
let input2;
|
|
321
|
+
try {
|
|
322
|
+
input2 = currentToolArgs ? JSON.parse(currentToolArgs) : {};
|
|
323
|
+
} catch (err) {
|
|
324
|
+
input2 = {};
|
|
325
|
+
toolParseErrors.set(
|
|
326
|
+
currentToolId,
|
|
327
|
+
`Failed to parse tool input JSON: ${err instanceof Error ? err.message : String(err)}. Raw input: ${currentToolArgs}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const toolCall = {
|
|
331
|
+
id: currentToolId,
|
|
332
|
+
name: currentToolName,
|
|
333
|
+
input: input2
|
|
334
|
+
};
|
|
335
|
+
accumulatedToolCalls.push(toolCall);
|
|
336
|
+
yield { type: "tool_call", toolCall };
|
|
337
|
+
}
|
|
338
|
+
currentToolId = void 0;
|
|
339
|
+
currentToolName = void 0;
|
|
340
|
+
currentToolArgs = "";
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "thinking":
|
|
344
|
+
if (chunk.text) {
|
|
345
|
+
yield { type: "thinking", text: chunk.text };
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
case "usage":
|
|
349
|
+
if (chunk.usage) {
|
|
350
|
+
yield {
|
|
351
|
+
type: "usage",
|
|
352
|
+
inputTokens: chunk.usage.inputTokens,
|
|
353
|
+
outputTokens: chunk.usage.outputTokens
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
case "done":
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (isContextTooLongError(error)) {
|
|
363
|
+
const compacted = await this.contextManager.forceCompact(this.session.getMessages());
|
|
364
|
+
this.session.setMessages(compacted);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
368
|
+
yield { type: "error", error: err };
|
|
369
|
+
yield { type: "done", messages: this.session.getMessages() };
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const assistantMessage = {
|
|
373
|
+
role: "assistant",
|
|
374
|
+
content: accumulatedText,
|
|
375
|
+
...accumulatedToolCalls.length > 0 ? { toolCalls: accumulatedToolCalls } : {}
|
|
376
|
+
};
|
|
377
|
+
const msgs = this.session.getMessages();
|
|
378
|
+
msgs.push(assistantMessage);
|
|
379
|
+
this.session.setMessages(msgs);
|
|
380
|
+
if (accumulatedToolCalls.length > 0) {
|
|
381
|
+
const toolResults = await this.executeToolCalls(accumulatedToolCalls, toolParseErrors);
|
|
382
|
+
const currentMsgs = this.session.getMessages();
|
|
383
|
+
for (const result of toolResults) {
|
|
384
|
+
yield {
|
|
385
|
+
type: "tool_result",
|
|
386
|
+
toolCallId: result.toolCallId,
|
|
387
|
+
result: {
|
|
388
|
+
content: typeof result.content === "string" ? result.content : "",
|
|
389
|
+
isError: result.isError
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
currentMsgs.push(result);
|
|
393
|
+
}
|
|
394
|
+
this.session.setMessages(currentMsgs);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
yield { type: "done", messages: this.session.getMessages() };
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
yield {
|
|
401
|
+
type: "error",
|
|
402
|
+
error: new Error(`Agent exceeded maximum turns (${this.maxTurns})`)
|
|
403
|
+
};
|
|
404
|
+
yield { type: "done", messages: this.session.getMessages() };
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Simple API that wraps run() — sends a message and returns the final text response.
|
|
408
|
+
*/
|
|
409
|
+
async chat(input) {
|
|
410
|
+
let result = "";
|
|
411
|
+
for await (const event of this.run(input)) {
|
|
412
|
+
if (event.type === "text") {
|
|
413
|
+
result += event.text;
|
|
414
|
+
}
|
|
415
|
+
if (event.type === "error") {
|
|
416
|
+
throw event.error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
/** Abort the current run. */
|
|
422
|
+
abort() {
|
|
423
|
+
this.abortController?.abort();
|
|
424
|
+
}
|
|
425
|
+
/** Replace the provider (e.g. to switch models mid-conversation). */
|
|
426
|
+
setProvider(provider) {
|
|
427
|
+
this.provider = provider;
|
|
428
|
+
}
|
|
429
|
+
/** Get the full message history. */
|
|
430
|
+
getMessages() {
|
|
431
|
+
return this.session.getMessages();
|
|
432
|
+
}
|
|
433
|
+
/** Clear the message history. */
|
|
434
|
+
clearMessages() {
|
|
435
|
+
this.session.clear();
|
|
436
|
+
}
|
|
437
|
+
/** Add a tool to the registry. */
|
|
438
|
+
addTool(tool) {
|
|
439
|
+
this.toolRegistry.register(tool);
|
|
440
|
+
}
|
|
441
|
+
/** Remove a tool from the registry. */
|
|
442
|
+
removeTool(name) {
|
|
443
|
+
return this.toolRegistry.unregister(name);
|
|
444
|
+
}
|
|
445
|
+
/** Replace the permission handler at runtime. */
|
|
446
|
+
setPermissionHandler(handler) {
|
|
447
|
+
this.permissionHandler = handler;
|
|
448
|
+
}
|
|
449
|
+
// -------------------------------------------------------------------------
|
|
450
|
+
// Private helpers
|
|
451
|
+
// -------------------------------------------------------------------------
|
|
452
|
+
async executeToolCalls(toolCalls, parseErrors) {
|
|
453
|
+
const results = [];
|
|
454
|
+
for (const tc of toolCalls) {
|
|
455
|
+
const parseError = parseErrors?.get(tc.id);
|
|
456
|
+
if (parseError) {
|
|
457
|
+
results.push({
|
|
458
|
+
role: "tool",
|
|
459
|
+
toolCallId: tc.id,
|
|
460
|
+
content: parseError,
|
|
461
|
+
isError: true
|
|
462
|
+
});
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const toolDef = this.toolRegistry.get(tc.name);
|
|
466
|
+
const permissionResult = await this.permissionHandler({
|
|
467
|
+
tool: tc.name,
|
|
468
|
+
input: tc.input,
|
|
469
|
+
isReadOnly: toolDef?.isReadOnly
|
|
470
|
+
});
|
|
471
|
+
if (permissionResult.decision === "deny") {
|
|
472
|
+
results.push({
|
|
473
|
+
role: "tool",
|
|
474
|
+
toolCallId: tc.id,
|
|
475
|
+
content: `Permission denied for tool "${tc.name}"${permissionResult.reason ? `: ${permissionResult.reason}` : ""}`,
|
|
476
|
+
isError: true
|
|
477
|
+
});
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const context = {
|
|
481
|
+
workingDirectory: this.workingDirectory,
|
|
482
|
+
abortSignal: this.abortController?.signal ?? AbortSignal.timeout(12e4)
|
|
483
|
+
};
|
|
484
|
+
const result = await this.toolRegistry.execute(tc.name, tc.input, context);
|
|
485
|
+
results.push({
|
|
486
|
+
role: "tool",
|
|
487
|
+
toolCallId: tc.id,
|
|
488
|
+
content: result.content,
|
|
489
|
+
isError: result.isError
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return results;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
function isContextTooLongError(error) {
|
|
496
|
+
if (!(error instanceof Error)) return false;
|
|
497
|
+
const msg = error.message.toLowerCase();
|
|
498
|
+
return msg.includes("context_length_exceeded") || msg.includes("maximum context length") || msg.includes("too many tokens") || msg.includes("request too large");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/compaction/sliding-window.ts
|
|
502
|
+
var SlidingWindowCompaction = class {
|
|
503
|
+
compact(messages, maxTokens) {
|
|
504
|
+
if (messages.length === 0) return messages;
|
|
505
|
+
const systemMessages = [];
|
|
506
|
+
const conversationMessages = [];
|
|
507
|
+
for (const msg of messages) {
|
|
508
|
+
if (msg.role === "system") {
|
|
509
|
+
systemMessages.push(msg);
|
|
510
|
+
} else {
|
|
511
|
+
conversationMessages.push(msg);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
let totalTokens = 0;
|
|
515
|
+
for (const msg of systemMessages) {
|
|
516
|
+
totalTokens += estimateTokens(msg);
|
|
517
|
+
}
|
|
518
|
+
if (totalTokens >= maxTokens) {
|
|
519
|
+
return systemMessages;
|
|
520
|
+
}
|
|
521
|
+
const remainingBudget = maxTokens - totalTokens;
|
|
522
|
+
const kept = [];
|
|
523
|
+
let usedTokens = 0;
|
|
524
|
+
for (let i = conversationMessages.length - 1; i >= 0; i--) {
|
|
525
|
+
const msg = conversationMessages[i];
|
|
526
|
+
const msgTokens = estimateTokens(msg);
|
|
527
|
+
if (usedTokens + msgTokens > remainingBudget) {
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
kept.unshift(msg);
|
|
531
|
+
usedTokens += msgTokens;
|
|
532
|
+
}
|
|
533
|
+
while (kept.length > 0 && kept[0].role === "tool") {
|
|
534
|
+
kept.shift();
|
|
535
|
+
}
|
|
536
|
+
return [...systemMessages, ...kept];
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// src/compaction/summarization.ts
|
|
541
|
+
var SUMMARY_PROMPT = "Please summarize the following conversation history concisely. Capture the key information, decisions, and context needed to continue the conversation. Be comprehensive but brief.";
|
|
542
|
+
var SummarizationCompaction = class {
|
|
543
|
+
keepRecentN;
|
|
544
|
+
thresholdFraction;
|
|
545
|
+
summaryMaxTokens;
|
|
546
|
+
summaryModel;
|
|
547
|
+
provider;
|
|
548
|
+
constructor(provider, options = {}) {
|
|
549
|
+
this.provider = provider;
|
|
550
|
+
this.keepRecentN = options.keepRecentN ?? 10;
|
|
551
|
+
this.thresholdFraction = options.thresholdFraction ?? 0.75;
|
|
552
|
+
this.summaryMaxTokens = options.summaryMaxTokens ?? 2e3;
|
|
553
|
+
this.summaryModel = options.summaryModel ?? "claude-3-5-haiku-20241022";
|
|
554
|
+
}
|
|
555
|
+
/** Return true when the current usage exceeds the configured threshold. */
|
|
556
|
+
shouldCompact(messages, tokenCount, contextLimit) {
|
|
557
|
+
return tokenCount >= contextLimit * this.thresholdFraction;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Async compaction conforming to the CompactionStrategy interface.
|
|
561
|
+
* Uses the LLM provider to summarize older messages before dropping them.
|
|
562
|
+
*/
|
|
563
|
+
async compact(messages, _maxTokens) {
|
|
564
|
+
const result = await this.compactAsync(messages);
|
|
565
|
+
return result.messages;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Async compaction that uses the LLM provider to summarize older messages.
|
|
569
|
+
* Returns a `CompactionResult` with full token stats.
|
|
570
|
+
*/
|
|
571
|
+
async compactAsync(messages) {
|
|
572
|
+
const tokensBefore = estimateTotalTokens(messages);
|
|
573
|
+
const systemMessages = messages.filter((m) => m.role === "system");
|
|
574
|
+
const conversationMessages = messages.filter((m) => m.role !== "system");
|
|
575
|
+
if (conversationMessages.length <= this.keepRecentN) {
|
|
576
|
+
return {
|
|
577
|
+
messages,
|
|
578
|
+
tokensBefore,
|
|
579
|
+
tokensAfter: tokensBefore,
|
|
580
|
+
strategy: "summarization"
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const toSummarize = conversationMessages.slice(0, -this.keepRecentN);
|
|
584
|
+
const toKeep = conversationMessages.slice(-this.keepRecentN);
|
|
585
|
+
const transcript = toSummarize.map((m) => {
|
|
586
|
+
const role = m.role.toUpperCase();
|
|
587
|
+
const text = typeof m.content === "string" ? m.content : m.content.map((part) => part.type === "text" ? part.text : "[image]").join("");
|
|
588
|
+
return `${role}: ${text}`;
|
|
589
|
+
}).join("\n\n");
|
|
590
|
+
const summaryMessages = [
|
|
591
|
+
{
|
|
592
|
+
role: "user",
|
|
593
|
+
content: `${SUMMARY_PROMPT}
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
${transcript}`
|
|
598
|
+
}
|
|
599
|
+
];
|
|
600
|
+
let summaryText = "";
|
|
601
|
+
const stream = this.provider.chat({
|
|
602
|
+
model: this.summaryModel,
|
|
603
|
+
messages: summaryMessages,
|
|
604
|
+
maxTokens: this.summaryMaxTokens
|
|
605
|
+
});
|
|
606
|
+
for await (const chunk of stream) {
|
|
607
|
+
if (chunk.type === "text" && chunk.text) {
|
|
608
|
+
summaryText += chunk.text;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const summaryUserMessage = {
|
|
612
|
+
role: "user",
|
|
613
|
+
content: `[Summary of earlier conversation]
|
|
614
|
+
|
|
615
|
+
${summaryText}`
|
|
616
|
+
};
|
|
617
|
+
const understoodMessage = {
|
|
618
|
+
role: "assistant",
|
|
619
|
+
content: "Understood."
|
|
620
|
+
};
|
|
621
|
+
const compactedMessages = [
|
|
622
|
+
...systemMessages,
|
|
623
|
+
summaryUserMessage,
|
|
624
|
+
understoodMessage,
|
|
625
|
+
...toKeep
|
|
626
|
+
];
|
|
627
|
+
return {
|
|
628
|
+
messages: compactedMessages,
|
|
629
|
+
tokensBefore,
|
|
630
|
+
tokensAfter: estimateTotalTokens(compactedMessages),
|
|
631
|
+
strategy: "summarization"
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// src/session/file.ts
|
|
637
|
+
import { promises as fs } from "fs";
|
|
638
|
+
import path from "path";
|
|
639
|
+
function isErrnoException(err) {
|
|
640
|
+
return err instanceof Error && "code" in err;
|
|
641
|
+
}
|
|
642
|
+
var FileSession = class {
|
|
643
|
+
directory;
|
|
644
|
+
id;
|
|
645
|
+
messages = [];
|
|
646
|
+
constructor(directory, id) {
|
|
647
|
+
this.directory = directory;
|
|
648
|
+
this.id = id;
|
|
649
|
+
}
|
|
650
|
+
/** Return a snapshot of the in-memory messages. */
|
|
651
|
+
getMessages() {
|
|
652
|
+
return [...this.messages];
|
|
653
|
+
}
|
|
654
|
+
/** Replace the in-memory messages and persist to disk. */
|
|
655
|
+
setMessages(messages) {
|
|
656
|
+
this.messages = [...messages];
|
|
657
|
+
}
|
|
658
|
+
/** Clear in-memory messages (does not delete the file). */
|
|
659
|
+
clear() {
|
|
660
|
+
this.messages = [];
|
|
661
|
+
}
|
|
662
|
+
// ---------------------------------------------------------------------------
|
|
663
|
+
// Persistence helpers (used by FileSessionStore)
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
/** Resolve the .jsonl file path for this session. */
|
|
666
|
+
filePath() {
|
|
667
|
+
return path.join(this.directory, `${this.id}.jsonl`);
|
|
668
|
+
}
|
|
669
|
+
/** Load messages from disk into memory. Returns self for chaining. */
|
|
670
|
+
async load() {
|
|
671
|
+
try {
|
|
672
|
+
const content = await fs.readFile(this.filePath(), "utf8");
|
|
673
|
+
this.messages = content.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
674
|
+
} catch (err) {
|
|
675
|
+
if (isErrnoException(err) && err.code === "ENOENT") {
|
|
676
|
+
this.messages = [];
|
|
677
|
+
} else {
|
|
678
|
+
throw err;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return this;
|
|
682
|
+
}
|
|
683
|
+
/** Write current in-memory messages to disk (overwrites the file). */
|
|
684
|
+
async save() {
|
|
685
|
+
await fs.mkdir(this.directory, { recursive: true });
|
|
686
|
+
const content = this.messages.map((m) => JSON.stringify(m)).join("\n");
|
|
687
|
+
await fs.writeFile(this.filePath(), content, "utf8");
|
|
688
|
+
}
|
|
689
|
+
/** Append a single message to the file without rewriting it. */
|
|
690
|
+
async append(message) {
|
|
691
|
+
await fs.mkdir(this.directory, { recursive: true });
|
|
692
|
+
await fs.appendFile(this.filePath(), JSON.stringify(message) + "\n", "utf8");
|
|
693
|
+
this.messages.push(message);
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
var FileSessionStore = class {
|
|
697
|
+
constructor(directory) {
|
|
698
|
+
this.directory = directory;
|
|
699
|
+
}
|
|
700
|
+
/** Create a new (empty) session without persisting it. */
|
|
701
|
+
create(id) {
|
|
702
|
+
return new FileSession(this.directory, id);
|
|
703
|
+
}
|
|
704
|
+
/** Load an existing session from disk. Returns null if not found. */
|
|
705
|
+
async load(id) {
|
|
706
|
+
const session = new FileSession(this.directory, id);
|
|
707
|
+
try {
|
|
708
|
+
await session.load();
|
|
709
|
+
return session;
|
|
710
|
+
} catch {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/** Persist the current messages of a session to disk. */
|
|
715
|
+
async save(id, session) {
|
|
716
|
+
const target = new FileSession(this.directory, id);
|
|
717
|
+
target.setMessages(session.getMessages());
|
|
718
|
+
await target.save();
|
|
719
|
+
}
|
|
720
|
+
/** List all session IDs in the directory. */
|
|
721
|
+
async list() {
|
|
722
|
+
try {
|
|
723
|
+
const entries = await fs.readdir(this.directory);
|
|
724
|
+
return entries.filter((f) => f.endsWith(".jsonl")).map((f) => f.slice(0, -".jsonl".length));
|
|
725
|
+
} catch (err) {
|
|
726
|
+
if (isErrnoException(err) && err.code === "ENOENT") {
|
|
727
|
+
return [];
|
|
728
|
+
}
|
|
729
|
+
throw err;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/** Delete a session file from disk. */
|
|
733
|
+
async delete(id) {
|
|
734
|
+
const filePath = path.join(this.directory, `${id}.jsonl`);
|
|
735
|
+
try {
|
|
736
|
+
await fs.unlink(filePath);
|
|
737
|
+
} catch (err) {
|
|
738
|
+
if (!isErrnoException(err) || err.code !== "ENOENT") {
|
|
739
|
+
throw err;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// src/providers/anthropic.ts
|
|
746
|
+
var AnthropicProvider = class {
|
|
747
|
+
constructor(options = {}) {
|
|
748
|
+
this.options = options;
|
|
749
|
+
this.clientPromise = loadSDK().then(
|
|
750
|
+
(SDK) => new SDK({
|
|
751
|
+
apiKey: options.apiKey,
|
|
752
|
+
...options.baseURL ? { baseURL: options.baseURL } : {}
|
|
753
|
+
})
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
clientPromise;
|
|
757
|
+
async *chat(options) {
|
|
758
|
+
const client = await this.clientPromise;
|
|
759
|
+
const { systemPrompt, messages: rawMessages, tools, model, maxTokens, temperature, signal } = options;
|
|
760
|
+
const anthropicMessages = rawMessages.filter((m) => m.role !== "system").map((m) => toAnthropicMessage(m));
|
|
761
|
+
const anthropicTools = tools?.map(toAnthropicTool);
|
|
762
|
+
const params = {
|
|
763
|
+
model,
|
|
764
|
+
max_tokens: maxTokens ?? 4096,
|
|
765
|
+
...temperature !== void 0 ? { temperature } : {},
|
|
766
|
+
...systemPrompt ? { system: systemPrompt } : {},
|
|
767
|
+
messages: anthropicMessages,
|
|
768
|
+
...anthropicTools?.length ? { tools: anthropicTools } : {}
|
|
769
|
+
};
|
|
770
|
+
const stream = client.messages.stream(params, { signal });
|
|
771
|
+
let currentToolId;
|
|
772
|
+
for await (const event of stream) {
|
|
773
|
+
switch (event.type) {
|
|
774
|
+
case "content_block_start": {
|
|
775
|
+
const block = event.content_block;
|
|
776
|
+
if (block.type === "text") {
|
|
777
|
+
} else if (block.type === "tool_use") {
|
|
778
|
+
currentToolId = block.id;
|
|
779
|
+
yield {
|
|
780
|
+
type: "tool_use_start",
|
|
781
|
+
toolCall: { id: block.id, name: block.name }
|
|
782
|
+
};
|
|
783
|
+
} else if (block.type === "thinking") {
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
case "content_block_delta": {
|
|
788
|
+
const delta = event.delta;
|
|
789
|
+
if (delta.type === "text_delta") {
|
|
790
|
+
yield { type: "text", text: delta.text };
|
|
791
|
+
} else if (delta.type === "input_json_delta") {
|
|
792
|
+
yield { type: "tool_use_delta", text: delta.partial_json };
|
|
793
|
+
} else if (delta.type === "thinking_delta") {
|
|
794
|
+
yield { type: "thinking", text: delta.thinking };
|
|
795
|
+
}
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
case "content_block_stop": {
|
|
799
|
+
if (currentToolId) {
|
|
800
|
+
yield { type: "tool_use_end" };
|
|
801
|
+
currentToolId = void 0;
|
|
802
|
+
}
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
case "message_delta": {
|
|
806
|
+
if (event.usage) {
|
|
807
|
+
yield {
|
|
808
|
+
type: "usage",
|
|
809
|
+
usage: {
|
|
810
|
+
inputTokens: 0,
|
|
811
|
+
outputTokens: event.usage.output_tokens
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
case "message_start": {
|
|
818
|
+
if (event.message.usage) {
|
|
819
|
+
yield {
|
|
820
|
+
type: "usage",
|
|
821
|
+
usage: {
|
|
822
|
+
inputTokens: event.message.usage.input_tokens,
|
|
823
|
+
outputTokens: event.message.usage.output_tokens
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
yield { type: "done" };
|
|
832
|
+
}
|
|
833
|
+
async countTokens(messages) {
|
|
834
|
+
let total = 0;
|
|
835
|
+
for (const msg of messages) {
|
|
836
|
+
if (typeof msg.content === "string") {
|
|
837
|
+
total += Math.ceil(msg.content.length / 4);
|
|
838
|
+
} else {
|
|
839
|
+
for (const part of msg.content) {
|
|
840
|
+
if (part.type === "text") {
|
|
841
|
+
total += Math.ceil(part.text.length / 4);
|
|
842
|
+
} else {
|
|
843
|
+
total += 1e3;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return total;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
function toAnthropicMessage(msg) {
|
|
852
|
+
if (msg.role === "user") {
|
|
853
|
+
if (typeof msg.content === "string") {
|
|
854
|
+
return { role: "user", content: msg.content };
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
role: "user",
|
|
858
|
+
content: msg.content.map((part) => {
|
|
859
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
860
|
+
return {
|
|
861
|
+
type: "image",
|
|
862
|
+
source: { type: "base64", media_type: part.mediaType, data: part.data }
|
|
863
|
+
};
|
|
864
|
+
})
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
if (msg.role === "assistant") {
|
|
868
|
+
const content = [];
|
|
869
|
+
if (typeof msg.content === "string") {
|
|
870
|
+
if (msg.content) {
|
|
871
|
+
content.push({ type: "text", text: msg.content });
|
|
872
|
+
}
|
|
873
|
+
} else {
|
|
874
|
+
for (const part of msg.content) {
|
|
875
|
+
if (part.type === "text") content.push({ type: "text", text: part.text });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (msg.toolCalls) {
|
|
879
|
+
for (const tc of msg.toolCalls) {
|
|
880
|
+
content.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.input });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return { role: "assistant", content };
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
role: "user",
|
|
887
|
+
content: [
|
|
888
|
+
{
|
|
889
|
+
type: "tool_result",
|
|
890
|
+
tool_use_id: msg.toolCallId,
|
|
891
|
+
content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
|
|
892
|
+
...msg.isError ? { is_error: true } : {}
|
|
893
|
+
}
|
|
894
|
+
]
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function toAnthropicTool(tool) {
|
|
898
|
+
return {
|
|
899
|
+
name: tool.name,
|
|
900
|
+
description: tool.description,
|
|
901
|
+
input_schema: tool.inputSchema
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
var sdkModule = null;
|
|
905
|
+
async function loadSDK() {
|
|
906
|
+
if (sdkModule) return sdkModule.default;
|
|
907
|
+
try {
|
|
908
|
+
sdkModule = await import("@anthropic-ai/sdk");
|
|
909
|
+
return sdkModule.default;
|
|
910
|
+
} catch {
|
|
911
|
+
throw new Error(
|
|
912
|
+
'AnthropicProvider requires "@anthropic-ai/sdk" package. Install it with: pnpm add @anthropic-ai/sdk'
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// src/providers/openai.ts
|
|
918
|
+
var OpenAIProvider = class {
|
|
919
|
+
constructor(options = {}) {
|
|
920
|
+
this.options = options;
|
|
921
|
+
this.clientPromise = loadSDK2().then(
|
|
922
|
+
(SDK) => new SDK({
|
|
923
|
+
apiKey: options.apiKey,
|
|
924
|
+
...options.baseURL ? { baseURL: options.baseURL } : {}
|
|
925
|
+
})
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
clientPromise;
|
|
929
|
+
async *chat(options) {
|
|
930
|
+
const client = await this.clientPromise;
|
|
931
|
+
const { model, messages, tools, systemPrompt, maxTokens, temperature, signal } = options;
|
|
932
|
+
const openaiMessages = [];
|
|
933
|
+
if (systemPrompt) {
|
|
934
|
+
openaiMessages.push({ role: "system", content: systemPrompt });
|
|
935
|
+
}
|
|
936
|
+
for (const msg of messages) {
|
|
937
|
+
if (msg.role === "system") {
|
|
938
|
+
openaiMessages.push({ role: "system", content: msg.content });
|
|
939
|
+
} else {
|
|
940
|
+
openaiMessages.push(
|
|
941
|
+
toOpenAIMessage(msg)
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
const openaiTools = tools?.map(toOpenAITool);
|
|
946
|
+
const params = {
|
|
947
|
+
model,
|
|
948
|
+
messages: openaiMessages,
|
|
949
|
+
...maxTokens ? { max_tokens: maxTokens } : {},
|
|
950
|
+
...temperature !== void 0 ? { temperature } : {},
|
|
951
|
+
...openaiTools?.length ? { tools: openaiTools } : {},
|
|
952
|
+
stream: true
|
|
953
|
+
};
|
|
954
|
+
const stream = await client.chat.completions.create(params, { signal });
|
|
955
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
956
|
+
let inputTokens = 0;
|
|
957
|
+
let outputTokens = 0;
|
|
958
|
+
for await (const chunk of stream) {
|
|
959
|
+
const choice = chunk.choices?.[0];
|
|
960
|
+
if (!choice) continue;
|
|
961
|
+
const delta = choice.delta;
|
|
962
|
+
if (delta?.content) {
|
|
963
|
+
yield { type: "text", text: delta.content };
|
|
964
|
+
}
|
|
965
|
+
if (delta?.tool_calls) {
|
|
966
|
+
for (const tc of delta.tool_calls) {
|
|
967
|
+
const idx = tc.index;
|
|
968
|
+
if (!toolCalls.has(idx)) {
|
|
969
|
+
toolCalls.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", args: "" });
|
|
970
|
+
}
|
|
971
|
+
const entry = toolCalls.get(idx);
|
|
972
|
+
if (tc.id) entry.id = tc.id;
|
|
973
|
+
if (tc.function?.name) {
|
|
974
|
+
entry.name = tc.function.name;
|
|
975
|
+
yield { type: "tool_use_start", toolCall: { id: entry.id, name: entry.name } };
|
|
976
|
+
}
|
|
977
|
+
if (tc.function?.arguments) {
|
|
978
|
+
entry.args += tc.function.arguments;
|
|
979
|
+
yield { type: "tool_use_delta", text: tc.function.arguments };
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (chunk.usage) {
|
|
984
|
+
inputTokens = chunk.usage.prompt_tokens ?? 0;
|
|
985
|
+
outputTokens = chunk.usage.completion_tokens ?? 0;
|
|
986
|
+
}
|
|
987
|
+
if (choice.finish_reason) {
|
|
988
|
+
for (const [, _entry] of toolCalls) {
|
|
989
|
+
yield { type: "tool_use_end" };
|
|
990
|
+
}
|
|
991
|
+
toolCalls.clear();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (inputTokens > 0 || outputTokens > 0) {
|
|
995
|
+
yield { type: "usage", usage: { inputTokens, outputTokens } };
|
|
996
|
+
}
|
|
997
|
+
yield { type: "done" };
|
|
998
|
+
}
|
|
999
|
+
async countTokens(messages) {
|
|
1000
|
+
let total = 0;
|
|
1001
|
+
for (const msg of messages) {
|
|
1002
|
+
if (typeof msg.content === "string") {
|
|
1003
|
+
total += Math.ceil(msg.content.length / 4);
|
|
1004
|
+
} else {
|
|
1005
|
+
for (const part of msg.content) {
|
|
1006
|
+
if (part.type === "text") total += Math.ceil(part.text.length / 4);
|
|
1007
|
+
else total += 1e3;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return total;
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
function toOpenAIMessage(msg) {
|
|
1015
|
+
if (msg.role === "user") {
|
|
1016
|
+
if (typeof msg.content === "string") {
|
|
1017
|
+
return { role: "user", content: msg.content };
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
role: "user",
|
|
1021
|
+
content: msg.content.map((part) => {
|
|
1022
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
1023
|
+
return {
|
|
1024
|
+
type: "image_url",
|
|
1025
|
+
image_url: { url: `data:${part.mediaType};base64,${part.data}` }
|
|
1026
|
+
};
|
|
1027
|
+
})
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
if (msg.role === "assistant") {
|
|
1031
|
+
const result = { role: "assistant" };
|
|
1032
|
+
if (typeof msg.content === "string") {
|
|
1033
|
+
result.content = msg.content || null;
|
|
1034
|
+
} else {
|
|
1035
|
+
const text = msg.content.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
1036
|
+
result.content = text || null;
|
|
1037
|
+
}
|
|
1038
|
+
if (msg.toolCalls?.length) {
|
|
1039
|
+
result.tool_calls = msg.toolCalls.map((tc) => ({
|
|
1040
|
+
id: tc.id,
|
|
1041
|
+
type: "function",
|
|
1042
|
+
function: {
|
|
1043
|
+
name: tc.name,
|
|
1044
|
+
arguments: JSON.stringify(tc.input)
|
|
1045
|
+
}
|
|
1046
|
+
}));
|
|
1047
|
+
}
|
|
1048
|
+
return result;
|
|
1049
|
+
}
|
|
1050
|
+
return {
|
|
1051
|
+
role: "tool",
|
|
1052
|
+
tool_call_id: msg.toolCallId,
|
|
1053
|
+
content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function toOpenAITool(tool) {
|
|
1057
|
+
return {
|
|
1058
|
+
type: "function",
|
|
1059
|
+
function: {
|
|
1060
|
+
name: tool.name,
|
|
1061
|
+
description: tool.description,
|
|
1062
|
+
parameters: tool.inputSchema
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
var sdkModule2 = null;
|
|
1067
|
+
async function loadSDK2() {
|
|
1068
|
+
if (sdkModule2) return sdkModule2.default;
|
|
1069
|
+
try {
|
|
1070
|
+
sdkModule2 = await import("openai");
|
|
1071
|
+
return sdkModule2.default;
|
|
1072
|
+
} catch {
|
|
1073
|
+
throw new Error(
|
|
1074
|
+
'OpenAIProvider requires "openai" package. Install it with: pnpm add openai'
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/providers/mock.ts
|
|
1080
|
+
var MockProvider = class {
|
|
1081
|
+
queue;
|
|
1082
|
+
callLog = [];
|
|
1083
|
+
constructor(responses) {
|
|
1084
|
+
this.queue = [...responses];
|
|
1085
|
+
}
|
|
1086
|
+
async *chat(options) {
|
|
1087
|
+
this.callLog.push(options);
|
|
1088
|
+
const response = this.queue.shift();
|
|
1089
|
+
if (!response) {
|
|
1090
|
+
throw new Error("MockProvider: no more scripted responses in queue");
|
|
1091
|
+
}
|
|
1092
|
+
for (const chunk of response) {
|
|
1093
|
+
yield chunk;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
async countTokens(messages) {
|
|
1097
|
+
let total = 0;
|
|
1098
|
+
for (const msg of messages) {
|
|
1099
|
+
if (typeof msg.content === "string") {
|
|
1100
|
+
total += Math.ceil(msg.content.length / 4);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return total;
|
|
1104
|
+
}
|
|
1105
|
+
/** Get the log of all chat() calls made to this provider. */
|
|
1106
|
+
getCalls() {
|
|
1107
|
+
return this.callLog;
|
|
1108
|
+
}
|
|
1109
|
+
/** Check if all scripted responses have been consumed. */
|
|
1110
|
+
isEmpty() {
|
|
1111
|
+
return this.queue.length === 0;
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
// src/auth/storage.ts
|
|
1116
|
+
import * as fs2 from "fs";
|
|
1117
|
+
import * as path2 from "path";
|
|
1118
|
+
import * as os from "os";
|
|
1119
|
+
var DEFAULT_STORAGE_PATH = path2.join(os.homedir(), ".claude-code-kit", "credentials.json");
|
|
1120
|
+
var FileAuthStorage = class {
|
|
1121
|
+
constructor(filePath = DEFAULT_STORAGE_PATH) {
|
|
1122
|
+
this.filePath = filePath;
|
|
1123
|
+
}
|
|
1124
|
+
async get(provider) {
|
|
1125
|
+
const data = await this.read();
|
|
1126
|
+
return data[provider] ?? null;
|
|
1127
|
+
}
|
|
1128
|
+
async set(provider, credential) {
|
|
1129
|
+
const data = await this.read();
|
|
1130
|
+
data[provider] = credential;
|
|
1131
|
+
await this.write(data);
|
|
1132
|
+
}
|
|
1133
|
+
async delete(provider) {
|
|
1134
|
+
const data = await this.read();
|
|
1135
|
+
delete data[provider];
|
|
1136
|
+
await this.write(data);
|
|
1137
|
+
}
|
|
1138
|
+
async list() {
|
|
1139
|
+
const data = await this.read();
|
|
1140
|
+
return Object.keys(data);
|
|
1141
|
+
}
|
|
1142
|
+
async read() {
|
|
1143
|
+
try {
|
|
1144
|
+
const content = fs2.readFileSync(this.filePath, "utf-8");
|
|
1145
|
+
return JSON.parse(content);
|
|
1146
|
+
} catch {
|
|
1147
|
+
return {};
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
async write(data) {
|
|
1151
|
+
const dir = path2.dirname(this.filePath);
|
|
1152
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1153
|
+
fs2.writeFileSync(this.filePath, JSON.stringify(data, null, 2), { mode: 384 });
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
var MemoryAuthStorage = class {
|
|
1157
|
+
store = /* @__PURE__ */ new Map();
|
|
1158
|
+
async get(provider) {
|
|
1159
|
+
return this.store.get(provider) ?? null;
|
|
1160
|
+
}
|
|
1161
|
+
async set(provider, credential) {
|
|
1162
|
+
this.store.set(provider, credential);
|
|
1163
|
+
}
|
|
1164
|
+
async delete(provider) {
|
|
1165
|
+
this.store.delete(provider);
|
|
1166
|
+
}
|
|
1167
|
+
async list() {
|
|
1168
|
+
return [...this.store.keys()];
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
// src/auth/registry.ts
|
|
1173
|
+
var AuthRegistry = class {
|
|
1174
|
+
providers = /* @__PURE__ */ new Map();
|
|
1175
|
+
storage;
|
|
1176
|
+
constructor(options) {
|
|
1177
|
+
this.storage = options?.storage ?? new FileAuthStorage(options?.storagePath);
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Register a provider. Open — any provider, any auth type.
|
|
1181
|
+
* Overwrites existing registration with the same name.
|
|
1182
|
+
*/
|
|
1183
|
+
register(name, registration) {
|
|
1184
|
+
this.providers.set(name, registration);
|
|
1185
|
+
}
|
|
1186
|
+
unregister(name) {
|
|
1187
|
+
return this.providers.delete(name);
|
|
1188
|
+
}
|
|
1189
|
+
getRegistration(name) {
|
|
1190
|
+
return this.providers.get(name);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Get the list of available models for a registered provider.
|
|
1194
|
+
*/
|
|
1195
|
+
getModels(providerName) {
|
|
1196
|
+
const reg = this.providers.get(providerName);
|
|
1197
|
+
if (!reg) {
|
|
1198
|
+
throw new Error(
|
|
1199
|
+
`Provider "${providerName}" is not registered. Available: ${[...this.providers.keys()].join(", ")}`
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
return reg.models ?? [];
|
|
1203
|
+
}
|
|
1204
|
+
// -------------------------------------------------------------------------
|
|
1205
|
+
// Resolve credential from an auth method (env → storage → null)
|
|
1206
|
+
// -------------------------------------------------------------------------
|
|
1207
|
+
resolveEnvCredential(method) {
|
|
1208
|
+
if (method.type === "none") return {};
|
|
1209
|
+
const envVar = method.type === "api-key" ? method.envVar : method.envVar;
|
|
1210
|
+
if (envVar) {
|
|
1211
|
+
const value = process.env[envVar];
|
|
1212
|
+
if (value) {
|
|
1213
|
+
if (method.type === "base-url-key") {
|
|
1214
|
+
return { apiKey: value, baseURL: method.defaultBaseURL || void 0 };
|
|
1215
|
+
}
|
|
1216
|
+
return { apiKey: value };
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Authenticate and get a configured LLMProvider.
|
|
1223
|
+
*
|
|
1224
|
+
* Resolution order for each auth method (tried in order):
|
|
1225
|
+
* 1. Environment variable (if method has `envVar`)
|
|
1226
|
+
* 2. Stored credential (from AuthStorage)
|
|
1227
|
+
* 3. Next auth method
|
|
1228
|
+
*
|
|
1229
|
+
* If no method resolves, throws.
|
|
1230
|
+
*
|
|
1231
|
+
* For providers with a `type: 'none'` auth method, no credential is required.
|
|
1232
|
+
*/
|
|
1233
|
+
async authenticate(name) {
|
|
1234
|
+
const reg = this.providers.get(name);
|
|
1235
|
+
if (!reg) {
|
|
1236
|
+
throw new Error(
|
|
1237
|
+
`Provider "${name}" is not registered. Available: ${[...this.providers.keys()].join(", ")}`
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
for (const method of reg.authMethods) {
|
|
1241
|
+
if (method.type === "none") {
|
|
1242
|
+
return reg.createProvider({});
|
|
1243
|
+
}
|
|
1244
|
+
const envCred = this.resolveEnvCredential(method);
|
|
1245
|
+
if (envCred) {
|
|
1246
|
+
return reg.createProvider(envCred);
|
|
1247
|
+
}
|
|
1248
|
+
const stored = await this.storage.get(name);
|
|
1249
|
+
if (stored) {
|
|
1250
|
+
if (method.type === "base-url-key") {
|
|
1251
|
+
return reg.createProvider({
|
|
1252
|
+
apiKey: stored,
|
|
1253
|
+
baseURL: method.defaultBaseURL || void 0
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
return reg.createProvider({ apiKey: stored });
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
const envVars = reg.authMethods.map((m) => {
|
|
1260
|
+
if (m.type === "api-key" || m.type === "base-url-key") return m.envVar;
|
|
1261
|
+
return void 0;
|
|
1262
|
+
}).filter(Boolean);
|
|
1263
|
+
const envHint = envVars.length > 0 ? ` Set ${envVars.join(" or ")} or call registry.storeCredential("${name}", key).` : "";
|
|
1264
|
+
throw new Error(`No credential found for provider "${name}".${envHint}`);
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Get a provider using only environment variables — no storage, no prompts.
|
|
1268
|
+
* Throws if no env var resolves (or provider has a 'none' method).
|
|
1269
|
+
*/
|
|
1270
|
+
fromEnv(name) {
|
|
1271
|
+
const reg = this.providers.get(name);
|
|
1272
|
+
if (!reg) {
|
|
1273
|
+
throw new Error(
|
|
1274
|
+
`Provider "${name}" is not registered. Available: ${[...this.providers.keys()].join(", ")}`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
for (const method of reg.authMethods) {
|
|
1278
|
+
if (method.type === "none") {
|
|
1279
|
+
return reg.createProvider({});
|
|
1280
|
+
}
|
|
1281
|
+
const envCred = this.resolveEnvCredential(method);
|
|
1282
|
+
if (envCred) {
|
|
1283
|
+
return reg.createProvider(envCred);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const envVars = reg.authMethods.map((m) => {
|
|
1287
|
+
if (m.type === "api-key" || m.type === "base-url-key") return m.envVar;
|
|
1288
|
+
return void 0;
|
|
1289
|
+
}).filter(Boolean);
|
|
1290
|
+
if (envVars.length === 0) {
|
|
1291
|
+
throw new Error(
|
|
1292
|
+
`Provider "${name}" has no envVar configured for env-only auth.`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
`Environment variable ${envVars.join(" / ")} is not set for provider "${name}".`
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
async storeCredential(name, credential) {
|
|
1300
|
+
await this.storage.set(name, credential);
|
|
1301
|
+
}
|
|
1302
|
+
async listProviders() {
|
|
1303
|
+
const stored = await this.storage.list();
|
|
1304
|
+
const storedSet = new Set(stored);
|
|
1305
|
+
const result = [];
|
|
1306
|
+
for (const [name, registration] of this.providers) {
|
|
1307
|
+
let hasCredential = registration.authMethods.some(
|
|
1308
|
+
(m) => m.type === "none"
|
|
1309
|
+
);
|
|
1310
|
+
if (!hasCredential) {
|
|
1311
|
+
for (const method of registration.authMethods) {
|
|
1312
|
+
const envCred = this.resolveEnvCredential(method);
|
|
1313
|
+
if (envCred) {
|
|
1314
|
+
hasCredential = true;
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (!hasCredential && storedSet.has(name)) {
|
|
1320
|
+
hasCredential = true;
|
|
1321
|
+
}
|
|
1322
|
+
result.push({ name, registration, hasCredential });
|
|
1323
|
+
}
|
|
1324
|
+
return result;
|
|
1325
|
+
}
|
|
1326
|
+
async logout(name) {
|
|
1327
|
+
await this.storage.delete(name);
|
|
1328
|
+
}
|
|
1329
|
+
// -------------------------------------------------------------------------
|
|
1330
|
+
// Interactive auth flow — step-by-step state machine
|
|
1331
|
+
// -------------------------------------------------------------------------
|
|
1332
|
+
/**
|
|
1333
|
+
* Start the interactive auth flow.
|
|
1334
|
+
* Returns the initial state with all available providers listed.
|
|
1335
|
+
*/
|
|
1336
|
+
interactive() {
|
|
1337
|
+
const providers = [...this.providers.entries()].map(([name, reg]) => ({
|
|
1338
|
+
name,
|
|
1339
|
+
displayName: reg.displayName,
|
|
1340
|
+
description: reg.description
|
|
1341
|
+
}));
|
|
1342
|
+
return {
|
|
1343
|
+
step: "select-provider",
|
|
1344
|
+
providers
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Advance the interactive flow by selecting a provider.
|
|
1349
|
+
*/
|
|
1350
|
+
selectProvider(providerName) {
|
|
1351
|
+
const reg = this.providers.get(providerName);
|
|
1352
|
+
if (!reg) {
|
|
1353
|
+
throw new Error(
|
|
1354
|
+
`Provider "${providerName}" is not registered. Available: ${[...this.providers.keys()].join(", ")}`
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
if (reg.authMethods.length === 1) {
|
|
1358
|
+
const method = reg.authMethods[0];
|
|
1359
|
+
if (method.type === "none") {
|
|
1360
|
+
if (reg.models && reg.models.length > 0) {
|
|
1361
|
+
return {
|
|
1362
|
+
step: "select-model",
|
|
1363
|
+
currentProvider: providerName,
|
|
1364
|
+
currentAuthMethod: method,
|
|
1365
|
+
models: reg.models,
|
|
1366
|
+
currentModel: reg.defaultModel
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
return {
|
|
1370
|
+
step: "done",
|
|
1371
|
+
currentProvider: providerName,
|
|
1372
|
+
currentAuthMethod: method,
|
|
1373
|
+
currentModel: reg.defaultModel,
|
|
1374
|
+
result: {
|
|
1375
|
+
provider: reg.createProvider({}),
|
|
1376
|
+
model: reg.defaultModel ?? "",
|
|
1377
|
+
providerName
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
return {
|
|
1382
|
+
step: "input-credentials",
|
|
1383
|
+
currentProvider: providerName,
|
|
1384
|
+
currentAuthMethod: method
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
return {
|
|
1388
|
+
step: "select-auth-method",
|
|
1389
|
+
currentProvider: providerName,
|
|
1390
|
+
authMethods: reg.authMethods
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Advance the interactive flow by selecting an auth method.
|
|
1395
|
+
* Only needed when a provider has multiple auth methods.
|
|
1396
|
+
*/
|
|
1397
|
+
selectAuthMethod(providerName, methodIndex) {
|
|
1398
|
+
const reg = this.providers.get(providerName);
|
|
1399
|
+
if (!reg) {
|
|
1400
|
+
throw new Error(`Provider "${providerName}" is not registered.`);
|
|
1401
|
+
}
|
|
1402
|
+
const method = reg.authMethods[methodIndex];
|
|
1403
|
+
if (!method) {
|
|
1404
|
+
throw new Error(
|
|
1405
|
+
`Auth method index ${methodIndex} is out of range for "${providerName}" (has ${reg.authMethods.length} methods).`
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
if (method.type === "none") {
|
|
1409
|
+
if (reg.models && reg.models.length > 0) {
|
|
1410
|
+
return {
|
|
1411
|
+
step: "select-model",
|
|
1412
|
+
currentProvider: providerName,
|
|
1413
|
+
currentAuthMethod: method,
|
|
1414
|
+
models: reg.models,
|
|
1415
|
+
currentModel: reg.defaultModel
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
return {
|
|
1419
|
+
step: "done",
|
|
1420
|
+
currentProvider: providerName,
|
|
1421
|
+
currentAuthMethod: method,
|
|
1422
|
+
currentModel: reg.defaultModel,
|
|
1423
|
+
result: {
|
|
1424
|
+
provider: reg.createProvider({}),
|
|
1425
|
+
model: reg.defaultModel ?? "",
|
|
1426
|
+
providerName
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
return {
|
|
1431
|
+
step: "input-credentials",
|
|
1432
|
+
currentProvider: providerName,
|
|
1433
|
+
currentAuthMethod: method
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Advance the interactive flow by providing credentials.
|
|
1438
|
+
* Stores the credential and moves to model selection (or done).
|
|
1439
|
+
*/
|
|
1440
|
+
async inputCredentials(providerName, method, credentials) {
|
|
1441
|
+
const reg = this.providers.get(providerName);
|
|
1442
|
+
if (!reg) {
|
|
1443
|
+
throw new Error(`Provider "${providerName}" is not registered.`);
|
|
1444
|
+
}
|
|
1445
|
+
if (credentials.apiKey) {
|
|
1446
|
+
await this.storage.set(providerName, credentials.apiKey);
|
|
1447
|
+
}
|
|
1448
|
+
const resolvedCredentials = { ...credentials };
|
|
1449
|
+
if (method.type === "base-url-key" && !resolvedCredentials.baseURL && method.defaultBaseURL) {
|
|
1450
|
+
resolvedCredentials.baseURL = method.defaultBaseURL;
|
|
1451
|
+
}
|
|
1452
|
+
if (reg.models && reg.models.length > 0) {
|
|
1453
|
+
return {
|
|
1454
|
+
step: "select-model",
|
|
1455
|
+
currentProvider: providerName,
|
|
1456
|
+
currentAuthMethod: method,
|
|
1457
|
+
models: reg.models,
|
|
1458
|
+
currentModel: reg.defaultModel
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
return {
|
|
1462
|
+
step: "done",
|
|
1463
|
+
currentProvider: providerName,
|
|
1464
|
+
currentAuthMethod: method,
|
|
1465
|
+
currentModel: reg.defaultModel,
|
|
1466
|
+
result: {
|
|
1467
|
+
provider: reg.createProvider(resolvedCredentials),
|
|
1468
|
+
model: reg.defaultModel ?? "",
|
|
1469
|
+
providerName
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Advance the interactive flow by selecting a model.
|
|
1475
|
+
* Returns the final 'done' state with a configured provider.
|
|
1476
|
+
*/
|
|
1477
|
+
async selectModel(providerName, method, model, credentials) {
|
|
1478
|
+
const reg = this.providers.get(providerName);
|
|
1479
|
+
if (!reg) {
|
|
1480
|
+
throw new Error(`Provider "${providerName}" is not registered.`);
|
|
1481
|
+
}
|
|
1482
|
+
let resolvedCreds = credentials;
|
|
1483
|
+
if (!resolvedCreds || !resolvedCreds.apiKey && !resolvedCreds.token) {
|
|
1484
|
+
if (method.type === "none") {
|
|
1485
|
+
resolvedCreds = {};
|
|
1486
|
+
} else {
|
|
1487
|
+
const envCred = this.resolveEnvCredential(method);
|
|
1488
|
+
if (envCred) {
|
|
1489
|
+
resolvedCreds = envCred;
|
|
1490
|
+
} else {
|
|
1491
|
+
const stored = await this.storage.get(providerName);
|
|
1492
|
+
if (stored) {
|
|
1493
|
+
resolvedCreds = { apiKey: stored };
|
|
1494
|
+
if (method.type === "base-url-key" && method.defaultBaseURL) {
|
|
1495
|
+
resolvedCreds.baseURL = method.defaultBaseURL;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (!resolvedCreds) {
|
|
1502
|
+
throw new Error(
|
|
1503
|
+
`No credentials available for provider "${providerName}". Provide credentials or set env var.`
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
return {
|
|
1507
|
+
step: "done",
|
|
1508
|
+
currentProvider: providerName,
|
|
1509
|
+
currentAuthMethod: method,
|
|
1510
|
+
currentModel: model,
|
|
1511
|
+
result: {
|
|
1512
|
+
provider: reg.createProvider(resolvedCreds),
|
|
1513
|
+
model,
|
|
1514
|
+
providerName
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
// src/auth/presets.ts
|
|
1521
|
+
var PRESET_PROVIDERS = {
|
|
1522
|
+
anthropic: {
|
|
1523
|
+
displayName: "Anthropic",
|
|
1524
|
+
description: "Claude Opus, Sonnet, Haiku",
|
|
1525
|
+
authMethods: [
|
|
1526
|
+
{
|
|
1527
|
+
type: "api-key",
|
|
1528
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
1529
|
+
inputLabel: "Anthropic API Key"
|
|
1530
|
+
}
|
|
1531
|
+
],
|
|
1532
|
+
models: ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"],
|
|
1533
|
+
defaultModel: "claude-sonnet-4-6",
|
|
1534
|
+
createProvider: ({ apiKey }) => new AnthropicProvider({ apiKey })
|
|
1535
|
+
},
|
|
1536
|
+
openai: {
|
|
1537
|
+
displayName: "OpenAI",
|
|
1538
|
+
description: "GPT-4o, GPT-4, o1",
|
|
1539
|
+
authMethods: [
|
|
1540
|
+
{
|
|
1541
|
+
type: "api-key",
|
|
1542
|
+
envVar: "OPENAI_API_KEY",
|
|
1543
|
+
inputLabel: "OpenAI API Key"
|
|
1544
|
+
}
|
|
1545
|
+
],
|
|
1546
|
+
models: ["gpt-4o", "gpt-4-turbo", "o1", "o1-mini"],
|
|
1547
|
+
defaultModel: "gpt-4o",
|
|
1548
|
+
createProvider: ({ apiKey }) => new OpenAIProvider({ apiKey })
|
|
1549
|
+
},
|
|
1550
|
+
deepseek: {
|
|
1551
|
+
displayName: "DeepSeek",
|
|
1552
|
+
description: "DeepSeek-V3, DeepSeek-R1",
|
|
1553
|
+
authMethods: [
|
|
1554
|
+
{
|
|
1555
|
+
type: "api-key",
|
|
1556
|
+
envVar: "DEEPSEEK_API_KEY",
|
|
1557
|
+
inputLabel: "DeepSeek API Key"
|
|
1558
|
+
}
|
|
1559
|
+
],
|
|
1560
|
+
models: ["deepseek-chat", "deepseek-reasoner"],
|
|
1561
|
+
defaultModel: "deepseek-chat",
|
|
1562
|
+
createProvider: ({ apiKey }) => new OpenAIProvider({ apiKey, baseURL: "https://api.deepseek.com/v1" })
|
|
1563
|
+
},
|
|
1564
|
+
siliconflow: {
|
|
1565
|
+
displayName: "SiliconFlow",
|
|
1566
|
+
description: "Qwen, DeepSeek, GLM via SiliconFlow",
|
|
1567
|
+
authMethods: [
|
|
1568
|
+
{
|
|
1569
|
+
type: "base-url-key",
|
|
1570
|
+
defaultBaseURL: "https://api.siliconflow.cn/v1",
|
|
1571
|
+
envVar: "SILICONFLOW_API_KEY",
|
|
1572
|
+
inputLabel: "SiliconFlow API Key"
|
|
1573
|
+
}
|
|
1574
|
+
],
|
|
1575
|
+
models: ["Qwen/Qwen2.5-72B-Instruct", "deepseek-ai/DeepSeek-V3"],
|
|
1576
|
+
defaultModel: "Qwen/Qwen2.5-72B-Instruct",
|
|
1577
|
+
createProvider: ({ apiKey, baseURL }) => new OpenAIProvider({
|
|
1578
|
+
apiKey,
|
|
1579
|
+
baseURL: baseURL || "https://api.siliconflow.cn/v1"
|
|
1580
|
+
})
|
|
1581
|
+
},
|
|
1582
|
+
moonshot: {
|
|
1583
|
+
displayName: "Moonshot (Kimi)",
|
|
1584
|
+
description: "Kimi K2.5, Moonshot v1",
|
|
1585
|
+
authMethods: [
|
|
1586
|
+
{
|
|
1587
|
+
type: "api-key",
|
|
1588
|
+
envVar: "MOONSHOT_API_KEY",
|
|
1589
|
+
inputLabel: "Moonshot API Key"
|
|
1590
|
+
}
|
|
1591
|
+
],
|
|
1592
|
+
models: ["moonshot-v1-auto", "kimi-k2.5"],
|
|
1593
|
+
defaultModel: "moonshot-v1-auto",
|
|
1594
|
+
createProvider: ({ apiKey }) => new OpenAIProvider({ apiKey, baseURL: "https://api.moonshot.cn/v1" })
|
|
1595
|
+
},
|
|
1596
|
+
groq: {
|
|
1597
|
+
displayName: "Groq",
|
|
1598
|
+
description: "Fast inference for Llama, Mixtral",
|
|
1599
|
+
authMethods: [
|
|
1600
|
+
{
|
|
1601
|
+
type: "api-key",
|
|
1602
|
+
envVar: "GROQ_API_KEY",
|
|
1603
|
+
inputLabel: "Groq API Key"
|
|
1604
|
+
}
|
|
1605
|
+
],
|
|
1606
|
+
models: ["llama-3.3-70b-versatile", "mixtral-8x7b-32768"],
|
|
1607
|
+
defaultModel: "llama-3.3-70b-versatile",
|
|
1608
|
+
createProvider: ({ apiKey }) => new OpenAIProvider({
|
|
1609
|
+
apiKey,
|
|
1610
|
+
baseURL: "https://api.groq.com/openai/v1"
|
|
1611
|
+
})
|
|
1612
|
+
},
|
|
1613
|
+
ollama: {
|
|
1614
|
+
displayName: "Ollama (local)",
|
|
1615
|
+
description: "Local models via Ollama",
|
|
1616
|
+
authMethods: [{ type: "none" }],
|
|
1617
|
+
models: ["llama3.1", "qwen2.5", "mistral"],
|
|
1618
|
+
defaultModel: "llama3.1",
|
|
1619
|
+
createProvider: () => new OpenAIProvider({
|
|
1620
|
+
apiKey: "ollama",
|
|
1621
|
+
baseURL: "http://localhost:11434/v1"
|
|
1622
|
+
})
|
|
1623
|
+
},
|
|
1624
|
+
custom: {
|
|
1625
|
+
displayName: "Custom (OpenAI-compatible)",
|
|
1626
|
+
description: "Any OpenAI-compatible API endpoint",
|
|
1627
|
+
authMethods: [
|
|
1628
|
+
{
|
|
1629
|
+
type: "base-url-key",
|
|
1630
|
+
defaultBaseURL: "",
|
|
1631
|
+
inputLabel: "Base URL + API Key"
|
|
1632
|
+
}
|
|
1633
|
+
],
|
|
1634
|
+
createProvider: ({ apiKey, baseURL }) => new OpenAIProvider({ apiKey: apiKey || "x", baseURL })
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
// src/auth/index.ts
|
|
1639
|
+
function createAuth(options) {
|
|
1640
|
+
const registry = new AuthRegistry(options);
|
|
1641
|
+
for (const [name, reg] of Object.entries(PRESET_PROVIDERS)) {
|
|
1642
|
+
registry.register(name, reg);
|
|
1643
|
+
}
|
|
1644
|
+
return registry;
|
|
1645
|
+
}
|
|
1646
|
+
export {
|
|
1647
|
+
Agent,
|
|
1648
|
+
AnthropicProvider,
|
|
1649
|
+
AuthRegistry,
|
|
1650
|
+
ContextManager,
|
|
1651
|
+
FileAuthStorage,
|
|
1652
|
+
FileSession,
|
|
1653
|
+
FileSessionStore,
|
|
1654
|
+
InMemorySession,
|
|
1655
|
+
MemoryAuthStorage,
|
|
1656
|
+
MockProvider,
|
|
1657
|
+
NoopCompaction,
|
|
1658
|
+
OpenAIProvider,
|
|
1659
|
+
PRESET_PROVIDERS,
|
|
1660
|
+
SlidingWindowCompaction,
|
|
1661
|
+
SummarizationCompaction,
|
|
1662
|
+
ToolRegistry,
|
|
1663
|
+
allowAll,
|
|
1664
|
+
allowReadOnly,
|
|
1665
|
+
createAuth,
|
|
1666
|
+
createPermissionHandler,
|
|
1667
|
+
denyAll,
|
|
1668
|
+
estimateTokens,
|
|
1669
|
+
estimateTotalTokens
|
|
1670
|
+
};
|