@codedir/mimir-code 0.1.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.md +661 -0
- package/README.md +47 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +7105 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +754 -0
- package/dist/index.js +1656 -0
- package/dist/index.js.map +1 -0
- package/package.json +110 -0
- package/scripts/templates/commands/docs.yml +53 -0
- package/scripts/templates/commands/perf.yml +56 -0
- package/scripts/templates/commands/refactor.yml +52 -0
- package/scripts/templates/commands/review.yml +62 -0
- package/scripts/templates/commands/security.yml +51 -0
- package/scripts/templates/commands/test.yml +50 -0
- package/src/cli/themes/dark-colorblind.json +20 -0
- package/src/cli/themes/dark.json +20 -0
- package/src/cli/themes/light-colorblind.json +20 -0
- package/src/cli/themes/light.json +20 -0
- package/src/cli/themes/mimir.json +20 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1656 @@
|
|
|
1
|
+
// src/types/index.ts
|
|
2
|
+
var createOk = (value) => ({ ok: true, value });
|
|
3
|
+
var createErr = (error) => ({ ok: false, error });
|
|
4
|
+
|
|
5
|
+
// src/core/Agent.ts
|
|
6
|
+
var Agent = class {
|
|
7
|
+
// private provider: ILLMProvider; // TODO: Will be used when LLM integration is implemented
|
|
8
|
+
toolRegistry;
|
|
9
|
+
config;
|
|
10
|
+
conversationHistory = [];
|
|
11
|
+
currentIteration = 0;
|
|
12
|
+
constructor(_provider, toolRegistry, config) {
|
|
13
|
+
this.toolRegistry = toolRegistry;
|
|
14
|
+
this.config = config;
|
|
15
|
+
}
|
|
16
|
+
async run(task) {
|
|
17
|
+
this.conversationHistory = [
|
|
18
|
+
{
|
|
19
|
+
role: "system",
|
|
20
|
+
content: "You are a helpful coding assistant."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
role: "user",
|
|
24
|
+
content: task
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
while (this.currentIteration < this.config.maxIterations) {
|
|
28
|
+
const action = await this.reason();
|
|
29
|
+
if (action.type === "finish") {
|
|
30
|
+
return createOk(action.result);
|
|
31
|
+
}
|
|
32
|
+
const observation = await this.act(action);
|
|
33
|
+
await this.observe(observation);
|
|
34
|
+
this.currentIteration++;
|
|
35
|
+
}
|
|
36
|
+
return createErr(new Error("Max iterations reached"));
|
|
37
|
+
}
|
|
38
|
+
async reason() {
|
|
39
|
+
return {
|
|
40
|
+
type: "finish",
|
|
41
|
+
result: "Task completed"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async act(action) {
|
|
45
|
+
if (!action.toolName || !action.arguments) {
|
|
46
|
+
return {
|
|
47
|
+
type: "error",
|
|
48
|
+
error: "Invalid action: missing toolName or arguments"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const result = await this.toolRegistry.execute(action.toolName, action.arguments);
|
|
52
|
+
return {
|
|
53
|
+
type: "tool_result",
|
|
54
|
+
data: result
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async observe(observation) {
|
|
58
|
+
this.conversationHistory.push({
|
|
59
|
+
role: "assistant",
|
|
60
|
+
content: JSON.stringify(observation)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// src/core/Tool.ts
|
|
66
|
+
var ToolRegistry = class {
|
|
67
|
+
tools = /* @__PURE__ */ new Map();
|
|
68
|
+
register(tool) {
|
|
69
|
+
this.tools.set(tool.name, tool);
|
|
70
|
+
}
|
|
71
|
+
unregister(toolName) {
|
|
72
|
+
this.tools.delete(toolName);
|
|
73
|
+
}
|
|
74
|
+
get(toolName) {
|
|
75
|
+
return this.tools.get(toolName);
|
|
76
|
+
}
|
|
77
|
+
getAll() {
|
|
78
|
+
return Array.from(this.tools.values());
|
|
79
|
+
}
|
|
80
|
+
has(toolName) {
|
|
81
|
+
return this.tools.has(toolName);
|
|
82
|
+
}
|
|
83
|
+
async execute(toolName, args) {
|
|
84
|
+
const tool = this.get(toolName);
|
|
85
|
+
if (!tool) {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
error: `Tool not found: ${toolName}`
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const validatedArgs = tool.schema.parse(args);
|
|
93
|
+
return await tool.execute(validatedArgs);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: error instanceof Error ? error.message : String(error)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/core/RiskAssessor.ts
|
|
104
|
+
var RiskAssessor = class {
|
|
105
|
+
// Critical patterns - system-destroying commands
|
|
106
|
+
criticalPatterns = [
|
|
107
|
+
{ pattern: /rm\s+-rf\s+\/(?!tmp|var\/tmp)/, reason: "Deletes root filesystem" },
|
|
108
|
+
{ pattern: /format\s+[a-z]:/i, reason: "Formats entire drive" },
|
|
109
|
+
{ pattern: /del\s+\/[sf]/i, reason: "Deletes system files (Windows)" },
|
|
110
|
+
{ pattern: /shutdown|reboot|poweroff/, reason: "System shutdown/reboot" },
|
|
111
|
+
{ pattern: /dd\s+.*of=\/dev\/(sda|hda|nvme)/, reason: "Direct disk write (can destroy data)" },
|
|
112
|
+
{ pattern: /mkfs/, reason: "Formats filesystem" },
|
|
113
|
+
{
|
|
114
|
+
pattern: /(>|vim|vi|nano|emacs|edit).*\/etc\/(passwd|shadow|sudoers)/,
|
|
115
|
+
reason: "Modifies critical system files"
|
|
116
|
+
},
|
|
117
|
+
{ pattern: /curl.*\|\s*(bash|sh|python)/, reason: "Executes remote script without inspection" },
|
|
118
|
+
{ pattern: /wget.*\|\s*(bash|sh|python)/, reason: "Executes remote script without inspection" }
|
|
119
|
+
];
|
|
120
|
+
// High risk patterns - destructive but recoverable
|
|
121
|
+
highPatterns = [
|
|
122
|
+
{ pattern: /rm\s+-rf\s+(?!\/($|\s))/, reason: "Recursive force delete" },
|
|
123
|
+
{ pattern: /sudo\s+rm/, reason: "Elevated permissions file deletion" },
|
|
124
|
+
{ pattern: /git\s+push\s+--force/, reason: "Force pushes can overwrite history" },
|
|
125
|
+
{ pattern: /npm\s+publish/, reason: "Publishes package to registry" },
|
|
126
|
+
{ pattern: /docker\s+rmi.*-f/, reason: "Force removes Docker images" },
|
|
127
|
+
{ pattern: /docker\s+system\s+prune\s+-a/, reason: "Removes all unused Docker data" },
|
|
128
|
+
{ pattern: /git\s+reset\s+--hard\s+HEAD~/, reason: "Permanently deletes commits" },
|
|
129
|
+
{ pattern: /git\s+clean\s+-fd/, reason: "Deletes untracked files" },
|
|
130
|
+
{ pattern: /chmod\s+777/, reason: "Makes files world-writable (security risk)" },
|
|
131
|
+
{ pattern: /chown\s+-R/, reason: "Recursive ownership change" }
|
|
132
|
+
];
|
|
133
|
+
// Medium risk patterns - potentially problematic
|
|
134
|
+
mediumPatterns = [
|
|
135
|
+
{ pattern: /npm\s+install/, reason: "Installs dependencies (can include malicious packages)" },
|
|
136
|
+
{ pattern: /yarn\s+add/, reason: "Installs dependencies (can include malicious packages)" },
|
|
137
|
+
{ pattern: /pip\s+install/, reason: "Installs Python packages" },
|
|
138
|
+
{ pattern: /git\s+push/, reason: "Pushes changes to remote" },
|
|
139
|
+
{ pattern: /docker\s+run/, reason: "Runs Docker container" },
|
|
140
|
+
{ pattern: /docker\s+exec/, reason: "Executes command in container" },
|
|
141
|
+
{ pattern: /ssh\s+/, reason: "Remote connection" },
|
|
142
|
+
{ pattern: /scp\s+/, reason: "Remote file transfer" },
|
|
143
|
+
{ pattern: /rsync\s+/, reason: "File synchronization" },
|
|
144
|
+
{ pattern: /npm\s+run\s+build/, reason: "Runs build scripts" }
|
|
145
|
+
];
|
|
146
|
+
/**
|
|
147
|
+
* Assess risk level and provide detailed reasons
|
|
148
|
+
*/
|
|
149
|
+
assess(command) {
|
|
150
|
+
const reasons = [];
|
|
151
|
+
let maxScore = 0;
|
|
152
|
+
for (const { pattern, reason } of this.criticalPatterns) {
|
|
153
|
+
if (pattern.test(command)) {
|
|
154
|
+
reasons.push(`\u{1F534} CRITICAL: ${reason}`);
|
|
155
|
+
maxScore = Math.max(maxScore, 100);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const { pattern, reason } of this.highPatterns) {
|
|
159
|
+
if (pattern.test(command)) {
|
|
160
|
+
reasons.push(`\u{1F7E0} HIGH: ${reason}`);
|
|
161
|
+
maxScore = Math.max(maxScore, 75);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const { pattern, reason } of this.mediumPatterns) {
|
|
165
|
+
if (pattern.test(command)) {
|
|
166
|
+
reasons.push(`\u{1F7E1} MEDIUM: ${reason}`);
|
|
167
|
+
maxScore = Math.max(maxScore, 50);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const additionalRisks = this.assessAdditionalRisks(command);
|
|
171
|
+
reasons.push(...additionalRisks.reasons);
|
|
172
|
+
maxScore = Math.max(maxScore, additionalRisks.score);
|
|
173
|
+
const level = this.scoreToLevel(maxScore);
|
|
174
|
+
if (reasons.length === 0) {
|
|
175
|
+
reasons.push("No specific risks detected");
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
level,
|
|
179
|
+
reasons,
|
|
180
|
+
score: maxScore
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Additional risk checks (file size, complexity, etc.)
|
|
185
|
+
*/
|
|
186
|
+
assessAdditionalRisks(command) {
|
|
187
|
+
const reasons = [];
|
|
188
|
+
let score = 0;
|
|
189
|
+
if (command.length > 500) {
|
|
190
|
+
reasons.push("\u26A0\uFE0F Command is unusually long (possible obfuscation)");
|
|
191
|
+
score = Math.max(score, 30);
|
|
192
|
+
}
|
|
193
|
+
const chainCount = (command.match(/[;&|]+/g) || []).length;
|
|
194
|
+
if (chainCount > 3) {
|
|
195
|
+
reasons.push(`\u26A0\uFE0F Multiple chained commands (${chainCount} chains)`);
|
|
196
|
+
score = Math.max(score, 40);
|
|
197
|
+
}
|
|
198
|
+
if (/>\/dev\/null|2>&1/.test(command)) {
|
|
199
|
+
reasons.push("\u26A0\uFE0F Output redirected (hiding results)");
|
|
200
|
+
score = Math.max(score, 20);
|
|
201
|
+
}
|
|
202
|
+
if (/sudo\s*$/.test(command)) {
|
|
203
|
+
reasons.push("\u{1F7E0} Elevated permissions without specific command");
|
|
204
|
+
score = Math.max(score, 60);
|
|
205
|
+
}
|
|
206
|
+
if (/export\s+|setenv\s+/.test(command) && /PATH/.test(command)) {
|
|
207
|
+
reasons.push("\u{1F7E1} Modifies PATH environment variable");
|
|
208
|
+
score = Math.max(score, 45);
|
|
209
|
+
}
|
|
210
|
+
if (/base64\s+--decode|echo\s+.*\|\s*base64/.test(command)) {
|
|
211
|
+
reasons.push("\u26A0\uFE0F Uses Base64 encoding (possible obfuscation)");
|
|
212
|
+
score = Math.max(score, 35);
|
|
213
|
+
}
|
|
214
|
+
if (/eval\s+/.test(command)) {
|
|
215
|
+
reasons.push("\u{1F7E0} Uses eval (dynamic code execution)");
|
|
216
|
+
score = Math.max(score, 65);
|
|
217
|
+
}
|
|
218
|
+
return { reasons, score };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Convert numeric score to risk level
|
|
222
|
+
*/
|
|
223
|
+
scoreToLevel(score) {
|
|
224
|
+
if (score >= 80) return "critical";
|
|
225
|
+
if (score >= 60) return "high";
|
|
226
|
+
if (score >= 30) return "medium";
|
|
227
|
+
return "low";
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Check if command matches any allowed patterns
|
|
231
|
+
*/
|
|
232
|
+
isAllowed(command, allowlist) {
|
|
233
|
+
return allowlist.some((pattern) => {
|
|
234
|
+
try {
|
|
235
|
+
const regex = new RegExp(pattern);
|
|
236
|
+
return regex.test(command);
|
|
237
|
+
} catch {
|
|
238
|
+
return command.includes(pattern);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Check if command matches any blocked patterns
|
|
244
|
+
*/
|
|
245
|
+
isBlocked(command, blocklist) {
|
|
246
|
+
return blocklist.some((pattern) => {
|
|
247
|
+
try {
|
|
248
|
+
const regex = new RegExp(pattern);
|
|
249
|
+
return regex.test(command);
|
|
250
|
+
} catch {
|
|
251
|
+
return command.includes(pattern);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get a human-readable summary of the risk assessment
|
|
257
|
+
*/
|
|
258
|
+
getSummary(assessment) {
|
|
259
|
+
const levelEmoji = {
|
|
260
|
+
low: "\u{1F7E2}",
|
|
261
|
+
medium: "\u{1F7E1}",
|
|
262
|
+
high: "\u{1F7E0}",
|
|
263
|
+
critical: "\u{1F534}"
|
|
264
|
+
};
|
|
265
|
+
const lines = [
|
|
266
|
+
`${levelEmoji[assessment.level]} Risk Level: ${assessment.level.toUpperCase()} (score: ${assessment.score}/100)`,
|
|
267
|
+
"",
|
|
268
|
+
"Reasons:",
|
|
269
|
+
...assessment.reasons.map((r) => ` \u2022 ${r}`)
|
|
270
|
+
];
|
|
271
|
+
return lines.join("\n");
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/core/PermissionManager.ts
|
|
276
|
+
var PermissionManager = class {
|
|
277
|
+
riskAssessor;
|
|
278
|
+
allowlist = /* @__PURE__ */ new Set();
|
|
279
|
+
blocklist = /* @__PURE__ */ new Set();
|
|
280
|
+
auditLog = [];
|
|
281
|
+
constructor() {
|
|
282
|
+
this.riskAssessor = new RiskAssessor();
|
|
283
|
+
}
|
|
284
|
+
async checkPermission(command) {
|
|
285
|
+
const assessment = this.riskAssessor.assess(command);
|
|
286
|
+
if (this.riskAssessor.isBlocked(command, Array.from(this.blocklist))) {
|
|
287
|
+
this.logDecision(command, assessment.level, "deny");
|
|
288
|
+
return { allowed: false, assessment };
|
|
289
|
+
}
|
|
290
|
+
if (this.riskAssessor.isAllowed(command, Array.from(this.allowlist))) {
|
|
291
|
+
this.logDecision(command, assessment.level, "allow");
|
|
292
|
+
return { allowed: true, assessment };
|
|
293
|
+
}
|
|
294
|
+
if (assessment.level === "high" || assessment.level === "critical") {
|
|
295
|
+
this.logDecision(command, assessment.level, "deny");
|
|
296
|
+
return { allowed: false, assessment };
|
|
297
|
+
}
|
|
298
|
+
this.logDecision(command, assessment.level, "allow");
|
|
299
|
+
return { allowed: true, assessment };
|
|
300
|
+
}
|
|
301
|
+
addToAllowlist(pattern) {
|
|
302
|
+
this.allowlist.add(pattern);
|
|
303
|
+
}
|
|
304
|
+
addToBlocklist(pattern) {
|
|
305
|
+
this.blocklist.add(pattern);
|
|
306
|
+
}
|
|
307
|
+
logDecision(command, riskLevel, decision) {
|
|
308
|
+
this.auditLog.push({
|
|
309
|
+
command,
|
|
310
|
+
riskLevel,
|
|
311
|
+
decision,
|
|
312
|
+
timestamp: Date.now()
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
getAuditLog() {
|
|
316
|
+
return [...this.auditLog];
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// src/utils/errors.ts
|
|
321
|
+
var MimirError = class extends Error {
|
|
322
|
+
constructor(message) {
|
|
323
|
+
super(message);
|
|
324
|
+
this.name = "MimirError";
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
var ConfigurationError = class extends MimirError {
|
|
328
|
+
constructor(message) {
|
|
329
|
+
super(message);
|
|
330
|
+
this.name = "ConfigurationError";
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
var ProviderError = class extends MimirError {
|
|
334
|
+
constructor(message, provider) {
|
|
335
|
+
super(message);
|
|
336
|
+
this.provider = provider;
|
|
337
|
+
this.name = "ProviderError";
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
var ToolExecutionError = class extends MimirError {
|
|
341
|
+
constructor(message, toolName) {
|
|
342
|
+
super(message);
|
|
343
|
+
this.toolName = toolName;
|
|
344
|
+
this.name = "ToolExecutionError";
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
var PermissionDeniedError = class extends MimirError {
|
|
348
|
+
constructor(message, command) {
|
|
349
|
+
super(message);
|
|
350
|
+
this.command = command;
|
|
351
|
+
this.name = "PermissionDeniedError";
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
var DockerError = class extends MimirError {
|
|
355
|
+
constructor(message) {
|
|
356
|
+
super(message);
|
|
357
|
+
this.name = "DockerError";
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
var NetworkError = class extends MimirError {
|
|
361
|
+
constructor(message, statusCode) {
|
|
362
|
+
super(message);
|
|
363
|
+
this.statusCode = statusCode;
|
|
364
|
+
this.name = "NetworkError";
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
var RateLimitError = class extends MimirError {
|
|
368
|
+
constructor(message, retryAfter) {
|
|
369
|
+
super(message);
|
|
370
|
+
this.retryAfter = retryAfter;
|
|
371
|
+
this.name = "RateLimitError";
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/providers/BaseLLMProvider.ts
|
|
376
|
+
var BaseLLMProvider = class {
|
|
377
|
+
config;
|
|
378
|
+
retryConfig;
|
|
379
|
+
constructor(config, retryConfig) {
|
|
380
|
+
this.config = config;
|
|
381
|
+
this.retryConfig = retryConfig ?? {
|
|
382
|
+
maxRetries: 3,
|
|
383
|
+
retryDelay: 1e3,
|
|
384
|
+
backoffMultiplier: 2
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
getProviderName() {
|
|
388
|
+
return this.config.provider;
|
|
389
|
+
}
|
|
390
|
+
getModelName() {
|
|
391
|
+
return this.config.model;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Retry wrapper for API calls
|
|
395
|
+
* Only retries on NetworkError (5xx server errors)
|
|
396
|
+
* Does NOT retry on auth errors, rate limits, or client errors
|
|
397
|
+
*/
|
|
398
|
+
async withRetry(fn) {
|
|
399
|
+
let lastError;
|
|
400
|
+
let delay = this.retryConfig.retryDelay;
|
|
401
|
+
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
402
|
+
try {
|
|
403
|
+
return await fn();
|
|
404
|
+
} catch (error) {
|
|
405
|
+
lastError = error;
|
|
406
|
+
const shouldRetry = error instanceof NetworkError && attempt < this.retryConfig.maxRetries;
|
|
407
|
+
if (shouldRetry) {
|
|
408
|
+
await this.sleep(delay);
|
|
409
|
+
delay *= this.retryConfig.backoffMultiplier;
|
|
410
|
+
} else {
|
|
411
|
+
throw lastError;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
throw lastError;
|
|
416
|
+
}
|
|
417
|
+
sleep(ms) {
|
|
418
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// src/providers/DeepSeekProvider.ts
|
|
423
|
+
import { encoding_for_model } from "tiktoken";
|
|
424
|
+
|
|
425
|
+
// src/providers/utils/apiClient.ts
|
|
426
|
+
import axios from "axios";
|
|
427
|
+
var APIClient = class {
|
|
428
|
+
axiosInstance;
|
|
429
|
+
providerName;
|
|
430
|
+
constructor(config) {
|
|
431
|
+
this.axiosInstance = axios.create({
|
|
432
|
+
baseURL: config.baseURL,
|
|
433
|
+
headers: config.headers,
|
|
434
|
+
timeout: config.timeout || 6e4
|
|
435
|
+
// 60 seconds default
|
|
436
|
+
});
|
|
437
|
+
this.providerName = this.extractProviderName(config.baseURL);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* POST request with JSON payload
|
|
441
|
+
*/
|
|
442
|
+
async post(endpoint, data) {
|
|
443
|
+
try {
|
|
444
|
+
const response = await this.axiosInstance.post(endpoint, data);
|
|
445
|
+
return response.data;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
throw this.mapError(error);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* GET request
|
|
452
|
+
*/
|
|
453
|
+
async get(endpoint) {
|
|
454
|
+
try {
|
|
455
|
+
const response = await this.axiosInstance.get(endpoint);
|
|
456
|
+
return response.data;
|
|
457
|
+
} catch (error) {
|
|
458
|
+
throw this.mapError(error);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Stream request for Server-Sent Events (SSE)
|
|
463
|
+
* Returns async iterable of string chunks
|
|
464
|
+
*/
|
|
465
|
+
async *stream(endpoint, data) {
|
|
466
|
+
try {
|
|
467
|
+
const response = await this.axiosInstance.post(endpoint, data, {
|
|
468
|
+
responseType: "stream",
|
|
469
|
+
headers: {
|
|
470
|
+
Accept: "text/event-stream"
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
const stream = response.data;
|
|
474
|
+
for await (const chunk of stream) {
|
|
475
|
+
const chunkStr = chunk.toString("utf-8");
|
|
476
|
+
yield chunkStr;
|
|
477
|
+
}
|
|
478
|
+
} catch (error) {
|
|
479
|
+
throw this.mapError(error);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Map axios errors to custom error types
|
|
484
|
+
*/
|
|
485
|
+
mapError(error) {
|
|
486
|
+
if (!axios.isAxiosError(error)) {
|
|
487
|
+
return error;
|
|
488
|
+
}
|
|
489
|
+
const axiosError = error;
|
|
490
|
+
const status = axiosError.response?.status;
|
|
491
|
+
const errorData = axiosError.response?.data;
|
|
492
|
+
const message = errorData?.error?.message || errorData?.message || axiosError.message;
|
|
493
|
+
if (status === 429) {
|
|
494
|
+
const retryAfter = parseInt(axiosError.response?.headers["retry-after"] || "60");
|
|
495
|
+
return new RateLimitError(`${this.providerName} rate limit exceeded: ${message}`, retryAfter);
|
|
496
|
+
}
|
|
497
|
+
if (status === 401 || status === 403) {
|
|
498
|
+
return new ProviderError(
|
|
499
|
+
`${this.providerName} authentication failed: ${message}`,
|
|
500
|
+
this.providerName
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
if (status && status >= 500) {
|
|
504
|
+
return new NetworkError(`${this.providerName} server error: ${message}`, status);
|
|
505
|
+
}
|
|
506
|
+
if (status && status >= 400) {
|
|
507
|
+
return new ProviderError(`${this.providerName} request error: ${message}`, this.providerName);
|
|
508
|
+
}
|
|
509
|
+
if (axiosError.code === "ECONNABORTED" || axiosError.code === "ETIMEDOUT") {
|
|
510
|
+
return new NetworkError(`${this.providerName} request timeout: ${message}`);
|
|
511
|
+
}
|
|
512
|
+
return new ProviderError(`${this.providerName} error: ${message}`, this.providerName);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Extract provider name from base URL
|
|
516
|
+
*/
|
|
517
|
+
extractProviderName(baseURL) {
|
|
518
|
+
try {
|
|
519
|
+
const url = new URL(baseURL);
|
|
520
|
+
const hostname = url.hostname;
|
|
521
|
+
if (hostname.includes("deepseek")) return "DeepSeek";
|
|
522
|
+
if (hostname.includes("anthropic")) return "Anthropic";
|
|
523
|
+
if (hostname.includes("openai")) return "OpenAI";
|
|
524
|
+
if (hostname.includes("google")) return "Google";
|
|
525
|
+
return hostname;
|
|
526
|
+
} catch {
|
|
527
|
+
return "Unknown Provider";
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// src/providers/pricing/pricingData.ts
|
|
533
|
+
var STATIC_PRICING_TABLE = {
|
|
534
|
+
deepseek: {
|
|
535
|
+
"deepseek-chat": {
|
|
536
|
+
inputPerMillionTokens: 0.14,
|
|
537
|
+
outputPerMillionTokens: 0.28
|
|
538
|
+
},
|
|
539
|
+
"deepseek-reasoner": {
|
|
540
|
+
inputPerMillionTokens: 0.55,
|
|
541
|
+
outputPerMillionTokens: 2.19
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
anthropic: {
|
|
545
|
+
"claude-opus-4-5-20251101": {
|
|
546
|
+
inputPerMillionTokens: 15,
|
|
547
|
+
outputPerMillionTokens: 75
|
|
548
|
+
},
|
|
549
|
+
"claude-opus-4-5": {
|
|
550
|
+
inputPerMillionTokens: 15,
|
|
551
|
+
outputPerMillionTokens: 75
|
|
552
|
+
},
|
|
553
|
+
"claude-sonnet-4-5-20250929": {
|
|
554
|
+
inputPerMillionTokens: 3,
|
|
555
|
+
outputPerMillionTokens: 15
|
|
556
|
+
},
|
|
557
|
+
"claude-sonnet-4-5": {
|
|
558
|
+
inputPerMillionTokens: 3,
|
|
559
|
+
outputPerMillionTokens: 15
|
|
560
|
+
},
|
|
561
|
+
"claude-haiku-4-5": {
|
|
562
|
+
inputPerMillionTokens: 1,
|
|
563
|
+
outputPerMillionTokens: 5
|
|
564
|
+
},
|
|
565
|
+
"claude-3-7-sonnet-latest": {
|
|
566
|
+
inputPerMillionTokens: 3,
|
|
567
|
+
outputPerMillionTokens: 15
|
|
568
|
+
},
|
|
569
|
+
"claude-3-5-haiku-latest": {
|
|
570
|
+
inputPerMillionTokens: 1,
|
|
571
|
+
outputPerMillionTokens: 5
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
576
|
+
function getStaticPricing(provider, model) {
|
|
577
|
+
const providerPricing = STATIC_PRICING_TABLE[provider.toLowerCase()];
|
|
578
|
+
if (!providerPricing) {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
return providerPricing[model] || null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/providers/utils/toolFormatters.ts
|
|
585
|
+
function toOpenAITools(tools) {
|
|
586
|
+
return tools.map((tool) => ({
|
|
587
|
+
type: "function",
|
|
588
|
+
function: {
|
|
589
|
+
name: tool.name,
|
|
590
|
+
description: tool.description,
|
|
591
|
+
parameters: tool.schema
|
|
592
|
+
// Already JSON Schema
|
|
593
|
+
}
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
596
|
+
function toAnthropicTools(tools) {
|
|
597
|
+
return tools.map((tool) => ({
|
|
598
|
+
name: tool.name,
|
|
599
|
+
description: tool.description,
|
|
600
|
+
input_schema: {
|
|
601
|
+
type: "object",
|
|
602
|
+
...tool.schema
|
|
603
|
+
// Merge with existing schema
|
|
604
|
+
}
|
|
605
|
+
}));
|
|
606
|
+
}
|
|
607
|
+
function parseOpenAIToolCalls(response) {
|
|
608
|
+
const toolCalls = response.choices?.[0]?.message?.tool_calls || [];
|
|
609
|
+
return toolCalls.map((tc) => {
|
|
610
|
+
let parsedArgs;
|
|
611
|
+
try {
|
|
612
|
+
parsedArgs = JSON.parse(tc.function.arguments);
|
|
613
|
+
} catch {
|
|
614
|
+
parsedArgs = {};
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
id: tc.id,
|
|
618
|
+
name: tc.function.name,
|
|
619
|
+
arguments: parsedArgs
|
|
620
|
+
};
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
function parseAnthropicToolCalls(response) {
|
|
624
|
+
const content = response.content || [];
|
|
625
|
+
return content.filter(
|
|
626
|
+
(block) => block.type === "tool_use"
|
|
627
|
+
).map((block) => ({
|
|
628
|
+
id: block.id,
|
|
629
|
+
name: block.name,
|
|
630
|
+
arguments: block.input
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
function mapOpenAIFinishReason(reason) {
|
|
634
|
+
switch (reason) {
|
|
635
|
+
case "stop":
|
|
636
|
+
return "stop";
|
|
637
|
+
case "tool_calls":
|
|
638
|
+
return "tool_calls";
|
|
639
|
+
case "length":
|
|
640
|
+
return "length";
|
|
641
|
+
case "content_filter":
|
|
642
|
+
case "insufficient_system_resource":
|
|
643
|
+
return "error";
|
|
644
|
+
default:
|
|
645
|
+
return "error";
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function mapAnthropicFinishReason(reason) {
|
|
649
|
+
switch (reason) {
|
|
650
|
+
case "end_turn":
|
|
651
|
+
return "stop";
|
|
652
|
+
case "tool_use":
|
|
653
|
+
return "tool_calls";
|
|
654
|
+
case "max_tokens":
|
|
655
|
+
return "length";
|
|
656
|
+
case "stop_sequence":
|
|
657
|
+
return "stop";
|
|
658
|
+
default:
|
|
659
|
+
return "error";
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/providers/utils/streamParsers.ts
|
|
664
|
+
async function* parseOpenAIStream(stream) {
|
|
665
|
+
let buffer = "";
|
|
666
|
+
for await (const chunk of stream) {
|
|
667
|
+
buffer += chunk;
|
|
668
|
+
const lines = buffer.split("\n");
|
|
669
|
+
buffer = lines.pop() || "";
|
|
670
|
+
for (const line of lines) {
|
|
671
|
+
const trimmed = line.trim();
|
|
672
|
+
if (!trimmed) continue;
|
|
673
|
+
if (trimmed === "data: [DONE]") {
|
|
674
|
+
yield { content: "", done: true };
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (trimmed.startsWith("data: ")) {
|
|
678
|
+
const jsonStr = trimmed.substring(6);
|
|
679
|
+
try {
|
|
680
|
+
const data = JSON.parse(jsonStr);
|
|
681
|
+
const delta = data.choices?.[0]?.delta;
|
|
682
|
+
const content = delta?.content || "";
|
|
683
|
+
const finishReason = data.choices?.[0]?.finish_reason;
|
|
684
|
+
if (content) {
|
|
685
|
+
yield { content, done: false };
|
|
686
|
+
}
|
|
687
|
+
if (finishReason) {
|
|
688
|
+
yield { content: "", done: true };
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
} catch (error) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (buffer.trim()) {
|
|
698
|
+
yield { content: "", done: true };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async function* parseAnthropicStream(stream) {
|
|
702
|
+
let buffer = "";
|
|
703
|
+
let currentEvent = "";
|
|
704
|
+
for await (const chunk of stream) {
|
|
705
|
+
buffer += chunk;
|
|
706
|
+
const events = buffer.split("\n\n");
|
|
707
|
+
buffer = events.pop() || "";
|
|
708
|
+
for (const event of events) {
|
|
709
|
+
const lines = event.split("\n");
|
|
710
|
+
for (const line of lines) {
|
|
711
|
+
const trimmed = line.trim();
|
|
712
|
+
if (trimmed.startsWith("event: ")) {
|
|
713
|
+
currentEvent = trimmed.substring(7);
|
|
714
|
+
} else if (trimmed.startsWith("data: ")) {
|
|
715
|
+
const jsonStr = trimmed.substring(6);
|
|
716
|
+
try {
|
|
717
|
+
const data = JSON.parse(jsonStr);
|
|
718
|
+
if (currentEvent === "content_block_delta") {
|
|
719
|
+
const content = data.delta?.text || "";
|
|
720
|
+
if (content) {
|
|
721
|
+
yield { content, done: false };
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (currentEvent === "content_block_start") {
|
|
725
|
+
const content = data.content_block?.text || "";
|
|
726
|
+
if (content) {
|
|
727
|
+
yield { content, done: false };
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (currentEvent === "message_delta") {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (currentEvent === "message_stop") {
|
|
734
|
+
yield { content: "", done: true };
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (currentEvent === "error") {
|
|
738
|
+
throw new Error(data.error?.message || "Anthropic streaming error");
|
|
739
|
+
}
|
|
740
|
+
} catch (error) {
|
|
741
|
+
if (currentEvent === "error") {
|
|
742
|
+
throw error;
|
|
743
|
+
}
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (buffer.trim()) {
|
|
751
|
+
yield { content: "", done: true };
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/providers/DeepSeekProvider.ts
|
|
756
|
+
var DeepSeekProvider = class extends BaseLLMProvider {
|
|
757
|
+
apiClient;
|
|
758
|
+
encoder;
|
|
759
|
+
constructor(config) {
|
|
760
|
+
super(config);
|
|
761
|
+
const apiKey = config.apiKey || process.env.DEEPSEEK_API_KEY;
|
|
762
|
+
if (!apiKey) {
|
|
763
|
+
throw new ConfigurationError(
|
|
764
|
+
"DEEPSEEK_API_KEY not found in config or environment variables. Please set DEEPSEEK_API_KEY in your .env file or pass it via config."
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
const baseURL = config.baseURL || "https://api.deepseek.com";
|
|
768
|
+
this.apiClient = new APIClient({
|
|
769
|
+
baseURL,
|
|
770
|
+
headers: {
|
|
771
|
+
Authorization: `Bearer ${apiKey}`,
|
|
772
|
+
"Content-Type": "application/json"
|
|
773
|
+
},
|
|
774
|
+
timeout: 6e4
|
|
775
|
+
});
|
|
776
|
+
this.encoder = encoding_for_model("gpt-4");
|
|
777
|
+
}
|
|
778
|
+
async chat(messages, tools) {
|
|
779
|
+
return this.withRetry(async () => {
|
|
780
|
+
const requestBody = {
|
|
781
|
+
model: this.config.model,
|
|
782
|
+
messages: this.formatMessages(messages),
|
|
783
|
+
tools: tools ? toOpenAITools(tools) : void 0,
|
|
784
|
+
temperature: this.config.temperature,
|
|
785
|
+
max_tokens: this.config.maxTokens
|
|
786
|
+
};
|
|
787
|
+
const response = await this.apiClient.post(
|
|
788
|
+
"/chat/completions",
|
|
789
|
+
requestBody
|
|
790
|
+
);
|
|
791
|
+
return this.parseResponse(response);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
async *streamChat(messages, tools) {
|
|
795
|
+
const requestBody = {
|
|
796
|
+
model: this.config.model,
|
|
797
|
+
messages: this.formatMessages(messages),
|
|
798
|
+
tools: tools ? toOpenAITools(tools) : void 0,
|
|
799
|
+
temperature: this.config.temperature,
|
|
800
|
+
max_tokens: this.config.maxTokens,
|
|
801
|
+
stream: true
|
|
802
|
+
};
|
|
803
|
+
const stream = this.apiClient.stream("/chat/completions", requestBody);
|
|
804
|
+
for await (const chunk of parseOpenAIStream(stream)) {
|
|
805
|
+
yield chunk;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
countTokens(text) {
|
|
809
|
+
return this.encoder.encode(text).length;
|
|
810
|
+
}
|
|
811
|
+
calculateCost(inputTokens, outputTokens) {
|
|
812
|
+
const pricing = getStaticPricing("deepseek", this.config.model);
|
|
813
|
+
if (!pricing) {
|
|
814
|
+
return 0;
|
|
815
|
+
}
|
|
816
|
+
return inputTokens / 1e6 * pricing.inputPerMillionTokens + outputTokens / 1e6 * pricing.outputPerMillionTokens;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Format messages for OpenAI-compatible API
|
|
820
|
+
*/
|
|
821
|
+
formatMessages(messages) {
|
|
822
|
+
return messages.map((msg) => ({
|
|
823
|
+
role: msg.role,
|
|
824
|
+
content: msg.content,
|
|
825
|
+
name: msg.name
|
|
826
|
+
}));
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Parse DeepSeek API response
|
|
830
|
+
*/
|
|
831
|
+
parseResponse(response) {
|
|
832
|
+
const choice = response.choices[0];
|
|
833
|
+
const message = choice?.message;
|
|
834
|
+
const usage = response.usage;
|
|
835
|
+
if (!choice || !message) {
|
|
836
|
+
throw new Error("Invalid response from DeepSeek API: missing choice or message");
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
content: message.content || "",
|
|
840
|
+
toolCalls: parseOpenAIToolCalls(response),
|
|
841
|
+
finishReason: mapOpenAIFinishReason(choice.finish_reason),
|
|
842
|
+
usage: {
|
|
843
|
+
inputTokens: usage.prompt_tokens,
|
|
844
|
+
outputTokens: usage.completion_tokens,
|
|
845
|
+
totalTokens: usage.total_tokens
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/providers/AnthropicProvider.ts
|
|
852
|
+
import { encoding_for_model as encoding_for_model2 } from "tiktoken";
|
|
853
|
+
var AnthropicProvider = class extends BaseLLMProvider {
|
|
854
|
+
apiClient;
|
|
855
|
+
encoder;
|
|
856
|
+
constructor(config) {
|
|
857
|
+
super(config);
|
|
858
|
+
const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
|
|
859
|
+
if (!apiKey) {
|
|
860
|
+
throw new ConfigurationError(
|
|
861
|
+
"ANTHROPIC_API_KEY not found in config or environment variables. Please set ANTHROPIC_API_KEY in your .env file or pass it via config."
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
const baseURL = config.baseURL || "https://api.anthropic.com";
|
|
865
|
+
this.apiClient = new APIClient({
|
|
866
|
+
baseURL,
|
|
867
|
+
headers: {
|
|
868
|
+
"x-api-key": apiKey,
|
|
869
|
+
"anthropic-version": "2023-06-01",
|
|
870
|
+
"Content-Type": "application/json"
|
|
871
|
+
},
|
|
872
|
+
timeout: 6e4
|
|
873
|
+
});
|
|
874
|
+
this.encoder = encoding_for_model2("gpt-4");
|
|
875
|
+
}
|
|
876
|
+
async chat(messages, tools) {
|
|
877
|
+
return this.withRetry(async () => {
|
|
878
|
+
const { system, messages: userMessages } = this.formatAnthropicMessages(messages);
|
|
879
|
+
const requestBody = {
|
|
880
|
+
model: this.config.model,
|
|
881
|
+
messages: userMessages,
|
|
882
|
+
max_tokens: this.config.maxTokens,
|
|
883
|
+
temperature: this.config.temperature
|
|
884
|
+
};
|
|
885
|
+
if (system) {
|
|
886
|
+
requestBody.system = system;
|
|
887
|
+
}
|
|
888
|
+
if (tools && tools.length > 0) {
|
|
889
|
+
requestBody.tools = toAnthropicTools(tools);
|
|
890
|
+
}
|
|
891
|
+
const response = await this.apiClient.post(
|
|
892
|
+
"/v1/messages",
|
|
893
|
+
requestBody
|
|
894
|
+
);
|
|
895
|
+
return this.parseResponse(response);
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
async *streamChat(messages, tools) {
|
|
899
|
+
const { system, messages: userMessages } = this.formatAnthropicMessages(messages);
|
|
900
|
+
const requestBody = {
|
|
901
|
+
model: this.config.model,
|
|
902
|
+
messages: userMessages,
|
|
903
|
+
max_tokens: this.config.maxTokens,
|
|
904
|
+
temperature: this.config.temperature,
|
|
905
|
+
stream: true
|
|
906
|
+
};
|
|
907
|
+
if (system) {
|
|
908
|
+
requestBody.system = system;
|
|
909
|
+
}
|
|
910
|
+
if (tools && tools.length > 0) {
|
|
911
|
+
requestBody.tools = toAnthropicTools(tools);
|
|
912
|
+
}
|
|
913
|
+
const stream = this.apiClient.stream("/v1/messages", requestBody);
|
|
914
|
+
for await (const chunk of parseAnthropicStream(stream)) {
|
|
915
|
+
yield chunk;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
countTokens(text) {
|
|
919
|
+
return this.encoder.encode(text).length;
|
|
920
|
+
}
|
|
921
|
+
calculateCost(inputTokens, outputTokens) {
|
|
922
|
+
const pricing = getStaticPricing("anthropic", this.config.model);
|
|
923
|
+
if (!pricing) {
|
|
924
|
+
return 0;
|
|
925
|
+
}
|
|
926
|
+
return inputTokens / 1e6 * pricing.inputPerMillionTokens + outputTokens / 1e6 * pricing.outputPerMillionTokens;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Format messages for Anthropic API
|
|
930
|
+
* Extracts system messages into separate parameter
|
|
931
|
+
*/
|
|
932
|
+
formatAnthropicMessages(messages) {
|
|
933
|
+
const systemMessages = messages.filter((m) => m.role === "system");
|
|
934
|
+
const userMessages = messages.filter((m) => m.role !== "system");
|
|
935
|
+
const system = systemMessages.length > 0 ? systemMessages.map((m) => m.content).join("\n\n") : void 0;
|
|
936
|
+
return {
|
|
937
|
+
system,
|
|
938
|
+
messages: userMessages.map((msg) => ({
|
|
939
|
+
role: msg.role === "assistant" ? "assistant" : "user",
|
|
940
|
+
content: msg.content
|
|
941
|
+
}))
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Parse Anthropic API response
|
|
946
|
+
*/
|
|
947
|
+
parseResponse(response) {
|
|
948
|
+
const content = response.content || [];
|
|
949
|
+
const usage = response.usage;
|
|
950
|
+
const textContent = content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
951
|
+
return {
|
|
952
|
+
content: textContent,
|
|
953
|
+
toolCalls: parseAnthropicToolCalls(response),
|
|
954
|
+
finishReason: mapAnthropicFinishReason(response.stop_reason),
|
|
955
|
+
usage: {
|
|
956
|
+
inputTokens: usage.input_tokens,
|
|
957
|
+
outputTokens: usage.output_tokens,
|
|
958
|
+
totalTokens: usage.input_tokens + usage.output_tokens
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// src/providers/ProviderFactory.ts
|
|
965
|
+
var ProviderFactory = class {
|
|
966
|
+
static create(config) {
|
|
967
|
+
switch (config.provider.toLowerCase()) {
|
|
968
|
+
case "deepseek":
|
|
969
|
+
return new DeepSeekProvider(config);
|
|
970
|
+
case "anthropic":
|
|
971
|
+
return new AnthropicProvider(config);
|
|
972
|
+
case "openai":
|
|
973
|
+
throw new Error(
|
|
974
|
+
"OpenAI provider coming soon - see roadmap Phase 3. In the meantime, you can use DeepSeek which is OpenAI-compatible."
|
|
975
|
+
);
|
|
976
|
+
case "google":
|
|
977
|
+
case "gemini":
|
|
978
|
+
throw new Error("Google/Gemini provider coming soon - see roadmap Phase 3.");
|
|
979
|
+
case "qwen":
|
|
980
|
+
throw new Error(
|
|
981
|
+
"Qwen provider coming soon - see roadmap Phase 3. In the meantime, you can use DeepSeek which is similar."
|
|
982
|
+
);
|
|
983
|
+
case "ollama":
|
|
984
|
+
throw new Error("Ollama provider coming soon - see roadmap Phase 3.");
|
|
985
|
+
default:
|
|
986
|
+
throw new Error(`Unsupported provider: ${config.provider}`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// src/config/schemas.ts
|
|
992
|
+
import { z } from "zod";
|
|
993
|
+
var ThemeSchema = z.enum([
|
|
994
|
+
"mimir",
|
|
995
|
+
"dark",
|
|
996
|
+
"light",
|
|
997
|
+
"dark-colorblind",
|
|
998
|
+
"light-colorblind",
|
|
999
|
+
"dark-ansi",
|
|
1000
|
+
"light-ansi"
|
|
1001
|
+
]);
|
|
1002
|
+
var UIConfigSchema = z.object({
|
|
1003
|
+
theme: ThemeSchema.default("mimir"),
|
|
1004
|
+
syntaxHighlighting: z.boolean().default(true),
|
|
1005
|
+
showLineNumbers: z.boolean().default(true),
|
|
1006
|
+
compactMode: z.boolean().default(false),
|
|
1007
|
+
// Autocomplete behavior
|
|
1008
|
+
autocompleteAutoShow: z.boolean().default(true),
|
|
1009
|
+
// Automatically show autocomplete when suggestions available
|
|
1010
|
+
autocompleteExecuteOnSelect: z.boolean().default(true)
|
|
1011
|
+
// Execute command immediately if no more parameters needed
|
|
1012
|
+
});
|
|
1013
|
+
var LLMConfigSchema = z.object({
|
|
1014
|
+
provider: z.enum(["deepseek", "anthropic", "openai", "google", "gemini", "qwen", "ollama"]),
|
|
1015
|
+
model: z.string(),
|
|
1016
|
+
apiKey: z.string().optional(),
|
|
1017
|
+
baseURL: z.string().optional(),
|
|
1018
|
+
temperature: z.number().min(0).max(2).default(0.7),
|
|
1019
|
+
maxTokens: z.number().default(4096)
|
|
1020
|
+
});
|
|
1021
|
+
var PermissionsConfigSchema = z.object({
|
|
1022
|
+
autoAccept: z.boolean().default(false),
|
|
1023
|
+
acceptRiskLevel: z.enum(["low", "medium", "high", "critical"]).default("medium"),
|
|
1024
|
+
alwaysAcceptCommands: z.array(z.string()).nullable().default([]).transform((val) => val ?? [])
|
|
1025
|
+
});
|
|
1026
|
+
var shortcutSchema = z.union([z.string(), z.array(z.string()).min(1)]).transform((val) => Array.isArray(val) ? val : [val]);
|
|
1027
|
+
var KeyBindingsConfigSchema = z.object({
|
|
1028
|
+
// Core actions - Ctrl+C and Escape share the same 'interrupt' logic
|
|
1029
|
+
interrupt: shortcutSchema.default(["Ctrl+C", "Escape"]),
|
|
1030
|
+
accept: shortcutSchema.default(["Enter"]),
|
|
1031
|
+
// Mode and navigation
|
|
1032
|
+
modeSwitch: shortcutSchema.default(["Shift+Tab"]),
|
|
1033
|
+
editCommand: shortcutSchema.default(["Ctrl+E"]),
|
|
1034
|
+
// Autocomplete/tooltips
|
|
1035
|
+
showTooltip: shortcutSchema.default(["Ctrl+Space", "Tab"]),
|
|
1036
|
+
navigateUp: shortcutSchema.default(["ArrowUp"]),
|
|
1037
|
+
navigateDown: shortcutSchema.default(["ArrowDown"]),
|
|
1038
|
+
// Utility
|
|
1039
|
+
help: shortcutSchema.default(["?"]),
|
|
1040
|
+
clearScreen: shortcutSchema.default(["Ctrl+L"]),
|
|
1041
|
+
undo: shortcutSchema.default(["Ctrl+Z"]),
|
|
1042
|
+
redo: shortcutSchema.default(["Ctrl+Y"]),
|
|
1043
|
+
// Auto-converted to Cmd+Shift+Z on Mac
|
|
1044
|
+
// Legacy/deprecated - kept for backwards compatibility
|
|
1045
|
+
reject: shortcutSchema.default([]).optional()
|
|
1046
|
+
});
|
|
1047
|
+
var DockerConfigSchema = z.object({
|
|
1048
|
+
enabled: z.boolean().default(true),
|
|
1049
|
+
baseImage: z.string().default("alpine:latest"),
|
|
1050
|
+
cpuLimit: z.number().optional(),
|
|
1051
|
+
memoryLimit: z.string().optional()
|
|
1052
|
+
});
|
|
1053
|
+
var MonitoringConfigSchema = z.object({
|
|
1054
|
+
metricsRetentionDays: z.number().min(1).max(365).default(90),
|
|
1055
|
+
enableHealthChecks: z.boolean().default(true),
|
|
1056
|
+
healthCheckIntervalSeconds: z.number().min(10).max(3600).default(300),
|
|
1057
|
+
slowOperationThresholdMs: z.number().min(100).default(5e3),
|
|
1058
|
+
batchWriteIntervalSeconds: z.number().min(1).max(60).default(10)
|
|
1059
|
+
});
|
|
1060
|
+
var BudgetConfigSchema = z.object({
|
|
1061
|
+
enabled: z.boolean().default(false),
|
|
1062
|
+
dailyLimit: z.number().min(0).optional(),
|
|
1063
|
+
// USD
|
|
1064
|
+
weeklyLimit: z.number().min(0).optional(),
|
|
1065
|
+
monthlyLimit: z.number().min(0).optional(),
|
|
1066
|
+
warningThreshold: z.number().min(0).max(1).default(0.8)
|
|
1067
|
+
// 80%
|
|
1068
|
+
});
|
|
1069
|
+
var RateLimitConfigSchema = z.object({
|
|
1070
|
+
enabled: z.boolean().default(true),
|
|
1071
|
+
commandsPerMinute: z.number().min(1).default(60),
|
|
1072
|
+
toolExecutionsPerMinute: z.number().min(1).default(30),
|
|
1073
|
+
llmCallsPerMinute: z.number().min(1).default(20),
|
|
1074
|
+
maxFileSizeMB: z.number().min(1).default(100)
|
|
1075
|
+
});
|
|
1076
|
+
var ConfigSchema = z.object({
|
|
1077
|
+
llm: LLMConfigSchema,
|
|
1078
|
+
permissions: PermissionsConfigSchema,
|
|
1079
|
+
keyBindings: KeyBindingsConfigSchema,
|
|
1080
|
+
docker: DockerConfigSchema,
|
|
1081
|
+
ui: UIConfigSchema,
|
|
1082
|
+
monitoring: MonitoringConfigSchema,
|
|
1083
|
+
budget: BudgetConfigSchema,
|
|
1084
|
+
rateLimit: RateLimitConfigSchema
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
// src/utils/logger.ts
|
|
1088
|
+
import winston from "winston";
|
|
1089
|
+
import DailyRotateFile from "winston-daily-rotate-file";
|
|
1090
|
+
import fs from "fs";
|
|
1091
|
+
import path from "path";
|
|
1092
|
+
var Logger = class {
|
|
1093
|
+
logger;
|
|
1094
|
+
fileLoggingEnabled = false;
|
|
1095
|
+
consoleTransport;
|
|
1096
|
+
constructor(logDir = ".mimir/logs") {
|
|
1097
|
+
const absoluteLogDir = path.resolve(process.cwd(), logDir);
|
|
1098
|
+
try {
|
|
1099
|
+
if (!fs.existsSync(absoluteLogDir)) {
|
|
1100
|
+
fs.mkdirSync(absoluteLogDir, { recursive: true });
|
|
1101
|
+
}
|
|
1102
|
+
this.fileLoggingEnabled = true;
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
console.warn(
|
|
1105
|
+
`[Logger] Warning: Failed to create log directory at ${absoluteLogDir}. File logging disabled. Error: ${error}`
|
|
1106
|
+
);
|
|
1107
|
+
this.fileLoggingEnabled = false;
|
|
1108
|
+
}
|
|
1109
|
+
this.consoleTransport = new winston.transports.Console({
|
|
1110
|
+
format: winston.format.combine(winston.format.colorize(), winston.format.simple())
|
|
1111
|
+
});
|
|
1112
|
+
const transports = [this.consoleTransport];
|
|
1113
|
+
if (this.fileLoggingEnabled) {
|
|
1114
|
+
transports.push(
|
|
1115
|
+
// Error logs with daily rotation
|
|
1116
|
+
new DailyRotateFile({
|
|
1117
|
+
dirname: absoluteLogDir,
|
|
1118
|
+
filename: "%DATE%-error.log",
|
|
1119
|
+
datePattern: "YYYYMMDD",
|
|
1120
|
+
level: "error",
|
|
1121
|
+
maxSize: "10m",
|
|
1122
|
+
// Rotate when file reaches 10MB
|
|
1123
|
+
maxFiles: "30d",
|
|
1124
|
+
// Keep logs for 30 days
|
|
1125
|
+
zippedArchive: true
|
|
1126
|
+
// Compress old logs
|
|
1127
|
+
}),
|
|
1128
|
+
// Combined logs with daily rotation
|
|
1129
|
+
new DailyRotateFile({
|
|
1130
|
+
dirname: absoluteLogDir,
|
|
1131
|
+
filename: "%DATE%.log",
|
|
1132
|
+
datePattern: "YYYYMMDD",
|
|
1133
|
+
maxSize: "10m",
|
|
1134
|
+
// Rotate when file reaches 10MB
|
|
1135
|
+
maxFiles: "30d",
|
|
1136
|
+
// Keep logs for 30 days
|
|
1137
|
+
zippedArchive: true
|
|
1138
|
+
// Compress old logs
|
|
1139
|
+
})
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
this.logger = winston.createLogger({
|
|
1143
|
+
level: process.env.LOG_LEVEL || "info",
|
|
1144
|
+
format: winston.format.combine(
|
|
1145
|
+
winston.format.timestamp(),
|
|
1146
|
+
winston.format.errors({ stack: true }),
|
|
1147
|
+
winston.format.json()
|
|
1148
|
+
),
|
|
1149
|
+
transports
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
error(message, meta) {
|
|
1153
|
+
this.logger.error(message, meta);
|
|
1154
|
+
}
|
|
1155
|
+
warn(message, meta) {
|
|
1156
|
+
this.logger.warn(message, meta);
|
|
1157
|
+
}
|
|
1158
|
+
info(message, meta) {
|
|
1159
|
+
this.logger.info(message, meta);
|
|
1160
|
+
}
|
|
1161
|
+
debug(message, meta) {
|
|
1162
|
+
this.logger.debug(message, meta);
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Disable console logging (useful when Ink UI is active)
|
|
1166
|
+
*/
|
|
1167
|
+
disableConsole() {
|
|
1168
|
+
this.logger.remove(this.consoleTransport);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Enable console logging
|
|
1172
|
+
*/
|
|
1173
|
+
enableConsole() {
|
|
1174
|
+
if (!this.logger.transports.includes(this.consoleTransport)) {
|
|
1175
|
+
this.logger.add(this.consoleTransport);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
var logger = new Logger();
|
|
1180
|
+
|
|
1181
|
+
// src/config/AllowlistLoader.ts
|
|
1182
|
+
import yaml from "yaml";
|
|
1183
|
+
import path2 from "path";
|
|
1184
|
+
import { z as z2 } from "zod";
|
|
1185
|
+
var AllowlistSchema = z2.object({
|
|
1186
|
+
// Command patterns that are always allowed
|
|
1187
|
+
commands: z2.array(z2.string()).default([]),
|
|
1188
|
+
// File patterns that can be modified without confirmation
|
|
1189
|
+
files: z2.array(z2.string()).default([]),
|
|
1190
|
+
// Network destinations that are allowed
|
|
1191
|
+
urls: z2.array(z2.string()).default([]),
|
|
1192
|
+
// Environment variables that can be accessed
|
|
1193
|
+
envVars: z2.array(z2.string()).default([]),
|
|
1194
|
+
// Specific bash commands that are safe
|
|
1195
|
+
bashCommands: z2.array(z2.string()).default([])
|
|
1196
|
+
});
|
|
1197
|
+
var AllowlistLoader = class {
|
|
1198
|
+
constructor(fs3) {
|
|
1199
|
+
this.fs = fs3;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Load allowlist from project .mimir/allowlist.yml
|
|
1203
|
+
*/
|
|
1204
|
+
async loadProjectAllowlist(projectRoot) {
|
|
1205
|
+
const allowlistPath = path2.join(projectRoot, ".mimir", "allowlist.yml");
|
|
1206
|
+
return await this.loadAllowlistFile(allowlistPath, "project");
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Load allowlist from global ~/.mimir/allowlist.yml
|
|
1210
|
+
*/
|
|
1211
|
+
async loadGlobalAllowlist() {
|
|
1212
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "~";
|
|
1213
|
+
const allowlistPath = path2.join(homeDir, ".mimir", "allowlist.yml");
|
|
1214
|
+
return await this.loadAllowlistFile(allowlistPath, "global");
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Load and parse allowlist file
|
|
1218
|
+
*/
|
|
1219
|
+
async loadAllowlistFile(filePath, scope) {
|
|
1220
|
+
try {
|
|
1221
|
+
if (!await this.fs.exists(filePath)) {
|
|
1222
|
+
logger.debug(`No ${scope} allowlist found`, { path: filePath });
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
const content = await this.fs.readFile(filePath);
|
|
1226
|
+
const parsed = yaml.parse(content);
|
|
1227
|
+
const allowlist = AllowlistSchema.parse(parsed);
|
|
1228
|
+
logger.info(`Loaded ${scope} allowlist`, {
|
|
1229
|
+
path: filePath,
|
|
1230
|
+
commands: allowlist.commands.length,
|
|
1231
|
+
files: allowlist.files.length,
|
|
1232
|
+
urls: allowlist.urls.length
|
|
1233
|
+
});
|
|
1234
|
+
return allowlist;
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
logger.error(`Failed to load ${scope} allowlist`, {
|
|
1237
|
+
path: filePath,
|
|
1238
|
+
error
|
|
1239
|
+
});
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Merge multiple allowlists (global + project)
|
|
1245
|
+
* Project allowlist takes precedence
|
|
1246
|
+
*/
|
|
1247
|
+
merge(global, project) {
|
|
1248
|
+
const merged = {
|
|
1249
|
+
commands: [],
|
|
1250
|
+
files: [],
|
|
1251
|
+
urls: [],
|
|
1252
|
+
envVars: [],
|
|
1253
|
+
bashCommands: []
|
|
1254
|
+
};
|
|
1255
|
+
if (global) {
|
|
1256
|
+
merged.commands.push(...global.commands);
|
|
1257
|
+
merged.files.push(...global.files);
|
|
1258
|
+
merged.urls.push(...global.urls);
|
|
1259
|
+
merged.envVars.push(...global.envVars);
|
|
1260
|
+
merged.bashCommands.push(...global.bashCommands);
|
|
1261
|
+
}
|
|
1262
|
+
if (project) {
|
|
1263
|
+
merged.commands = [.../* @__PURE__ */ new Set([...merged.commands, ...project.commands])];
|
|
1264
|
+
merged.files = [.../* @__PURE__ */ new Set([...merged.files, ...project.files])];
|
|
1265
|
+
merged.urls = [.../* @__PURE__ */ new Set([...merged.urls, ...project.urls])];
|
|
1266
|
+
merged.envVars = [.../* @__PURE__ */ new Set([...merged.envVars, ...project.envVars])];
|
|
1267
|
+
merged.bashCommands = [.../* @__PURE__ */ new Set([...merged.bashCommands, ...project.bashCommands])];
|
|
1268
|
+
}
|
|
1269
|
+
return merged;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Create example allowlist file
|
|
1273
|
+
*/
|
|
1274
|
+
async createExample(filePath, scope) {
|
|
1275
|
+
const exampleContent = scope === "global" ? this.getGlobalExample() : this.getProjectExample();
|
|
1276
|
+
try {
|
|
1277
|
+
const dir = path2.dirname(filePath);
|
|
1278
|
+
if (!await this.fs.exists(dir)) {
|
|
1279
|
+
await this.fs.mkdir(dir, { recursive: true });
|
|
1280
|
+
}
|
|
1281
|
+
await this.fs.writeFile(filePath, exampleContent);
|
|
1282
|
+
logger.info(`Created example ${scope} allowlist`, { path: filePath });
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
logger.error(`Failed to create example allowlist`, {
|
|
1285
|
+
path: filePath,
|
|
1286
|
+
error
|
|
1287
|
+
});
|
|
1288
|
+
throw error;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Get example global allowlist
|
|
1293
|
+
*/
|
|
1294
|
+
getGlobalExample() {
|
|
1295
|
+
return `# Global Allowlist
|
|
1296
|
+
# Commands, files, and operations that are safe across all projects
|
|
1297
|
+
|
|
1298
|
+
# Commands that don't require permission prompt
|
|
1299
|
+
commands:
|
|
1300
|
+
- '/status' # Git status
|
|
1301
|
+
- '/diff' # Git diff
|
|
1302
|
+
- '/help' # Show help
|
|
1303
|
+
- '/version' # Show version
|
|
1304
|
+
- '/doctor' # System diagnostics
|
|
1305
|
+
|
|
1306
|
+
# Safe bash commands
|
|
1307
|
+
bashCommands:
|
|
1308
|
+
- 'git status'
|
|
1309
|
+
- 'git diff'
|
|
1310
|
+
- 'git log'
|
|
1311
|
+
- 'ls'
|
|
1312
|
+
- 'pwd'
|
|
1313
|
+
- 'echo *'
|
|
1314
|
+
- 'cat *.md'
|
|
1315
|
+
|
|
1316
|
+
# Files that can be modified without confirmation (use globs)
|
|
1317
|
+
files:
|
|
1318
|
+
- '**/*.md' # Documentation files
|
|
1319
|
+
- '**/*.txt' # Text files
|
|
1320
|
+
- '**/README.*' # README files
|
|
1321
|
+
|
|
1322
|
+
# URLs that can be accessed without confirmation
|
|
1323
|
+
urls:
|
|
1324
|
+
- 'https://api.github.com/**'
|
|
1325
|
+
- 'https://registry.npmjs.org/**'
|
|
1326
|
+
|
|
1327
|
+
# Environment variables that can be read
|
|
1328
|
+
envVars:
|
|
1329
|
+
- 'NODE_ENV'
|
|
1330
|
+
- 'PATH'
|
|
1331
|
+
- 'USER'
|
|
1332
|
+
- 'HOME'
|
|
1333
|
+
`;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Get example project allowlist
|
|
1337
|
+
*/
|
|
1338
|
+
getProjectExample() {
|
|
1339
|
+
return `# Project Allowlist
|
|
1340
|
+
# Team-shared permissions for this project
|
|
1341
|
+
# Commit this file to version control for consistent team experience
|
|
1342
|
+
|
|
1343
|
+
# Custom slash commands that are safe to run
|
|
1344
|
+
commands:
|
|
1345
|
+
- '/test' # Run test suite
|
|
1346
|
+
- '/lint' # Run linter
|
|
1347
|
+
- '/build' # Build project
|
|
1348
|
+
- '/test-coverage' # Run tests with coverage
|
|
1349
|
+
|
|
1350
|
+
# Safe bash commands specific to this project
|
|
1351
|
+
bashCommands:
|
|
1352
|
+
- 'yarn test'
|
|
1353
|
+
- 'yarn lint'
|
|
1354
|
+
- 'yarn build'
|
|
1355
|
+
- 'npm run test'
|
|
1356
|
+
- 'npm run lint'
|
|
1357
|
+
|
|
1358
|
+
# Files that can be auto-formatted or modified
|
|
1359
|
+
files:
|
|
1360
|
+
- 'src/**/*.ts' # TypeScript source files
|
|
1361
|
+
- 'src/**/*.tsx' # React TypeScript files
|
|
1362
|
+
- 'tests/**/*.ts' # Test files
|
|
1363
|
+
- '*.json' # JSON config files
|
|
1364
|
+
- '.prettierrc.*' # Prettier config
|
|
1365
|
+
- '.eslintrc.*' # ESLint config
|
|
1366
|
+
|
|
1367
|
+
# API endpoints used by this project
|
|
1368
|
+
urls:
|
|
1369
|
+
- 'https://api.example.com/**'
|
|
1370
|
+
- 'https://staging.example.com/**'
|
|
1371
|
+
|
|
1372
|
+
# Environment variables specific to this project
|
|
1373
|
+
envVars:
|
|
1374
|
+
- 'API_KEY'
|
|
1375
|
+
- 'DATABASE_URL'
|
|
1376
|
+
- 'REDIS_URL'
|
|
1377
|
+
`;
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
// src/config/ConfigLoader.ts
|
|
1382
|
+
import yaml2 from "yaml";
|
|
1383
|
+
import path3 from "path";
|
|
1384
|
+
import os from "os";
|
|
1385
|
+
import dotenv from "dotenv";
|
|
1386
|
+
var ConfigLoader = class {
|
|
1387
|
+
constructor(fs3) {
|
|
1388
|
+
this.fs = fs3;
|
|
1389
|
+
this.allowlistLoader = new AllowlistLoader(fs3);
|
|
1390
|
+
}
|
|
1391
|
+
allowlistLoader;
|
|
1392
|
+
/**
|
|
1393
|
+
* Load configuration with full hierarchy:
|
|
1394
|
+
* 1. Default config
|
|
1395
|
+
* 2. Global (~/.mimir/config.yml)
|
|
1396
|
+
* 3. Project (.mimir/config.yml)
|
|
1397
|
+
* 4. Environment variables (.env)
|
|
1398
|
+
* 5. CLI flags
|
|
1399
|
+
*
|
|
1400
|
+
* Also loads allowlist from:
|
|
1401
|
+
* 1. Global (~/.mimir/allowlist.yml)
|
|
1402
|
+
* 2. Project (.mimir/allowlist.yml)
|
|
1403
|
+
*/
|
|
1404
|
+
async load(options = {}) {
|
|
1405
|
+
let config = this.getDefaults();
|
|
1406
|
+
const globalConfig = await this.loadGlobalConfig();
|
|
1407
|
+
if (globalConfig) {
|
|
1408
|
+
config = this.merge(config, globalConfig);
|
|
1409
|
+
}
|
|
1410
|
+
if (options.projectRoot) {
|
|
1411
|
+
const projectConfig = await this.loadProjectConfig(options.projectRoot);
|
|
1412
|
+
if (projectConfig) {
|
|
1413
|
+
config = this.merge(config, projectConfig);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
const envConfig = this.loadEnvConfig(options.projectRoot);
|
|
1417
|
+
if (envConfig) {
|
|
1418
|
+
config = this.merge(config, envConfig);
|
|
1419
|
+
}
|
|
1420
|
+
if (options.cliFlags) {
|
|
1421
|
+
config = this.merge(config, options.cliFlags);
|
|
1422
|
+
}
|
|
1423
|
+
const validatedConfig = ConfigSchema.parse(config);
|
|
1424
|
+
const globalAllowlist = await this.allowlistLoader.loadGlobalAllowlist();
|
|
1425
|
+
const projectAllowlist = options.projectRoot ? await this.allowlistLoader.loadProjectAllowlist(options.projectRoot) : null;
|
|
1426
|
+
const mergedAllowlist = this.allowlistLoader.merge(globalAllowlist, projectAllowlist);
|
|
1427
|
+
if (mergedAllowlist.commands.length > 0) {
|
|
1428
|
+
validatedConfig.permissions.alwaysAcceptCommands = [
|
|
1429
|
+
.../* @__PURE__ */ new Set([
|
|
1430
|
+
...validatedConfig.permissions.alwaysAcceptCommands,
|
|
1431
|
+
...mergedAllowlist.commands
|
|
1432
|
+
])
|
|
1433
|
+
];
|
|
1434
|
+
}
|
|
1435
|
+
return {
|
|
1436
|
+
config: validatedConfig,
|
|
1437
|
+
allowlist: mergedAllowlist
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
getDefaults() {
|
|
1441
|
+
const isWindows = process.platform === "win32";
|
|
1442
|
+
return {
|
|
1443
|
+
llm: {
|
|
1444
|
+
provider: "deepseek",
|
|
1445
|
+
model: "deepseek-chat",
|
|
1446
|
+
temperature: 0.7,
|
|
1447
|
+
maxTokens: 4096
|
|
1448
|
+
},
|
|
1449
|
+
permissions: {
|
|
1450
|
+
autoAccept: false,
|
|
1451
|
+
acceptRiskLevel: "medium",
|
|
1452
|
+
alwaysAcceptCommands: []
|
|
1453
|
+
},
|
|
1454
|
+
keyBindings: {
|
|
1455
|
+
interrupt: ["Ctrl+C", "Escape"],
|
|
1456
|
+
accept: ["Enter"],
|
|
1457
|
+
modeSwitch: ["Shift+Tab"],
|
|
1458
|
+
editCommand: ["Ctrl+E"],
|
|
1459
|
+
// Windows: Ctrl+Space is intercepted by terminal - use Tab only
|
|
1460
|
+
// macOS/Linux: Both work
|
|
1461
|
+
showTooltip: isWindows ? ["Tab"] : ["Ctrl+Space", "Tab"],
|
|
1462
|
+
navigateUp: ["ArrowUp"],
|
|
1463
|
+
navigateDown: ["ArrowDown"],
|
|
1464
|
+
help: ["?"],
|
|
1465
|
+
clearScreen: ["Ctrl+L"],
|
|
1466
|
+
undo: ["Ctrl+Z"],
|
|
1467
|
+
redo: ["Ctrl+Y"]
|
|
1468
|
+
},
|
|
1469
|
+
docker: {
|
|
1470
|
+
enabled: true,
|
|
1471
|
+
baseImage: "alpine:latest"
|
|
1472
|
+
},
|
|
1473
|
+
ui: {
|
|
1474
|
+
theme: "mimir",
|
|
1475
|
+
syntaxHighlighting: true,
|
|
1476
|
+
showLineNumbers: true,
|
|
1477
|
+
compactMode: false,
|
|
1478
|
+
autocompleteAutoShow: true,
|
|
1479
|
+
autocompleteExecuteOnSelect: true
|
|
1480
|
+
},
|
|
1481
|
+
monitoring: {
|
|
1482
|
+
metricsRetentionDays: 90,
|
|
1483
|
+
enableHealthChecks: true,
|
|
1484
|
+
healthCheckIntervalSeconds: 300,
|
|
1485
|
+
slowOperationThresholdMs: 5e3,
|
|
1486
|
+
batchWriteIntervalSeconds: 10
|
|
1487
|
+
},
|
|
1488
|
+
budget: {
|
|
1489
|
+
enabled: false,
|
|
1490
|
+
warningThreshold: 0.8
|
|
1491
|
+
},
|
|
1492
|
+
rateLimit: {
|
|
1493
|
+
enabled: true,
|
|
1494
|
+
commandsPerMinute: 60,
|
|
1495
|
+
toolExecutionsPerMinute: 30,
|
|
1496
|
+
llmCallsPerMinute: 20,
|
|
1497
|
+
maxFileSizeMB: 100
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
async loadGlobalConfig() {
|
|
1502
|
+
try {
|
|
1503
|
+
const configPath = path3.join(os.homedir(), ".mimir", "config.yml");
|
|
1504
|
+
if (!await this.fs.exists(configPath)) {
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
const content = await this.fs.readFile(configPath);
|
|
1508
|
+
return yaml2.parse(content);
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
logger.warn("Failed to load global config", { error });
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
async loadProjectConfig(projectRoot) {
|
|
1515
|
+
try {
|
|
1516
|
+
const configPath = path3.join(projectRoot, ".mimir", "config.yml");
|
|
1517
|
+
if (!await this.fs.exists(configPath)) {
|
|
1518
|
+
return null;
|
|
1519
|
+
}
|
|
1520
|
+
const content = await this.fs.readFile(configPath);
|
|
1521
|
+
return yaml2.parse(content);
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
logger.warn("Failed to load project config", { error });
|
|
1524
|
+
return null;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
loadEnvConfig(projectRoot) {
|
|
1528
|
+
try {
|
|
1529
|
+
const envPath = projectRoot ? path3.join(projectRoot, ".env") : ".env";
|
|
1530
|
+
dotenv.config({ path: envPath });
|
|
1531
|
+
const envConfig = {};
|
|
1532
|
+
if (process.env.DEEPSEEK_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY) {
|
|
1533
|
+
const provider = process.env.LLM_PROVIDER?.toUpperCase();
|
|
1534
|
+
const apiKey = provider ? process.env[`${provider}_API_KEY`] : void 0;
|
|
1535
|
+
const baseURL = provider ? process.env[`${provider}_BASE_URL`] : void 0;
|
|
1536
|
+
if (apiKey || baseURL) {
|
|
1537
|
+
envConfig.llm = {
|
|
1538
|
+
...apiKey && { apiKey },
|
|
1539
|
+
...baseURL && { baseURL }
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
if (process.env.DOCKER_ENABLED) {
|
|
1544
|
+
envConfig.docker = {
|
|
1545
|
+
enabled: process.env.DOCKER_ENABLED === "true"
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
if (process.env.MIMIR_THEME) {
|
|
1549
|
+
envConfig.ui = {
|
|
1550
|
+
theme: process.env.MIMIR_THEME
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
return Object.keys(envConfig).length > 0 ? envConfig : null;
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
logger.warn("Failed to load .env config", { error });
|
|
1556
|
+
return null;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
merge(base, override) {
|
|
1560
|
+
return {
|
|
1561
|
+
llm: { ...base.llm, ...override.llm },
|
|
1562
|
+
permissions: { ...base.permissions, ...override.permissions },
|
|
1563
|
+
keyBindings: { ...base.keyBindings, ...override.keyBindings },
|
|
1564
|
+
docker: { ...base.docker, ...override.docker },
|
|
1565
|
+
ui: { ...base.ui, ...override.ui },
|
|
1566
|
+
monitoring: { ...base.monitoring, ...override.monitoring },
|
|
1567
|
+
budget: { ...base.budget, ...override.budget },
|
|
1568
|
+
rateLimit: { ...base.rateLimit, ...override.rateLimit }
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
async save(config, scope, projectRoot) {
|
|
1572
|
+
const configPath = scope === "global" ? path3.join(os.homedir(), ".mimir", "config.yml") : path3.join(projectRoot || process.cwd(), ".mimir", "config.yml");
|
|
1573
|
+
const configDir = path3.dirname(configPath);
|
|
1574
|
+
if (!await this.fs.exists(configDir)) {
|
|
1575
|
+
await this.fs.mkdir(configDir, { recursive: true });
|
|
1576
|
+
}
|
|
1577
|
+
const yamlContent = yaml2.stringify(config);
|
|
1578
|
+
await this.fs.writeFile(configPath, yamlContent);
|
|
1579
|
+
logger.info(`Config saved to ${configPath}`);
|
|
1580
|
+
}
|
|
1581
|
+
validate(config) {
|
|
1582
|
+
return ConfigSchema.parse(config);
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
// src/platform/FileSystemAdapter.ts
|
|
1587
|
+
import fs2 from "fs/promises";
|
|
1588
|
+
import { globby } from "globby";
|
|
1589
|
+
var FileSystemAdapter = class {
|
|
1590
|
+
async readFile(path4, encoding = "utf-8") {
|
|
1591
|
+
return fs2.readFile(path4, encoding);
|
|
1592
|
+
}
|
|
1593
|
+
async writeFile(path4, content, encoding = "utf-8") {
|
|
1594
|
+
await fs2.writeFile(path4, content, encoding);
|
|
1595
|
+
}
|
|
1596
|
+
async exists(path4) {
|
|
1597
|
+
try {
|
|
1598
|
+
await fs2.access(path4);
|
|
1599
|
+
return true;
|
|
1600
|
+
} catch {
|
|
1601
|
+
return false;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
async mkdir(path4, options) {
|
|
1605
|
+
await fs2.mkdir(path4, options);
|
|
1606
|
+
}
|
|
1607
|
+
async readdir(path4) {
|
|
1608
|
+
return fs2.readdir(path4);
|
|
1609
|
+
}
|
|
1610
|
+
async stat(path4) {
|
|
1611
|
+
const stats = await fs2.stat(path4);
|
|
1612
|
+
return {
|
|
1613
|
+
isFile: () => stats.isFile(),
|
|
1614
|
+
isDirectory: () => stats.isDirectory(),
|
|
1615
|
+
size: stats.size,
|
|
1616
|
+
mtime: stats.mtime
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
async unlink(path4) {
|
|
1620
|
+
await fs2.unlink(path4);
|
|
1621
|
+
}
|
|
1622
|
+
async rmdir(path4, options) {
|
|
1623
|
+
await fs2.rmdir(path4, options);
|
|
1624
|
+
}
|
|
1625
|
+
async copyFile(src, dest) {
|
|
1626
|
+
await fs2.copyFile(src, dest);
|
|
1627
|
+
}
|
|
1628
|
+
async glob(pattern, options) {
|
|
1629
|
+
return globby(pattern, {
|
|
1630
|
+
cwd: options?.cwd,
|
|
1631
|
+
ignore: options?.ignore
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
export {
|
|
1636
|
+
Agent,
|
|
1637
|
+
BaseLLMProvider,
|
|
1638
|
+
ConfigLoader,
|
|
1639
|
+
ConfigurationError,
|
|
1640
|
+
DockerError,
|
|
1641
|
+
FileSystemAdapter,
|
|
1642
|
+
Logger,
|
|
1643
|
+
MimirError,
|
|
1644
|
+
NetworkError,
|
|
1645
|
+
PermissionDeniedError,
|
|
1646
|
+
PermissionManager,
|
|
1647
|
+
ProviderError,
|
|
1648
|
+
ProviderFactory,
|
|
1649
|
+
RateLimitError,
|
|
1650
|
+
ToolExecutionError,
|
|
1651
|
+
ToolRegistry,
|
|
1652
|
+
createErr,
|
|
1653
|
+
createOk,
|
|
1654
|
+
logger
|
|
1655
|
+
};
|
|
1656
|
+
//# sourceMappingURL=index.js.map
|