@qxbyte/muse 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 +21 -0
- package/README.md +320 -0
- package/dist/cli.js +3015 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +1405 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1405 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/llm/providers/openai-compatible.ts
|
|
4
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
5
|
+
import { streamText, jsonSchema, tool } from "ai";
|
|
6
|
+
|
|
7
|
+
// src/log/index.ts
|
|
8
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
9
|
+
import { dirname } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
var LEVELS = {
|
|
13
|
+
trace: 10,
|
|
14
|
+
debug: 20,
|
|
15
|
+
info: 30,
|
|
16
|
+
warn: 40,
|
|
17
|
+
error: 50
|
|
18
|
+
};
|
|
19
|
+
var Logger = class {
|
|
20
|
+
level = "info";
|
|
21
|
+
logPath;
|
|
22
|
+
fileEnabled = true;
|
|
23
|
+
constructor() {
|
|
24
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
25
|
+
this.logPath = join(homedir(), ".muse", "logs", `${date}.jsonl`);
|
|
26
|
+
try {
|
|
27
|
+
mkdirSync(dirname(this.logPath), { recursive: true });
|
|
28
|
+
} catch {
|
|
29
|
+
this.fileEnabled = false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
setLevel(level) {
|
|
33
|
+
this.level = level;
|
|
34
|
+
}
|
|
35
|
+
write(level, msg, extra) {
|
|
36
|
+
if (LEVELS[level] < LEVELS[this.level]) return;
|
|
37
|
+
const entry = {
|
|
38
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
39
|
+
level,
|
|
40
|
+
msg,
|
|
41
|
+
...extra
|
|
42
|
+
};
|
|
43
|
+
if (this.fileEnabled) {
|
|
44
|
+
try {
|
|
45
|
+
appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (level === "warn" || level === "error") {
|
|
50
|
+
const prefix = level === "error" ? "[error]" : "[warn]";
|
|
51
|
+
process.stderr.write(`${prefix} ${msg}
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
trace(msg, extra) {
|
|
56
|
+
this.write("trace", msg, extra);
|
|
57
|
+
}
|
|
58
|
+
debug(msg, extra) {
|
|
59
|
+
this.write("debug", msg, extra);
|
|
60
|
+
}
|
|
61
|
+
info(msg, extra) {
|
|
62
|
+
this.write("info", msg, extra);
|
|
63
|
+
}
|
|
64
|
+
warn(msg, extra) {
|
|
65
|
+
this.write("warn", msg, extra);
|
|
66
|
+
}
|
|
67
|
+
error(msg, extra) {
|
|
68
|
+
this.write("error", msg, extra);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var log = new Logger();
|
|
72
|
+
function redactApiKey(key) {
|
|
73
|
+
if (!key) return "<unset>";
|
|
74
|
+
if (key.length <= 12) return "***";
|
|
75
|
+
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/llm/providers/openai-compatible.ts
|
|
79
|
+
var DEFAULT_CAPABILITIES = {
|
|
80
|
+
toolCalling: true,
|
|
81
|
+
parallelToolCalls: true,
|
|
82
|
+
vision: false,
|
|
83
|
+
jsonMode: true,
|
|
84
|
+
maxContextWindow: 32e3
|
|
85
|
+
};
|
|
86
|
+
var OpenAICompatibleClient = class {
|
|
87
|
+
providerName;
|
|
88
|
+
model;
|
|
89
|
+
capabilities;
|
|
90
|
+
modelProvider;
|
|
91
|
+
constructor(opts) {
|
|
92
|
+
this.providerName = opts.providerName;
|
|
93
|
+
this.model = opts.model;
|
|
94
|
+
this.capabilities = { ...DEFAULT_CAPABILITIES, ...opts.capabilities };
|
|
95
|
+
const provider = createOpenAICompatible({
|
|
96
|
+
name: opts.providerName,
|
|
97
|
+
baseURL: opts.baseUrl,
|
|
98
|
+
apiKey: opts.apiKey
|
|
99
|
+
});
|
|
100
|
+
this.modelProvider = provider(opts.model);
|
|
101
|
+
log.debug("LLM provider initialized", {
|
|
102
|
+
provider: opts.providerName,
|
|
103
|
+
model: opts.model,
|
|
104
|
+
baseUrl: opts.baseUrl,
|
|
105
|
+
apiKey: redactApiKey(opts.apiKey)
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async *stream(opts) {
|
|
109
|
+
const { messages, tools, systemPrompt, temperature, maxTokens, abortSignal } = opts;
|
|
110
|
+
const aiMessages = convertMessages(messages, systemPrompt);
|
|
111
|
+
const aiTools = tools ? convertTools(tools) : void 0;
|
|
112
|
+
try {
|
|
113
|
+
const result = streamText({
|
|
114
|
+
model: this.modelProvider,
|
|
115
|
+
messages: aiMessages,
|
|
116
|
+
tools: aiTools,
|
|
117
|
+
temperature,
|
|
118
|
+
maxTokens,
|
|
119
|
+
abortSignal
|
|
120
|
+
});
|
|
121
|
+
const seenToolCalls = /* @__PURE__ */ new Set();
|
|
122
|
+
for await (const part of result.fullStream) {
|
|
123
|
+
switch (part.type) {
|
|
124
|
+
case "text-delta":
|
|
125
|
+
yield { type: "text", delta: part.textDelta };
|
|
126
|
+
break;
|
|
127
|
+
case "tool-call":
|
|
128
|
+
if (!seenToolCalls.has(part.toolCallId)) {
|
|
129
|
+
seenToolCalls.add(part.toolCallId);
|
|
130
|
+
yield { type: "tool_call_start", id: part.toolCallId, name: part.toolName };
|
|
131
|
+
}
|
|
132
|
+
yield {
|
|
133
|
+
type: "tool_call_complete",
|
|
134
|
+
id: part.toolCallId,
|
|
135
|
+
name: part.toolName,
|
|
136
|
+
args: part.args
|
|
137
|
+
};
|
|
138
|
+
break;
|
|
139
|
+
case "finish":
|
|
140
|
+
yield {
|
|
141
|
+
type: "finish",
|
|
142
|
+
reason: mapFinishReason(part.finishReason),
|
|
143
|
+
usage: part.usage ? {
|
|
144
|
+
inputTokens: part.usage.promptTokens ?? 0,
|
|
145
|
+
outputTokens: part.usage.completionTokens ?? 0,
|
|
146
|
+
totalTokens: part.usage.totalTokens ?? 0
|
|
147
|
+
} : void 0
|
|
148
|
+
};
|
|
149
|
+
break;
|
|
150
|
+
case "error":
|
|
151
|
+
yield { type: "error", error: part.error instanceof Error ? part.error : new Error(String(part.error)) };
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
function convertMessages(messages, systemPrompt) {
|
|
163
|
+
const result = [];
|
|
164
|
+
if (systemPrompt) {
|
|
165
|
+
result.push({ role: "system", content: systemPrompt });
|
|
166
|
+
}
|
|
167
|
+
for (const msg of messages) {
|
|
168
|
+
switch (msg.role) {
|
|
169
|
+
case "system":
|
|
170
|
+
result.push({ role: "system", content: msg.content });
|
|
171
|
+
break;
|
|
172
|
+
case "user":
|
|
173
|
+
if (typeof msg.content === "string") {
|
|
174
|
+
result.push({ role: "user", content: msg.content });
|
|
175
|
+
} else {
|
|
176
|
+
const text = msg.content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
|
|
177
|
+
result.push({ role: "user", content: text });
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
case "assistant":
|
|
181
|
+
result.push({ role: "assistant", content: convertAssistantContent(msg) });
|
|
182
|
+
break;
|
|
183
|
+
case "tool":
|
|
184
|
+
result.push({
|
|
185
|
+
role: "tool",
|
|
186
|
+
content: [
|
|
187
|
+
{
|
|
188
|
+
type: "tool-result",
|
|
189
|
+
toolCallId: msg.toolUseId,
|
|
190
|
+
toolName: "_tool",
|
|
191
|
+
result: msg.content,
|
|
192
|
+
isError: msg.isError ?? false
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
});
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
function convertAssistantContent(msg) {
|
|
202
|
+
const parts = [];
|
|
203
|
+
for (const part of msg.content) {
|
|
204
|
+
if (part.type === "text") {
|
|
205
|
+
parts.push({ type: "text", text: part.text });
|
|
206
|
+
} else if (part.type === "tool_use") {
|
|
207
|
+
parts.push({
|
|
208
|
+
type: "tool-call",
|
|
209
|
+
toolCallId: part.id,
|
|
210
|
+
toolName: part.name,
|
|
211
|
+
args: part.args
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (parts.length === 0) return "";
|
|
216
|
+
return parts;
|
|
217
|
+
}
|
|
218
|
+
function convertTools(tools) {
|
|
219
|
+
const result = {};
|
|
220
|
+
for (const t of tools) {
|
|
221
|
+
result[t.name] = tool({
|
|
222
|
+
description: t.description,
|
|
223
|
+
parameters: jsonSchema(t.parameters)
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
function mapFinishReason(reason) {
|
|
229
|
+
switch (reason) {
|
|
230
|
+
case "stop":
|
|
231
|
+
case "stop-sequence":
|
|
232
|
+
return "stop";
|
|
233
|
+
case "tool-calls":
|
|
234
|
+
case "tool_calls":
|
|
235
|
+
return "tool_calls";
|
|
236
|
+
case "length":
|
|
237
|
+
return "length";
|
|
238
|
+
case "content-filter":
|
|
239
|
+
case "content_filter":
|
|
240
|
+
return "content_filter";
|
|
241
|
+
case "error":
|
|
242
|
+
return "error";
|
|
243
|
+
default:
|
|
244
|
+
return "unknown";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
var PRESETS = {
|
|
248
|
+
openai: {
|
|
249
|
+
baseUrl: "https://api.openai.com/v1",
|
|
250
|
+
defaultModel: "gpt-4o-mini"
|
|
251
|
+
},
|
|
252
|
+
deepseek: {
|
|
253
|
+
baseUrl: "https://api.deepseek.com/v1",
|
|
254
|
+
defaultModel: "deepseek-chat",
|
|
255
|
+
capabilities: { maxContextWindow: 128e3 }
|
|
256
|
+
},
|
|
257
|
+
qwen: {
|
|
258
|
+
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
259
|
+
defaultModel: "qwen-plus",
|
|
260
|
+
capabilities: { maxContextWindow: 128e3 }
|
|
261
|
+
},
|
|
262
|
+
moonshot: {
|
|
263
|
+
baseUrl: "https://api.moonshot.cn/v1",
|
|
264
|
+
defaultModel: "moonshot-v1-32k",
|
|
265
|
+
capabilities: { maxContextWindow: 32e3 }
|
|
266
|
+
},
|
|
267
|
+
zhipu: {
|
|
268
|
+
baseUrl: "https://open.bigmodel.cn/api/paas/v4",
|
|
269
|
+
defaultModel: "glm-4-flash",
|
|
270
|
+
capabilities: { maxContextWindow: 128e3 }
|
|
271
|
+
},
|
|
272
|
+
ollama: {
|
|
273
|
+
baseUrl: "http://localhost:11434/v1",
|
|
274
|
+
defaultModel: "llama3.1",
|
|
275
|
+
capabilities: { maxContextWindow: 8e3 }
|
|
276
|
+
},
|
|
277
|
+
openrouter: {
|
|
278
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
279
|
+
defaultModel: "openai/gpt-4o-mini"
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
function createPresetClient(providerName, config, model) {
|
|
283
|
+
const preset = PRESETS[providerName];
|
|
284
|
+
if (!preset) {
|
|
285
|
+
throw new Error(`Unknown provider preset: ${providerName}. Available: ${Object.keys(PRESETS).join(", ")}`);
|
|
286
|
+
}
|
|
287
|
+
return new OpenAICompatibleClient({
|
|
288
|
+
providerName,
|
|
289
|
+
baseUrl: config.baseUrl ?? preset.baseUrl,
|
|
290
|
+
apiKey: config.apiKey ?? "",
|
|
291
|
+
model: model ?? preset.defaultModel,
|
|
292
|
+
capabilities: preset.capabilities
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/types/index.ts
|
|
297
|
+
var MuseError = class extends Error {
|
|
298
|
+
constructor(message, code, cause) {
|
|
299
|
+
super(message);
|
|
300
|
+
this.code = code;
|
|
301
|
+
this.cause = cause;
|
|
302
|
+
this.name = "MuseError";
|
|
303
|
+
}
|
|
304
|
+
code;
|
|
305
|
+
cause;
|
|
306
|
+
};
|
|
307
|
+
var ToolError = class extends MuseError {
|
|
308
|
+
constructor(message, toolName, cause) {
|
|
309
|
+
super(message, "TOOL_ERROR", cause);
|
|
310
|
+
this.toolName = toolName;
|
|
311
|
+
this.name = "ToolError";
|
|
312
|
+
}
|
|
313
|
+
toolName;
|
|
314
|
+
};
|
|
315
|
+
var PermissionDeniedError = class extends MuseError {
|
|
316
|
+
constructor(toolName, reason) {
|
|
317
|
+
super(`Permission denied for ${toolName}: ${reason}`, "PERMISSION_DENIED");
|
|
318
|
+
this.toolName = toolName;
|
|
319
|
+
this.reason = reason;
|
|
320
|
+
this.name = "PermissionDeniedError";
|
|
321
|
+
}
|
|
322
|
+
toolName;
|
|
323
|
+
reason;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// src/llm/client.ts
|
|
327
|
+
function createLLMClient(opts) {
|
|
328
|
+
const { provider, model, providers } = opts;
|
|
329
|
+
const config = providers[provider];
|
|
330
|
+
if (!config) {
|
|
331
|
+
throw new MuseError(
|
|
332
|
+
`Provider "${provider}" is not configured. Add a "providers.${provider}" entry to your settings.json.`,
|
|
333
|
+
"PROVIDER_NOT_CONFIGURED"
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (PRESETS[provider]) {
|
|
337
|
+
if (!config.apiKey && provider !== "ollama") {
|
|
338
|
+
throw new MuseError(
|
|
339
|
+
`Provider "${provider}" requires apiKey. Set it in settings.json or via the corresponding env var.`,
|
|
340
|
+
"MISSING_API_KEY"
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return createPresetClient(provider, config, model);
|
|
344
|
+
}
|
|
345
|
+
if (config.baseUrl) {
|
|
346
|
+
return new OpenAICompatibleClient({
|
|
347
|
+
providerName: provider,
|
|
348
|
+
baseUrl: config.baseUrl,
|
|
349
|
+
apiKey: config.apiKey ?? "",
|
|
350
|
+
model
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
throw new MuseError(
|
|
354
|
+
`Unknown provider "${provider}". Either use a preset (${Object.keys(PRESETS).join(", ")}) or set "baseUrl" in providers.${provider}.`,
|
|
355
|
+
"UNKNOWN_PROVIDER"
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/tools/registry.ts
|
|
360
|
+
function zodToJsonSchema(schema) {
|
|
361
|
+
const def = schema._def;
|
|
362
|
+
if (def.typeName === "ZodObject") {
|
|
363
|
+
const shape = schema.shape;
|
|
364
|
+
const properties = {};
|
|
365
|
+
const required = [];
|
|
366
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
367
|
+
properties[key] = zodToJsonSchema(value);
|
|
368
|
+
if (!value.isOptional?.()) {
|
|
369
|
+
required.push(key);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
type: "object",
|
|
374
|
+
properties,
|
|
375
|
+
...required.length > 0 ? { required } : {},
|
|
376
|
+
additionalProperties: false
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (def.typeName === "ZodString") {
|
|
380
|
+
const d = def;
|
|
381
|
+
return { type: "string", ...d.description ? { description: d.description } : {} };
|
|
382
|
+
}
|
|
383
|
+
if (def.typeName === "ZodNumber") {
|
|
384
|
+
return { type: "number" };
|
|
385
|
+
}
|
|
386
|
+
if (def.typeName === "ZodBoolean") {
|
|
387
|
+
return { type: "boolean" };
|
|
388
|
+
}
|
|
389
|
+
if (def.typeName === "ZodArray") {
|
|
390
|
+
const inner = def.type;
|
|
391
|
+
return { type: "array", items: zodToJsonSchema(inner) };
|
|
392
|
+
}
|
|
393
|
+
if (def.typeName === "ZodOptional" || def.typeName === "ZodDefault") {
|
|
394
|
+
const inner = def.innerType;
|
|
395
|
+
return zodToJsonSchema(inner);
|
|
396
|
+
}
|
|
397
|
+
if (def.typeName === "ZodEnum") {
|
|
398
|
+
const values = def.values;
|
|
399
|
+
return { type: "string", enum: values };
|
|
400
|
+
}
|
|
401
|
+
if (def.typeName === "ZodUnion") {
|
|
402
|
+
const opts = def.options;
|
|
403
|
+
return { anyOf: opts.map(zodToJsonSchema) };
|
|
404
|
+
}
|
|
405
|
+
return {};
|
|
406
|
+
}
|
|
407
|
+
function getDescription(schema) {
|
|
408
|
+
return schema.description;
|
|
409
|
+
}
|
|
410
|
+
var ToolRegistry = class {
|
|
411
|
+
tools = /* @__PURE__ */ new Map();
|
|
412
|
+
register(tool2) {
|
|
413
|
+
if (this.tools.has(tool2.name)) {
|
|
414
|
+
throw new Error(`Tool "${tool2.name}" already registered.`);
|
|
415
|
+
}
|
|
416
|
+
this.tools.set(tool2.name, tool2);
|
|
417
|
+
}
|
|
418
|
+
registerAll(tools) {
|
|
419
|
+
for (const tool2 of tools) this.register(tool2);
|
|
420
|
+
}
|
|
421
|
+
get(name) {
|
|
422
|
+
return this.tools.get(name);
|
|
423
|
+
}
|
|
424
|
+
has(name) {
|
|
425
|
+
return this.tools.has(name);
|
|
426
|
+
}
|
|
427
|
+
list() {
|
|
428
|
+
return Array.from(this.tools.values());
|
|
429
|
+
}
|
|
430
|
+
/** 转为 LLM 可读的 tool definition 数组。可选 filter(如 plan 模式过滤只读工具)。 */
|
|
431
|
+
toLLMDefinitions(filter) {
|
|
432
|
+
let tools = this.list();
|
|
433
|
+
if (filter) tools = tools.filter(filter);
|
|
434
|
+
return tools.map((t) => {
|
|
435
|
+
const schema = zodToJsonSchema(t.parameters);
|
|
436
|
+
const desc = getDescription(t.parameters);
|
|
437
|
+
if (desc && typeof schema === "object" && schema !== null) {
|
|
438
|
+
schema.description = desc;
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
name: t.name,
|
|
442
|
+
description: t.description,
|
|
443
|
+
parameters: schema
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/** 调用工具:校验参数 → 执行。 */
|
|
448
|
+
async execute(name, rawArgs, ctx) {
|
|
449
|
+
const tool2 = this.tools.get(name);
|
|
450
|
+
if (!tool2) {
|
|
451
|
+
throw new ToolError(`Tool "${name}" not found.`, name);
|
|
452
|
+
}
|
|
453
|
+
const parseResult = tool2.parameters.safeParse(rawArgs);
|
|
454
|
+
if (!parseResult.success) {
|
|
455
|
+
return {
|
|
456
|
+
content: `Invalid arguments for ${name}: ${parseResult.error.message}`,
|
|
457
|
+
isError: true
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
return await tool2.execute(parseResult.data, ctx);
|
|
462
|
+
} catch (err) {
|
|
463
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
464
|
+
return { content: `Tool ${name} threw: ${msg}`, isError: true };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// src/tools/types.ts
|
|
470
|
+
function defineTool(def) {
|
|
471
|
+
return {
|
|
472
|
+
name: def.name,
|
|
473
|
+
description: def.description,
|
|
474
|
+
parameters: def.parameters,
|
|
475
|
+
permission: def.permission,
|
|
476
|
+
summarize: def.summarize,
|
|
477
|
+
execute: def.execute
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/tools/builtin/read.ts
|
|
482
|
+
import { readFile, stat } from "fs/promises";
|
|
483
|
+
import { resolve, isAbsolute } from "path";
|
|
484
|
+
import { z } from "zod";
|
|
485
|
+
var ReadArgs = z.object({
|
|
486
|
+
file_path: z.string().describe("Absolute or cwd-relative path to the file."),
|
|
487
|
+
offset: z.number().int().min(0).optional().describe("Line offset (0-based)."),
|
|
488
|
+
limit: z.number().int().positive().optional().describe("Max lines to read. Default 2000.")
|
|
489
|
+
});
|
|
490
|
+
var DEFAULT_LIMIT = 2e3;
|
|
491
|
+
var MAX_LINE_LENGTH = 2e3;
|
|
492
|
+
var ReadTool = defineTool({
|
|
493
|
+
name: "Read",
|
|
494
|
+
description: "Read a file from the local filesystem. Returns content with 1-indexed line numbers (cat -n format). Use offset/limit for large files.",
|
|
495
|
+
parameters: ReadArgs,
|
|
496
|
+
permission: "read",
|
|
497
|
+
summarize: (args) => `Read(${args.file_path}${args.offset != null ? `, offset=${args.offset}` : ""}${args.limit != null ? `, limit=${args.limit}` : ""})`,
|
|
498
|
+
async execute(args, ctx) {
|
|
499
|
+
const path = isAbsolute(args.file_path) ? args.file_path : resolve(ctx.cwd, args.file_path);
|
|
500
|
+
let info;
|
|
501
|
+
try {
|
|
502
|
+
info = await stat(path);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
throw new ToolError(`File not found: ${path}`, "Read", err);
|
|
505
|
+
}
|
|
506
|
+
if (!info.isFile()) {
|
|
507
|
+
throw new ToolError(`Not a regular file: ${path}`, "Read");
|
|
508
|
+
}
|
|
509
|
+
const content = await readFile(path, "utf-8");
|
|
510
|
+
const lines = content.split(/\r?\n/);
|
|
511
|
+
const offset = args.offset ?? 0;
|
|
512
|
+
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
513
|
+
const slice = lines.slice(offset, offset + limit);
|
|
514
|
+
const numbered = slice.map((line, i) => {
|
|
515
|
+
const lineNo = offset + i + 1;
|
|
516
|
+
const truncated = line.length > MAX_LINE_LENGTH ? line.slice(0, MAX_LINE_LENGTH) + "... [truncated]" : line;
|
|
517
|
+
return `${String(lineNo).padStart(5, " ")} ${truncated}`;
|
|
518
|
+
});
|
|
519
|
+
let result = numbered.join("\n");
|
|
520
|
+
if (offset + limit < lines.length) {
|
|
521
|
+
result += `
|
|
522
|
+
... [${lines.length - offset - limit} more lines, use offset=${offset + limit} to read next]`;
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
content: result || "(empty file)",
|
|
526
|
+
summary: `Read ${slice.length} lines from ${args.file_path}`
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// src/tools/builtin/write.ts
|
|
532
|
+
import { writeFile, mkdir, stat as stat2 } from "fs/promises";
|
|
533
|
+
import { resolve as resolve2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
|
|
534
|
+
import { z as z2 } from "zod";
|
|
535
|
+
var WriteArgs = z2.object({
|
|
536
|
+
file_path: z2.string().describe("Absolute or cwd-relative path to the file."),
|
|
537
|
+
content: z2.string().describe("Full content of the file.")
|
|
538
|
+
});
|
|
539
|
+
var WriteTool = defineTool({
|
|
540
|
+
name: "Write",
|
|
541
|
+
description: "Write a complete file to the local filesystem. Creates parent directories if needed. Overwrites existing files \u2014 prefer Edit for partial updates.",
|
|
542
|
+
parameters: WriteArgs,
|
|
543
|
+
permission: "write",
|
|
544
|
+
summarize: (args) => `Write(${args.file_path}, ${args.content.length} chars)`,
|
|
545
|
+
async execute(args, ctx) {
|
|
546
|
+
const path = isAbsolute2(args.file_path) ? args.file_path : resolve2(ctx.cwd, args.file_path);
|
|
547
|
+
let existed = false;
|
|
548
|
+
try {
|
|
549
|
+
const info = await stat2(path);
|
|
550
|
+
existed = info.isFile();
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
await mkdir(dirname2(path), { recursive: true });
|
|
554
|
+
await writeFile(path, args.content, "utf-8");
|
|
555
|
+
return {
|
|
556
|
+
content: existed ? `Overwrote ${path} (${args.content.length} bytes).` : `Created ${path} (${args.content.length} bytes).`,
|
|
557
|
+
summary: `${existed ? "Overwrote" : "Created"} ${args.file_path}`
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// src/tools/builtin/edit.ts
|
|
563
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
564
|
+
import { resolve as resolve3, isAbsolute as isAbsolute3 } from "path";
|
|
565
|
+
import { z as z3 } from "zod";
|
|
566
|
+
var EditArgs = z3.object({
|
|
567
|
+
file_path: z3.string().describe("Absolute or cwd-relative path to the file."),
|
|
568
|
+
old_string: z3.string().describe("Exact substring to replace. Must be unique unless replace_all=true."),
|
|
569
|
+
new_string: z3.string().describe("Replacement string."),
|
|
570
|
+
replace_all: z3.boolean().optional().describe("Replace every occurrence. Default false.")
|
|
571
|
+
});
|
|
572
|
+
var EditTool = defineTool({
|
|
573
|
+
name: "Edit",
|
|
574
|
+
description: "Perform an exact string replacement in a file. Old string must be unique unless replace_all=true. Cheaper than Write when only a small part needs to change.",
|
|
575
|
+
parameters: EditArgs,
|
|
576
|
+
permission: "write",
|
|
577
|
+
summarize: (args) => `Edit(${args.file_path})`,
|
|
578
|
+
async execute(args, ctx) {
|
|
579
|
+
const path = isAbsolute3(args.file_path) ? args.file_path : resolve3(ctx.cwd, args.file_path);
|
|
580
|
+
let content;
|
|
581
|
+
try {
|
|
582
|
+
content = await readFile2(path, "utf-8");
|
|
583
|
+
} catch (err) {
|
|
584
|
+
throw new ToolError(`Cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`, "Edit", err);
|
|
585
|
+
}
|
|
586
|
+
if (args.old_string === args.new_string) {
|
|
587
|
+
return { content: "old_string is identical to new_string; nothing to do.", isError: true };
|
|
588
|
+
}
|
|
589
|
+
const occurrences = countOccurrences(content, args.old_string);
|
|
590
|
+
if (occurrences === 0) {
|
|
591
|
+
return {
|
|
592
|
+
content: `old_string not found in ${args.file_path}. Did you read the file first? Check whitespace and indentation.`,
|
|
593
|
+
isError: true
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
if (occurrences > 1 && !args.replace_all) {
|
|
597
|
+
return {
|
|
598
|
+
content: `old_string occurs ${occurrences} times in ${args.file_path}. Either expand context to make it unique, or set replace_all=true.`,
|
|
599
|
+
isError: true
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const newContent = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
|
|
603
|
+
await writeFile2(path, newContent, "utf-8");
|
|
604
|
+
return {
|
|
605
|
+
content: `Edited ${path}: replaced ${args.replace_all ? occurrences : 1} occurrence(s).`,
|
|
606
|
+
summary: `Edited ${args.file_path}`
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
function countOccurrences(haystack, needle) {
|
|
611
|
+
if (needle.length === 0) return 0;
|
|
612
|
+
let count = 0;
|
|
613
|
+
let pos = 0;
|
|
614
|
+
while ((pos = haystack.indexOf(needle, pos)) !== -1) {
|
|
615
|
+
count += 1;
|
|
616
|
+
pos += needle.length;
|
|
617
|
+
}
|
|
618
|
+
return count;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/tools/builtin/bash.ts
|
|
622
|
+
import { execa } from "execa";
|
|
623
|
+
import { z as z4 } from "zod";
|
|
624
|
+
var BashArgs = z4.object({
|
|
625
|
+
command: z4.string().describe("Shell command to run. Will be executed via sh -c."),
|
|
626
|
+
timeout: z4.number().int().positive().optional().describe("Timeout in milliseconds. Default 120000 (2 min). Max 600000."),
|
|
627
|
+
description: z4.string().optional().describe("Brief description (3-10 words) for the UI.")
|
|
628
|
+
});
|
|
629
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
630
|
+
var MAX_TIMEOUT_MS = 6e5;
|
|
631
|
+
var MAX_OUTPUT_BYTES = 1e5;
|
|
632
|
+
var HARD_DENY_PATTERNS = [
|
|
633
|
+
/\brm\s+-rf\s+\/(?:\s|$)/,
|
|
634
|
+
// rm -rf /
|
|
635
|
+
/\brm\s+-rf\s+~(?:\/|\s|$)/,
|
|
636
|
+
// rm -rf ~ or ~/...
|
|
637
|
+
/\brm\s+-rf\s+\*/,
|
|
638
|
+
// rm -rf *
|
|
639
|
+
/\bdd\s+.*of=\/dev\//,
|
|
640
|
+
// dd of=/dev/*
|
|
641
|
+
/\bmkfs\b/,
|
|
642
|
+
// mkfs
|
|
643
|
+
/:\(\)\s*\{\s*:\|:&\s*\}\s*;\s*:/,
|
|
644
|
+
// fork bomb
|
|
645
|
+
/\bsudo\b/,
|
|
646
|
+
// sudo(v0.1 简单粗暴禁掉)
|
|
647
|
+
/\bcurl\s+[^|]*\|\s*(?:sh|bash|zsh)/,
|
|
648
|
+
// curl ... | sh
|
|
649
|
+
/\bwget\s+[^|]*\|\s*(?:sh|bash|zsh)/
|
|
650
|
+
// wget ... | sh
|
|
651
|
+
];
|
|
652
|
+
function checkDangerous(command) {
|
|
653
|
+
for (const pattern of HARD_DENY_PATTERNS) {
|
|
654
|
+
if (pattern.test(command)) {
|
|
655
|
+
return { dangerous: true, reason: `matches pattern ${pattern}` };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return { dangerous: false };
|
|
659
|
+
}
|
|
660
|
+
var BashTool = defineTool({
|
|
661
|
+
name: "Bash",
|
|
662
|
+
description: "Execute a shell command via sh -c. Use for git, file system listings, builds, tests, etc. Avoid interactive commands (prefer non-interactive flags). For file edits use Edit/Write, not sed/echo.",
|
|
663
|
+
parameters: BashArgs,
|
|
664
|
+
permission: "execute",
|
|
665
|
+
summarize: (args) => args.description ?? `Bash: ${args.command.length > 60 ? args.command.slice(0, 60) + "..." : args.command}`,
|
|
666
|
+
async execute(args, ctx) {
|
|
667
|
+
const danger = checkDangerous(args.command);
|
|
668
|
+
if (danger.dangerous) {
|
|
669
|
+
return {
|
|
670
|
+
content: `Refused: command blocked by hard deny list (${danger.reason}). If you really need this, ask the user to run it manually.`,
|
|
671
|
+
isError: true
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
const timeout = Math.min(args.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
675
|
+
try {
|
|
676
|
+
const result = await execa(args.command, {
|
|
677
|
+
shell: "/bin/sh",
|
|
678
|
+
cwd: ctx.cwd,
|
|
679
|
+
timeout,
|
|
680
|
+
reject: false,
|
|
681
|
+
stripFinalNewline: false,
|
|
682
|
+
maxBuffer: MAX_OUTPUT_BYTES * 2,
|
|
683
|
+
cancelSignal: ctx.abortSignal
|
|
684
|
+
});
|
|
685
|
+
const stdout = truncate(result.stdout ?? "", MAX_OUTPUT_BYTES, "stdout");
|
|
686
|
+
const stderr = truncate(result.stderr ?? "", MAX_OUTPUT_BYTES, "stderr");
|
|
687
|
+
const parts = [];
|
|
688
|
+
if (stdout) parts.push(`<stdout>
|
|
689
|
+
${stdout}
|
|
690
|
+
</stdout>`);
|
|
691
|
+
if (stderr) parts.push(`<stderr>
|
|
692
|
+
${stderr}
|
|
693
|
+
</stderr>`);
|
|
694
|
+
if (result.timedOut) parts.push(`<timeout>Command exceeded ${timeout}ms.</timeout>`);
|
|
695
|
+
if (result.failed && !result.timedOut) parts.push(`<exit_code>${result.exitCode ?? "unknown"}</exit_code>`);
|
|
696
|
+
const body = parts.length > 0 ? parts.join("\n") : "(no output)";
|
|
697
|
+
return {
|
|
698
|
+
content: body,
|
|
699
|
+
isError: result.failed,
|
|
700
|
+
summary: result.failed ? `Bash exited ${result.exitCode ?? "?"}` : `Bash ok`
|
|
701
|
+
};
|
|
702
|
+
} catch (err) {
|
|
703
|
+
return {
|
|
704
|
+
content: `Bash threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
705
|
+
isError: true
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
function truncate(text, max, label) {
|
|
711
|
+
if (text.length <= max) return text;
|
|
712
|
+
return text.slice(0, max) + `
|
|
713
|
+
... [${label} truncated, original ${text.length} bytes]`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/tools/builtin/grep.ts
|
|
717
|
+
import { execa as execa2 } from "execa";
|
|
718
|
+
import { z as z5 } from "zod";
|
|
719
|
+
var GrepArgs = z5.object({
|
|
720
|
+
pattern: z5.string().describe("Regex pattern to search for."),
|
|
721
|
+
path: z5.string().optional().describe("File or directory to search in. Default: cwd."),
|
|
722
|
+
glob: z5.string().optional().describe('Glob filter, e.g. "*.ts" or "src/**/*.tsx".'),
|
|
723
|
+
output_mode: z5.enum(["content", "files_with_matches", "count"]).optional().describe("Default: files_with_matches."),
|
|
724
|
+
context: z5.number().int().min(0).max(50).optional().describe("Context lines around each match (use only with output_mode=content)."),
|
|
725
|
+
case_insensitive: z5.boolean().optional()
|
|
726
|
+
});
|
|
727
|
+
var rgChecked = false;
|
|
728
|
+
var rgAvailable = false;
|
|
729
|
+
async function checkRipgrep() {
|
|
730
|
+
if (rgChecked) return rgAvailable;
|
|
731
|
+
try {
|
|
732
|
+
await execa2("rg", ["--version"], { reject: false });
|
|
733
|
+
rgAvailable = true;
|
|
734
|
+
} catch {
|
|
735
|
+
rgAvailable = false;
|
|
736
|
+
}
|
|
737
|
+
rgChecked = true;
|
|
738
|
+
return rgAvailable;
|
|
739
|
+
}
|
|
740
|
+
var GrepTool = defineTool({
|
|
741
|
+
name: "Grep",
|
|
742
|
+
description: "Search file contents using regex. Prefer this over Bash(grep|find) \u2014 handles ignore files & is much faster on large trees.",
|
|
743
|
+
parameters: GrepArgs,
|
|
744
|
+
permission: "read",
|
|
745
|
+
summarize: (args) => `Grep(${args.pattern}${args.path ? `, ${args.path}` : ""})`,
|
|
746
|
+
async execute(args, ctx) {
|
|
747
|
+
const hasRg = await checkRipgrep();
|
|
748
|
+
const mode = args.output_mode ?? "files_with_matches";
|
|
749
|
+
if (hasRg) {
|
|
750
|
+
const cliArgs2 = [];
|
|
751
|
+
if (args.case_insensitive) cliArgs2.push("-i");
|
|
752
|
+
if (mode === "files_with_matches") cliArgs2.push("-l");
|
|
753
|
+
else if (mode === "count") cliArgs2.push("-c");
|
|
754
|
+
else if (args.context != null) cliArgs2.push("-C", String(args.context));
|
|
755
|
+
if (args.glob) cliArgs2.push("--glob", args.glob);
|
|
756
|
+
cliArgs2.push("--", args.pattern, args.path ?? ".");
|
|
757
|
+
const result2 = await execa2("rg", cliArgs2, { cwd: ctx.cwd, reject: false, cancelSignal: ctx.abortSignal });
|
|
758
|
+
const out2 = (result2.stdout ?? "").trim();
|
|
759
|
+
if (result2.exitCode === 0 || result2.exitCode === 1) {
|
|
760
|
+
return { content: out2 || "(no matches)", summary: `Grep ${args.pattern}` };
|
|
761
|
+
}
|
|
762
|
+
return { content: `rg failed: ${result2.stderr}`, isError: true };
|
|
763
|
+
}
|
|
764
|
+
const cliArgs = ["-r", "-n"];
|
|
765
|
+
if (args.case_insensitive) cliArgs.push("-i");
|
|
766
|
+
if (mode === "files_with_matches") cliArgs.push("-l");
|
|
767
|
+
else if (mode === "count") cliArgs.push("-c");
|
|
768
|
+
cliArgs.push("-E", args.pattern, args.path ?? ".");
|
|
769
|
+
const result = await execa2("grep", cliArgs, { cwd: ctx.cwd, reject: false, cancelSignal: ctx.abortSignal });
|
|
770
|
+
const out = (result.stdout ?? "").trim();
|
|
771
|
+
if (result.exitCode === 0 || result.exitCode === 1) {
|
|
772
|
+
return { content: out || "(no matches)", summary: `Grep ${args.pattern}` };
|
|
773
|
+
}
|
|
774
|
+
return { content: `grep failed: ${result.stderr}`, isError: true };
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// src/tools/builtin/glob.ts
|
|
779
|
+
import fg from "fast-glob";
|
|
780
|
+
import { z as z6 } from "zod";
|
|
781
|
+
var GlobArgs = z6.object({
|
|
782
|
+
pattern: z6.string().describe('Glob pattern, e.g. "src/**/*.ts" or "**/*.{md,json}".'),
|
|
783
|
+
path: z6.string().optional().describe("Base directory to search from. Default: cwd."),
|
|
784
|
+
limit: z6.number().int().positive().max(1e3).optional().describe("Max results. Default 100.")
|
|
785
|
+
});
|
|
786
|
+
var DEFAULT_LIMIT2 = 100;
|
|
787
|
+
var GlobTool = defineTool({
|
|
788
|
+
name: "Glob",
|
|
789
|
+
description: "Find files by glob pattern. Returns relative paths sorted by modification time (newest first).",
|
|
790
|
+
parameters: GlobArgs,
|
|
791
|
+
permission: "read",
|
|
792
|
+
summarize: (args) => `Glob(${args.pattern}${args.path ? `, ${args.path}` : ""})`,
|
|
793
|
+
async execute(args, ctx) {
|
|
794
|
+
const cwd = args.path ?? ctx.cwd;
|
|
795
|
+
const limit = args.limit ?? DEFAULT_LIMIT2;
|
|
796
|
+
const entries = await fg(args.pattern, {
|
|
797
|
+
cwd,
|
|
798
|
+
onlyFiles: true,
|
|
799
|
+
stats: true,
|
|
800
|
+
dot: false,
|
|
801
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/.muse/**"]
|
|
802
|
+
});
|
|
803
|
+
entries.sort((a, b) => {
|
|
804
|
+
const ta = a.stats?.mtime?.getTime() ?? 0;
|
|
805
|
+
const tb = b.stats?.mtime?.getTime() ?? 0;
|
|
806
|
+
return tb - ta;
|
|
807
|
+
});
|
|
808
|
+
const truncated = entries.length > limit;
|
|
809
|
+
const paths = entries.slice(0, limit).map((e) => e.path);
|
|
810
|
+
let result = paths.join("\n") || "(no matches)";
|
|
811
|
+
if (truncated) {
|
|
812
|
+
result += `
|
|
813
|
+
... [${entries.length - limit} more, increase limit to see]`;
|
|
814
|
+
}
|
|
815
|
+
return { content: result, summary: `Glob found ${entries.length} file(s)` };
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// src/tools/builtin/index.ts
|
|
820
|
+
var BUILTIN_TOOLS = [
|
|
821
|
+
ReadTool,
|
|
822
|
+
WriteTool,
|
|
823
|
+
EditTool,
|
|
824
|
+
BashTool,
|
|
825
|
+
GrepTool,
|
|
826
|
+
GlobTool
|
|
827
|
+
];
|
|
828
|
+
|
|
829
|
+
// src/permission/index.ts
|
|
830
|
+
var MODE_CYCLE = [
|
|
831
|
+
"default",
|
|
832
|
+
"acceptEdits",
|
|
833
|
+
"plan",
|
|
834
|
+
"bypassPermissions"
|
|
835
|
+
];
|
|
836
|
+
var PermissionGate = class {
|
|
837
|
+
rules;
|
|
838
|
+
mode = "default";
|
|
839
|
+
constructor(rules = {}) {
|
|
840
|
+
this.rules = {
|
|
841
|
+
allow: rules.allow ?? [],
|
|
842
|
+
ask: rules.ask ?? [],
|
|
843
|
+
deny: rules.deny ?? [],
|
|
844
|
+
defaultMode: rules.defaultMode ?? "ask"
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
setMode(mode) {
|
|
848
|
+
this.mode = mode;
|
|
849
|
+
}
|
|
850
|
+
getMode() {
|
|
851
|
+
return this.mode;
|
|
852
|
+
}
|
|
853
|
+
cycleMode() {
|
|
854
|
+
const i = MODE_CYCLE.indexOf(this.mode);
|
|
855
|
+
this.mode = MODE_CYCLE[(i + 1) % MODE_CYCLE.length];
|
|
856
|
+
return this.mode;
|
|
857
|
+
}
|
|
858
|
+
decide(input) {
|
|
859
|
+
if (this.matches(this.rules.deny, input)) return "deny";
|
|
860
|
+
switch (this.mode) {
|
|
861
|
+
case "bypassPermissions":
|
|
862
|
+
return "allow";
|
|
863
|
+
case "plan":
|
|
864
|
+
return input.permission === "read" ? "allow" : "deny";
|
|
865
|
+
case "acceptEdits":
|
|
866
|
+
if (input.toolName === "Edit" || input.toolName === "Write") return "allow";
|
|
867
|
+
return this.defaultDecide(input);
|
|
868
|
+
case "default":
|
|
869
|
+
default:
|
|
870
|
+
return this.defaultDecide(input);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
defaultDecide(input) {
|
|
874
|
+
if (this.matches(this.rules.allow, input)) return "allow";
|
|
875
|
+
if (this.matches(this.rules.ask, input)) return "ask";
|
|
876
|
+
switch (this.rules.defaultMode) {
|
|
877
|
+
case "strict":
|
|
878
|
+
return "ask";
|
|
879
|
+
case "relaxed":
|
|
880
|
+
return "allow";
|
|
881
|
+
case "ask":
|
|
882
|
+
default:
|
|
883
|
+
return "ask";
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
matches(patterns, input) {
|
|
887
|
+
for (const pattern of patterns) {
|
|
888
|
+
if (this.matchOne(pattern, input)) return true;
|
|
889
|
+
}
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
matchOne(pattern, input) {
|
|
893
|
+
if (!pattern.includes("(")) {
|
|
894
|
+
return pattern === input.toolName;
|
|
895
|
+
}
|
|
896
|
+
const m = pattern.match(/^([A-Za-z_][A-Za-z0-9_]*)\(([^)]*)\)$/);
|
|
897
|
+
if (!m) return false;
|
|
898
|
+
const [, toolName, sub] = m;
|
|
899
|
+
if (toolName !== input.toolName) return false;
|
|
900
|
+
if (input.toolName === "Bash" && typeof input.args === "object" && input.args !== null) {
|
|
901
|
+
const cmd = input.args.command ?? "";
|
|
902
|
+
if (sub.endsWith(":*")) {
|
|
903
|
+
const prefix = sub.slice(0, -2);
|
|
904
|
+
return cmd.startsWith(prefix);
|
|
905
|
+
}
|
|
906
|
+
return cmd === sub || cmd.startsWith(sub + " ");
|
|
907
|
+
}
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// src/session/jsonl.ts
|
|
913
|
+
import { appendFile, mkdir as mkdir2, readdir, readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
914
|
+
import { existsSync } from "fs";
|
|
915
|
+
import { homedir as homedir2 } from "os";
|
|
916
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
917
|
+
import { createHash, randomUUID } from "crypto";
|
|
918
|
+
function projectHash(cwd) {
|
|
919
|
+
return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
920
|
+
}
|
|
921
|
+
function sessionsDir(cwd) {
|
|
922
|
+
return join2(homedir2(), ".muse", "projects", projectHash(cwd), "sessions");
|
|
923
|
+
}
|
|
924
|
+
var Session = class _Session {
|
|
925
|
+
meta;
|
|
926
|
+
writeQueue = Promise.resolve();
|
|
927
|
+
constructor(meta) {
|
|
928
|
+
this.meta = meta;
|
|
929
|
+
}
|
|
930
|
+
static async create(cwd) {
|
|
931
|
+
const id = randomUUID();
|
|
932
|
+
const dir = sessionsDir(cwd);
|
|
933
|
+
await mkdir2(dir, { recursive: true });
|
|
934
|
+
const path = join2(dir, `${id}.jsonl`);
|
|
935
|
+
const meta = {
|
|
936
|
+
id,
|
|
937
|
+
cwd,
|
|
938
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
939
|
+
path
|
|
940
|
+
};
|
|
941
|
+
log.debug("session created", { id, path });
|
|
942
|
+
return new _Session(meta);
|
|
943
|
+
}
|
|
944
|
+
static async findLatest(cwd) {
|
|
945
|
+
const list = await _Session.listAll(cwd, 1);
|
|
946
|
+
return list[0];
|
|
947
|
+
}
|
|
948
|
+
static async resolve(cwd, idOrPrefix) {
|
|
949
|
+
const dir = sessionsDir(cwd);
|
|
950
|
+
if (!existsSync(dir)) return void 0;
|
|
951
|
+
const entries = await readdir(dir);
|
|
952
|
+
const matches = entries.filter((e) => e.endsWith(".jsonl") && e.startsWith(idOrPrefix));
|
|
953
|
+
if (matches.length === 0) return void 0;
|
|
954
|
+
if (matches.length > 1) {
|
|
955
|
+
throw new Error(`Ambiguous session id "${idOrPrefix}" matches ${matches.length} sessions; use more characters.`);
|
|
956
|
+
}
|
|
957
|
+
const top = matches[0];
|
|
958
|
+
const st = await stat3(join2(dir, top));
|
|
959
|
+
return {
|
|
960
|
+
id: top.replace(/\.jsonl$/, ""),
|
|
961
|
+
cwd,
|
|
962
|
+
createdAt: st.mtime.toISOString(),
|
|
963
|
+
path: join2(dir, top)
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* 按修改时间倒序列出当前 cwd 下的 session,附带 preview 与消息数。
|
|
968
|
+
* 读 preview 需要打开每个文件;调用方通过 limit 控制 IO 量。
|
|
969
|
+
*/
|
|
970
|
+
static async listAll(cwd, limit) {
|
|
971
|
+
const dir = sessionsDir(cwd);
|
|
972
|
+
if (!existsSync(dir)) return [];
|
|
973
|
+
const entries = await readdir(dir);
|
|
974
|
+
const files = entries.filter((e) => e.endsWith(".jsonl"));
|
|
975
|
+
if (files.length === 0) return [];
|
|
976
|
+
const stats = await Promise.all(
|
|
977
|
+
files.map(async (f) => {
|
|
978
|
+
const path = join2(dir, f);
|
|
979
|
+
const st = await stat3(path);
|
|
980
|
+
return { file: f, path, mtime: st.mtime };
|
|
981
|
+
})
|
|
982
|
+
);
|
|
983
|
+
stats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
984
|
+
const truncated = typeof limit === "number" ? stats.slice(0, limit) : stats;
|
|
985
|
+
const summaries = [];
|
|
986
|
+
for (const s of truncated) {
|
|
987
|
+
const meta = {
|
|
988
|
+
id: s.file.replace(/\.jsonl$/, ""),
|
|
989
|
+
cwd,
|
|
990
|
+
createdAt: s.mtime.toISOString(),
|
|
991
|
+
path: s.path
|
|
992
|
+
};
|
|
993
|
+
const summary = await readSummary(meta);
|
|
994
|
+
summaries.push(summary);
|
|
995
|
+
}
|
|
996
|
+
return summaries;
|
|
997
|
+
}
|
|
998
|
+
static async open(meta) {
|
|
999
|
+
const session = new _Session(meta);
|
|
1000
|
+
const events = await session.readAll();
|
|
1001
|
+
return { session, events };
|
|
1002
|
+
}
|
|
1003
|
+
/** 从已加载的 events 重建 messages 数组(按时序)。 */
|
|
1004
|
+
static messagesFromEvents(events) {
|
|
1005
|
+
const out = [];
|
|
1006
|
+
for (const ev of events) {
|
|
1007
|
+
if (ev.type === "message") out.push(ev.message);
|
|
1008
|
+
}
|
|
1009
|
+
return out;
|
|
1010
|
+
}
|
|
1011
|
+
async append(event) {
|
|
1012
|
+
const line = JSON.stringify(event) + "\n";
|
|
1013
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
1014
|
+
try {
|
|
1015
|
+
await mkdir2(dirname3(this.meta.path), { recursive: true });
|
|
1016
|
+
await appendFile(this.meta.path, line, "utf-8");
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
log.warn(`session append failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
return this.writeQueue;
|
|
1022
|
+
}
|
|
1023
|
+
async readAll() {
|
|
1024
|
+
if (!existsSync(this.meta.path)) return [];
|
|
1025
|
+
const raw = await readFile3(this.meta.path, "utf-8");
|
|
1026
|
+
const events = [];
|
|
1027
|
+
for (const line of raw.split("\n")) {
|
|
1028
|
+
if (!line.trim()) continue;
|
|
1029
|
+
try {
|
|
1030
|
+
events.push(JSON.parse(line));
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return events;
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
async function readSummary(meta) {
|
|
1038
|
+
let events = [];
|
|
1039
|
+
try {
|
|
1040
|
+
const raw = await readFile3(meta.path, "utf-8");
|
|
1041
|
+
for (const line of raw.split("\n")) {
|
|
1042
|
+
if (!line.trim()) continue;
|
|
1043
|
+
try {
|
|
1044
|
+
events.push(JSON.parse(line));
|
|
1045
|
+
} catch {
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
} catch {
|
|
1049
|
+
}
|
|
1050
|
+
const messages = events.filter((e) => e.type === "message");
|
|
1051
|
+
const firstUser = messages.find((e) => e.message.role === "user");
|
|
1052
|
+
let preview;
|
|
1053
|
+
if (firstUser) {
|
|
1054
|
+
const c = firstUser.message.content;
|
|
1055
|
+
const text = typeof c === "string" ? c : c.map((p) => p.type === "text" ? p.text : "").join(" ").trim();
|
|
1056
|
+
preview = text.slice(0, 60).replace(/\s+/g, " ");
|
|
1057
|
+
}
|
|
1058
|
+
return { ...meta, preview, messageCount: messages.length };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/loop/agent.ts
|
|
1062
|
+
var Agent = class {
|
|
1063
|
+
constructor(ctx) {
|
|
1064
|
+
this.ctx = ctx;
|
|
1065
|
+
}
|
|
1066
|
+
ctx;
|
|
1067
|
+
messages = [];
|
|
1068
|
+
getMessages() {
|
|
1069
|
+
return this.messages;
|
|
1070
|
+
}
|
|
1071
|
+
setMessages(msgs) {
|
|
1072
|
+
this.messages = msgs;
|
|
1073
|
+
}
|
|
1074
|
+
/** 执行一次完整的"用户输入 → 助手响应(含工具循环) → 等待下一轮输入"。 */
|
|
1075
|
+
async runTurn(userInput) {
|
|
1076
|
+
const userMessage = { role: "user", content: userInput };
|
|
1077
|
+
this.messages.push(userMessage);
|
|
1078
|
+
await this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: userMessage });
|
|
1079
|
+
while (true) {
|
|
1080
|
+
const mode = this.ctx.permissions.getMode();
|
|
1081
|
+
const tools = this.ctx.tools.toLLMDefinitions(
|
|
1082
|
+
mode === "plan" ? (t) => t.permission === "read" : void 0
|
|
1083
|
+
);
|
|
1084
|
+
const stream = this.ctx.llm.stream({
|
|
1085
|
+
messages: this.messages,
|
|
1086
|
+
tools,
|
|
1087
|
+
systemPrompt: this.ctx.systemPrompt,
|
|
1088
|
+
abortSignal: this.ctx.abortSignal
|
|
1089
|
+
});
|
|
1090
|
+
const assistantParts = [];
|
|
1091
|
+
const toolCallsToRun = [];
|
|
1092
|
+
let lastError;
|
|
1093
|
+
for await (const ev of stream) {
|
|
1094
|
+
this.handleEvent(ev, assistantParts, toolCallsToRun, (e) => {
|
|
1095
|
+
lastError = e;
|
|
1096
|
+
});
|
|
1097
|
+
if (lastError) break;
|
|
1098
|
+
}
|
|
1099
|
+
if (lastError) {
|
|
1100
|
+
this.ctx.events?.onError?.(lastError);
|
|
1101
|
+
log.error("agent stream error", { msg: lastError.message });
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const assistantMessage = { role: "assistant", content: assistantParts };
|
|
1105
|
+
this.messages.push(assistantMessage);
|
|
1106
|
+
await this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: assistantMessage });
|
|
1107
|
+
if (toolCallsToRun.length === 0) {
|
|
1108
|
+
this.ctx.events?.onTurnEnd?.();
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
for (const call of toolCallsToRun) {
|
|
1112
|
+
await this.runToolCall(call);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
handleEvent(ev, assistantParts, toolCallsToRun, onError) {
|
|
1117
|
+
switch (ev.type) {
|
|
1118
|
+
case "text":
|
|
1119
|
+
{
|
|
1120
|
+
const last = assistantParts[assistantParts.length - 1];
|
|
1121
|
+
if (last && last.type === "text") {
|
|
1122
|
+
last.text += ev.delta;
|
|
1123
|
+
} else {
|
|
1124
|
+
assistantParts.push({ type: "text", text: ev.delta });
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
this.ctx.events?.onText?.(ev.delta);
|
|
1128
|
+
break;
|
|
1129
|
+
case "tool_call_start":
|
|
1130
|
+
this.ctx.events?.onToolCallStart?.(ev.id, ev.name);
|
|
1131
|
+
break;
|
|
1132
|
+
case "tool_call_complete": {
|
|
1133
|
+
const callPart = { type: "tool_use", id: ev.id, name: ev.name, args: ev.args };
|
|
1134
|
+
assistantParts.push(callPart);
|
|
1135
|
+
toolCallsToRun.push(callPart);
|
|
1136
|
+
this.ctx.events?.onToolCallArgs?.(ev.id, ev.args);
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
case "finish":
|
|
1140
|
+
if (ev.usage) {
|
|
1141
|
+
this.ctx.events?.onUsage?.(ev.usage);
|
|
1142
|
+
this.ctx.session.append({
|
|
1143
|
+
type: "usage",
|
|
1144
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1145
|
+
usage: ev.usage,
|
|
1146
|
+
provider: this.ctx.llm.providerName,
|
|
1147
|
+
model: this.ctx.llm.model
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
break;
|
|
1151
|
+
case "error":
|
|
1152
|
+
onError(ev.error);
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
async runToolCall(call) {
|
|
1157
|
+
const tool2 = this.ctx.tools.get(call.name);
|
|
1158
|
+
if (!tool2) {
|
|
1159
|
+
const result2 = `Tool "${call.name}" is not available.`;
|
|
1160
|
+
this.recordToolResult(call.id, call.name, result2, true);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const summary = tool2.summarize?.(call.args) ?? `${call.name}(...)`;
|
|
1164
|
+
const decision = this.ctx.permissions.decide({
|
|
1165
|
+
toolName: call.name,
|
|
1166
|
+
args: call.args,
|
|
1167
|
+
permission: tool2.permission
|
|
1168
|
+
});
|
|
1169
|
+
let approved = decision === "allow";
|
|
1170
|
+
if (decision === "deny") {
|
|
1171
|
+
const reason = this.ctx.permissions.getMode() === "plan" ? `Denied: you are in plan mode. Only read-only tools are available. Propose changes instead of executing.` : `Denied by policy: ${call.name}.`;
|
|
1172
|
+
this.recordToolResult(call.id, call.name, reason, true);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (decision === "ask") {
|
|
1176
|
+
approved = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? false;
|
|
1177
|
+
if (!approved) {
|
|
1178
|
+
this.recordToolResult(call.id, call.name, `User rejected ${call.name}.`, true);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
const toolCtx = {
|
|
1183
|
+
cwd: this.ctx.cwd,
|
|
1184
|
+
abortSignal: this.ctx.abortSignal,
|
|
1185
|
+
askPermission: async () => true
|
|
1186
|
+
// 已在外层处理
|
|
1187
|
+
};
|
|
1188
|
+
const result = await this.ctx.tools.execute(call.name, call.args, toolCtx);
|
|
1189
|
+
this.recordToolResult(call.id, call.name, result.content, result.isError ?? false, result.summary);
|
|
1190
|
+
}
|
|
1191
|
+
recordToolResult(id, name, content, isError, summary) {
|
|
1192
|
+
const toolMsg = {
|
|
1193
|
+
role: "tool",
|
|
1194
|
+
toolUseId: id,
|
|
1195
|
+
content,
|
|
1196
|
+
isError
|
|
1197
|
+
};
|
|
1198
|
+
this.messages.push(toolMsg);
|
|
1199
|
+
this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: toolMsg });
|
|
1200
|
+
this.ctx.events?.onToolResult?.(id, name, content, isError, summary);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
// src/loop/system-prompt.ts
|
|
1205
|
+
import { homedir as homedir3 } from "os";
|
|
1206
|
+
function buildSystemPrompt(opts) {
|
|
1207
|
+
const { cwd, model, provider, lang, toolNames } = opts;
|
|
1208
|
+
const home = homedir3();
|
|
1209
|
+
const displayCwd = cwd.startsWith(home) ? cwd.replace(home, "~") : cwd;
|
|
1210
|
+
const sections = [];
|
|
1211
|
+
sections.push(`You are Muse, a CLI coding assistant. You are running on the user's local machine via a terminal interface.`);
|
|
1212
|
+
sections.push(
|
|
1213
|
+
`# Environment
|
|
1214
|
+
- Working directory: ${displayCwd}
|
|
1215
|
+
- LLM backend: ${provider} (${model})
|
|
1216
|
+
- Date: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`
|
|
1217
|
+
);
|
|
1218
|
+
sections.push(
|
|
1219
|
+
`# Available tools
|
|
1220
|
+
` + toolNames.map((n) => `- ${n}`).join("\n") + `
|
|
1221
|
+
|
|
1222
|
+
Prefer the dedicated tool over Bash when one fits (Read for file reading, Edit for partial updates, Write for new files / full rewrites, Grep for content search, Glob for file lookup).`
|
|
1223
|
+
);
|
|
1224
|
+
sections.push(
|
|
1225
|
+
`# Behavior
|
|
1226
|
+
- Be concise. State results, not your thinking. Don't narrate every step.
|
|
1227
|
+
- Before editing a file you have not seen, Read it first.
|
|
1228
|
+
- For Write/Edit/Bash the user may need to approve \u2014 proceed normally; the host will gate dangerous calls.
|
|
1229
|
+
- If a command may be destructive (rm -rf, force push, drop table, etc.), warn first and let the user run it manually.
|
|
1230
|
+
- When the user asks a question that does not need tools, just answer.`
|
|
1231
|
+
);
|
|
1232
|
+
if (lang === "zh-CN") {
|
|
1233
|
+
sections.push(`# Output language
|
|
1234
|
+
Reply in Chinese (\u7B80\u4F53\u4E2D\u6587) unless the user writes in English.`);
|
|
1235
|
+
}
|
|
1236
|
+
return sections.join("\n\n");
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/config/loader.ts
|
|
1240
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1241
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1242
|
+
import { homedir as homedir4 } from "os";
|
|
1243
|
+
import { join as join3, resolve as resolve4 } from "path";
|
|
1244
|
+
|
|
1245
|
+
// src/config/types.ts
|
|
1246
|
+
import { z as z7 } from "zod";
|
|
1247
|
+
var ProviderConfigSchema = z7.object({
|
|
1248
|
+
apiKey: z7.string().optional(),
|
|
1249
|
+
baseUrl: z7.string().optional(),
|
|
1250
|
+
extraHeaders: z7.record(z7.string()).optional()
|
|
1251
|
+
}).passthrough();
|
|
1252
|
+
var LLMConfigSchema = z7.object({
|
|
1253
|
+
provider: z7.string().optional().describe("Fallback provider preset (only used when no models.json entry matches)."),
|
|
1254
|
+
model: z7.string().optional().describe("Active model id; should match an id in models.json."),
|
|
1255
|
+
temperature: z7.number().min(0).max(2).optional(),
|
|
1256
|
+
maxTokens: z7.number().int().positive().optional()
|
|
1257
|
+
});
|
|
1258
|
+
var PermissionsSchema = z7.object({
|
|
1259
|
+
allow: z7.array(z7.string()).optional(),
|
|
1260
|
+
ask: z7.array(z7.string()).optional(),
|
|
1261
|
+
deny: z7.array(z7.string()).optional(),
|
|
1262
|
+
defaultMode: z7.enum(["strict", "relaxed", "ask"]).optional()
|
|
1263
|
+
});
|
|
1264
|
+
var UISchema = z7.object({
|
|
1265
|
+
theme: z7.enum(["dark", "light"]).optional(),
|
|
1266
|
+
lang: z7.enum(["en", "zh-CN"]).optional(),
|
|
1267
|
+
showBanner: z7.boolean().optional()
|
|
1268
|
+
});
|
|
1269
|
+
var SettingsSchema = z7.object({
|
|
1270
|
+
llm: LLMConfigSchema.optional(),
|
|
1271
|
+
providers: z7.record(ProviderConfigSchema).optional(),
|
|
1272
|
+
permissions: PermissionsSchema.optional(),
|
|
1273
|
+
ui: UISchema.optional(),
|
|
1274
|
+
mcpServers: z7.record(z7.unknown()).optional(),
|
|
1275
|
+
skills: z7.object({
|
|
1276
|
+
enabled: z7.boolean().optional(),
|
|
1277
|
+
disabled: z7.array(z7.string()).optional()
|
|
1278
|
+
}).optional()
|
|
1279
|
+
}).passthrough();
|
|
1280
|
+
|
|
1281
|
+
// src/config/_env.ts
|
|
1282
|
+
var ENV_PATTERN = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
1283
|
+
function expandEnvVars(value) {
|
|
1284
|
+
if (typeof value === "string") {
|
|
1285
|
+
return value.replace(ENV_PATTERN, (_match, name) => process.env[name] ?? "");
|
|
1286
|
+
}
|
|
1287
|
+
if (Array.isArray(value)) {
|
|
1288
|
+
return value.map(expandEnvVars);
|
|
1289
|
+
}
|
|
1290
|
+
if (value && typeof value === "object") {
|
|
1291
|
+
const result = {};
|
|
1292
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1293
|
+
result[k] = expandEnvVars(v);
|
|
1294
|
+
}
|
|
1295
|
+
return result;
|
|
1296
|
+
}
|
|
1297
|
+
return value;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/config/loader.ts
|
|
1301
|
+
function formatZodIssues(issues) {
|
|
1302
|
+
return issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
|
|
1303
|
+
}
|
|
1304
|
+
var DEFAULTS = {
|
|
1305
|
+
llm: {
|
|
1306
|
+
provider: "deepseek",
|
|
1307
|
+
model: "deepseek-chat"
|
|
1308
|
+
},
|
|
1309
|
+
providers: {
|
|
1310
|
+
deepseek: { apiKey: "${DEEPSEEK_API_KEY}" },
|
|
1311
|
+
openai: { apiKey: "${OPENAI_API_KEY}" },
|
|
1312
|
+
qwen: { apiKey: "${DASHSCOPE_API_KEY}" },
|
|
1313
|
+
moonshot: { apiKey: "${MOONSHOT_API_KEY}" },
|
|
1314
|
+
zhipu: { apiKey: "${ZHIPU_API_KEY}" },
|
|
1315
|
+
openrouter: { apiKey: "${OPENROUTER_API_KEY}" },
|
|
1316
|
+
ollama: { baseUrl: "http://localhost:11434/v1" }
|
|
1317
|
+
},
|
|
1318
|
+
permissions: {
|
|
1319
|
+
allow: ["Read", "Grep", "Glob"],
|
|
1320
|
+
ask: ["Write", "Edit", "Bash"],
|
|
1321
|
+
deny: [],
|
|
1322
|
+
defaultMode: "ask"
|
|
1323
|
+
},
|
|
1324
|
+
ui: {
|
|
1325
|
+
showBanner: true,
|
|
1326
|
+
lang: "en"
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
async function readJsonIfExists(path) {
|
|
1330
|
+
if (!existsSync2(path)) return void 0;
|
|
1331
|
+
try {
|
|
1332
|
+
const raw = await readFile4(path, "utf-8");
|
|
1333
|
+
return JSON.parse(raw);
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
log.warn(`Failed to parse settings at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1336
|
+
return void 0;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
function deepMerge(low, high) {
|
|
1340
|
+
if (high == null) return low;
|
|
1341
|
+
if (typeof low !== "object" || typeof high !== "object" || low === null || high === null) {
|
|
1342
|
+
return high;
|
|
1343
|
+
}
|
|
1344
|
+
if (Array.isArray(high)) return high;
|
|
1345
|
+
const result = { ...low };
|
|
1346
|
+
for (const [k, v] of Object.entries(high)) {
|
|
1347
|
+
const existing = low[k];
|
|
1348
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
1349
|
+
result[k] = deepMerge(existing, v);
|
|
1350
|
+
} else {
|
|
1351
|
+
result[k] = v;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return result;
|
|
1355
|
+
}
|
|
1356
|
+
async function loadSettings(cwd = process.cwd()) {
|
|
1357
|
+
const sources = ["<defaults>"];
|
|
1358
|
+
let merged = DEFAULTS;
|
|
1359
|
+
const candidates = [
|
|
1360
|
+
join3(homedir4(), ".muse", "settings.json"),
|
|
1361
|
+
join3(cwd, ".muse", "settings.json"),
|
|
1362
|
+
join3(cwd, ".muse", "settings.local.json")
|
|
1363
|
+
];
|
|
1364
|
+
for (const path of candidates) {
|
|
1365
|
+
const raw = await readJsonIfExists(path);
|
|
1366
|
+
if (raw != null) {
|
|
1367
|
+
const parsed = SettingsSchema.safeParse(raw);
|
|
1368
|
+
if (parsed.success) {
|
|
1369
|
+
merged = deepMerge(merged, parsed.data);
|
|
1370
|
+
sources.push(path);
|
|
1371
|
+
} else {
|
|
1372
|
+
log.warn(`Invalid settings at ${path}: ${formatZodIssues(parsed.error.issues)}`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (process.env.MUSE_PROVIDER && merged.llm) {
|
|
1377
|
+
merged = { ...merged, llm: { ...merged.llm, provider: process.env.MUSE_PROVIDER } };
|
|
1378
|
+
sources.push("env:MUSE_PROVIDER");
|
|
1379
|
+
}
|
|
1380
|
+
if (process.env.MUSE_MODEL && merged.llm) {
|
|
1381
|
+
merged = { ...merged, llm: { ...merged.llm, model: process.env.MUSE_MODEL } };
|
|
1382
|
+
sources.push("env:MUSE_MODEL");
|
|
1383
|
+
}
|
|
1384
|
+
merged = expandEnvVars(merged);
|
|
1385
|
+
return { settings: merged, sources };
|
|
1386
|
+
}
|
|
1387
|
+
export {
|
|
1388
|
+
Agent,
|
|
1389
|
+
BUILTIN_TOOLS,
|
|
1390
|
+
MuseError,
|
|
1391
|
+
PRESETS,
|
|
1392
|
+
PermissionDeniedError,
|
|
1393
|
+
PermissionGate,
|
|
1394
|
+
Session,
|
|
1395
|
+
SettingsSchema,
|
|
1396
|
+
ToolError,
|
|
1397
|
+
ToolRegistry,
|
|
1398
|
+
buildSystemPrompt,
|
|
1399
|
+
createLLMClient,
|
|
1400
|
+
defineTool,
|
|
1401
|
+
loadSettings,
|
|
1402
|
+
log,
|
|
1403
|
+
redactApiKey
|
|
1404
|
+
};
|
|
1405
|
+
//# sourceMappingURL=index.js.map
|