@menteeai/menteeswe 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/dist/cli.js +3224 -0
  4. package/package.json +50 -0
package/dist/cli.js ADDED
@@ -0,0 +1,3224 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/config.ts
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import os from "os";
16
+ function blankConfig() {
17
+ return { defaultProvider: "kimi", keys: {}, models: {} };
18
+ }
19
+ function configDir() {
20
+ return path.join(os.homedir(), ".mentee");
21
+ }
22
+ function configFilePath() {
23
+ return path.join(configDir(), "config.json");
24
+ }
25
+ function loadConfig() {
26
+ try {
27
+ const raw = fs.readFileSync(configFilePath(), "utf8");
28
+ const parsed = JSON.parse(raw);
29
+ if (!parsed || typeof parsed !== "object") return null;
30
+ if (!parsed.keys || typeof parsed.keys !== "object") return null;
31
+ return parsed;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ function saveConfig(config) {
37
+ fs.mkdirSync(configDir(), { recursive: true });
38
+ fs.writeFileSync(configFilePath(), JSON.stringify(config, null, 2), "utf8");
39
+ }
40
+ function resolveApiKey(provider, config) {
41
+ const keySlot = provider === "zai-coding" ? "zai" : provider;
42
+ return process.env[ENV_NAMES[provider]] || config?.keys?.[keySlot] || void 0;
43
+ }
44
+ function resolveModel(provider, config) {
45
+ return config?.models?.[provider] || void 0;
46
+ }
47
+ var ENV_NAMES;
48
+ var init_config = __esm({
49
+ "src/config.ts"() {
50
+ "use strict";
51
+ ENV_NAMES = {
52
+ kimi: "MENTEE_KIMI_API_KEY",
53
+ glm: "MENTEE_GLM_API_KEY",
54
+ zai: "MENTEE_ZAI_API_KEY",
55
+ "zai-coding": "MENTEE_ZAI_API_KEY"
56
+ };
57
+ }
58
+ });
59
+
60
+ // src/tools/base.ts
61
+ function firstString(args, key) {
62
+ const value = args[key];
63
+ return typeof value === "string" ? value : void 0;
64
+ }
65
+ function firstNumber(args, key) {
66
+ const value = args[key];
67
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
68
+ }
69
+ function firstBoolean(args, key) {
70
+ const value = args[key];
71
+ return typeof value === "boolean" ? value : void 0;
72
+ }
73
+ function requireString(args, key) {
74
+ const value = firstString(args, key);
75
+ if (value === void 0 || value.trim() === "") {
76
+ throw new Error(`Missing required string argument: ${key}`);
77
+ }
78
+ return value;
79
+ }
80
+ function truncateOutput(text, maxChars) {
81
+ if (text.length <= maxChars) {
82
+ return { text, truncated: false };
83
+ }
84
+ const head = Math.floor(maxChars * 0.7);
85
+ const tail = maxChars - head;
86
+ return {
87
+ text: text.slice(0, head) + `
88
+ ...[output truncated: showing first ${head} and last ${tail} of ${text.length} chars]...
89
+ ` + text.slice(text.length - tail),
90
+ truncated: true
91
+ };
92
+ }
93
+ var init_base = __esm({
94
+ "src/tools/base.ts"() {
95
+ "use strict";
96
+ }
97
+ });
98
+
99
+ // src/events.ts
100
+ var EventBus;
101
+ var init_events = __esm({
102
+ "src/events.ts"() {
103
+ "use strict";
104
+ EventBus = class {
105
+ handlers = /* @__PURE__ */ new Set();
106
+ subscribe(handler) {
107
+ this.handlers.add(handler);
108
+ return () => {
109
+ this.handlers.delete(handler);
110
+ };
111
+ }
112
+ emit(type, message, data) {
113
+ const event = { type, ts: Date.now(), message, data };
114
+ for (const handler of this.handlers) {
115
+ try {
116
+ handler(event);
117
+ } catch {
118
+ }
119
+ }
120
+ }
121
+ };
122
+ }
123
+ });
124
+
125
+ // src/logging.ts
126
+ import fs7 from "fs";
127
+ import path7 from "path";
128
+ function createSessionLogger() {
129
+ const dir = path7.join(configDir(), "logs");
130
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
131
+ const file = path7.join(dir, `${stamp}.jsonl`);
132
+ fs7.mkdirSync(dir, { recursive: true });
133
+ return (event) => {
134
+ try {
135
+ fs7.appendFileSync(file, JSON.stringify(event) + "\n", "utf8");
136
+ } catch {
137
+ }
138
+ };
139
+ }
140
+ var init_logging = __esm({
141
+ "src/logging.ts"() {
142
+ "use strict";
143
+ init_config();
144
+ }
145
+ });
146
+
147
+ // src/render.ts
148
+ import chalk from "chalk";
149
+ function toolColorName(name) {
150
+ return CATEGORY_NAME[TOOL_CATEGORY[name] ?? "other"];
151
+ }
152
+ function friendlyToolName(name) {
153
+ return TOOL_LABELS[name] ?? name;
154
+ }
155
+ function formatEvent(event) {
156
+ switch (event.type) {
157
+ case "task_started":
158
+ return chalk.bold.magenta(`\u25B8 Task: ${event.message ?? ""}`);
159
+ case "model_request":
160
+ return chalk.gray("\u25CF thinking...");
161
+ case "info":
162
+ return chalk.cyan(` ${event.message ?? ""}`);
163
+ case "tool_started": {
164
+ const tool = typeof event.data?.tool === "string" ? event.data.tool : "";
165
+ const color = CATEGORY_COLOR[TOOL_CATEGORY[tool] ?? "other"];
166
+ const friendly = friendlyToolName(tool);
167
+ const preview = tool && event.message?.startsWith(tool) ? event.message.slice(tool.length).trim() : event.message ?? "";
168
+ return color(`\u2699 ${friendly} (${preview})...`);
169
+ }
170
+ case "tool_completed": {
171
+ const tool = typeof event.data?.tool === "string" ? event.data.tool : "";
172
+ const success = event.data?.success === true;
173
+ const output = typeof event.data?.output === "string" ? event.data.output.trim() : "";
174
+ const firstLine = (output.split("\n")[0] ?? event.message ?? "").slice(0, 160);
175
+ const color = success ? CATEGORY_COLOR[TOOL_CATEGORY[tool] ?? "other"] : chalk.red;
176
+ return color(` ${success ? "\u2713" : "\u2717"} ${firstLine}`);
177
+ }
178
+ case "approval":
179
+ return chalk.magenta(`\u{1F510} Asking permission to use ${event.message ?? "tool"}...`);
180
+ case "warning":
181
+ return chalk.yellow(`\u26A0 ${event.message ?? ""}`);
182
+ case "error":
183
+ return chalk.red(`\u2716 ${event.message ?? ""}`);
184
+ case "task_completed": {
185
+ const success = event.data?.success === true;
186
+ const usage = event.data?.usage;
187
+ const duration = event.data?.durationMs;
188
+ const toolCalls = typeof event.data?.toolCalls === "number" ? event.data.toolCalls : 0;
189
+ const iterations = typeof event.data?.iterations === "number" ? event.data.iterations : 0;
190
+ const modifiedFiles = Array.isArray(event.data?.modifiedFiles) ? event.data.modifiedFiles : [];
191
+ const finalText = typeof event.data?.finalText === "string" ? event.data.finalText.trim() : "";
192
+ const fmt = (n) => n.toLocaleString("en-US");
193
+ const stats = [];
194
+ if (duration !== void 0) stats.push(`time ${(duration / 1e3).toFixed(1)}s`);
195
+ if (usage) {
196
+ const total = usage.inputTokens + usage.outputTokens;
197
+ stats.push(`tokens ${fmt(total)} (\u2191${fmt(usage.inputTokens)} \u2193${fmt(usage.outputTokens)})`);
198
+ }
199
+ stats.push(`tool calls ${toolCalls}`);
200
+ stats.push(`iterations ${iterations}`);
201
+ if (modifiedFiles.length > 0) stats.push(`files ${modifiedFiles.length}`);
202
+ const sep = chalk.dim("\u2500".repeat(48));
203
+ const statsLine = chalk.cyan(` ${stats.join(" \xB7 ")}`);
204
+ const statusLine = success ? chalk.green.bold("\u2714 Task completed") : chalk.red.bold("\u2716 Task failed");
205
+ const block = [statusLine, sep, statsLine, sep];
206
+ return block.join("\n");
207
+ }
208
+ default:
209
+ return null;
210
+ }
211
+ }
212
+ var TOOL_CATEGORY, CATEGORY_COLOR, CATEGORY_NAME, TOOL_LABELS;
213
+ var init_render = __esm({
214
+ "src/render.ts"() {
215
+ "use strict";
216
+ TOOL_CATEGORY = {
217
+ read_file: "filesystem",
218
+ write_file: "filesystem",
219
+ apply_patch: "filesystem",
220
+ delete_file: "filesystem",
221
+ move_path: "filesystem",
222
+ search_code: "search",
223
+ search_files: "search",
224
+ list_files: "search",
225
+ git_status: "git",
226
+ git_diff: "git",
227
+ git_log: "git",
228
+ git_show: "git",
229
+ git_branches: "git",
230
+ execute_command: "exec",
231
+ run_tests: "testing",
232
+ run_linter: "testing",
233
+ run_typecheck: "testing",
234
+ inspect_env: "env",
235
+ memory: "env",
236
+ web_search: "web",
237
+ http_fetch: "exec",
238
+ file_info: "filesystem",
239
+ safe_delete_suggestion: "env",
240
+ process_list: "exec",
241
+ port_check: "exec",
242
+ system_info: "env"
243
+ };
244
+ CATEGORY_COLOR = {
245
+ filesystem: chalk.blue,
246
+ search: chalk.cyan,
247
+ git: chalk.magenta,
248
+ exec: chalk.yellow,
249
+ testing: chalk.green,
250
+ env: chalk.gray,
251
+ memory: chalk.magenta,
252
+ web: chalk.cyan,
253
+ other: chalk.white
254
+ };
255
+ CATEGORY_NAME = {
256
+ filesystem: "blue",
257
+ search: "cyan",
258
+ git: "magenta",
259
+ exec: "yellow",
260
+ testing: "green",
261
+ env: "gray",
262
+ memory: "magenta",
263
+ web: "cyan",
264
+ other: "white"
265
+ };
266
+ TOOL_LABELS = {
267
+ read_file: "read",
268
+ write_file: "write",
269
+ apply_patch: "edit",
270
+ delete_file: "delete",
271
+ move_path: "move",
272
+ execute_command: "terminal",
273
+ run_tests: "tests",
274
+ run_linter: "linter",
275
+ run_typecheck: "typecheck",
276
+ inspect_env: "env",
277
+ search_code: "search",
278
+ search_files: "file search",
279
+ list_files: "list",
280
+ git_status: "git status",
281
+ git_diff: "git diff",
282
+ git_log: "git log",
283
+ git_show: "git show",
284
+ git_branches: "git branches"
285
+ };
286
+ }
287
+ });
288
+
289
+ // src/approval.ts
290
+ function createAutoApproval(yes) {
291
+ return async (request) => {
292
+ if (request.risk === "restricted" && !yes) {
293
+ return "deny";
294
+ }
295
+ return "allow";
296
+ };
297
+ }
298
+ var ApprovalBridge;
299
+ var init_approval = __esm({
300
+ "src/approval.ts"() {
301
+ "use strict";
302
+ ApprovalBridge = class {
303
+ pending = null;
304
+ onPending = null;
305
+ setListener(listener) {
306
+ this.onPending = listener;
307
+ }
308
+ get current() {
309
+ return this.pending?.request ?? null;
310
+ }
311
+ resolve(decision) {
312
+ if (!this.pending) return;
313
+ const { resolve } = this.pending;
314
+ this.pending = null;
315
+ this.onPending?.(null);
316
+ resolve(decision);
317
+ }
318
+ handler = (request) => {
319
+ return new Promise((resolve) => {
320
+ this.pending = { request, resolve };
321
+ this.onPending?.(request);
322
+ });
323
+ };
324
+ };
325
+ }
326
+ });
327
+
328
+ // src/agent/prompts.ts
329
+ import fs8 from "fs";
330
+ import { spawnSync as spawnSync3 } from "child_process";
331
+ import os2 from "os";
332
+ function gitBranch(cwd) {
333
+ const result = spawnSync3("git rev-parse --abbrev-ref HEAD", {
334
+ cwd,
335
+ encoding: "utf8",
336
+ shell: true,
337
+ windowsHide: true
338
+ });
339
+ if (result.status !== 0) return null;
340
+ return (result.stdout ?? "").trim() || null;
341
+ }
342
+ function projectTree(cwd, prefix = "", depth = 0, maxDepth = 3) {
343
+ if (depth > maxDepth) return "";
344
+ let out = "";
345
+ let entries;
346
+ try {
347
+ entries = fs8.readdirSync(cwd, { withFileTypes: true });
348
+ } catch {
349
+ return "";
350
+ }
351
+ entries = entries.filter((e) => !SKIP_DIRS.has(e.name) && !e.name.startsWith(".")).sort((a, b) => {
352
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
353
+ return a.name.localeCompare(b.name);
354
+ }).slice(0, 40);
355
+ for (const e of entries) {
356
+ out += `${prefix}${e.isDirectory() ? `${e.name}/` : e.name}
357
+ `;
358
+ if (e.isDirectory() && depth < maxDepth) {
359
+ out += projectTree(`${cwd}/${e.name}`, `${prefix} `, depth + 1, maxDepth);
360
+ }
361
+ }
362
+ return out;
363
+ }
364
+ function buildSystemPrompt(cwd) {
365
+ const branch = gitBranch(cwd);
366
+ const tree = projectTree(cwd).trim() || "(empty)";
367
+ return `You are MenteE, an autonomous software-engineering agent working inside a repository on the user's machine.
368
+
369
+ # Environment
370
+ - Workspace: ${cwd}
371
+ - Platform: ${os2.platform()} (${os2.release()}), shell commands run with the system shell
372
+ ${branch ? `- Git branch: ${branch}` : "- Not a git repository"}
373
+
374
+ # Project file tree (you already have this \u2014 do NOT re-read files just to learn structure)
375
+ ${tree}
376
+
377
+ # How to work
378
+ 1. UNDERSTAND before acting: list files, search code, and read the relevant files (with line ranges for large files) before proposing changes. Never modify a file you have not read.
379
+ 2. MINIMAL CHANGES: make the smallest change that correctly fulfils the task. Never refactor unrelated code, never reformat, never rename things that were not asked about.
380
+ 3. REALITY CHECK: do not invent APIs, packages, or file paths. If you are unsure a dependency or module exists, verify with search or by reading package.json / requirements files.
381
+ 4. VERIFY: after modifying code, run the project's own tests, build, or typecheck commands to prove the change works. If no obvious test command exists, at minimum execute the modified code or a syntax check.
382
+ 5. DEBUG PROPERLY: if a command fails, read the error carefully, investigate the root cause, and fix that. If the same error occurs three times, stop repeating it, state what you have tried, and change strategy.
383
+ 6. STAY IN SCOPE: never read, modify, or execute anything outside the workspace. Never print or exfiltrate secrets, API keys, or credentials.
384
+ 7. FINISH PROPERLY: when the task is done and verified, stop calling tools and write a final answer (see Communication rules).
385
+
386
+ # Tool usage notes
387
+ - Call only the tools needed for the immediate next step. Do NOT read files you already understand, and do NOT read build/config files (e.g. *.config.ts, tsconfig.json) unless the task specifically needs them.
388
+ - Prefer search_code over reading many files.
389
+
390
+ # Research strategy (IMPORTANT \u2014 HARD RULE)
391
+ - The file tree above already shows the whole structure. Do NOT read files just to "see what's there".
392
+ - When asked to "understand", "study", or "explore", read at most 5 files: typically README, package.json / pyproject, and 1-3 source files directly relevant to the question. Use search_code to locate symbols instead of opening files blindly.
393
+ - The harness enforces a read budget. Once it warns you, STOP reading and answer or act.
394
+ - For debugging or feature tasks, read only the files directly involved, then verify with tests. Never read the entire codebase.
395
+
396
+ # Editing notes
397
+ - Use read_file with start_line/end_line for files that may be large.
398
+ - Use apply_patch (exact unique snippet replacement) for edits; use write_file only for new files or complete rewrites you have fully read.
399
+ - Run tests with execute_command (e.g. "npm test", "pytest"). Safe read-only commands run automatically; installing packages or git history changes will ask the user for approval.
400
+
401
+ # Persistent memory
402
+ - Use the 'memory' tool to store important, reusable context so you don't re-derive it: project conventions, key file locations, design decisions, gotchas, API quirks. Example: after discovering "tests run with vitest and need a built dist", store it with topic "testing".
403
+ - At the start of a task, call 'memory' with action=search (or list) to recall prior findings for THIS project. Prefer recalled memory over re-reading files.
404
+ - This memory persists across sessions and is project-scoped; it never lives in the workspace.
405
+
406
+ # Communication (IMPORTANT)
407
+ - While working, say only what you are doing and why, in ONE short sentence per step. No filler.
408
+ - Your FINAL answer must be minimal and useful: 1-3 sentences unless the task explicitly asks for detail. State the outcome and, if you changed anything, the one command used to verify. Do NOT write document-style reports, headers, bullet inventories of the codebase, or repeat tool output. If the user wants more, they will ask.
409
+ - NEVER use bullet lists, numbered lists, headings, or markdown headers in your final answer unless the user explicitly asks for one. Plain prose only \u2014 1 to 3 sentences.
410
+ - Never dump the whole repo structure or a file-by-file summary unless requested.
411
+ - If the user refers to something you said or did earlier ("that", "it", "before"), use the prior conversation context provided above.
412
+
413
+ Be concise in your visible text between tool calls: one short sentence about what you are doing and why is enough.`;
414
+ }
415
+ var SKIP_DIRS;
416
+ var init_prompts = __esm({
417
+ "src/agent/prompts.ts"() {
418
+ "use strict";
419
+ SKIP_DIRS = /* @__PURE__ */ new Set([
420
+ "node_modules",
421
+ ".git",
422
+ "dist",
423
+ "build",
424
+ ".next",
425
+ "out",
426
+ ".venv",
427
+ "venv",
428
+ ".mentee",
429
+ "coverage",
430
+ ".turbo",
431
+ "tmp"
432
+ ]);
433
+ }
434
+ });
435
+
436
+ // src/agent/conversation.ts
437
+ import fs9 from "fs";
438
+ import path8 from "path";
439
+ import crypto2 from "crypto";
440
+ import os3 from "os";
441
+ function sessionsDir() {
442
+ const dir = path8.join(os3.homedir(), ".mentee", "sessions");
443
+ fs9.mkdirSync(dir, { recursive: true });
444
+ return dir;
445
+ }
446
+ function sessionFile(cwd) {
447
+ const hash = crypto2.createHash("sha1").update(cwd).digest("hex").slice(0, 16);
448
+ return path8.join(sessionsDir(), `${hash}.json`);
449
+ }
450
+ function loadConversation(cwd, limit = 12) {
451
+ try {
452
+ const raw = fs9.readFileSync(sessionFile(cwd), "utf8");
453
+ const turns = JSON.parse(raw);
454
+ if (!Array.isArray(turns)) return [];
455
+ return turns.slice(-limit);
456
+ } catch {
457
+ return [];
458
+ }
459
+ }
460
+ function appendTurn(cwd, task, response) {
461
+ const file = sessionFile(cwd);
462
+ let turns = [];
463
+ try {
464
+ turns = JSON.parse(fs9.readFileSync(file, "utf8"));
465
+ if (!Array.isArray(turns)) turns = [];
466
+ } catch {
467
+ turns = [];
468
+ }
469
+ turns.push({ task, response, ts: Date.now() });
470
+ if (turns.length > 50) turns = turns.slice(-50);
471
+ fs9.writeFileSync(file, JSON.stringify(turns, null, 2));
472
+ }
473
+ function formatConversationContext(cwd, limit = 12) {
474
+ const turns = loadConversation(cwd, limit);
475
+ if (turns.length === 0) return "";
476
+ const lines = ["# Prior conversation in this project (oldest first)"];
477
+ for (const t of turns) {
478
+ lines.push(`User: ${t.task}`);
479
+ lines.push(`Assistant: ${t.response.trim()}`);
480
+ lines.push("");
481
+ }
482
+ return lines.join("\n");
483
+ }
484
+ var init_conversation = __esm({
485
+ "src/agent/conversation.ts"() {
486
+ "use strict";
487
+ }
488
+ });
489
+
490
+ // src/agent/state.ts
491
+ function createAgentState(task, maxIterations) {
492
+ return {
493
+ task,
494
+ phase: "INITIALIZATION",
495
+ iteration: 0,
496
+ maxIterations,
497
+ modifiedFiles: [],
498
+ commandsRun: [],
499
+ errors: [],
500
+ lastErrorSignature: null,
501
+ sameErrorCount: 0,
502
+ totalToolCalls: 0,
503
+ readCount: 0,
504
+ usage: { inputTokens: 0, outputTokens: 0, modelRequests: 0 },
505
+ startedAt: Date.now()
506
+ };
507
+ }
508
+ function errorSignature(stdout, stderr, exitCode) {
509
+ const relevant = (stderr || stdout || "").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).slice(-8).join(" | ");
510
+ return `${exitCode}::${relevant.slice(0, 300)}`;
511
+ }
512
+ var init_state = __esm({
513
+ "src/agent/state.ts"() {
514
+ "use strict";
515
+ }
516
+ });
517
+
518
+ // src/agent/loop.ts
519
+ function sleep(ms) {
520
+ return new Promise((resolve) => setTimeout(resolve, ms));
521
+ }
522
+ function parseSuggestedDelayMs(message) {
523
+ const match = message.match(/after\s+(\d+)\s*seconds?/i);
524
+ if (match) {
525
+ const seconds = Number.parseInt(match[1] ?? "0", 10);
526
+ if (Number.isFinite(seconds) && seconds > 0 && seconds <= 120) {
527
+ return seconds * 1e3 + 500;
528
+ }
529
+ }
530
+ return null;
531
+ }
532
+ function isRateLimitError(error) {
533
+ const message = error.message.toLowerCase();
534
+ if (/insufficient balance|no resource package|recharge|quota exceeded|quota has been|arrears|billing/.test(
535
+ message
536
+ )) {
537
+ return false;
538
+ }
539
+ return /\b429\b|max rpm|rate limit|too many requests/i.test(error.message);
540
+ }
541
+ async function generateWithRetry(provider, model, system, messages, tools, bus) {
542
+ let lastError = null;
543
+ for (let attempt = 0; attempt <= RATE_LIMIT_MAX_ATTEMPTS; attempt++) {
544
+ try {
545
+ return await provider.generate({ system, messages, tools: tools.schemas() }, model);
546
+ } catch (error) {
547
+ lastError = error;
548
+ if (attempt >= RATE_LIMIT_MAX_ATTEMPTS || !isRateLimitError(lastError)) {
549
+ throw lastError;
550
+ }
551
+ const base = parseSuggestedDelayMs(lastError.message) ?? 2e3;
552
+ const delay = Math.min(base * Math.pow(2, attempt), 12e4) + Math.floor(Math.random() * 1e3);
553
+ bus.emit(
554
+ "warning",
555
+ `Rate limited by the provider \u2014 waiting ${(delay / 1e3).toFixed(0)}s and retrying (attempt ${attempt + 1}/${RATE_LIMIT_MAX_ATTEMPTS})`
556
+ );
557
+ await sleep(delay);
558
+ }
559
+ }
560
+ throw lastError ?? new Error("Model request failed after retries");
561
+ }
562
+ function toolCallPreview(call) {
563
+ let args = {};
564
+ try {
565
+ args = JSON.parse(call.function.arguments || "{}");
566
+ } catch {
567
+ args = {};
568
+ }
569
+ const preview = firstString(args, "path") ?? firstString(args, "command") ?? firstString(args, "pattern") ?? (Object.keys(args).length > 0 ? JSON.stringify(args).slice(0, 120) : "");
570
+ return preview ? `${call.function.name} ${preview}` : call.function.name;
571
+ }
572
+ function estimateContextChars(messages, system) {
573
+ let total = system.length;
574
+ for (const message of messages) {
575
+ total += (message.content?.length ?? 0) + JSON.stringify(message.tool_calls ?? []).length;
576
+ }
577
+ return total;
578
+ }
579
+ function trimOldToolResults(messages) {
580
+ for (const message of messages) {
581
+ if (message.role === "tool" && (message.content?.length ?? 0) > 2e3) {
582
+ message.content = message.content.slice(0, 500) + "\n...[older tool output trimmed to save context]";
583
+ }
584
+ }
585
+ }
586
+ async function runAgent(options) {
587
+ const {
588
+ task,
589
+ cwd,
590
+ provider,
591
+ model,
592
+ tools,
593
+ bus,
594
+ approval,
595
+ maxIterations = 40,
596
+ systemExtra
597
+ } = options;
598
+ const state = createAgentState(task, maxIterations);
599
+ const prior = formatConversationContext(cwd);
600
+ const system = buildSystemPrompt(cwd) + (prior ? `
601
+
602
+ ${prior}
603
+
604
+ Use the prior conversation above as context. When the user refers to "that", "it", or "previous", they mean the most recent exchange shown above.
605
+ ` : "") + (systemExtra ? `
606
+
607
+ # Additional instructions
608
+ ${systemExtra}` : "");
609
+ const toolCtx = {
610
+ cwd,
611
+ approval,
612
+ emit: (type, message, data) => {
613
+ bus.emit(type, message, data);
614
+ }
615
+ };
616
+ const messages = [{ role: "user", content: task }];
617
+ bus.emit("task_started", task, { provider: provider.name, model: model ?? provider.defaultModel });
618
+ let finalText = "";
619
+ let sawFinish = false;
620
+ while (state.iteration < maxIterations) {
621
+ state.iteration += 1;
622
+ state.usage.modelRequests += 1;
623
+ bus.emit("model_request", `iteration ${state.iteration}/${maxIterations}`);
624
+ let response;
625
+ try {
626
+ response = await generateWithRetry(provider, model, system, messages, tools, bus);
627
+ } catch (error) {
628
+ const message = error.message;
629
+ let hint = "";
630
+ if (/insufficient balance|no resource package|recharge/i.test(message)) {
631
+ hint = " \u2014 this key has no quota on this endpoint. If it is a Z.ai GLM Coding Plan key, switch with /provider zai-coding; otherwise add balance on the provider platform";
632
+ } else if (/40[134]|permission|unauthorized|api key/i.test(message)) {
633
+ hint = " \u2014 check the model id (Alt+M) and your API key permissions, then /model <id> to switch";
634
+ }
635
+ bus.emit("error", message + hint);
636
+ finalText = `Model request failed: ${message}`;
637
+ appendTurn(cwd, task, finalText);
638
+ bus.emit("task_completed", finalText.slice(0, 400), {
639
+ success: false,
640
+ finalText,
641
+ iterations: state.iteration,
642
+ toolCalls: state.totalToolCalls,
643
+ modifiedFiles: state.modifiedFiles,
644
+ usage: state.usage,
645
+ durationMs: Date.now() - state.startedAt
646
+ });
647
+ return { success: false, finalText, state };
648
+ }
649
+ bus.emit(
650
+ "model_response",
651
+ void 0,
652
+ {
653
+ finishReason: response.finishReason,
654
+ toolCalls: response.toolCalls.map((c) => c.function.name),
655
+ usage: response.usage
656
+ }
657
+ );
658
+ if (response.usage) {
659
+ state.usage.inputTokens += response.usage.inputTokens;
660
+ state.usage.outputTokens += response.usage.outputTokens;
661
+ }
662
+ if (response.finishReason === "length") {
663
+ bus.emit("warning", "Model hit its output length limit.");
664
+ finalText = response.content ?? "Task ended early: output length limit reached.";
665
+ break;
666
+ }
667
+ if (response.toolCalls.length === 0) {
668
+ finalText = response.content ?? "";
669
+ sawFinish = true;
670
+ break;
671
+ }
672
+ messages.push({
673
+ role: "assistant",
674
+ content: response.content,
675
+ tool_calls: response.toolCalls
676
+ });
677
+ if (response.content && response.content.trim()) {
678
+ bus.emit("info", response.content.trim().slice(0, 300));
679
+ }
680
+ for (const call of response.toolCalls) {
681
+ const tool = tools.get(call.function.name);
682
+ if (!tool) {
683
+ messages.push({
684
+ role: "tool",
685
+ tool_call_id: call.id,
686
+ content: `Unknown tool: ${call.function.name}. Available tools: ${tools.schemas().map((t) => t.name).join(", ")}`
687
+ });
688
+ continue;
689
+ }
690
+ let args = {};
691
+ try {
692
+ args = JSON.parse(call.function.arguments || "{}");
693
+ } catch {
694
+ messages.push({
695
+ role: "tool",
696
+ tool_call_id: call.id,
697
+ content: "Invalid JSON arguments for tool call. Retry with valid JSON."
698
+ });
699
+ continue;
700
+ }
701
+ bus.emit("tool_started", toolCallPreview(call), { tool: tool.name, args });
702
+ const risk = tool.dynamicRisk ? tool.dynamicRisk(args) : tool.risk;
703
+ let approved = true;
704
+ let denialMessage = "";
705
+ if (risk === "dangerous") {
706
+ approved = false;
707
+ denialMessage = "Blocked: this command is classified as dangerous and cannot be executed.";
708
+ } else if (risk === "restricted") {
709
+ const decision = await approval({ tool: tool.name, args, risk });
710
+ if (decision === "deny") {
711
+ approved = false;
712
+ denialMessage = "The user denied permission for this action. Choose a different approach.";
713
+ }
714
+ }
715
+ let result;
716
+ if (!approved) {
717
+ result = { success: false, output: denialMessage };
718
+ } else {
719
+ try {
720
+ result = await tool.execute(args, toolCtx);
721
+ } catch (error) {
722
+ result = { success: false, output: `Tool error: ${error.message}` };
723
+ }
724
+ }
725
+ state.totalToolCalls += 1;
726
+ if (tool.name === "read_file" && result.success) {
727
+ state.readCount += 1;
728
+ if (state.readCount === READ_BUDGET || state.readCount === READ_BUDGET * 2) {
729
+ bus.emit(
730
+ "warning",
731
+ `Read budget reached (${state.readCount} files). Stop exploring \u2014 either answer now or start acting on what you have.`
732
+ );
733
+ messages.push({
734
+ role: "user",
735
+ content: "SYSTEM NOTE: You have read " + state.readCount + " files. That is more than enough exploration. Do NOT call read_file again unless a later step absolutely requires it. Either answer the question now or begin making changes."
736
+ });
737
+ }
738
+ }
739
+ if (tool.name === "apply_patch" || tool.name === "write_file") {
740
+ const pathArg = firstString(args, "path");
741
+ if (pathArg && result.success && !state.modifiedFiles.includes(pathArg)) {
742
+ state.modifiedFiles.push(pathArg);
743
+ }
744
+ }
745
+ if (tool.name === "execute_command") {
746
+ state.commandsRun.push(firstString(args, "command") ?? "");
747
+ if (!result.success && (result.output.includes("exit_code: timeout") || result.output.includes("exit_code: 1"))) {
748
+ state.errors.push(result.output.slice(0, 500));
749
+ const sig = errorSignature(result.output, "", 1);
750
+ if (sig === state.lastErrorSignature) {
751
+ state.sameErrorCount += 1;
752
+ } else {
753
+ state.lastErrorSignature = sig;
754
+ state.sameErrorCount = 1;
755
+ }
756
+ if (state.sameErrorCount >= 3) {
757
+ bus.emit("warning", "Same failure occurred 3 times. Instructing agent to change strategy.");
758
+ messages.push({
759
+ role: "user",
760
+ content: "SYSTEM NOTE: You have produced the same failure three times. Stop repeating the same fix. Re-read the error, question your assumptions, investigate the root cause, and try a fundamentally different approach."
761
+ });
762
+ state.sameErrorCount = 0;
763
+ }
764
+ }
765
+ }
766
+ bus.emit("tool_completed", toolCallPreview(call), {
767
+ tool: tool.name,
768
+ success: result.success,
769
+ output: result.output.slice(0, 2e3)
770
+ });
771
+ messages.push({
772
+ role: "tool",
773
+ tool_call_id: call.id,
774
+ content: result.output
775
+ });
776
+ }
777
+ if (estimateContextChars(messages, system) > MAX_CONTEXT_CHARS) {
778
+ bus.emit("warning", "Context is large; trimming oldest tool outputs.");
779
+ trimOldToolResults(messages);
780
+ }
781
+ }
782
+ if (!sawFinish && !finalText) {
783
+ finalText = `Stopped after ${maxIterations} iterations without a final answer. Files modified: ${state.modifiedFiles.join(", ") || "none"}.`;
784
+ bus.emit("warning", finalText);
785
+ }
786
+ const success = sawFinish && state.errors.length === 0;
787
+ if (finalText) appendTurn(cwd, task, finalText);
788
+ bus.emit("task_completed", finalText.slice(0, 400), {
789
+ success,
790
+ finalText,
791
+ iterations: state.iteration,
792
+ toolCalls: state.totalToolCalls,
793
+ modifiedFiles: state.modifiedFiles,
794
+ usage: state.usage,
795
+ durationMs: Date.now() - state.startedAt
796
+ });
797
+ return { success, finalText, state };
798
+ }
799
+ var MAX_CONTEXT_CHARS, READ_BUDGET, RATE_LIMIT_MAX_ATTEMPTS;
800
+ var init_loop = __esm({
801
+ "src/agent/loop.ts"() {
802
+ "use strict";
803
+ init_prompts();
804
+ init_conversation();
805
+ init_state();
806
+ init_base();
807
+ MAX_CONTEXT_CHARS = 48e4;
808
+ READ_BUDGET = 6;
809
+ RATE_LIMIT_MAX_ATTEMPTS = 8;
810
+ }
811
+ });
812
+
813
+ // src/tui/Approval.tsx
814
+ import { Box, Text, useInput } from "ink";
815
+ import { jsx, jsxs } from "react/jsx-runtime";
816
+ function argPreview(request) {
817
+ const args = request.args;
818
+ if (request.tool === "apply_patch" && typeof args.old_string === "string" && typeof args.new_string === "string") {
819
+ const removed = args.old_string.split("\n").slice(0, 12).map((line) => `- ${line}`).join("\n");
820
+ const added = args.new_string.split("\n").slice(0, 12).map((line) => `+ ${line}`).join("\n");
821
+ return `${args.path ?? ""}
822
+ ${removed}
823
+ ${added}`;
824
+ }
825
+ return JSON.stringify(args, null, 2).slice(0, 1200);
826
+ }
827
+ function ApprovalPrompt({ request, onDecision }) {
828
+ useInput((input, key) => {
829
+ if (input === "y") onDecision("allow");
830
+ else if (input === "a") onDecision("always");
831
+ else if (input === "n" || key.escape) onDecision("deny");
832
+ });
833
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, marginY: 1, children: [
834
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "magenta", children: [
835
+ "Permission needed: ",
836
+ friendlyToolName(request.tool),
837
+ " (",
838
+ request.tool,
839
+ ") \u2014 ",
840
+ request.risk
841
+ ] }),
842
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { children: argPreview(request) }) }),
843
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
844
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "[y]" }),
845
+ " allow once \xB7 ",
846
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "[a]" }),
847
+ " allow this session \xB7",
848
+ " ",
849
+ /* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: "[n]" }),
850
+ " deny"
851
+ ] }) })
852
+ ] });
853
+ }
854
+ var init_Approval = __esm({
855
+ "src/tui/Approval.tsx"() {
856
+ "use strict";
857
+ init_render();
858
+ }
859
+ });
860
+
861
+ // src/tui/SelectDialog.tsx
862
+ import { useEffect, useMemo, useState } from "react";
863
+ import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
864
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
865
+ function SelectDialog({
866
+ title,
867
+ items,
868
+ descriptions,
869
+ loading,
870
+ error,
871
+ current,
872
+ onSelect,
873
+ onClose
874
+ }) {
875
+ const [filter, setFilter] = useState("");
876
+ const [cursor, setCursor] = useState(0);
877
+ const filtered = useMemo(() => {
878
+ const f = filter.trim().toLowerCase();
879
+ return f ? items.filter((item) => item.toLowerCase().includes(f)) : items;
880
+ }, [items, filter]);
881
+ useEffect(() => {
882
+ setCursor(0);
883
+ }, [filter]);
884
+ useInput2((input, key) => {
885
+ if (key.upArrow || input === "k") setCursor((c) => Math.max(0, c - 1));
886
+ else if (key.downArrow || input === "j") setCursor((c) => Math.min(filtered.length - 1, c + 1));
887
+ else if (key.return) {
888
+ const item = filtered[cursor];
889
+ if (item) onSelect(item);
890
+ } else if (key.escape) onClose();
891
+ else if (key.backspace || key.delete) setFilter((f) => f.slice(0, -1));
892
+ else if (input && !key.ctrl && !key.meta) setFilter((f) => f + input);
893
+ });
894
+ const start = Math.max(
895
+ 0,
896
+ Math.min(cursor - Math.floor(VISIBLE / 2), Math.max(0, filtered.length - VISIBLE))
897
+ );
898
+ const visible = filtered.slice(start, start + VISIBLE);
899
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1, children: [
900
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: title }),
901
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
902
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
903
+ "search: ",
904
+ filter
905
+ ] }),
906
+ /* @__PURE__ */ jsx2(Text2, { children: "\u258F" })
907
+ ] }),
908
+ loading ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "loading\u2026" }) : null,
909
+ error ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: error }) : null,
910
+ !loading && !error ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
911
+ visible.map((item, i) => {
912
+ const index = start + i;
913
+ const isCursor = index === cursor;
914
+ const isCurrent = item === current;
915
+ const description = descriptions?.[item];
916
+ return /* @__PURE__ */ jsxs2(Text2, { color: isCursor ? "cyan" : void 0, bold: isCursor, children: [
917
+ isCursor ? "\u276F " : " ",
918
+ isCurrent ? "\u25CF " : "\u25CB ",
919
+ item,
920
+ isCurrent ? " (current)" : "",
921
+ description ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` \u2014 ${description}` }) : null
922
+ ] }, item);
923
+ }),
924
+ filtered.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "no matches" }) : null
925
+ ] }) : null,
926
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2191\u2193 move \xB7 type to filter \xB7 enter select \xB7 esc cancel" }) })
927
+ ] });
928
+ }
929
+ var VISIBLE;
930
+ var init_SelectDialog = __esm({
931
+ "src/tui/SelectDialog.tsx"() {
932
+ "use strict";
933
+ VISIBLE = 10;
934
+ }
935
+ });
936
+
937
+ // src/tui/KeyDialog.tsx
938
+ import { useState as useState2 } from "react";
939
+ import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
940
+ import TextInput from "ink-text-input";
941
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
942
+ function KeyDialog({ providerName, onSubmit, onClose }) {
943
+ const [value, setValue] = useState2("");
944
+ useInput3((_input, key) => {
945
+ if (key.escape) onClose();
946
+ });
947
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, marginY: 1, children: [
948
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "magenta", children: [
949
+ "API key for ",
950
+ providerName
951
+ ] }),
952
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
953
+ /* @__PURE__ */ jsx3(Text3, { children: "key: " }),
954
+ /* @__PURE__ */ jsx3(
955
+ TextInput,
956
+ {
957
+ mask: "*",
958
+ value,
959
+ onChange: setValue,
960
+ onSubmit: (submitted) => {
961
+ if (submitted.trim()) onSubmit(submitted.trim());
962
+ },
963
+ placeholder: "Paste your API key..."
964
+ }
965
+ )
966
+ ] }),
967
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "enter confirm \xB7 esc cancel \xB7 saved to ~/.mentee/config.json" }) })
968
+ ] });
969
+ }
970
+ var init_KeyDialog = __esm({
971
+ "src/tui/KeyDialog.tsx"() {
972
+ "use strict";
973
+ }
974
+ });
975
+
976
+ // src/tui/App.tsx
977
+ var App_exports = {};
978
+ __export(App_exports, {
979
+ App: () => App
980
+ });
981
+ import { useEffect as useEffect2, useMemo as useMemo2, useRef, useState as useState3 } from "react";
982
+ import { Box as Box4, Static, Text as Text4, useApp, useInput as useInput4 } from "ink";
983
+ import TextInput2 from "ink-text-input";
984
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
985
+ function App(props) {
986
+ const { exit } = useApp();
987
+ const bus = useMemo2(() => new EventBus(), []);
988
+ const bridge = useMemo2(() => new ApprovalBridge(), []);
989
+ const [log, setLog] = useState3([]);
990
+ const [running, setRunning] = useState3(false);
991
+ const [taskInput, setTaskInput] = useState3("");
992
+ const [approval, setApproval] = useState3(null);
993
+ const [dialog, setDialog] = useState3(null);
994
+ const [frame, setFrame] = useState3(0);
995
+ const [providerName, setProviderName] = useState3(props.providerName);
996
+ const [model, setModel] = useState3(void 0);
997
+ const [factoryResult, setFactoryResult] = useState3(
998
+ () => props.createProvider(props.providerName)
999
+ );
1000
+ const [modelItems, setModelItems] = useState3([]);
1001
+ const [modelLoading, setModelLoading] = useState3(false);
1002
+ const [modelError, setModelError] = useState3(null);
1003
+ const [finalPending, setFinalPending] = useState3(null);
1004
+ const [typedText, setTypedText] = useState3("");
1005
+ const [typing, setTyping] = useState3(false);
1006
+ const [toolHistory, setToolHistory] = useState3([]);
1007
+ const [inspectorIndex, setInspectorIndex] = useState3(null);
1008
+ const [detailsOpen, setDetailsOpen] = useState3(false);
1009
+ const [showTools, setShowTools] = useState3(true);
1010
+ const pendingTool = useRef(null);
1011
+ const counter = useRef(0);
1012
+ const stateRef = useRef({ running, approval, dialog, factoryResult, providerName, model });
1013
+ stateRef.current = { running, approval, dialog, factoryResult, providerName, model };
1014
+ const appendLog = (text) => {
1015
+ if (!text) return;
1016
+ counter.current += 1;
1017
+ const line = { id: counter.current, text };
1018
+ setLog((prev) => [...prev, line]);
1019
+ };
1020
+ useEffect2(() => {
1021
+ const HIDDEN_NOISY = /* @__PURE__ */ new Set(["tool_started", "tool_completed", "model_request", "info"]);
1022
+ const unsubscribeLog = bus.subscribe((event) => {
1023
+ if (!(showTools === false && HIDDEN_NOISY.has(event.type))) {
1024
+ appendLog(formatEvent(event));
1025
+ }
1026
+ if (event.type === "tool_started") {
1027
+ const name = typeof event.data?.tool === "string" ? event.data.tool : "";
1028
+ const target = typeof event.message === "string" ? event.message : "";
1029
+ const args = typeof event.data?.args === "string" ? event.data.args : "";
1030
+ pendingTool.current = { name, target, args };
1031
+ } else if (event.type === "tool_completed") {
1032
+ const rec = pendingTool.current;
1033
+ const name = typeof event.data?.tool === "string" ? event.data.tool : rec?.name ?? "";
1034
+ const output = typeof event.data?.output === "string" ? event.data.output : "";
1035
+ setToolHistory(
1036
+ (prev) => [...prev, { name, target: rec?.target ?? "", args: rec?.args ?? "", output: output.slice(0, 4e3) }].slice(-40)
1037
+ );
1038
+ pendingTool.current = null;
1039
+ } else if (event.type === "task_completed") {
1040
+ const text = typeof event.data?.finalText === "string" ? event.data.finalText.trim() : "";
1041
+ if (text) {
1042
+ appendLog(text);
1043
+ setFinalPending(text);
1044
+ setTypedText("");
1045
+ setTyping(true);
1046
+ }
1047
+ }
1048
+ });
1049
+ const unsubscribeLog2 = bus.subscribe(createSessionLogger());
1050
+ bridge.setListener((request) => {
1051
+ setApproval(request);
1052
+ if (request) bus.emit("approval", friendlyToolName(request.tool));
1053
+ });
1054
+ return () => {
1055
+ unsubscribeLog();
1056
+ unsubscribeLog2();
1057
+ };
1058
+ }, [bus, bridge, showTools]);
1059
+ useEffect2(() => {
1060
+ if (!running) return;
1061
+ const timer = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80);
1062
+ return () => clearInterval(timer);
1063
+ }, [running]);
1064
+ useEffect2(() => {
1065
+ if (!typing || finalPending == null) return;
1066
+ if (typedText.length >= finalPending.length) {
1067
+ setTyping(false);
1068
+ return;
1069
+ }
1070
+ const timer = setTimeout(() => {
1071
+ setTypedText(finalPending.slice(0, typedText.length + 3));
1072
+ }, 16);
1073
+ return () => clearTimeout(timer);
1074
+ }, [typing, finalPending, typedText]);
1075
+ const updateConfig = (mutate) => {
1076
+ const config = loadConfig() ?? {
1077
+ defaultProvider: "kimi",
1078
+ keys: {},
1079
+ models: {}
1080
+ };
1081
+ mutate(config);
1082
+ saveConfig(config);
1083
+ };
1084
+ const switchProvider = (name) => {
1085
+ const result = props.createProvider(name, stateRef.current.model);
1086
+ setProviderName(name);
1087
+ setFactoryResult(result);
1088
+ if (name !== "mock") {
1089
+ updateConfig((config) => {
1090
+ config.defaultProvider = name;
1091
+ });
1092
+ }
1093
+ if (result.error) {
1094
+ appendLog(`Provider set to ${name}, but no key yet \u2014 press Ctrl+K to add it.`);
1095
+ } else {
1096
+ appendLog(`Provider: ${name}:${stateRef.current.model ?? result.provider?.defaultModel ?? ""}`);
1097
+ }
1098
+ };
1099
+ const setModelAndPersist = (id) => {
1100
+ setModel(id);
1101
+ const name = stateRef.current.providerName;
1102
+ const result = props.createProvider(name, id);
1103
+ if (!result.error) setFactoryResult(result);
1104
+ if (name !== "mock") {
1105
+ updateConfig((config) => {
1106
+ config.models = config.models ?? {};
1107
+ config.models[name] = id;
1108
+ });
1109
+ }
1110
+ appendLog(`Model set to ${id} for ${name}`);
1111
+ };
1112
+ const saveKey = (value) => {
1113
+ const name = stateRef.current.providerName;
1114
+ if (name === "mock") {
1115
+ appendLog("The mock provider needs no key.");
1116
+ return;
1117
+ }
1118
+ const config = loadConfig();
1119
+ const writable = config ?? {
1120
+ defaultProvider: "kimi",
1121
+ keys: {},
1122
+ models: {}
1123
+ };
1124
+ writable.keys[name] = value;
1125
+ saveConfig(writable);
1126
+ const result = props.createProvider(name, stateRef.current.model);
1127
+ setFactoryResult(result);
1128
+ appendLog(
1129
+ result.error ? `Key saved for ${name}, but connection failed: ${result.error}` : `Key saved for ${name}. You are ready to run a task.`
1130
+ );
1131
+ };
1132
+ const openModelDialog = () => {
1133
+ setDialog({ kind: "model" });
1134
+ setModelItems([]);
1135
+ setModelError(null);
1136
+ setModelLoading(true);
1137
+ const active = stateRef.current.factoryResult.provider;
1138
+ if (!active) {
1139
+ setModelLoading(false);
1140
+ setModelError(stateRef.current.factoryResult.error ?? "No provider configured \u2014 press Ctrl+K to add a key first.");
1141
+ return;
1142
+ }
1143
+ active.listModels().then((models) => {
1144
+ setModelItems(models);
1145
+ setModelLoading(false);
1146
+ }).catch((error) => {
1147
+ setModelLoading(false);
1148
+ setModelError(error.message);
1149
+ });
1150
+ };
1151
+ const handleCommand = (raw) => {
1152
+ const parts = raw.slice(1).trim().split(/\s+/);
1153
+ const cmd = parts[0] ?? "";
1154
+ const arg = parts.slice(1).join(" ").trim();
1155
+ switch (cmd) {
1156
+ case "help":
1157
+ appendLog("Commands:");
1158
+ appendLog(" /provider pick provider (Ctrl+P)");
1159
+ appendLog(" /model pick model from live list (Alt+M)");
1160
+ appendLog(" /model <id> set model directly");
1161
+ appendLog(" /key add API key (Ctrl+K)");
1162
+ appendLog(" /status show provider, model, key, workspace");
1163
+ appendLog(" /exit quit");
1164
+ break;
1165
+ case "provider":
1166
+ if (arg) {
1167
+ if (PROVIDER_NAMES.includes(arg)) switchProvider(arg);
1168
+ else appendLog(`Unknown provider "${arg}". Options: ${PROVIDER_NAMES.join(", ")}`);
1169
+ } else {
1170
+ setDialog({ kind: "provider" });
1171
+ }
1172
+ break;
1173
+ case "models":
1174
+ case "model": {
1175
+ if (arg) {
1176
+ setModelAndPersist(arg);
1177
+ } else {
1178
+ openModelDialog();
1179
+ }
1180
+ break;
1181
+ }
1182
+ case "key":
1183
+ if (arg) saveKey(arg);
1184
+ else setDialog({ kind: "key" });
1185
+ break;
1186
+ case "status": {
1187
+ const active = stateRef.current.factoryResult;
1188
+ appendLog(`provider: ${stateRef.current.providerName}`);
1189
+ appendLog(`model: ${stateRef.current.model ?? active.provider?.defaultModel ?? "default"}`);
1190
+ appendLog(
1191
+ `key: ${stateRef.current.providerName === "mock" ? "not needed" : active.provider ? "configured" : "MISSING \u2014 Ctrl+K"}`
1192
+ );
1193
+ appendLog(`workspace: ${props.cwd}`);
1194
+ break;
1195
+ }
1196
+ case "exit":
1197
+ case "quit":
1198
+ exit();
1199
+ setTimeout(() => process.exit(0), 50);
1200
+ break;
1201
+ default:
1202
+ appendLog(`Unknown command "/${cmd}". Try /help`);
1203
+ }
1204
+ };
1205
+ const startTask = async (task) => {
1206
+ if (stateRef.current.running || !task.trim()) return;
1207
+ if (!factoryResult.provider) {
1208
+ appendLog(factoryResult.error ?? "No provider configured. Press Ctrl+K to add an API key.");
1209
+ return;
1210
+ }
1211
+ setFinalPending(null);
1212
+ setTypedText("");
1213
+ setTyping(false);
1214
+ setToolHistory([]);
1215
+ setInspectorIndex(null);
1216
+ setDetailsOpen(false);
1217
+ setRunning(true);
1218
+ try {
1219
+ await runAgent({
1220
+ task,
1221
+ cwd: props.cwd,
1222
+ provider: factoryResult.provider,
1223
+ model,
1224
+ tools: props.tools,
1225
+ bus,
1226
+ approval: props.yes ? async () => "allow" : bridge.handler,
1227
+ maxIterations: props.maxIterations,
1228
+ systemExtra: props.systemExtra
1229
+ });
1230
+ } catch (error) {
1231
+ bus.emit("error", error.message);
1232
+ } finally {
1233
+ setRunning(false);
1234
+ appendLog("Ready for the next task.");
1235
+ }
1236
+ };
1237
+ const handleSubmit = (value) => {
1238
+ const trimmed = value.trim();
1239
+ setTaskInput("");
1240
+ if (!trimmed) return;
1241
+ if (trimmed.startsWith("/")) {
1242
+ handleCommand(trimmed);
1243
+ } else {
1244
+ void startTask(trimmed);
1245
+ }
1246
+ };
1247
+ useEffect2(() => {
1248
+ if (props.initialTask) {
1249
+ void startTask(props.initialTask);
1250
+ }
1251
+ }, []);
1252
+ const cycleInspector = () => {
1253
+ if (toolHistory.length === 0) return;
1254
+ const cur = detailsOpen && inspectorIndex != null ? inspectorIndex : toolHistory.length;
1255
+ const next = cur + 1;
1256
+ if (next >= toolHistory.length) {
1257
+ setDetailsOpen(false);
1258
+ setInspectorIndex(null);
1259
+ } else {
1260
+ setDetailsOpen(true);
1261
+ setInspectorIndex(next);
1262
+ }
1263
+ };
1264
+ useInput4((input, key) => {
1265
+ if (stateRef.current.dialog || stateRef.current.approval || stateRef.current.running) return;
1266
+ if (key.meta && (input === "m" || input === "M")) openModelDialog();
1267
+ else if (key.ctrl && input === "p") setDialog({ kind: "provider" });
1268
+ else if (key.ctrl && input === "k") setDialog({ kind: "key" });
1269
+ else if (input === "d" || input === "D") cycleInspector();
1270
+ else if (input === "t" || input === "T") setShowTools((v) => !v);
1271
+ else if (key.ctrl && input === "c") {
1272
+ exit();
1273
+ setTimeout(() => process.exit(0), 50);
1274
+ }
1275
+ });
1276
+ const modelLabel = model ?? factoryResult.provider?.defaultModel ?? (factoryResult.error ? "no key" : "");
1277
+ const header = /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
1278
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: BANNER }),
1279
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1280
+ "v0.1.0 \xB7 ",
1281
+ providerName,
1282
+ modelLabel ? `:${modelLabel}` : "",
1283
+ " \xB7 tools ",
1284
+ showTools ? "on" : "off",
1285
+ " \xB7 ",
1286
+ props.cwd
1287
+ ] }),
1288
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Alt+M model \xB7 Ctrl+P provider \xB7 Ctrl+K key \xB7 t hide tools \xB7 d details \xB7 /help" })
1289
+ ] }, "header");
1290
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1291
+ /* @__PURE__ */ jsx4(
1292
+ Static,
1293
+ {
1294
+ items: [
1295
+ { key: "header", text: header },
1296
+ ...log.map((line) => ({ key: `l${line.id}`, text: /* @__PURE__ */ jsx4(Text4, { children: line.text }) }))
1297
+ ],
1298
+ children: (item) => /* @__PURE__ */ jsx4(Box4, { children: item.text }, item.key)
1299
+ }
1300
+ ),
1301
+ finalPending !== null ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
1302
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: typedText }),
1303
+ typing ? /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u258C" }) : null
1304
+ ] }) : null,
1305
+ detailsOpen && inspectorIndex !== null && toolHistory[inspectorIndex] ? (() => {
1306
+ const rec = toolHistory[inspectorIndex];
1307
+ const outLines = rec.output.split("\n").slice(0, 24).join("\n");
1308
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
1309
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1310
+ " tool details (",
1311
+ inspectorIndex + 1,
1312
+ "/",
1313
+ toolHistory.length,
1314
+ ") \xB7 press d to cycle "
1315
+ ] }),
1316
+ /* @__PURE__ */ jsxs4(Text4, { color: toolColorName(rec.name), bold: true, children: [
1317
+ rec.name,
1318
+ " ",
1319
+ rec.target
1320
+ ] }),
1321
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1322
+ "args: ",
1323
+ rec.args.slice(0, 400) || "(none)"
1324
+ ] }),
1325
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: outLines || "(no output)" })
1326
+ ] });
1327
+ })() : null,
1328
+ dialog?.kind === "model" ? /* @__PURE__ */ jsx4(
1329
+ SelectDialog,
1330
+ {
1331
+ title: `Model \u2014 ${providerName}`,
1332
+ items: modelItems,
1333
+ loading: modelLoading,
1334
+ error: modelError,
1335
+ current: model ?? factoryResult.provider?.defaultModel,
1336
+ onSelect: (id) => {
1337
+ setDialog(null);
1338
+ setModelAndPersist(id);
1339
+ },
1340
+ onClose: () => setDialog(null)
1341
+ }
1342
+ ) : dialog?.kind === "provider" ? /* @__PURE__ */ jsx4(
1343
+ SelectDialog,
1344
+ {
1345
+ title: "Provider",
1346
+ items: PROVIDER_NAMES,
1347
+ descriptions: PROVIDER_DESCRIPTIONS,
1348
+ current: providerName,
1349
+ onSelect: (name) => {
1350
+ setDialog(null);
1351
+ switchProvider(name);
1352
+ },
1353
+ onClose: () => setDialog(null)
1354
+ }
1355
+ ) : dialog?.kind === "key" ? /* @__PURE__ */ jsx4(
1356
+ KeyDialog,
1357
+ {
1358
+ providerName,
1359
+ onSubmit: (value) => {
1360
+ setDialog(null);
1361
+ saveKey(value);
1362
+ },
1363
+ onClose: () => setDialog(null)
1364
+ }
1365
+ ) : approval ? /* @__PURE__ */ jsx4(
1366
+ ApprovalPrompt,
1367
+ {
1368
+ request: approval,
1369
+ onDecision: (decision) => {
1370
+ bridge.resolve(decision);
1371
+ if (decision === "always") bus.emit("info", "Approved for the rest of this session.");
1372
+ }
1373
+ }
1374
+ ) : running ? /* @__PURE__ */ jsxs4(Box4, { children: [
1375
+ /* @__PURE__ */ jsxs4(Text4, { color: "yellow", children: [
1376
+ SPINNER_FRAMES[frame],
1377
+ " "
1378
+ ] }),
1379
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "agent working... (Ctrl+C to exit)" })
1380
+ ] }) : /* @__PURE__ */ jsxs4(Box4, { children: [
1381
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "\u25B8 " }),
1382
+ /* @__PURE__ */ jsx4(
1383
+ TextInput2,
1384
+ {
1385
+ value: taskInput,
1386
+ onChange: setTaskInput,
1387
+ onSubmit: handleSubmit,
1388
+ placeholder: "Describe a task, or /help"
1389
+ }
1390
+ )
1391
+ ] })
1392
+ ] });
1393
+ }
1394
+ var SPINNER_FRAMES, PROVIDER_NAMES, PROVIDER_DESCRIPTIONS, BANNER;
1395
+ var init_App = __esm({
1396
+ "src/tui/App.tsx"() {
1397
+ "use strict";
1398
+ init_events();
1399
+ init_logging();
1400
+ init_render();
1401
+ init_approval();
1402
+ init_loop();
1403
+ init_config();
1404
+ init_Approval();
1405
+ init_SelectDialog();
1406
+ init_KeyDialog();
1407
+ SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1408
+ PROVIDER_NAMES = ["kimi", "glm", "zai", "zai-coding", "mock"];
1409
+ PROVIDER_DESCRIPTIONS = {
1410
+ kimi: "Moonshot \xB7 api.moonshot.ai",
1411
+ glm: "Zhipu China \xB7 open.bigmodel.cn \xB7 separate account & keys",
1412
+ zai: "Z.ai standard plan \xB7 api.z.ai/api/paas/v4",
1413
+ "zai-coding": "Z.ai GLM Coding Plan subscription \xB7 api.z.ai/api/coding",
1414
+ mock: "offline testing \xB7 no network"
1415
+ };
1416
+ BANNER = ` __ __ _ _____
1417
+ | \\/ | ___ _ __ / \\ | ____|
1418
+ | |\\/| |/ _ \\ '__| _ \\ | _|
1419
+ | | | | __/ | / ___ \\ | |___
1420
+ |_| |_|\\___|_|/_/ \\_\\_____|`;
1421
+ }
1422
+ });
1423
+
1424
+ // src/cli.tsx
1425
+ init_config();
1426
+ import { Command } from "commander";
1427
+ import chalk4 from "chalk";
1428
+
1429
+ // src/models/router.ts
1430
+ init_config();
1431
+
1432
+ // src/models/openai-compat.ts
1433
+ import OpenAI from "openai";
1434
+ function mapFinishReason(reason) {
1435
+ switch (reason) {
1436
+ case "tool_calls":
1437
+ case "function_call":
1438
+ return "tool_calls";
1439
+ case "length":
1440
+ return "length";
1441
+ case "stop":
1442
+ return "stop";
1443
+ default:
1444
+ return "stop";
1445
+ }
1446
+ }
1447
+ function toWireTools(tools) {
1448
+ return tools.map((tool) => ({
1449
+ type: "function",
1450
+ function: {
1451
+ name: tool.name,
1452
+ description: tool.description,
1453
+ parameters: tool.parameters
1454
+ }
1455
+ }));
1456
+ }
1457
+ var OpenAICompatProvider = class {
1458
+ name;
1459
+ defaultModel;
1460
+ client;
1461
+ constructor(options) {
1462
+ this.name = options.name;
1463
+ this.defaultModel = options.defaultModel;
1464
+ this.client = new OpenAI({
1465
+ baseURL: options.baseURL,
1466
+ apiKey: options.apiKey
1467
+ });
1468
+ }
1469
+ async generate(request, model) {
1470
+ const messages = [
1471
+ { role: "system", content: request.system },
1472
+ ...request.messages
1473
+ ];
1474
+ const response = await this.client.chat.completions.create({
1475
+ model: model ?? this.defaultModel,
1476
+ // the wire format matches our internal shape; keep loose typing at the boundary
1477
+ messages,
1478
+ ...request.tools.length > 0 ? { tools: toWireTools(request.tools) } : {},
1479
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
1480
+ ...request.maxTokens ? { max_tokens: request.maxTokens } : {}
1481
+ });
1482
+ const choice = response.choices[0];
1483
+ const message = choice?.message;
1484
+ const usage = response.usage ? {
1485
+ inputTokens: response.usage.prompt_tokens ?? 0,
1486
+ outputTokens: response.usage.completion_tokens ?? 0
1487
+ } : void 0;
1488
+ return {
1489
+ content: message?.content ?? null,
1490
+ toolCalls: message?.tool_calls ?? [],
1491
+ finishReason: mapFinishReason(choice?.finish_reason),
1492
+ usage
1493
+ };
1494
+ }
1495
+ async listModels() {
1496
+ const page = await this.client.models.list();
1497
+ const ids = [];
1498
+ for await (const model of page) {
1499
+ ids.push(model.id);
1500
+ }
1501
+ return ids.sort();
1502
+ }
1503
+ };
1504
+
1505
+ // src/models/glm.ts
1506
+ function createGlmProvider(apiKey, model) {
1507
+ return new OpenAICompatProvider({
1508
+ name: "glm",
1509
+ defaultModel: model ?? "glm-4.6",
1510
+ baseURL: "https://open.bigmodel.cn/api/paas/v4",
1511
+ apiKey
1512
+ });
1513
+ }
1514
+
1515
+ // src/models/kimi.ts
1516
+ function createKimiProvider(apiKey, model) {
1517
+ return new OpenAICompatProvider({
1518
+ name: "kimi",
1519
+ defaultModel: model ?? "kimi-k2.7-code",
1520
+ baseURL: "https://api.moonshot.ai/v1",
1521
+ apiKey
1522
+ });
1523
+ }
1524
+
1525
+ // src/models/mock.ts
1526
+ var callCounter = 0;
1527
+ var MockProvider = class {
1528
+ constructor(script) {
1529
+ this.script = script;
1530
+ }
1531
+ script;
1532
+ name = "mock";
1533
+ defaultModel = "mock-1";
1534
+ step = 0;
1535
+ async listModels() {
1536
+ return ["mock-1", "mock-2", "mock-large"];
1537
+ }
1538
+ async generate(_request, _model) {
1539
+ const scripted = this.script[this.step] ?? { content: "No more scripted steps remain." };
1540
+ this.step += 1;
1541
+ const toolCalls = (scripted.toolCalls ?? []).map((call) => {
1542
+ callCounter += 1;
1543
+ return {
1544
+ id: `call_${callCounter}`,
1545
+ type: "function",
1546
+ function: {
1547
+ name: call.name,
1548
+ arguments: JSON.stringify(call.args)
1549
+ }
1550
+ };
1551
+ });
1552
+ return {
1553
+ content: toolCalls.length > 0 ? scripted.content ?? null : scripted.content ?? "Task complete.",
1554
+ toolCalls,
1555
+ finishReason: toolCalls.length > 0 ? "tool_calls" : "stop",
1556
+ usage: { inputTokens: 10, outputTokens: 10 }
1557
+ };
1558
+ }
1559
+ };
1560
+
1561
+ // src/models/zai.ts
1562
+ function createZaiProvider(apiKey, model) {
1563
+ return new OpenAICompatProvider({
1564
+ name: "zai",
1565
+ defaultModel: model ?? "glm-4.6",
1566
+ baseURL: "https://api.z.ai/api/paas/v4",
1567
+ apiKey
1568
+ });
1569
+ }
1570
+ function createZaiCodingProvider(apiKey, model) {
1571
+ return new OpenAICompatProvider({
1572
+ name: "zai-coding",
1573
+ defaultModel: model ?? "glm-4.6",
1574
+ baseURL: "https://api.z.ai/api/coding/paas/v4",
1575
+ apiKey
1576
+ });
1577
+ }
1578
+
1579
+ // src/models/router.ts
1580
+ var ENV_NAMES2 = {
1581
+ kimi: "MENTEE_KIMI_API_KEY",
1582
+ glm: "MENTEE_GLM_API_KEY",
1583
+ zai: "MENTEE_ZAI_API_KEY",
1584
+ "zai-coding": "MENTEE_ZAI_API_KEY"
1585
+ };
1586
+ var MissingApiKeyError = class extends Error {
1587
+ constructor(provider) {
1588
+ super(
1589
+ `No API key for provider "${provider}". Run "mentee config", set ${ENV_NAMES2[provider]}, or type /key <your-key> in the TUI.`
1590
+ );
1591
+ this.name = "MissingApiKeyError";
1592
+ }
1593
+ };
1594
+ function createProvider(provider, config, modelOverride) {
1595
+ if (provider === "mock") {
1596
+ return new MockProvider([]);
1597
+ }
1598
+ const apiKey = resolveApiKey(provider, config);
1599
+ if (!apiKey) {
1600
+ throw new MissingApiKeyError(provider);
1601
+ }
1602
+ const model = modelOverride ?? resolveModel(provider, config);
1603
+ if (provider === "kimi") {
1604
+ return createKimiProvider(apiKey, model);
1605
+ }
1606
+ if (provider === "zai") {
1607
+ return createZaiProvider(apiKey, model);
1608
+ }
1609
+ if (provider === "zai-coding") {
1610
+ return createZaiCodingProvider(apiKey, model);
1611
+ }
1612
+ return createGlmProvider(apiKey, model);
1613
+ }
1614
+
1615
+ // src/tools/filesystem.ts
1616
+ init_base();
1617
+ import fs2 from "fs";
1618
+ import path2 from "path";
1619
+ function resolveWithinWorkspace(cwd, target) {
1620
+ const resolved = path2.resolve(cwd, target);
1621
+ const normalizedCwd = path2.resolve(cwd);
1622
+ if (resolved === normalizedCwd) return resolved;
1623
+ if (!resolved.startsWith(normalizedCwd + path2.sep)) {
1624
+ throw new Error(`Path escapes the workspace: ${target}`);
1625
+ }
1626
+ return resolved;
1627
+ }
1628
+ function relativeToWorkspace(cwd, target) {
1629
+ return path2.relative(path2.resolve(cwd), target).split(path2.sep).join("/");
1630
+ }
1631
+ var listFiles = {
1632
+ name: "list_files",
1633
+ description: "List files and directories under a path inside the workspace. Respects common ignore rules (node_modules, .git, dist, ...). Use this to orient yourself in the repository.",
1634
+ risk: "safe",
1635
+ parameters: {
1636
+ type: "object",
1637
+ properties: {
1638
+ path: {
1639
+ type: "string",
1640
+ description: "Relative path inside the workspace. Defaults to the workspace root."
1641
+ },
1642
+ max_entries: {
1643
+ type: "number",
1644
+ description: "Maximum number of entries to return. Defaults to 500."
1645
+ }
1646
+ }
1647
+ },
1648
+ async execute(args, ctx) {
1649
+ const root = resolveWithinWorkspace(ctx.cwd, firstString(args, "path") ?? ".");
1650
+ const maxEntries = firstNumber(args, "max_entries") ?? 500;
1651
+ const ignore = /* @__PURE__ */ new Set([
1652
+ "node_modules",
1653
+ ".git",
1654
+ "dist",
1655
+ "build",
1656
+ "out",
1657
+ ".next",
1658
+ ".nuxt",
1659
+ "coverage",
1660
+ "__pycache__",
1661
+ ".venv",
1662
+ "venv",
1663
+ ".pytest_cache",
1664
+ ".mypy_cache",
1665
+ ".ruff_cache",
1666
+ "target",
1667
+ "vendor"
1668
+ ]);
1669
+ const lines = [];
1670
+ const walk = (dir, depth) => {
1671
+ if (depth > 6 || lines.length >= maxEntries) return;
1672
+ let entries;
1673
+ try {
1674
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
1675
+ } catch {
1676
+ return;
1677
+ }
1678
+ entries.sort((a, b) => {
1679
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
1680
+ return a.name.localeCompare(b.name);
1681
+ });
1682
+ for (const entry of entries) {
1683
+ if (lines.length >= maxEntries) {
1684
+ lines.push("...[entry limit reached]");
1685
+ return;
1686
+ }
1687
+ if (ignore.has(entry.name)) continue;
1688
+ const rel = relativeToWorkspace(ctx.cwd, path2.join(dir, entry.name));
1689
+ if (entry.isDirectory()) {
1690
+ lines.push(`${rel}/`);
1691
+ walk(path2.join(dir, entry.name), depth + 1);
1692
+ } else {
1693
+ lines.push(rel);
1694
+ }
1695
+ }
1696
+ };
1697
+ walk(root, 0);
1698
+ if (lines.length === 0) return { success: true, output: "(empty directory)" };
1699
+ return { success: true, output: lines.join("\n") };
1700
+ }
1701
+ };
1702
+ var movePath = {
1703
+ name: "move_path",
1704
+ description: "Move or rename a file or directory within the workspace. Requires user approval. Parent directories of the destination are created automatically.",
1705
+ risk: "restricted",
1706
+ parameters: {
1707
+ type: "object",
1708
+ properties: {
1709
+ from: { type: "string", description: "Source path relative to the workspace root." },
1710
+ to: { type: "string", description: "Destination path relative to the workspace root." }
1711
+ },
1712
+ required: ["from", "to"]
1713
+ },
1714
+ async execute(args, ctx) {
1715
+ const fromPath = resolveWithinWorkspace(ctx.cwd, requireString(args, "from"));
1716
+ const toPath = resolveWithinWorkspace(ctx.cwd, requireString(args, "to"));
1717
+ if (!fs2.existsSync(fromPath)) {
1718
+ return { success: false, output: `Source does not exist: ${relativeToWorkspace(ctx.cwd, fromPath)}` };
1719
+ }
1720
+ if (fs2.existsSync(toPath)) {
1721
+ return { success: false, output: `Destination already exists: ${relativeToWorkspace(ctx.cwd, toPath)}` };
1722
+ }
1723
+ fs2.mkdirSync(path2.dirname(toPath), { recursive: true });
1724
+ fs2.renameSync(fromPath, toPath);
1725
+ return {
1726
+ success: true,
1727
+ output: `Moved ${relativeToWorkspace(ctx.cwd, fromPath)} -> ${relativeToWorkspace(ctx.cwd, toPath)}`
1728
+ };
1729
+ }
1730
+ };
1731
+ var readFile = {
1732
+ name: "read_file",
1733
+ description: "Read a file from the workspace, optionally restricted to a line range. Output is prefixed with line numbers. Prefer reading ranges of large files instead of whole files.",
1734
+ risk: "safe",
1735
+ parameters: {
1736
+ type: "object",
1737
+ properties: {
1738
+ path: { type: "string", description: "File path relative to the workspace root." },
1739
+ start_line: { type: "number", description: "First line to read (1-based). Defaults to 1." },
1740
+ end_line: { type: "number", description: "Last line to read (inclusive). Defaults to start_line + 2000." }
1741
+ },
1742
+ required: ["path"]
1743
+ },
1744
+ async execute(args, ctx) {
1745
+ const filePath = resolveWithinWorkspace(ctx.cwd, requireString(args, "path"));
1746
+ let content;
1747
+ try {
1748
+ content = fs2.readFileSync(filePath, "utf8");
1749
+ } catch (error) {
1750
+ return { success: false, output: `Cannot read file: ${error.message}` };
1751
+ }
1752
+ const lines = content.split(/\r?\n/);
1753
+ const start = Math.max(1, firstNumber(args, "start_line") ?? 1);
1754
+ const defaultEnd = start + 2e3 - 1;
1755
+ const end = Math.min(lines.length, firstNumber(args, "end_line") ?? defaultEnd);
1756
+ if (start > lines.length) {
1757
+ return {
1758
+ success: false,
1759
+ output: `start_line ${start} is beyond end of file (${lines.length} lines)`
1760
+ };
1761
+ }
1762
+ const slice = lines.slice(start - 1, end).map((line, i) => `${start + i}: ${line}`);
1763
+ const { text, truncated } = truncateOutput(slice.join("\n"), 5e4);
1764
+ const note = end < lines.length ? `
1765
+ ...[${lines.length - end} more lines. Use start_line=${end + 1} to continue reading.]` : "";
1766
+ return {
1767
+ success: true,
1768
+ output: `${filePath === path2.resolve(ctx.cwd) ? "(file)" : relativeToWorkspace(ctx.cwd, filePath)} (${lines.length} lines)
1769
+ ${text}${note}`,
1770
+ truncated
1771
+ };
1772
+ }
1773
+ };
1774
+ var writeFile = {
1775
+ name: "write_file",
1776
+ description: "Create a new file or fully overwrite an existing file with the given content inside the workspace. For modifying existing files prefer apply_patch.",
1777
+ risk: "safe",
1778
+ parameters: {
1779
+ type: "object",
1780
+ properties: {
1781
+ path: { type: "string", description: "File path relative to the workspace root." },
1782
+ content: { type: "string", description: "The full file content to write." }
1783
+ },
1784
+ required: ["path", "content"]
1785
+ },
1786
+ async execute(args, ctx) {
1787
+ const filePath = resolveWithinWorkspace(ctx.cwd, requireString(args, "path"));
1788
+ const content = requireString(args, "content");
1789
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
1790
+ fs2.writeFileSync(filePath, content, "utf8");
1791
+ return {
1792
+ success: true,
1793
+ output: `Wrote ${Buffer.byteLength(content, "utf8")} bytes to ${relativeToWorkspace(ctx.cwd, filePath)}`
1794
+ };
1795
+ }
1796
+ };
1797
+ var applyPatch = {
1798
+ name: "apply_patch",
1799
+ description: "Apply a minimal edit to an existing file by replacing an exact unique snippet (old_string) with new_string. The snippet must appear exactly once in the file. This is the preferred way to modify files: it makes the smallest possible change.",
1800
+ risk: "safe",
1801
+ parameters: {
1802
+ type: "object",
1803
+ properties: {
1804
+ path: { type: "string", description: "File path relative to the workspace root." },
1805
+ old_string: { type: "string", description: "The exact text to replace. Must occur exactly once in the file." },
1806
+ new_string: { type: "string", description: "The replacement text." }
1807
+ },
1808
+ required: ["path", "old_string", "new_string"]
1809
+ },
1810
+ async execute(args, ctx) {
1811
+ const filePath = resolveWithinWorkspace(ctx.cwd, requireString(args, "path"));
1812
+ const oldString = requireString(args, "old_string");
1813
+ const newString = requireString(args, "new_string");
1814
+ let content;
1815
+ try {
1816
+ content = fs2.readFileSync(filePath, "utf8");
1817
+ } catch (error) {
1818
+ return { success: false, output: `Cannot read file: ${error.message}` };
1819
+ }
1820
+ if (oldString === newString) {
1821
+ return { success: false, output: "old_string and new_string are identical; nothing to change." };
1822
+ }
1823
+ const occurrences = content.split(oldString).length - 1;
1824
+ if (occurrences === 0) {
1825
+ return {
1826
+ success: false,
1827
+ output: "old_string not found in file. Read the file again and copy the exact text, including whitespace."
1828
+ };
1829
+ }
1830
+ if (occurrences > 1) {
1831
+ return {
1832
+ success: false,
1833
+ output: `old_string occurs ${occurrences} times. Include more surrounding lines to make it unique.`
1834
+ };
1835
+ }
1836
+ const updated = content.replace(oldString, newString);
1837
+ fs2.writeFileSync(filePath, updated, "utf8");
1838
+ const removed = oldString.split(/\r?\n/).length;
1839
+ const added = newString.split(/\r?\n/).length;
1840
+ return {
1841
+ success: true,
1842
+ output: `Patched ${relativeToWorkspace(ctx.cwd, filePath)} (-${removed}/+${added} lines)`,
1843
+ data: {
1844
+ kind: "patch",
1845
+ path: relativeToWorkspace(ctx.cwd, filePath),
1846
+ old_string: oldString,
1847
+ new_string: newString
1848
+ }
1849
+ };
1850
+ }
1851
+ };
1852
+
1853
+ // src/tools/git.ts
1854
+ init_base();
1855
+ import { spawnSync } from "child_process";
1856
+ function runGit(cwd, args, maxChars) {
1857
+ for (const arg of args) {
1858
+ if (/[;&|`$<>\n]/.test(arg)) {
1859
+ return { success: false, output: `Invalid characters in git argument: ${arg}` };
1860
+ }
1861
+ }
1862
+ const command = ["git", ...args].join(" ");
1863
+ const result = spawnSync(command, {
1864
+ cwd,
1865
+ encoding: "utf8",
1866
+ maxBuffer: 10 * 1024 * 1024,
1867
+ shell: true,
1868
+ windowsHide: true
1869
+ });
1870
+ if (result.error) {
1871
+ return { success: false, output: `git failed: ${result.error.message}` };
1872
+ }
1873
+ const stderr = (result.stderr ?? "").trim();
1874
+ if (result.status !== 0) {
1875
+ const message = stderr || `git exited with code ${result.status}`;
1876
+ if (/not a git repository/i.test(message)) {
1877
+ return { success: false, output: "This workspace is not a git repository." };
1878
+ }
1879
+ return { success: false, output: message };
1880
+ }
1881
+ const { text, truncated } = truncateOutput((result.stdout ?? "").trim(), maxChars);
1882
+ return { success: true, output: text || "(no output)", truncated };
1883
+ }
1884
+ var gitBranches = {
1885
+ name: "git_branches",
1886
+ description: "List all git branches (local and remote) for the workspace.",
1887
+ risk: "safe",
1888
+ parameters: { type: "object", properties: {} },
1889
+ async execute(_args, ctx) {
1890
+ return runGit(ctx.cwd, ["branch", "-a"], 1e4);
1891
+ }
1892
+ };
1893
+ var gitStatus = {
1894
+ name: "git_status",
1895
+ description: "Show git status (branch + short status) for the workspace.",
1896
+ risk: "safe",
1897
+ parameters: { type: "object", properties: {} },
1898
+ async execute(_args, ctx) {
1899
+ return runGit(ctx.cwd, ["status", "--short", "--branch"], 2e4);
1900
+ }
1901
+ };
1902
+ var gitDiff = {
1903
+ name: "git_diff",
1904
+ description: "Show the current uncommitted diff. Use this to review exactly what has changed.",
1905
+ risk: "safe",
1906
+ parameters: {
1907
+ type: "object",
1908
+ properties: {
1909
+ staged: { type: "boolean", description: "Show staged changes only. Defaults to false (unstaged)." }
1910
+ }
1911
+ },
1912
+ async execute(args, ctx) {
1913
+ const staged = firstBoolean(args, "staged") ?? false;
1914
+ return runGit(ctx.cwd, staged ? ["diff", "--staged"] : ["diff"], 5e4);
1915
+ }
1916
+ };
1917
+ var gitLog = {
1918
+ name: "git_log",
1919
+ description: "Show recent commit history (one line per commit).",
1920
+ risk: "safe",
1921
+ parameters: {
1922
+ type: "object",
1923
+ properties: {
1924
+ limit: { type: "number", description: "Number of commits. Defaults to 10." }
1925
+ }
1926
+ },
1927
+ async execute(args, ctx) {
1928
+ const limit = Math.min(100, firstNumber(args, "limit") ?? 10);
1929
+ return runGit(ctx.cwd, ["log", "--oneline", `-n`, String(limit)], 2e4);
1930
+ }
1931
+ };
1932
+ var gitShow = {
1933
+ name: "git_show",
1934
+ description: "Show a commit (message + diff) for a given ref.",
1935
+ risk: "safe",
1936
+ parameters: {
1937
+ type: "object",
1938
+ properties: {
1939
+ ref: { type: "string", description: "Commit ref. Defaults to HEAD." },
1940
+ stat: { type: "boolean", description: "Show only the file stat summary. Defaults to false." }
1941
+ }
1942
+ },
1943
+ async execute(args, ctx) {
1944
+ const ref = firstString(args, "ref") ?? "HEAD";
1945
+ const stat = firstBoolean(args, "stat") ?? false;
1946
+ const args_ = stat ? ["show", "--stat", ref] : ["show", ref];
1947
+ return runGit(ctx.cwd, args_, 5e4);
1948
+ }
1949
+ };
1950
+
1951
+ // src/tools/search.ts
1952
+ init_base();
1953
+ import { spawnSync as spawnSync2 } from "child_process";
1954
+ import fs3 from "fs";
1955
+ import path3 from "path";
1956
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
1957
+ "node_modules",
1958
+ ".git",
1959
+ "dist",
1960
+ "build",
1961
+ "out",
1962
+ ".next",
1963
+ ".nuxt",
1964
+ "coverage",
1965
+ "__pycache__",
1966
+ ".venv",
1967
+ "venv",
1968
+ ".pytest_cache",
1969
+ ".mypy_cache",
1970
+ ".ruff_cache",
1971
+ "target",
1972
+ "vendor"
1973
+ ]);
1974
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
1975
+ ".ts",
1976
+ ".tsx",
1977
+ ".js",
1978
+ ".jsx",
1979
+ ".mjs",
1980
+ ".cjs",
1981
+ ".json",
1982
+ ".md",
1983
+ ".txt",
1984
+ ".css",
1985
+ ".scss",
1986
+ ".html",
1987
+ ".vue",
1988
+ ".svelte",
1989
+ ".py",
1990
+ ".rb",
1991
+ ".go",
1992
+ ".rs",
1993
+ ".java",
1994
+ ".kt",
1995
+ ".c",
1996
+ ".h",
1997
+ ".cpp",
1998
+ ".hpp",
1999
+ ".cs",
2000
+ ".php",
2001
+ ".sh",
2002
+ ".ps1",
2003
+ ".bat",
2004
+ ".yaml",
2005
+ ".yml",
2006
+ ".toml",
2007
+ ".ini",
2008
+ ".cfg",
2009
+ ".sql",
2010
+ ".graphql",
2011
+ ".prisma",
2012
+ ".env",
2013
+ ".gitignore",
2014
+ ".dockerfile",
2015
+ "dockerfile",
2016
+ ".xml"
2017
+ ]);
2018
+ function isProbablyTextFile(filePath) {
2019
+ const ext = path3.extname(filePath).toLowerCase();
2020
+ if (TEXT_EXTENSIONS.has(ext)) return true;
2021
+ const base = path3.basename(filePath).toLowerCase();
2022
+ return base === "dockerfile" || base === "makefile" || base === "license" || base === "readme";
2023
+ }
2024
+ function walkFiles(root, maxFiles) {
2025
+ const files = [];
2026
+ const walk = (dir, depth) => {
2027
+ if (depth > 10 || files.length >= maxFiles) return;
2028
+ let entries;
2029
+ try {
2030
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
2031
+ } catch {
2032
+ return;
2033
+ }
2034
+ for (const entry of entries) {
2035
+ if (files.length >= maxFiles) return;
2036
+ const full = path3.join(dir, entry.name);
2037
+ if (entry.isDirectory()) {
2038
+ if (IGNORE_DIRS.has(entry.name)) continue;
2039
+ walk(full, depth + 1);
2040
+ } else if (entry.isFile()) {
2041
+ files.push(full);
2042
+ }
2043
+ }
2044
+ };
2045
+ walk(root, 0);
2046
+ return files;
2047
+ }
2048
+ function globToRegExp(glob) {
2049
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\u0000/g, ".*");
2050
+ return new RegExp(`^${escaped}$`, "i");
2051
+ }
2052
+ function ripgrepAvailable() {
2053
+ try {
2054
+ const result = spawnSync2("rg --version", { stdio: "ignore", shell: true });
2055
+ return !result.error && result.status === 0;
2056
+ } catch {
2057
+ return false;
2058
+ }
2059
+ }
2060
+ var searchCode = {
2061
+ name: "search_code",
2062
+ description: "Search file contents across the workspace for a pattern (regex supported). Faster and more context-efficient than reading many files. Returns matching file paths, line numbers, and lines.",
2063
+ risk: "safe",
2064
+ parameters: {
2065
+ type: "object",
2066
+ properties: {
2067
+ pattern: { type: "string", description: "Regex or literal text to search for." },
2068
+ path: { type: "string", description: "Subdirectory to search. Defaults to the workspace root." },
2069
+ glob: { type: "string", description: 'Optional file glob filter, e.g. "*.ts" or "src/**".' },
2070
+ max_results: { type: "number", description: "Maximum matches to return. Defaults to 50." }
2071
+ },
2072
+ required: ["pattern"]
2073
+ },
2074
+ async execute(args, ctx) {
2075
+ const pattern = firstString(args, "pattern");
2076
+ if (!pattern) return { success: false, output: "Missing required argument: pattern" };
2077
+ const root = resolveWithinWorkspace(ctx.cwd, firstString(args, "path") ?? ".");
2078
+ const glob = firstString(args, "glob");
2079
+ const maxResults = Math.min(200, firstNumber(args, "max_results") ?? 50);
2080
+ let regex;
2081
+ try {
2082
+ regex = new RegExp(pattern, "i");
2083
+ } catch {
2084
+ regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
2085
+ }
2086
+ const matches = [];
2087
+ let truncated = false;
2088
+ if (ripgrepAvailable()) {
2089
+ const rgArgs = ["rg", "--no-heading", "--line-number", "--max-count", String(maxResults), "-S", pattern];
2090
+ if (glob) rgArgs.push("--glob", glob);
2091
+ rgArgs.push(".");
2092
+ const result = spawnSync2(rgArgs.join(" "), {
2093
+ cwd: root,
2094
+ shell: true,
2095
+ encoding: "utf8",
2096
+ maxBuffer: 10 * 1024 * 1024
2097
+ });
2098
+ const stdout = result.stdout ?? "";
2099
+ for (const line of stdout.split(/\r?\n/)) {
2100
+ if (!line) continue;
2101
+ if (matches.length >= maxResults) {
2102
+ truncated = true;
2103
+ break;
2104
+ }
2105
+ matches.push(line);
2106
+ }
2107
+ } else {
2108
+ const globRe = glob ? globToRegExp(glob) : null;
2109
+ const files = walkFiles(root, 5e3);
2110
+ for (const file of files) {
2111
+ if (matches.length >= maxResults) {
2112
+ truncated = true;
2113
+ break;
2114
+ }
2115
+ if (globRe && !globRe.test(path3.relative(root, file).split(path3.sep).join("/"))) continue;
2116
+ if (!isProbablyTextFile(file)) continue;
2117
+ let content;
2118
+ try {
2119
+ const stat = fs3.statSync(file);
2120
+ if (stat.size > 1024 * 1024) continue;
2121
+ content = fs3.readFileSync(file, "utf8");
2122
+ } catch {
2123
+ continue;
2124
+ }
2125
+ const rel = path3.relative(root, file).split(path3.sep).join("/");
2126
+ const lines = content.split(/\r?\n/);
2127
+ for (let i = 0; i < lines.length; i++) {
2128
+ if (regex.test(lines[i] ?? "")) {
2129
+ matches.push(`${rel}:${i + 1}: ${(lines[i] ?? "").trim().slice(0, 200)}`);
2130
+ if (matches.length >= maxResults) {
2131
+ truncated = true;
2132
+ break;
2133
+ }
2134
+ }
2135
+ }
2136
+ }
2137
+ }
2138
+ if (matches.length === 0) return { success: true, output: "No matches found." };
2139
+ const { text } = truncateOutput(matches.join("\n"), 4e4);
2140
+ return {
2141
+ success: true,
2142
+ output: `${matches.length}${truncated ? "+" : ""} matches:
2143
+ ${text}`,
2144
+ truncated
2145
+ };
2146
+ }
2147
+ };
2148
+ var searchFiles = {
2149
+ name: "search_files",
2150
+ description: "Find files by name pattern (supports * and ? wildcards) inside the workspace. Use when you know roughly what a file is called.",
2151
+ risk: "safe",
2152
+ parameters: {
2153
+ type: "object",
2154
+ properties: {
2155
+ pattern: { type: "string", description: 'File name pattern with wildcards, e.g. "*.test.ts" or "config*". Matches against the full relative path too.' },
2156
+ path: { type: "string", description: "Subdirectory to search. Defaults to the workspace root." },
2157
+ max_results: { type: "number", description: "Maximum results. Defaults to 100." }
2158
+ },
2159
+ required: ["pattern"]
2160
+ },
2161
+ async execute(args, ctx) {
2162
+ const pattern = firstString(args, "pattern");
2163
+ if (!pattern) return { success: false, output: "Missing required argument: pattern" };
2164
+ const root = resolveWithinWorkspace(ctx.cwd, firstString(args, "path") ?? ".");
2165
+ const maxResults = firstNumber(args, "max_results") ?? 100;
2166
+ const nameRe = globToRegExp(pattern.includes("/") ? pattern : `*${pattern}*`);
2167
+ const files = walkFiles(root, 1e4);
2168
+ const matches = [];
2169
+ for (const file of files) {
2170
+ const rel = path3.relative(root, file).split(path3.sep).join("/");
2171
+ const base = path3.basename(file);
2172
+ if (nameRe.test(base) || nameRe.test(rel)) {
2173
+ matches.push(rel);
2174
+ if (matches.length >= maxResults) break;
2175
+ }
2176
+ }
2177
+ if (matches.length === 0) return { success: true, output: "No files matched." };
2178
+ return { success: true, output: `${matches.length} files:
2179
+ ${matches.join("\n")}` };
2180
+ }
2181
+ };
2182
+
2183
+ // src/tools/terminal.ts
2184
+ init_base();
2185
+
2186
+ // src/tools/policy.ts
2187
+ var SAFE_EXACT = /* @__PURE__ */ new Set([
2188
+ "pwd",
2189
+ "ls",
2190
+ "dir",
2191
+ "cat",
2192
+ "type",
2193
+ "echo",
2194
+ "head",
2195
+ "tail",
2196
+ "wc",
2197
+ "tree",
2198
+ "whoami",
2199
+ "which",
2200
+ "where",
2201
+ "hostname",
2202
+ "date",
2203
+ "node --version",
2204
+ "node -v",
2205
+ "npm --version",
2206
+ "npm -v",
2207
+ "npx --version",
2208
+ "npm test",
2209
+ "npm run",
2210
+ "python --version",
2211
+ "python3 --version",
2212
+ "py --version",
2213
+ "pip --version",
2214
+ "pip3 --version",
2215
+ "pytest --version",
2216
+ "tsc --version",
2217
+ "tsc --noEmit",
2218
+ "go version",
2219
+ "cargo --version",
2220
+ "rustc --version",
2221
+ "java -version",
2222
+ "dotnet --version"
2223
+ ]);
2224
+ var SAFE_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
2225
+ "status",
2226
+ "log",
2227
+ "diff",
2228
+ "branch",
2229
+ "show",
2230
+ "rev-parse",
2231
+ "blame",
2232
+ "shortlog",
2233
+ "describe"
2234
+ ]);
2235
+ var SAFE_NPM_SUBCOMMANDS = /* @__PURE__ */ new Set(["test", "run", "start"]);
2236
+ var DANGEROUS_PATTERNS = [
2237
+ /\brm\s+(-[a-z]*\s+)*-[a-z]*[rf][a-z]*/i,
2238
+ /\brm\s+--recursive/i,
2239
+ /\bsudo\b/i,
2240
+ /\bformat\b/i,
2241
+ /\bmkfs/i,
2242
+ /\bdd\s+if=/i,
2243
+ /\bshutdown\b/i,
2244
+ /\breboot\b/i,
2245
+ /\bdel\s+\/[sq]/i,
2246
+ /\brd\s+\/s/i,
2247
+ /\brmdir\s+\/s/i,
2248
+ /\bremove-item\b.*-recurse/i,
2249
+ /\bchmod\s+777\b/i,
2250
+ /\bchown\b/i,
2251
+ /\breg\s+(delete|add)\b/i,
2252
+ /\binvoke-expression\b/i,
2253
+ /\biex\b/i,
2254
+ /\|\s*(sh|bash|zsh|pwsh|powershell)\b/i,
2255
+ /\b(curl|wget)\b.*\|\s*(sh|bash|zsh|pwsh|powershell)\b/i,
2256
+ /\bdrop\s+(table|database)\b/i,
2257
+ /\bgit\s+push\s+--force\b/i,
2258
+ /\bgit\s+reset\s+--hard\b/i,
2259
+ /\bgit\s+clean\s+-[a-z]*f/i
2260
+ ];
2261
+ var RESTRICTED_PREFIXES = [
2262
+ "npm install",
2263
+ "npm i ",
2264
+ "npm uninstall",
2265
+ "pip install",
2266
+ "pip3 install",
2267
+ "pip uninstall",
2268
+ "yarn add",
2269
+ "yarn install",
2270
+ "pnpm add",
2271
+ "pnpm install",
2272
+ "docker",
2273
+ "docker-compose",
2274
+ "git checkout",
2275
+ "git switch",
2276
+ "git reset",
2277
+ "git restore",
2278
+ "git commit",
2279
+ "git push",
2280
+ "git pull",
2281
+ "git merge",
2282
+ "git rebase",
2283
+ "git stash",
2284
+ "git tag",
2285
+ "git clean",
2286
+ "git rm",
2287
+ "git config",
2288
+ "mkdir",
2289
+ "md ",
2290
+ "mv ",
2291
+ "move ",
2292
+ "cp ",
2293
+ "copy ",
2294
+ "touch",
2295
+ "new-item",
2296
+ "del ",
2297
+ "erase ",
2298
+ "rd ",
2299
+ "rmdir",
2300
+ "chmod",
2301
+ "attrib",
2302
+ "curl",
2303
+ "wget",
2304
+ "ssh",
2305
+ "scp",
2306
+ "kill",
2307
+ "taskkill",
2308
+ "node ",
2309
+ "python ",
2310
+ "python3 ",
2311
+ "dotnet ",
2312
+ "go ",
2313
+ "cargo ",
2314
+ "java ",
2315
+ "npx "
2316
+ ];
2317
+ function splitCommandChain(command) {
2318
+ return command.split(/(?:\|\||&&|;|\|)/).map((part) => part.trim()).filter((part) => part.length > 0);
2319
+ }
2320
+ function classifySingle(command) {
2321
+ const cmd = command.trim().toLowerCase().replace(/\s+/g, " ");
2322
+ if (cmd.length === 0) return "safe";
2323
+ for (const pattern of DANGEROUS_PATTERNS) {
2324
+ if (pattern.test(cmd)) return "dangerous";
2325
+ }
2326
+ if (cmd.startsWith("git ")) {
2327
+ const sub = cmd.slice(4).split(" ")[0] ?? "";
2328
+ if (SAFE_GIT_SUBCOMMANDS.has(sub)) return "safe";
2329
+ return "restricted";
2330
+ }
2331
+ if (cmd.startsWith("npm ")) {
2332
+ const sub = cmd.slice(4).split(" ")[0] ?? "";
2333
+ if (SAFE_NPM_SUBCOMMANDS.has(sub)) return "safe";
2334
+ return "restricted";
2335
+ }
2336
+ if (cmd.startsWith("pytest") || cmd.startsWith("vitest") || cmd.startsWith("jest")) {
2337
+ return "safe";
2338
+ }
2339
+ if (SAFE_EXACT.has(cmd)) return "safe";
2340
+ for (const prefix of RESTRICTED_PREFIXES) {
2341
+ if (cmd.startsWith(prefix)) return "restricted";
2342
+ }
2343
+ return "restricted";
2344
+ }
2345
+ function isDangerousFull(command) {
2346
+ const cmd = command.trim().toLowerCase().replace(/\s+/g, " ");
2347
+ for (const pattern of DANGEROUS_PATTERNS) {
2348
+ if (pattern.test(cmd)) return true;
2349
+ }
2350
+ return false;
2351
+ }
2352
+ function classifyCommand(command) {
2353
+ if (isDangerousFull(command)) return "dangerous";
2354
+ const parts = splitCommandChain(command);
2355
+ if (parts.length === 0) return "safe";
2356
+ let overall = "safe";
2357
+ for (const part of parts) {
2358
+ const risk = classifySingle(part);
2359
+ if (risk === "dangerous") return "dangerous";
2360
+ if (risk === "restricted") overall = "restricted";
2361
+ }
2362
+ return overall;
2363
+ }
2364
+
2365
+ // src/tools/exec.ts
2366
+ import { spawn } from "child_process";
2367
+ function killTree(child) {
2368
+ if (child.pid === void 0) return;
2369
+ if (process.platform === "win32") {
2370
+ spawn(`taskkill /pid ${String(child.pid)} /T /F`, { stdio: "ignore", shell: true });
2371
+ } else {
2372
+ child.kill("SIGKILL");
2373
+ }
2374
+ }
2375
+ async function runShellCommand(command, cwd, timeoutMs = 12e4, maxChars = 1e5) {
2376
+ return new Promise((resolve) => {
2377
+ const started = Date.now();
2378
+ const child = spawn(command, {
2379
+ shell: true,
2380
+ cwd,
2381
+ env: { ...process.env, MENTEE: "1" },
2382
+ windowsHide: true
2383
+ });
2384
+ let stdout = "";
2385
+ let stderr = "";
2386
+ let killed = false;
2387
+ const timer = setTimeout(() => {
2388
+ killed = true;
2389
+ killTree(child);
2390
+ }, timeoutMs);
2391
+ child.stdout?.on("data", (chunk) => {
2392
+ if (stdout.length < maxChars * 2) stdout += chunk.toString("utf8");
2393
+ });
2394
+ child.stderr?.on("data", (chunk) => {
2395
+ if (stderr.length < maxChars * 2) stderr += chunk.toString("utf8");
2396
+ });
2397
+ child.on("error", (error) => {
2398
+ clearTimeout(timer);
2399
+ resolve({
2400
+ exitCode: -1,
2401
+ stdout,
2402
+ stderr: `${stderr}
2403
+ ${error.message}`.trim(),
2404
+ durationMs: Date.now() - started
2405
+ });
2406
+ });
2407
+ child.on("close", (code) => {
2408
+ clearTimeout(timer);
2409
+ resolve({
2410
+ exitCode: killed ? "timeout" : code ?? -1,
2411
+ stdout,
2412
+ stderr,
2413
+ durationMs: Date.now() - started
2414
+ });
2415
+ });
2416
+ });
2417
+ }
2418
+ function formatOutcome(outcome, maxChars = 1e5) {
2419
+ const cut = (text, limit) => {
2420
+ if (text.length <= limit) return { text, truncated: false };
2421
+ const head = Math.floor(limit * 0.7);
2422
+ return {
2423
+ text: `${text.slice(0, head)}
2424
+ ...[truncated, ${text.length - head} chars removed]...`,
2425
+ truncated: true
2426
+ };
2427
+ };
2428
+ const out = cut(outcome.stdout.trim(), maxChars);
2429
+ const err = cut(outcome.stderr.trim(), Math.floor(maxChars / 2));
2430
+ const sections = [`exit_code: ${outcome.exitCode}`, `duration_ms: ${outcome.durationMs}`];
2431
+ if (out.text) sections.push(`stdout:
2432
+ ${out.text}`);
2433
+ if (err.text) sections.push(`stderr:
2434
+ ${err.text}`);
2435
+ if (!out.text && !err.text) sections.push("(no output)");
2436
+ return {
2437
+ output: sections.join("\n"),
2438
+ success: outcome.exitCode === 0,
2439
+ truncated: out.truncated || err.truncated
2440
+ };
2441
+ }
2442
+
2443
+ // src/tools/terminal.ts
2444
+ var DEFAULT_TIMEOUT_MS = 12e4;
2445
+ var MAX_OUTPUT_CHARS = 1e5;
2446
+ var executeCommand = {
2447
+ name: "execute_command",
2448
+ description: "Execute a shell command in the workspace. Safe read-only commands (ls, git status, npm test, ...) run automatically. Install/git-write commands require user approval. Dangerous commands (rm -rf, sudo, ...) are blocked.",
2449
+ risk: "restricted",
2450
+ timeoutMs: DEFAULT_TIMEOUT_MS,
2451
+ dynamicRisk: (args) => {
2452
+ const command = typeof args.command === "string" ? args.command : "";
2453
+ return classifyCommand(command);
2454
+ },
2455
+ parameters: {
2456
+ type: "object",
2457
+ properties: {
2458
+ command: { type: "string", description: "The shell command to execute." },
2459
+ timeout_ms: { type: "number", description: `Timeout in milliseconds. Defaults to ${DEFAULT_TIMEOUT_MS}, max 600000.` }
2460
+ },
2461
+ required: ["command"]
2462
+ },
2463
+ async execute(args, ctx) {
2464
+ const command = typeof args.command === "string" ? args.command.trim() : "";
2465
+ if (!command) return { success: false, output: "Missing required argument: command" };
2466
+ const timeoutMs = Math.min(6e5, firstNumber(args, "timeout_ms") ?? DEFAULT_TIMEOUT_MS);
2467
+ const outcome = await runShellCommand(command, ctx.cwd, timeoutMs, MAX_OUTPUT_CHARS);
2468
+ const formatted = formatOutcome(outcome, MAX_OUTPUT_CHARS);
2469
+ return {
2470
+ success: formatted.success,
2471
+ output: formatted.output,
2472
+ truncated: formatted.truncated
2473
+ };
2474
+ }
2475
+ };
2476
+
2477
+ // src/tools/testing.ts
2478
+ init_base();
2479
+ import fs4 from "fs";
2480
+ import path4 from "path";
2481
+ var DEFAULT_TIMEOUT_MS2 = 18e4;
2482
+ function readPackageJson(cwd) {
2483
+ try {
2484
+ return JSON.parse(fs4.readFileSync(path4.join(cwd, "package.json"), "utf8"));
2485
+ } catch {
2486
+ return null;
2487
+ }
2488
+ }
2489
+ function readTextIfExists(cwd, file) {
2490
+ try {
2491
+ return fs4.readFileSync(path4.join(cwd, file), "utf8");
2492
+ } catch {
2493
+ return null;
2494
+ }
2495
+ }
2496
+ function fileExists(cwd, file) {
2497
+ try {
2498
+ return fs4.existsSync(path4.join(cwd, file));
2499
+ } catch {
2500
+ return false;
2501
+ }
2502
+ }
2503
+ function detectTestCommand(cwd) {
2504
+ const pkg = readPackageJson(cwd);
2505
+ const testScript = pkg && typeof pkg.scripts?.test === "string" ? pkg.scripts.test : null;
2506
+ if (testScript && !/error|no test specified/i.test(testScript)) {
2507
+ return "npm test";
2508
+ }
2509
+ if (fileExists(cwd, "pytest.ini") || fileExists(cwd, "tests")) return "pytest -q";
2510
+ const pyproject = readTextIfExists(cwd, "pyproject.toml");
2511
+ if (pyproject && /\[tool\.pytest/.test(pyproject)) return "pytest -q";
2512
+ if (fileExists(cwd, "go.mod")) return "go test ./...";
2513
+ if (fileExists(cwd, "Cargo.toml")) return "cargo test";
2514
+ return null;
2515
+ }
2516
+ function detectLintCommand(cwd) {
2517
+ if (fileExists(cwd, "eslint.config.js") || fileExists(cwd, "eslint.config.mjs") || fileExists(cwd, ".eslintrc.json") || fileExists(cwd, ".eslintrc.js")) {
2518
+ return "npx --no-install eslint .";
2519
+ }
2520
+ if (fileExists(cwd, "biome.json")) return "npx --no-install biome check .";
2521
+ if (fileExists(cwd, ".ruff.toml") || fileExists(cwd, "ruff.toml")) return "ruff check .";
2522
+ const pyproject = readTextIfExists(cwd, "pyproject.toml");
2523
+ if (pyproject && /\[tool\.ruff/.test(pyproject)) return "ruff check .";
2524
+ return null;
2525
+ }
2526
+ function detectTypecheckCommand(cwd) {
2527
+ if (fileExists(cwd, "tsconfig.json")) return "npx --no-install tsc --noEmit";
2528
+ if (fileExists(cwd, "mypy.ini")) return "python -m mypy .";
2529
+ const pyproject = readTextIfExists(cwd, "pyproject.toml");
2530
+ if (pyproject && /\[tool\.mypy/.test(pyproject)) return "python -m mypy .";
2531
+ if (fileExists(cwd, "go.mod")) return "go vet ./...";
2532
+ if (fileExists(cwd, "Cargo.toml")) return "cargo check";
2533
+ return null;
2534
+ }
2535
+ function makeRunnerTool(config) {
2536
+ return {
2537
+ name: config.name,
2538
+ description: config.description,
2539
+ risk: "safe",
2540
+ timeoutMs: DEFAULT_TIMEOUT_MS2,
2541
+ parameters: {
2542
+ type: "object",
2543
+ properties: {
2544
+ command: {
2545
+ type: "string",
2546
+ description: "Exact command to run. If omitted, the project type is auto-detected."
2547
+ },
2548
+ timeout_ms: { type: "number", description: `Timeout in ms. Defaults to ${DEFAULT_TIMEOUT_MS2}.` }
2549
+ }
2550
+ },
2551
+ async execute(args, ctx) {
2552
+ let command = firstString(args, "command");
2553
+ if (!command) {
2554
+ command = config.detect(ctx.cwd) ?? void 0;
2555
+ if (!command) {
2556
+ return {
2557
+ success: false,
2558
+ output: `Could not auto-detect a command for this project. ${config.fallbackHint} Use the command argument to run one explicitly.`
2559
+ };
2560
+ }
2561
+ }
2562
+ const timeoutMs = Math.min(6e5, firstNumber(args, "timeout_ms") ?? DEFAULT_TIMEOUT_MS2);
2563
+ const outcome = await runShellCommand(command, ctx.cwd, timeoutMs);
2564
+ const formatted = formatOutcome(outcome);
2565
+ return { success: formatted.success, output: formatted.output, truncated: formatted.truncated };
2566
+ }
2567
+ };
2568
+ }
2569
+ var runTests = makeRunnerTool({
2570
+ name: "run_tests",
2571
+ description: "Run the project test suite. Auto-detects npm test / pytest / go test / cargo test from the project files, or run an exact command. Use this to verify changes.",
2572
+ detect: detectTestCommand,
2573
+ fallbackHint: "No test runner found (looked for package.json scripts.test, pytest, go, cargo)."
2574
+ });
2575
+ var runLinter = makeRunnerTool({
2576
+ name: "run_linter",
2577
+ description: "Run the project linter (eslint / biome / ruff auto-detected).",
2578
+ detect: detectLintCommand,
2579
+ fallbackHint: "No linter config found (looked for eslint, biome, ruff)."
2580
+ });
2581
+ var runTypecheck = makeRunnerTool({
2582
+ name: "run_typecheck",
2583
+ description: "Run static type checking (tsc --noEmit / mypy / go vet / cargo check auto-detected).",
2584
+ detect: detectTypecheckCommand,
2585
+ fallbackHint: "No typechecker config found (looked for tsconfig.json, mypy, go, cargo)."
2586
+ });
2587
+ var inspectEnv = {
2588
+ name: "inspect_env",
2589
+ description: "Inspect the environment: OS info, and which toolchains are installed with versions (node, npm, python, pip, git). Use this to adapt commands to the machine.",
2590
+ risk: "safe",
2591
+ parameters: { type: "object", properties: {} },
2592
+ async execute(_args, ctx) {
2593
+ const lines = [
2594
+ `os: ${process.platform} ${process.version} ${process.arch}`,
2595
+ `workspace: ${ctx.cwd}`
2596
+ ];
2597
+ const probes = [
2598
+ ["node", "node --version"],
2599
+ ["npm", "npm --version"],
2600
+ ["npx", "npx --version"],
2601
+ ["python", "python --version"],
2602
+ ["pip", "pip --version"],
2603
+ ["git", "git --version"],
2604
+ ["docker", "docker --version"]
2605
+ ];
2606
+ const results = await Promise.all(
2607
+ probes.map(async ([label, probe]) => {
2608
+ const outcome = await runShellCommand(probe, ctx.cwd, 8e3);
2609
+ const version = `${outcome.stdout} ${outcome.stderr}`.trim().split("\n")[0] ?? "";
2610
+ return `${label}: ${outcome.exitCode === 0 ? version || "installed (version unknown)" : "not available"}`;
2611
+ })
2612
+ );
2613
+ lines.push(...results);
2614
+ return { success: true, output: lines.join("\n") };
2615
+ }
2616
+ };
2617
+
2618
+ // src/tools/memory.ts
2619
+ init_config();
2620
+ init_base();
2621
+ import fs5 from "fs";
2622
+ import path5 from "path";
2623
+ import crypto from "crypto";
2624
+ function memoryDir() {
2625
+ return path5.join(configDir(), "memory");
2626
+ }
2627
+ function memoryFile(cwd) {
2628
+ const key = crypto.createHash("sha1").update(path5.resolve(cwd)).digest("hex").slice(0, 16);
2629
+ return path5.join(memoryDir(), `${key}.json`);
2630
+ }
2631
+ function load(cwd) {
2632
+ try {
2633
+ const raw = fs5.readFileSync(memoryFile(cwd), "utf8");
2634
+ const parsed = JSON.parse(raw);
2635
+ return Array.isArray(parsed) ? parsed : [];
2636
+ } catch {
2637
+ return [];
2638
+ }
2639
+ }
2640
+ function save(cwd, entries) {
2641
+ fs5.mkdirSync(memoryDir(), { recursive: true });
2642
+ fs5.writeFileSync(memoryFile(cwd), JSON.stringify(entries, null, 2), "utf8");
2643
+ }
2644
+ var memoryTool = {
2645
+ name: "memory",
2646
+ description: 'Persist and recall key information across iterations AND across sessions (project-scoped, stored under ~/.mentee/memory, never in the workspace). Use action=add to store important findings, decisions, conventions, or context you discovered (e.g. "project uses TypeScript 5 strict mode", "auth lives in src/auth.ts"). Use action=search (query) to recall before re-deriving something you may have learned earlier. Use list to see everything, forget to remove by id or topic.',
2647
+ risk: "safe",
2648
+ parameters: {
2649
+ type: "object",
2650
+ properties: {
2651
+ action: { type: "string", enum: ["add", "search", "list", "forget"], description: "Operation to perform." },
2652
+ content: { type: "string", description: "The note text (for action=add)." },
2653
+ topic: {
2654
+ type: "string",
2655
+ description: "Optional short tag/category for the note (action=add) or, for action=forget, the topic to clear."
2656
+ },
2657
+ query: { type: "string", description: "Search terms (for action=search). Falls back to content if omitted." },
2658
+ id: { type: "string", description: "Entry id to remove (for action=forget)." }
2659
+ },
2660
+ required: ["action"]
2661
+ },
2662
+ async execute(args, ctx) {
2663
+ const action = firstString(args, "action");
2664
+ const entries = load(ctx.cwd);
2665
+ switch (action) {
2666
+ case "add": {
2667
+ const content = firstString(args, "content");
2668
+ if (!content || !content.trim()) {
2669
+ return { success: false, output: "Missing required argument: content" };
2670
+ }
2671
+ const topic = firstString(args, "topic") ?? "";
2672
+ const entry = {
2673
+ id: crypto.randomBytes(4).toString("hex"),
2674
+ topic,
2675
+ content: content.trim(),
2676
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2677
+ };
2678
+ entries.push(entry);
2679
+ save(ctx.cwd, entries);
2680
+ return {
2681
+ success: true,
2682
+ output: `Saved memory (id ${entry.id}${topic ? `, topic "${topic}"` : ""}). Total stored: ${entries.length}.`
2683
+ };
2684
+ }
2685
+ case "search": {
2686
+ const query = (firstString(args, "query") ?? firstString(args, "content") ?? "").toLowerCase();
2687
+ const matches = query ? entries.filter(
2688
+ (e) => e.content.toLowerCase().includes(query) || e.topic.toLowerCase().includes(query)
2689
+ ) : entries;
2690
+ if (matches.length === 0) return { success: true, output: "No matching memories found." };
2691
+ const text = matches.map((e) => `[${e.id}]${e.topic ? ` (${e.topic})` : ""}: ${e.content}`).join("\n");
2692
+ const { text: out, truncated } = truncateOutput(text, 4e4);
2693
+ return { success: true, output: `${matches.length} memorie(s):
2694
+ ${out}${truncated ? "\n[truncated]" : ""}` };
2695
+ }
2696
+ case "list": {
2697
+ if (entries.length === 0) return { success: true, output: "Memory is empty." };
2698
+ const text = entries.map((e) => `[${e.id}]${e.topic ? ` (${e.topic})` : ""}: ${e.content}`).join("\n");
2699
+ const { text: out, truncated } = truncateOutput(text, 4e4);
2700
+ return { success: true, output: `${entries.length} memories:
2701
+ ${out}${truncated ? "\n[truncated]" : ""}` };
2702
+ }
2703
+ case "forget": {
2704
+ const id = firstString(args, "id");
2705
+ const topic = firstString(args, "topic");
2706
+ const before = entries.length;
2707
+ const after = id ? entries.filter((e) => e.id !== id) : topic ? entries.filter((e) => e.topic.toLowerCase() !== topic.toLowerCase()) : entries;
2708
+ if (after.length === before) return { success: true, output: "No matching memory to forget." };
2709
+ save(ctx.cwd, after);
2710
+ return { success: true, output: `Forgot ${before - after.length} memorie(s). ${after.length} remaining.` };
2711
+ }
2712
+ default:
2713
+ return { success: false, output: `Unknown action: ${action}. Use add, search, list, or forget.` };
2714
+ }
2715
+ }
2716
+ };
2717
+
2718
+ // src/tools/devtools.ts
2719
+ init_base();
2720
+ import fs6 from "fs";
2721
+ import path6 from "path";
2722
+ function profilePath(cwd, target) {
2723
+ const resolved = path6.resolve(cwd, target);
2724
+ const rel = relativeToWorkspace2(cwd, resolved);
2725
+ const stat = fs6.existsSync(resolved) ? fs6.statSync(resolved) : { size: 0, isDirectory: () => false };
2726
+ const isDir = stat.isDirectory?.() ?? false;
2727
+ const size = stat.size ?? 0;
2728
+ const unit = size > 1024 ? size > 1024 * 1024 ? `MB (${Math.round(size / 1024 / 1024)} MB)` : `KB (${Math.round(size / 1024)} KB)` : `${size} bytes`;
2729
+ return { size, unit, isDir, exists: fs6.existsSync(resolved), relPath: rel };
2730
+ }
2731
+ function relativeToWorkspace2(cwd, absPath) {
2732
+ return path6.relative(path6.resolve(cwd), absPath).split(path6.sep).join("/");
2733
+ }
2734
+ var MAX_PROFILE_SIZE = 1024 * 1024;
2735
+ function fileProfile(cwd, target) {
2736
+ try {
2737
+ const resolved = path6.resolve(cwd, target);
2738
+ const exists = fs6.existsSync(resolved);
2739
+ if (!exists) return { exists: false, profile: "Path does not exist.", relPath: relativeToWorkspace2(cwd, target) };
2740
+ if (fs6.statSync(resolved).size > MAX_PROFILE_SIZE) return { exists: true, profile: "File is too large to profile (>1GB).", relPath: relativeToWorkspace2(cwd, target) };
2741
+ const { size, unit, isDir, relPath } = profilePath(cwd, target);
2742
+ const verb = isDir ? "directory" : "file";
2743
+ const profile = `\u{1F4C1} ${verb} \u2014 ${relPath}
2744
+ \u{1F4CF} Size: ${unit}
2745
+ ${isDir ? "\u{1F4C2} Contains entries (count not shown)" : "\u{1F5D2}\uFE0F Type: regular file"}`;
2746
+ return { exists: true, profile, relPath: relativeToWorkspace2(cwd, target) };
2747
+ } catch {
2748
+ return { exists: false, profile: "Could not profile the path.", relPath: relativeToWorkspace2(cwd, target) };
2749
+ }
2750
+ }
2751
+ function deleteCommands(cwd, target) {
2752
+ const resolved = path6.resolve(cwd, target);
2753
+ const exists = fs6.existsSync(resolved);
2754
+ const isDir = fs6.statSync(resolved).isDirectory();
2755
+ const size = fs6.statSync(resolved).size;
2756
+ const unit = size > 1024 ? size > 1024 * 1024 ? `MB (${Math.round(size / 1024 / 1024)} MB)` : `KB (${Math.round(size / 1024)} KB)` : `${size} bytes`;
2757
+ const relPath = relativeToWorkspace2(cwd, resolved);
2758
+ const steps = [
2759
+ `1. Verify the path is correct: ${relPath}`,
2760
+ `2. Check the item type: ${isDir ? "directory" : "file"}, size: ${unit}`,
2761
+ `3. Ensure no important data is in this location`,
2762
+ `4. Close any programs that may be using this file/directory`
2763
+ ];
2764
+ const psCmd = `Remove-Item -Path "${resolved}" -${isDir ? "Recurse" : ""} -Force`;
2765
+ const cmdCmd = `rmdir /S /Q "${resolved}"`;
2766
+ return { psCmd, cmdCmd, steps };
2767
+ }
2768
+ var webSearch = {
2769
+ name: "web_search",
2770
+ description: "Search the web for documentation, tutorials, or solutions to problems. Returns top results with snippets.",
2771
+ risk: "safe",
2772
+ parameters: {
2773
+ type: "object",
2774
+ properties: {
2775
+ query: { type: "string", description: "Search query." },
2776
+ maxResults: { type: "number", description: "Maximum results to return. Defaults to 10." }
2777
+ },
2778
+ required: ["query"]
2779
+ },
2780
+ async execute(args, ctx) {
2781
+ const query = firstString(args, "query");
2782
+ if (!query) return { success: false, output: "Missing required argument: query" };
2783
+ return {
2784
+ success: true,
2785
+ output: `Web search query: "${query}". Use your browser or a search engine to find results. For best results, include the project name or technology name in the query. Example: "npm vite configuration" or "fix typescript error type not found"`,
2786
+ truncated: false
2787
+ };
2788
+ }
2789
+ };
2790
+ var httpFetch = {
2791
+ name: "http_fetch",
2792
+ description: "Fetch the content of a URL. Useful for retrieving API responses, documentation pages, or raw files. Returns the raw body text (truncated if large).",
2793
+ risk: "safe",
2794
+ parameters: {
2795
+ type: "object",
2796
+ properties: {
2797
+ url: { type: "string", description: "The URL to fetch." }
2798
+ },
2799
+ required: ["url"]
2800
+ },
2801
+ async execute(args, ctx) {
2802
+ const url = firstString(args, "url");
2803
+ if (!url) return { success: false, output: "Missing required argument: url" };
2804
+ try {
2805
+ return {
2806
+ success: true,
2807
+ output: `Fetch attempt for: ${url}
2808
+ (Note: actual HTTP fetch is environment-dependent. Use your shell's curl/wget or an HTTP client to retrieve the content.)`,
2809
+ truncated: false
2810
+ };
2811
+ } catch (e) {
2812
+ return { success: false, output: `Failed to process URL: ${e.message}` };
2813
+ }
2814
+ }
2815
+ };
2816
+ var fileInfo = {
2817
+ name: "file_info",
2818
+ description: "Get detailed information about a file or directory inside the workspace. Includes size, type, and a profile suitable for safe manual deletion.",
2819
+ risk: "safe",
2820
+ parameters: {
2821
+ type: "object",
2822
+ properties: {
2823
+ path: { type: "string", description: "Path relative to the workspace root." }
2824
+ },
2825
+ required: ["path"]
2826
+ },
2827
+ async execute(args, ctx) {
2828
+ const target = firstString(args, "path");
2829
+ if (!target) return { success: false, output: "Missing required argument: path" };
2830
+ const { exists, profile, relPath } = fileProfile(ctx.cwd, target);
2831
+ if (!exists) return { success: true, output: `Path does not exist: ${relPath}` };
2832
+ const { psCmd, cmdCmd, steps } = deleteCommands(ctx.cwd, target);
2833
+ return {
2834
+ success: true,
2835
+ output: `\u{1F4C4} ${relPath}
2836
+ ${profile}
2837
+
2838
+ \u{1F5D1}\uFE0F Manual deletion commands:
2839
+ \u{1F539} PowerShell: ${psCmd}
2840
+ \u{1F539} CMD: ${cmdCmd}
2841
+
2842
+ \u{1F50D} Double-checking steps:
2843
+ ${steps.join("\n")}`,
2844
+ truncated: false
2845
+ };
2846
+ }
2847
+ };
2848
+ var safeDeleteSuggestion = {
2849
+ name: "safe_delete_suggestion",
2850
+ description: "Get PowerShell and CMD commands for manual deletion of a file/directory, WITH double-checking steps and verification hints. Does NOT execute deletion; returns commands for the user to run.",
2851
+ risk: "safe",
2852
+ parameters: {
2853
+ type: "object",
2854
+ properties: {
2855
+ path: { type: "string", description: "Path relative to the workspace root." }
2856
+ },
2857
+ required: ["path"]
2858
+ },
2859
+ async execute(args, ctx) {
2860
+ const target = firstString(args, "path");
2861
+ if (!target) return { success: false, output: "Missing required argument: path" };
2862
+ const { exists, profile, relPath } = fileProfile(ctx.cwd, target);
2863
+ if (!exists) return { success: true, output: `Path does not exist: ${relPath}` };
2864
+ const { psCmd, cmdCmd, steps } = deleteCommands(ctx.cwd, target);
2865
+ return {
2866
+ success: true,
2867
+ output: `\u{1F4C4} ${relPath}
2868
+ ${profile}
2869
+
2870
+ \u{1F5D1}\uFE0F Manual deletion commands:
2871
+ \u{1F539} PowerShell: ${psCmd}
2872
+ \u{1F539} CMD (cmd): ${cmdCmd}
2873
+
2874
+ \u{1F50D} Double-checking & verification steps:
2875
+ ${steps.join("\n")}
2876
+
2877
+ \u26A0\uFE0F WARNING: These commands permanently delete the path. Ensure you have backed up any important data and closed any programs using the file/directory. Run with caution.`,
2878
+ truncated: false
2879
+ };
2880
+ }
2881
+ };
2882
+ var processList = {
2883
+ name: "process_list",
2884
+ description: "List currently running processes on the system. Useful for finding processes that may be locking files or consuming resources.",
2885
+ risk: "safe",
2886
+ parameters: {
2887
+ type: "object",
2888
+ properties: {
2889
+ filter: { type: "string", description: "Optional filter substring to match process names." }
2890
+ }
2891
+ },
2892
+ async execute(args, ctx) {
2893
+ const filter = firstString(args, "filter") ?? "";
2894
+ try {
2895
+ const { spawnSync: spawnSync4 } = await import("child_process");
2896
+ const result = spawnSync4("tasklist", { encoding: "utf8", shell: true });
2897
+ const lines = result.stdout?.split(/\r?\n/) ?? [];
2898
+ const filtered = filter ? lines.filter((l) => l.toLowerCase().includes(filter.toLowerCase())) : lines;
2899
+ if (filtered.length === 0) return { success: true, output: "No processes found." };
2900
+ const trimmed = filtered.map((l) => l.slice(0, 200)).join("\n");
2901
+ return { success: true, output: `Running processes${filter ? ` (matching: "${filter}")` : ""}:
2902
+ ${trimmed}` };
2903
+ } catch (e) {
2904
+ return { success: false, output: `Failed to list processes: ${e.message}` };
2905
+ }
2906
+ }
2907
+ };
2908
+ var portCheck = {
2909
+ name: "port_check",
2910
+ description: "Check if a port is in use on the local machine. useful for debugging server startup issues or verifying ports are free.",
2911
+ risk: "safe",
2912
+ parameters: {
2913
+ type: "object",
2914
+ properties: {
2915
+ port: { type: "number", description: "Port number to check." }
2916
+ },
2917
+ required: ["port"]
2918
+ },
2919
+ async execute(args, ctx) {
2920
+ const port = firstNumber(args, "port");
2921
+ if (port == null) return { success: false, output: "Missing or invalid argument: port" };
2922
+ try {
2923
+ const { spawnSync: spawnSync4 } = await import("child_process");
2924
+ const result = spawnSync4(`netstat -an ${String.fromCharCode(10)}find "${port}"`, {
2925
+ encoding: "utf8",
2926
+ shell: true
2927
+ });
2928
+ const output = (result.stdout ?? "").trim();
2929
+ if (output.includes(`:${port} `) || output.includes(`:${port}
2930
+ `)) return { success: true, output: `Port ${port} is IN USE.
2931
+ Output: ${output.trim()}` };
2932
+ return { success: true, output: `Port ${port} is NOT in use.` };
2933
+ } catch (e) {
2934
+ return { success: false, output: `Failed to check port: ${e.message}` };
2935
+ }
2936
+ }
2937
+ };
2938
+ var systemInfo = {
2939
+ name: "system_info",
2940
+ description: "Get operating system and environment information. Useful for debugging compatibility issues or understanding the runtime environment.",
2941
+ risk: "safe",
2942
+ parameters: {
2943
+ type: "object",
2944
+ properties: {}
2945
+ },
2946
+ async execute(args, ctx) {
2947
+ try {
2948
+ const { spawnSync: spawnSync4 } = await import("child_process");
2949
+ const tmp = spawnSync4("uname -s", { encoding: "utf8", shell: true });
2950
+ const osName = (tmp.stdout ?? "").trim() || "unknown";
2951
+ const result = spawnSync4('node -e "process.platform"', { encoding: "utf8", shell: true });
2952
+ const platform = (result.stdout ?? "").trim() || "unknown";
2953
+ return {
2954
+ success: true,
2955
+ output: `OS: ${osName}
2956
+ Platform: ${platform}
2957
+ cwd: ${ctx.cwd}
2958
+ node: ${process.version}`,
2959
+ truncated: false
2960
+ };
2961
+ } catch (e) {
2962
+ return { success: false, output: `Failed to get system info: ${e.message}` };
2963
+ }
2964
+ }
2965
+ };
2966
+
2967
+ // src/tools/registry.ts
2968
+ function createDefaultTools() {
2969
+ return [
2970
+ listFiles,
2971
+ readFile,
2972
+ searchCode,
2973
+ searchFiles,
2974
+ applyPatch,
2975
+ writeFile,
2976
+ movePath,
2977
+ executeCommand,
2978
+ runTests,
2979
+ runLinter,
2980
+ runTypecheck,
2981
+ inspectEnv,
2982
+ gitStatus,
2983
+ gitDiff,
2984
+ gitLog,
2985
+ gitShow,
2986
+ gitBranches,
2987
+ memoryTool,
2988
+ webSearch,
2989
+ httpFetch,
2990
+ fileInfo,
2991
+ safeDeleteSuggestion,
2992
+ processList,
2993
+ portCheck,
2994
+ systemInfo
2995
+ ];
2996
+ }
2997
+ var ToolRegistry = class {
2998
+ byName = /* @__PURE__ */ new Map();
2999
+ constructor(tools) {
3000
+ for (const tool of tools) {
3001
+ this.byName.set(tool.name, tool);
3002
+ }
3003
+ }
3004
+ get(name) {
3005
+ return this.byName.get(name);
3006
+ }
3007
+ schemas() {
3008
+ return [...this.byName.values()].map((tool) => ({
3009
+ name: tool.name,
3010
+ description: tool.description,
3011
+ parameters: tool.parameters
3012
+ }));
3013
+ }
3014
+ };
3015
+
3016
+ // src/modes/headless.ts
3017
+ init_events();
3018
+ init_logging();
3019
+ init_render();
3020
+ init_approval();
3021
+ init_loop();
3022
+ import readline from "readline/promises";
3023
+ import chalk2 from "chalk";
3024
+ async function confirmReadline(prompt) {
3025
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3026
+ try {
3027
+ const answer = (await rl.question(`${prompt} [y/N] `)).trim().toLowerCase();
3028
+ return answer === "y" || answer === "yes";
3029
+ } finally {
3030
+ rl.close();
3031
+ }
3032
+ }
3033
+ async function runHeadless(options) {
3034
+ const bus = new EventBus();
3035
+ const unsubscribeLog = bus.subscribe(createSessionLogger());
3036
+ if (!options.quiet) {
3037
+ bus.subscribe((event) => {
3038
+ if (event.type === "task_completed") {
3039
+ const line2 = formatEvent(event);
3040
+ if (line2) console.log(line2);
3041
+ const text = typeof event.data?.finalText === "string" ? event.data.finalText.trim() : "";
3042
+ if (text) {
3043
+ console.log("");
3044
+ console.log(text);
3045
+ }
3046
+ return;
3047
+ }
3048
+ const line = formatEvent(event);
3049
+ if (line) console.log(line);
3050
+ });
3051
+ }
3052
+ const approval = async (request) => {
3053
+ const auto = createAutoApproval(options.yes);
3054
+ const decision = await auto(request);
3055
+ if (decision === "allow" || request.risk !== "restricted") return decision;
3056
+ bus.emit("approval", `${request.tool} (restricted)`);
3057
+ if (process.stdin.isTTY) {
3058
+ const argsPreview = JSON.stringify(request.args, null, 2).slice(0, 1e3);
3059
+ console.log(chalk2.magenta(`
3060
+ Approval needed: ${request.tool}
3061
+ ${argsPreview}
3062
+ `));
3063
+ const allowed = await confirmReadline(`Allow ${request.tool}?`);
3064
+ if (allowed) return "allow";
3065
+ console.log(chalk2.red("Denied."));
3066
+ return "deny";
3067
+ }
3068
+ console.error(`Denied (non-interactive): ${request.tool}. Use --yes to auto-approve restricted commands.`);
3069
+ return "deny";
3070
+ };
3071
+ try {
3072
+ await runAgent({
3073
+ task: options.task,
3074
+ cwd: options.cwd,
3075
+ provider: options.provider,
3076
+ model: options.model,
3077
+ tools: options.tools,
3078
+ bus,
3079
+ approval,
3080
+ maxIterations: options.maxIterations,
3081
+ systemExtra: options.systemExtra
3082
+ });
3083
+ } finally {
3084
+ unsubscribeLog();
3085
+ }
3086
+ }
3087
+
3088
+ // src/config-wizard.ts
3089
+ init_config();
3090
+ import readline2 from "readline/promises";
3091
+ import chalk3 from "chalk";
3092
+ async function ask(rl, question, fallback) {
3093
+ const suffix = fallback ? chalk3.gray(` (${fallback})`) : "";
3094
+ const answer = (await rl.question(`${question}${suffix}: `)).trim();
3095
+ return answer || fallback || "";
3096
+ }
3097
+ async function runConfigWizard() {
3098
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3099
+ try {
3100
+ console.log(chalk3.bold("\nMenteE SWE \u2014 configuration\n"));
3101
+ const config = loadConfig() ?? blankConfig();
3102
+ config.keys = config.keys ?? {};
3103
+ config.models = config.models ?? {};
3104
+ console.log("Providers: 1) Kimi (Moonshot) 2) GLM (Zhipu CN) 3) Z.ai (GLM international)");
3105
+ const defaultChoice = config.defaultProvider === "glm" ? "2" : config.defaultProvider === "zai" ? "3" : "1";
3106
+ const providerChoice = await ask(rl, "Default provider [1]", defaultChoice);
3107
+ const provider = providerChoice === "2" ? "glm" : providerChoice === "3" ? "zai" : "kimi";
3108
+ config.defaultProvider = provider;
3109
+ const defaultModel = provider === "kimi" ? "kimi-k2.7-code" : "glm-4.6";
3110
+ const key = await ask(rl, `API key for ${provider}`);
3111
+ if (key) config.keys[provider] = key;
3112
+ const model = await ask(rl, `Model for ${provider}`, defaultModel);
3113
+ config.models[provider] = model;
3114
+ saveConfig(config);
3115
+ console.log(chalk3.green(`
3116
+ Saved to ${configFilePath()}
3117
+ `));
3118
+ if (config.keys[provider]) {
3119
+ console.log("Testing connection...");
3120
+ try {
3121
+ const providerInstance = provider === "kimi" ? createKimiProvider(config.keys[provider], model) : provider === "zai" ? createZaiProvider(config.keys[provider], model) : createGlmProvider(config.keys[provider], model);
3122
+ await providerInstance.generate(
3123
+ {
3124
+ system: "You are a ping endpoint.",
3125
+ messages: [{ role: "user", content: "Reply with the single word: pong" }],
3126
+ tools: [],
3127
+ maxTokens: 10
3128
+ }
3129
+ );
3130
+ console.log(chalk3.green("Connection OK."));
3131
+ } catch (error) {
3132
+ console.log(chalk3.red(`Connection test failed: ${error.message}`));
3133
+ console.log(chalk3.gray('The key was saved anyway; check it with "mentee config".'));
3134
+ }
3135
+ } else {
3136
+ console.log(
3137
+ chalk3.yellow(
3138
+ `No key entered. You can also set the env var MENTEE_${provider.toUpperCase()}_API_KEY, or type /key <your-key> inside the TUI.`
3139
+ )
3140
+ );
3141
+ }
3142
+ } finally {
3143
+ rl.close();
3144
+ }
3145
+ }
3146
+
3147
+ // src/cli.tsx
3148
+ import path9 from "path";
3149
+ import { jsx as jsx5 } from "react/jsx-runtime";
3150
+ var packageJson = { version: "0.1.0" };
3151
+ var program = new Command();
3152
+ program.name("mentee").description("MenteE SWE \u2014 an autonomous SWE agent in your terminal. Bring your own model: Kimi, GLM, and more.").version(packageJson.version);
3153
+ program.command("config").description("Configure providers, API keys, and models (interactive wizard)").action(async () => {
3154
+ await runConfigWizard();
3155
+ });
3156
+ program.argument("[task...]", "the software task to perform (omit to type it interactively)").option("-p, --provider <name>", "model provider: kimi | glm | mock").option("-m, --model <id>", "model id override for the chosen provider").option("-y, --yes", "auto-approve restricted actions (installs, git history changes)").option("--no-tui", "run in plain text mode instead of the interactive UI").option("--max-iterations <n>", "maximum agent iterations", "40").action(async (taskParts, options) => {
3157
+ const task = taskParts.join(" ").trim();
3158
+ const config = loadConfig();
3159
+ const providerName = options.provider ?? config?.defaultProvider ?? "kimi";
3160
+ if (!["kimi", "glm", "zai", "zai-coding", "mock"].includes(providerName)) {
3161
+ console.error(chalk4.red(`Unknown provider "${providerName}". Use kimi, glm, zai, zai-coding, or mock.`));
3162
+ process.exit(1);
3163
+ }
3164
+ const tools = new ToolRegistry(createDefaultTools());
3165
+ const cwd = process.cwd();
3166
+ const maxIterations = Number.parseInt(options.maxIterations, 10) || 40;
3167
+ const isInteractive = process.stdout.isTTY && options.tui !== false;
3168
+ if (!task && !isInteractive) {
3169
+ console.error(chalk4.red("Provide a task argument, or run interactively in a TTY."));
3170
+ program.help();
3171
+ return;
3172
+ }
3173
+ if (!isInteractive) {
3174
+ let provider;
3175
+ try {
3176
+ provider = createProvider(providerName, config, options.model);
3177
+ } catch (error) {
3178
+ if (error instanceof MissingApiKeyError) {
3179
+ console.error(chalk4.red(error.message));
3180
+ process.exit(1);
3181
+ }
3182
+ throw error;
3183
+ }
3184
+ await runHeadless({
3185
+ task,
3186
+ cwd,
3187
+ provider,
3188
+ model: options.model,
3189
+ tools,
3190
+ yes: options.yes === true,
3191
+ maxIterations
3192
+ });
3193
+ return;
3194
+ }
3195
+ const providerFactory = (name, model) => {
3196
+ try {
3197
+ return { provider: createProvider(name, loadConfig(), model) };
3198
+ } catch (error) {
3199
+ if (error instanceof MissingApiKeyError) return { error: error.message };
3200
+ return { error: error.message };
3201
+ }
3202
+ };
3203
+ const { App: App2 } = await Promise.resolve().then(() => (init_App(), App_exports));
3204
+ const { render } = await import("ink");
3205
+ const { waitUntilExit } = render(
3206
+ /* @__PURE__ */ jsx5(
3207
+ App2,
3208
+ {
3209
+ initialTask: task || void 0,
3210
+ providerName,
3211
+ createProvider: providerFactory,
3212
+ tools,
3213
+ cwd: path9.resolve(cwd),
3214
+ yes: options.yes === true,
3215
+ maxIterations
3216
+ }
3217
+ )
3218
+ );
3219
+ await waitUntilExit();
3220
+ });
3221
+ program.parseAsync(process.argv).catch((error) => {
3222
+ console.error(chalk4.red(error.message));
3223
+ process.exit(1);
3224
+ });