@codemap-ai/cli 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 (41) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/.codex-plugin/plugin.json +35 -0
  3. package/.cursor-plugin/plugin.json +16 -0
  4. package/agent-pack/README.md +44 -0
  5. package/agent-pack/agents/codemap-explorer.md +12 -0
  6. package/agent-pack/commands/feature-area.md +8 -0
  7. package/agent-pack/rules/install.md +35 -0
  8. package/agent-pack/rules/mcp-first.md +18 -0
  9. package/agent-pack/rules/task-lifecycle.md +11 -0
  10. package/agent-pack/rules/workflow-skills.md +97 -0
  11. package/agent-pack/skills/brainstorming/SKILL.md +41 -0
  12. package/agent-pack/skills/executing-plans/SKILL.md +26 -0
  13. package/agent-pack/skills/feature-area-investigation/SKILL.md +15 -0
  14. package/agent-pack/skills/interpreting-codemap-output/SKILL.md +29 -0
  15. package/agent-pack/skills/mcp-first-exploration/SKILL.md +10 -0
  16. package/agent-pack/skills/safe-edit-and-reimport/SKILL.md +18 -0
  17. package/agent-pack/skills/symbol-level-debugging/SKILL.md +15 -0
  18. package/agent-pack/skills/test-driven-development/SKILL.md +36 -0
  19. package/agent-pack/skills/token-efficient-code-review/SKILL.md +15 -0
  20. package/agent-pack/skills/verification-before-completion/SKILL.md +25 -0
  21. package/agent-pack/skills/writing-plans/SKILL.md +32 -0
  22. package/agent-pack/templates/AGENTS.md +21 -0
  23. package/agent-pack/templates/CLAUDE.md +19 -0
  24. package/agent-pack/templates/COPILOT.md +75 -0
  25. package/agent-pack/templates/GEMINI.md +77 -0
  26. package/agent-pack/templates/design-spec.md +34 -0
  27. package/agent-pack/templates/implementation-plan.md +26 -0
  28. package/agent-pack/templates/opencode-AGENTS.md +76 -0
  29. package/agent-pack/templates/opencode-INSTALL.md +5 -0
  30. package/agent-pack/templates/verification-report.md +18 -0
  31. package/dist/chat-terminal-DFFYQ6AF.js +1743 -0
  32. package/dist/chunk-A2QHID6K.js +34 -0
  33. package/dist/chunk-AXGGL47C.js +457 -0
  34. package/dist/chunk-FFFJKKKM.js +218262 -0
  35. package/dist/chunk-KWYP3ADN.js +1742 -0
  36. package/dist/chunk-WNJNT3FC.js +216 -0
  37. package/dist/chunk-YRXVMFXS.js +99 -0
  38. package/dist/index.js +9225 -0
  39. package/dist/login-screen-2DJBDM47.js +143 -0
  40. package/dist/pi-tui-app-GVZ3AN42.js +2073 -0
  41. package/package.json +59 -0
@@ -0,0 +1,1742 @@
1
+ import{createRequire as __cmr}from"node:module";import{fileURLToPath as __f2p}from"node:url";import{dirname as __dn}from"node:path";var require=__cmr(import.meta.url);var __filename=__f2p(import.meta.url);var __dirname=__dn(__filename);
2
+ import {
3
+ MASTRA_DISABLED_TOOLS,
4
+ ensureLocalIndexWithSummary,
5
+ getMastraMcpStatusSummary,
6
+ getWorkspaceStatusLines,
7
+ listMastraThreadMessages,
8
+ listMastraThreads,
9
+ readWorkspacePath,
10
+ removeMcpServerEntry,
11
+ require_dist,
12
+ saveMcpServerEntry
13
+ } from "./chunk-FFFJKKKM.js";
14
+ import {
15
+ __toESM,
16
+ clearGlobalAuthConfig,
17
+ createCodeMapClient,
18
+ loadConfig,
19
+ startMcpLogin,
20
+ tryOpenLoginBrowser,
21
+ waitForLoginAuthorization
22
+ } from "./chunk-AXGGL47C.js";
23
+
24
+ // src/cli-agent/core/convention-synthesizer.ts
25
+ import { readFile, writeFile, mkdir, stat, readdir } from "fs/promises";
26
+ import { createHash } from "crypto";
27
+ import { spawnSync } from "child_process";
28
+ import path from "path";
29
+ var CONVENTION_SOURCES = [
30
+ { label: "Claude Code", paths: ["CLAUDE.md", ".claude/CLAUDE.md"] },
31
+ { label: "Codex", paths: [".codex/codemap-agent-pack.md", ".codex/config.toml", ".codex/agents/*.toml"] },
32
+ { label: "Cursor", paths: [".cursorrules", ".cursor/rules/*.mdc"] },
33
+ { label: "Windsurf", paths: [".windsurfrules"] },
34
+ { label: "GitHub Copilot", paths: [".github/copilot-instructions.md"] },
35
+ { label: "Cline", paths: [".clinerules"] },
36
+ { label: "OpenAI Codex", paths: ["AGENTS.md"] },
37
+ { label: "General", paths: ["CONVENTIONS.md"] }
38
+ ];
39
+ var RULE_SOURCES = [
40
+ { label: "Claude Code rules", paths: [".claude/rules/*.md"] }
41
+ ];
42
+ var SKILL_DIRS = [".claude/skills", ".codex/skills"];
43
+ function getWorkspaceRoot() {
44
+ const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", cwd: process.cwd() });
45
+ return r.stdout?.trim() || process.cwd();
46
+ }
47
+ function dotCodemap() {
48
+ return path.join(getWorkspaceRoot(), ".codemap");
49
+ }
50
+ function cachePaths() {
51
+ const dir = dotCodemap();
52
+ return {
53
+ conventions: { md: path.join(dir, "synthesized-conventions.md"), meta: path.join(dir, "conventions-meta.json") },
54
+ rules: { md: path.join(dir, "synthesized-rules.md"), meta: path.join(dir, "rules-meta.json") },
55
+ skills: { md: path.join(dir, "synthesized-skills.md"), meta: path.join(dir, "skills-meta.json") }
56
+ };
57
+ }
58
+ async function readConfig(root) {
59
+ try {
60
+ return JSON.parse(await readFile(path.join(root, ".codemap", "agent-context.json"), "utf8"));
61
+ } catch {
62
+ return {};
63
+ }
64
+ }
65
+ async function expandGlob(root, pattern) {
66
+ if (!pattern.includes("*")) {
67
+ try {
68
+ await stat(path.join(root, pattern));
69
+ return [pattern];
70
+ } catch {
71
+ return [];
72
+ }
73
+ }
74
+ const parts = pattern.split("/");
75
+ const starIdx = parts.findIndex((p) => p.includes("*"));
76
+ const parentDir = parts.slice(0, starIdx).join("/") || ".";
77
+ const starPart = parts[starIdx];
78
+ const rest = parts.slice(starIdx + 1).join("/");
79
+ let entries;
80
+ try {
81
+ entries = await readdir(path.join(root, parentDir));
82
+ } catch {
83
+ return [];
84
+ }
85
+ const results = [];
86
+ if (starPart === "*" && rest) {
87
+ for (const e of entries) {
88
+ const c = [parentDir === "." ? "" : parentDir, e, rest].filter(Boolean).join("/");
89
+ try {
90
+ await stat(path.join(root, c));
91
+ results.push(c);
92
+ } catch {
93
+ }
94
+ }
95
+ } else {
96
+ const ext = path.extname(rest || starPart).replace("*", "");
97
+ for (const e of entries) {
98
+ if (!ext || e.endsWith(ext))
99
+ results.push([parentDir === "." ? "" : parentDir, e].filter(Boolean).join("/"));
100
+ }
101
+ }
102
+ return results;
103
+ }
104
+ async function scanSources(root, sources) {
105
+ const results = [];
106
+ for (const { label, paths } of sources) {
107
+ for (const pattern of paths) {
108
+ for (const rel of await expandGlob(root, pattern)) {
109
+ try {
110
+ const content = await readFile(path.join(root, rel), "utf8");
111
+ if (content.trim()) results.push({ label, filePath: rel, content });
112
+ } catch {
113
+ }
114
+ }
115
+ }
116
+ }
117
+ return results;
118
+ }
119
+ async function scanSkills(root) {
120
+ const seen = /* @__PURE__ */ new Map();
121
+ for (const dir of SKILL_DIRS) {
122
+ let entries;
123
+ try {
124
+ entries = await readdir(path.join(root, dir));
125
+ } catch {
126
+ continue;
127
+ }
128
+ for (const skillName of entries) {
129
+ const rel = `${dir}/${skillName}/SKILL.md`;
130
+ if (seen.has(skillName)) continue;
131
+ try {
132
+ const content = await readFile(path.join(root, rel), "utf8");
133
+ if (content.trim()) seen.set(skillName, { label: `Skill: ${skillName}`, filePath: rel, content });
134
+ } catch {
135
+ }
136
+ }
137
+ }
138
+ return [...seen.values()];
139
+ }
140
+ function hashFiles(files) {
141
+ const h = createHash("sha256");
142
+ for (const f of files) h.update(f.filePath).update(f.content);
143
+ return h.digest("hex").slice(0, 16);
144
+ }
145
+ function estimateTokens(text) {
146
+ return Math.ceil(text.length / 4);
147
+ }
148
+ async function loadCache(paths, hash) {
149
+ try {
150
+ const [content, metaRaw] = await Promise.all([readFile(paths.md, "utf8"), readFile(paths.meta, "utf8")]);
151
+ const meta = JSON.parse(metaRaw);
152
+ if (meta.hash === hash && content.trim()) return content;
153
+ } catch {
154
+ }
155
+ return null;
156
+ }
157
+ async function saveCache(paths, content, hash, files) {
158
+ const meta = { hash, scannedFiles: files.map((f) => f.filePath), tokenCount: estimateTokens(content), synthesizedAt: Date.now() };
159
+ await mkdir(dotCodemap(), { recursive: true });
160
+ await Promise.all([writeFile(paths.md, content, "utf8"), writeFile(paths.meta, JSON.stringify(meta, null, 2), "utf8")]);
161
+ }
162
+ async function stream(provider, model, prompt, userContent) {
163
+ let result = "";
164
+ for await (const chunk of provider.stream({
165
+ model,
166
+ messages: [{ role: "user", content: `${prompt}
167
+
168
+ ${userContent}` }]
169
+ })) {
170
+ if (chunk.text) result += chunk.text;
171
+ }
172
+ return result.trim();
173
+ }
174
+ function buildInput(files) {
175
+ return files.map((f) => `=== ${f.label}: ${f.filePath} ===
176
+ ${f.content}`).join("\n\n");
177
+ }
178
+ var CONVENTIONS_PROMPT = `You are synthesizing project configuration and convention files from multiple AI agent configs into a unified project guide.
179
+
180
+ Instructions:
181
+ - Merge overlapping content, keep the most specific version
182
+ - Remove exact duplicates
183
+ - Keep: architecture, tech stack, coding style, naming conventions, commands, UI routes, design system
184
+ - Skip: rule lists and skill workflows (those are handled separately)
185
+ - Organize by topic with ## headers
186
+ - Output clean markdown
187
+
188
+ Output ONLY the unified guide. No preamble.`;
189
+ var RULES_PROMPT = `You are synthesizing project rule files into a unified rule set.
190
+
191
+ Instructions:
192
+ - Merge rules from all files
193
+ - Remove EXACT duplicate rules only \u2014 preserve all unique rules fully intact
194
+ - Do NOT compress, summarize, or shorten any rule
195
+ - Organize by topic with ## headers
196
+ - Output clean markdown
197
+
198
+ Output ONLY the unified rules. No preamble.`;
199
+ var SKILLS_PROMPT = `You are synthesizing project workflow skill files into a unified skills guide.
200
+
201
+ Instructions:
202
+ - Each skill has a name and step-by-step process
203
+ - If multiple files describe the same skill, keep the most complete version
204
+ - Keep ALL step-by-step processes fully intact \u2014 do NOT compress or summarize
205
+ - Organize skills with ## headers (one per skill)
206
+ - Output clean markdown
207
+
208
+ Output ONLY the unified skills guide. No preamble.`;
209
+ async function runPipeline(root, files, paths, prompt, provider, model, maxTokens, forceRefresh) {
210
+ if (files.length === 0) return null;
211
+ const hash = hashFiles(files);
212
+ if (!forceRefresh) {
213
+ const cached = await loadCache(paths, hash);
214
+ if (cached) return cached;
215
+ }
216
+ const content = await stream(provider, model, prompt, buildInput(files));
217
+ if (!content) return null;
218
+ let final = content;
219
+ if (maxTokens) {
220
+ const est = estimateTokens(content);
221
+ if (est > maxTokens) {
222
+ final = content.slice(0, maxTokens * 4).trimEnd() + `
223
+
224
+ _[Truncated \u2014 increase \`conventionMaxTokens\` in .codemap/agent-context.json]_`;
225
+ }
226
+ }
227
+ await saveCache(paths, final, hash, files).catch(() => {
228
+ });
229
+ return final;
230
+ }
231
+ async function loadOrSynthesizeAll(provider, model, forceRefresh = false) {
232
+ const root = getWorkspaceRoot();
233
+ const config = await readConfig(root);
234
+ const maxTokens = config.conventionMaxTokens ?? null;
235
+ const cp = cachePaths();
236
+ const [conventionFiles, ruleFiles, skillFiles] = await Promise.all([
237
+ scanSources(root, CONVENTION_SOURCES),
238
+ scanSources(root, RULE_SOURCES),
239
+ scanSkills(root)
240
+ ]);
241
+ if (conventionFiles.length === 0 && ruleFiles.length === 0 && skillFiles.length === 0) return null;
242
+ const [conventions, rules, skills] = await Promise.all([
243
+ runPipeline(root, conventionFiles, cp.conventions, CONVENTIONS_PROMPT, provider, model, maxTokens, forceRefresh),
244
+ runPipeline(root, ruleFiles, cp.rules, RULES_PROMPT, provider, model, null, forceRefresh),
245
+ runPipeline(root, skillFiles, cp.skills, SKILLS_PROMPT, provider, model, null, forceRefresh)
246
+ ]);
247
+ const fromCache = !forceRefresh;
248
+ return { conventions, rules, skills, fromCache };
249
+ }
250
+ async function refreshAll(provider, model) {
251
+ return loadOrSynthesizeAll(provider, model, true);
252
+ }
253
+ async function getCachedContext() {
254
+ const cp = cachePaths();
255
+ const [conventions, rules, skills] = await Promise.all([
256
+ readFile(cp.conventions.md, "utf8").then((s) => s.trim() || null).catch(() => null),
257
+ readFile(cp.rules.md, "utf8").then((s) => s.trim() || null).catch(() => null),
258
+ readFile(cp.skills.md, "utf8").then((s) => s.trim() || null).catch(() => null)
259
+ ]);
260
+ return { conventions, rules, skills };
261
+ }
262
+
263
+ // src/cli-agent/chat/slash-commands/shell.ts
264
+ import { exec } from "child_process";
265
+ import { promisify } from "util";
266
+ var execAsync = promisify(exec);
267
+ async function runShell(command) {
268
+ try {
269
+ const { stdout, stderr } = await execAsync(command, {
270
+ cwd: process.cwd(),
271
+ maxBuffer: 10 * 1024 * 1024,
272
+ shell: process.env.SHELL || "/bin/sh"
273
+ });
274
+ return `${stdout}${stderr}`;
275
+ } catch (err) {
276
+ const output = err;
277
+ return `${output.stdout ?? ""}${output.stderr ?? ""}`;
278
+ }
279
+ }
280
+
281
+ // src/cli-agent/chat/slash-commands/sessions.ts
282
+ function stringifyToolResult(result) {
283
+ return typeof result === "string" ? result : JSON.stringify(result ?? "");
284
+ }
285
+ function attachToolResult(messages, part, timestamp) {
286
+ const resultText = stringifyToolResult(part.result);
287
+ const content = part.isError ? `[ERROR] ${resultText}` : resultText;
288
+ const toolResult = {
289
+ name: part.name,
290
+ content,
291
+ fullContent: content,
292
+ success: !part.isError
293
+ };
294
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
295
+ const msg = messages[i];
296
+ if (!msg) continue;
297
+ if (msg.role === "user" || msg.role === "assistant" || msg.role === "system") break;
298
+ if (msg.role === "tool_call" && (msg.toolCallId === part.id || !msg.toolCallId && msg.name === part.name)) {
299
+ messages[i] = {
300
+ ...msg,
301
+ toolCallId: msg.toolCallId ?? part.id,
302
+ toolResults: [...msg.toolResults ?? [], toolResult],
303
+ expandedContent: content,
304
+ content: msg.content.endsWith(" \u2713") || msg.content.endsWith(" \u2717") ? msg.content : `${msg.content}${part.isError ? " \u2717" : " \u2713"}`
305
+ };
306
+ return;
307
+ }
308
+ }
309
+ messages.push({
310
+ role: "tool_call",
311
+ name: part.name,
312
+ toolCallId: part.id,
313
+ content: `Call ${part.name}${part.isError ? " \u2717" : " \u2713"}`,
314
+ toolResults: [toolResult],
315
+ expandedContent: content,
316
+ timestamp
317
+ });
318
+ }
319
+ function formatAge(date) {
320
+ const diff = Date.now() - date.getTime();
321
+ const m = Math.floor(diff / 6e4);
322
+ if (m < 60) return `${m}m ago`;
323
+ const h = Math.floor(m / 60);
324
+ if (h < 24) return `${h}h ago`;
325
+ return `${Math.floor(h / 24)}d ago`;
326
+ }
327
+ function getThreadTokenUsage(t) {
328
+ const metaUsage = t.metadata?.tokenUsage;
329
+ if (metaUsage?.totalTokens) return metaUsage;
330
+ return t.tokenUsage;
331
+ }
332
+ function formatThread(t, current) {
333
+ const usage = getThreadTokenUsage(t);
334
+ const tok = usage?.totalTokens ? ` \xB7 ${Math.round(usage.totalTokens / 1e3)}k tok` : "";
335
+ const title = t.title ?? t.id.slice(0, 8);
336
+ const bullet = current ? " \u25CF" : "";
337
+ return `\`${t.id.slice(0, 8)}\`${bullet} ${title}${tok} ${formatAge(t.updatedAt)}`;
338
+ }
339
+ function extractText(content) {
340
+ return content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
341
+ }
342
+ function extractTaskContent(raw) {
343
+ const match = raw.match(/<task>\n([\s\S]*?)\n<\/task>/);
344
+ return match?.[1]?.trim() ?? raw.trim();
345
+ }
346
+ function mapHarnessMessagesToUI(messages) {
347
+ const result = [];
348
+ for (const msg of messages) {
349
+ const ts = msg.createdAt.getTime();
350
+ if (msg.role === "user") {
351
+ const raw = extractText(msg.content);
352
+ result.push({ role: "user", content: extractTaskContent(raw), timestamp: ts });
353
+ } else if (msg.role === "assistant") {
354
+ let textParts = "";
355
+ for (const part of msg.content) {
356
+ if (part.type === "text") {
357
+ textParts += part.text;
358
+ } else if (part.type === "tool_call") {
359
+ const p = part;
360
+ if (textParts.trim()) {
361
+ result.push({ role: "assistant", content: textParts.trim(), timestamp: ts });
362
+ textParts = "";
363
+ }
364
+ result.push({ role: "tool_call", name: p.name, toolCallId: p.id, content: `Call ${p.name}`, timestamp: ts });
365
+ } else if (part.type === "tool_result") {
366
+ attachToolResult(result, part, ts);
367
+ }
368
+ }
369
+ if (textParts.trim()) {
370
+ result.push({ role: "assistant", content: textParts.trim(), timestamp: ts });
371
+ }
372
+ } else if (msg.role === "system") {
373
+ const text = extractText(msg.content);
374
+ if (text) result.push({ role: "system", content: text, timestamp: ts });
375
+ }
376
+ }
377
+ return result;
378
+ }
379
+ var sessionsCommand = {
380
+ name: "sessions",
381
+ description: "List or load Mastra chat threads. Usage: /sessions [new|load <thread-id>]",
382
+ execute: async (args, ctx) => {
383
+ const append = (content) => ctx.setMessages((prev) => [...prev, { role: "system", content, timestamp: Date.now() }]);
384
+ const [sub, ...rest] = args.trim().split(/\s+/);
385
+ if (sub === "new") {
386
+ ctx.newSession?.();
387
+ append("Started a new session.");
388
+ return;
389
+ }
390
+ if (sub === "load" && rest[0]) {
391
+ const prefix = rest[0];
392
+ const threads2 = await listMastraThreads();
393
+ const target = threads2.find((t) => t.id.startsWith(prefix));
394
+ if (!target) {
395
+ append(`No thread matching \`${prefix}\`. Run /sessions to list.`);
396
+ return;
397
+ }
398
+ await ctx.loadThreadById?.(target.id);
399
+ return;
400
+ }
401
+ const threads = await listMastraThreads();
402
+ if (threads.length === 0) {
403
+ append("No saved threads yet.");
404
+ return;
405
+ }
406
+ const currentId = ctx.getMastraThreadId?.() ?? null;
407
+ const lines = ["**Recent threads** (newest first):", ""];
408
+ [...threads].sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()).slice(0, 10).forEach((t) => lines.push(formatThread(t, t.id === currentId)));
409
+ lines.push("", "_/sessions new \xB7 /sessions load <thread-id>_");
410
+ append(lines.join("\n"));
411
+ }
412
+ };
413
+ async function loadThreadIntoUI(threadId, setMessages, appendMessage) {
414
+ const harnessMessages = await listMastraThreadMessages(threadId, 200);
415
+ if (harnessMessages.length === 0) {
416
+ appendMessage({ role: "system", content: "Thread is empty." });
417
+ return;
418
+ }
419
+ const uiMessages = mapHarnessMessagesToUI(harnessMessages);
420
+ setMessages([
421
+ ...uiMessages,
422
+ { role: "system", content: `_Thread loaded \xB7 ${uiMessages.filter((m) => m.role === "user").length} turns_`, timestamp: Date.now() }
423
+ ]);
424
+ }
425
+
426
+ // src/cli-agent/chat/slash-commands/help.ts
427
+ var helpCommand = {
428
+ name: "help",
429
+ description: "Show this help",
430
+ execute: (_args, ctx) => {
431
+ const lines = (ctx.getCommandList?.() ?? []).map(
432
+ (c) => `/${c.name.padEnd(18)} ${c.description}`
433
+ );
434
+ lines.push("", "@mention Type @ to autocomplete file paths");
435
+ ctx.setMessages((prev) => [
436
+ ...prev,
437
+ { role: "system", content: lines.join("\n") }
438
+ ]);
439
+ }
440
+ };
441
+
442
+ // src/cli-agent/chat/slash-commands/clear.ts
443
+ var clearCommand = {
444
+ name: "clear",
445
+ description: "Clear screen and start a new session (old session preserved in /sessions)",
446
+ execute: (_args, ctx) => {
447
+ ctx.newSession?.();
448
+ }
449
+ };
450
+
451
+ // src/cli-agent/chat/slash-commands/exit.ts
452
+ var exitCommand = {
453
+ name: "exit",
454
+ description: "Exit chat",
455
+ execute: (_args, ctx) => {
456
+ ctx.exit();
457
+ }
458
+ };
459
+
460
+ // src/cli-agent/chat/slash-commands/status.ts
461
+ var statusCommand = {
462
+ name: "status",
463
+ description: "Show model, session, and workspace status",
464
+ execute: async (_args, ctx) => {
465
+ const workspaceStatus = await getWorkspaceStatusLines();
466
+ ctx.setMessages((prev) => [
467
+ ...prev,
468
+ {
469
+ role: "system",
470
+ content: [
471
+ `Model: ${ctx.currentModel}`,
472
+ `History: ${ctx.getMessages().length} messages`,
473
+ ctx.availableModels?.length ? `Gateway: ${ctx.availableModels.length} models available` : "Gateway: not available",
474
+ ctx.getSessionTokens() > 0 ? `Tokens: ${ctx.getSessionTokens().toLocaleString()} total (thread)` : "",
475
+ "",
476
+ ...workspaceStatus
477
+ ].filter((l, i, arr) => l !== "" || arr[i - 1] !== "").join("\n")
478
+ }
479
+ ]);
480
+ }
481
+ };
482
+
483
+ // src/cli-agent/chat/slash-commands/models.ts
484
+ var modelsCommand = {
485
+ name: "models",
486
+ description: "Switch the active model",
487
+ execute: (_args, ctx) => {
488
+ if (!ctx.availableModels || ctx.availableModels.length === 0) {
489
+ ctx.setMessages((prev) => [
490
+ ...prev,
491
+ { role: "system", content: "No gateway models available." }
492
+ ]);
493
+ }
494
+ }
495
+ };
496
+
497
+ // src/cli-agent/chat/slash-commands/tool-categories.ts
498
+ var TOOL_CATEGORIES = {
499
+ // ── Local-only ───────────────────────────────────────────────────────────
500
+ refresh_local_index: "local",
501
+ diff: "local",
502
+ get_current_workspace_info: "local",
503
+ web_search: "local",
504
+ web_fetch: "local",
505
+ // ── Local index + optional cloud enhancement ─────────────────────────────
506
+ explore_task: "local",
507
+ search_codebase: "local",
508
+ get_file: "local",
509
+ find_related_files: "local",
510
+ symbol: "local",
511
+ rename_symbol: "local",
512
+ move_symbols: "local",
513
+ // ── Auth required, no project needed ─────────────────────────────────────
514
+ check_auth_status: "auth",
515
+ login: "auth",
516
+ logout: "auth",
517
+ get_project: "auth",
518
+ list_projects: "auth",
519
+ link_project: "auth",
520
+ manage_git_connection: "auth",
521
+ list_repositories: "auth",
522
+ // ── Cloud project required ────────────────────────────────────────────────
523
+ create_project: "cloud",
524
+ reimport: "cloud",
525
+ get_project_insights: "cloud",
526
+ get_project_map: "cloud"
527
+ };
528
+
529
+ // src/cli-agent/chat/slash-commands/tools.ts
530
+ var BOLD = "\x1B[1m";
531
+ var RESET = "\x1B[0m";
532
+ var C_GRAY = "\x1B[38;2;107;114;128m";
533
+ var C_CYAN = `${BOLD}\x1B[38;2;0;229;255m`;
534
+ var C_GREEN = "\x1B[38;2;34;197;94m";
535
+ var C_YELLOW = "\x1B[38;2;250;204;21m";
536
+ var C_MAGENTA = "\x1B[38;2;217;119;214m";
537
+ var SECTION = {
538
+ local: { label: "LOCAL", color: C_GREEN, note: "works offline, no auth" },
539
+ auth: { label: "AUTH", color: C_YELLOW, note: "login required" },
540
+ cloud: { label: "CLOUD", color: C_CYAN, note: "login + linked project" }
541
+ };
542
+ var EXTERNAL_SECTION = { label: "EXTERNAL", color: C_MAGENTA, note: "from user MCP servers" };
543
+ var toolsCommand = {
544
+ name: "tools",
545
+ description: "List available MCP tools grouped by category",
546
+ execute: async (_args, ctx) => {
547
+ ctx.setBusy(true);
548
+ try {
549
+ const allTools = await ctx.toolClient.listAllowedTools();
550
+ const disabledSet = new Set(MASTRA_DISABLED_TOOLS.map((n) => n.replace(/^codemap_/, "")));
551
+ const tools = allTools.filter((t) => !disabledSet.has(t.name));
552
+ const disabledTools = allTools.filter((t) => disabledSet.has(t.name));
553
+ const nameW = Math.min(32, Math.max(6, ...allTools.map((t) => t.name.length)));
554
+ const descW = 58;
555
+ const grouped = { local: [], auth: [], cloud: [] };
556
+ for (const t of tools) {
557
+ const cat = TOOL_CATEGORIES[t.name] ?? "cloud";
558
+ grouped[cat].push(t);
559
+ }
560
+ const mastraStatus = await getMastraMcpStatusSummary();
561
+ const externalTools = [];
562
+ if (mastraStatus?.statuses) {
563
+ for (const server of mastraStatus.statuses) {
564
+ if (server.toolNames.length === 0) continue;
565
+ if (server.name === "codemap") continue;
566
+ const serverTools = server.toolNames.map((name) => ({ name }));
567
+ externalTools.push({ server: server.name, tools: serverTools });
568
+ }
569
+ }
570
+ const lines = [
571
+ `${BOLD}Available tools${RESET} ${C_GRAY}${tools.length} MCP active, ${disabledTools.length} disabled${RESET}`,
572
+ ""
573
+ ];
574
+ for (const cat of ["local", "auth", "cloud"]) {
575
+ const group = grouped[cat];
576
+ if (group.length === 0) continue;
577
+ const { label, color, note } = SECTION[cat];
578
+ lines.push("");
579
+ lines.push(
580
+ `${color}${BOLD}${label}${RESET} ${C_GRAY}${note} \xB7 ${group.length} tools${RESET}`
581
+ );
582
+ lines.push(`${C_GRAY}${"\u2500".repeat(nameW + descW + 4)}${RESET}`);
583
+ for (const t of group) {
584
+ const name = t.name.padEnd(nameW).slice(0, nameW);
585
+ const raw = (t.description ?? "").replace(/\n.*/s, "").trim();
586
+ const desc = raw.length > descW ? raw.slice(0, descW - 1) + "\u2026" : raw;
587
+ lines.push(`${color}${name}${RESET} ${C_GRAY}${desc}${RESET}`);
588
+ }
589
+ lines.push("");
590
+ }
591
+ if (externalTools.length > 0) {
592
+ const { label, color, note } = EXTERNAL_SECTION;
593
+ const totalExternal = externalTools.reduce((sum, e) => sum + e.tools.length, 0);
594
+ lines.push("");
595
+ lines.push(
596
+ `${color}${BOLD}${label}${RESET} ${C_GRAY}${note} \xB7 ${totalExternal} tools from ${externalTools.length} server(s)${RESET}`
597
+ );
598
+ lines.push(`${C_GRAY}${"\u2500".repeat(nameW + descW + 4)}${RESET}`);
599
+ for (const { server, tools: serverTools } of externalTools) {
600
+ lines.push(`${C_GRAY}${" ".repeat(nameW + 2)}${color}[${server}]${RESET}`);
601
+ for (const t of serverTools) {
602
+ const name = t.name.padEnd(nameW).slice(0, nameW);
603
+ lines.push(`${color}${name}${RESET}`);
604
+ }
605
+ }
606
+ lines.push("");
607
+ }
608
+ if (disabledTools.length > 0) {
609
+ lines.push("");
610
+ lines.push(
611
+ `${C_GRAY}${BOLD}DISABLED${RESET} ${C_GRAY}handled by CLI /commands or internal \xB7 ${disabledTools.length} tools${RESET}`
612
+ );
613
+ lines.push(`${C_GRAY}${"\u2500".repeat(nameW + descW + 4)}${RESET}`);
614
+ for (const t of disabledTools) {
615
+ const name = t.name.padEnd(nameW).slice(0, nameW);
616
+ const raw = (t.description ?? "").replace(/\n.*/s, "").trim();
617
+ const desc = raw.length > descW ? raw.slice(0, descW - 1) + "\u2026" : raw;
618
+ lines.push(`${C_GRAY}${name} ${desc}${RESET}`);
619
+ }
620
+ lines.push("");
621
+ }
622
+ ctx.setMessages((prev) => [
623
+ ...prev,
624
+ { role: "system", content: lines.join("\n") }
625
+ ]);
626
+ } catch (err) {
627
+ ctx.setMessages((prev) => [
628
+ ...prev,
629
+ { role: "system", content: `Error listing tools: ${err}` }
630
+ ]);
631
+ }
632
+ ctx.setBusy(false);
633
+ }
634
+ };
635
+
636
+ // src/cli-agent/chat/slash-commands/diff.ts
637
+ var diffCommand = {
638
+ name: "diff",
639
+ description: "Show working diff",
640
+ execute: async (_args, ctx) => {
641
+ ctx.setBusy(true);
642
+ try {
643
+ const result = await ctx.toolClient.callTool("diff", {
644
+ mode: "working",
645
+ include_patch: false,
646
+ include_untracked: true
647
+ });
648
+ ctx.setMessages((prev) => [
649
+ ...prev,
650
+ { role: "system", content: result.content }
651
+ ]);
652
+ } catch (err) {
653
+ ctx.setMessages((prev) => [
654
+ ...prev,
655
+ { role: "system", content: `Error: ${err}` }
656
+ ]);
657
+ }
658
+ ctx.setBusy(false);
659
+ }
660
+ };
661
+
662
+ // src/cli-agent/chat/slash-commands/history.ts
663
+ var historyCommand = {
664
+ name: "history",
665
+ description: "Show conversation stats",
666
+ execute: (_args, ctx) => {
667
+ const msgs = ctx.getMessages();
668
+ const turns = msgs.filter((m) => m.role === "user").length;
669
+ ctx.setMessages((prev) => [
670
+ ...prev,
671
+ {
672
+ role: "system",
673
+ content: `Conversation: ${turns} user turns, ${msgs.length} total messages`
674
+ }
675
+ ]);
676
+ }
677
+ };
678
+
679
+ // src/cli-agent/chat/slash-commands/debug.ts
680
+ var debugCommand = {
681
+ name: "debug",
682
+ description: "Toggle stream debug logging to JSONL file",
683
+ execute: (args, ctx) => {
684
+ const set = (on) => {
685
+ ctx.setDebug(on);
686
+ const logLine = ctx.debugLogFile ? `
687
+ Log file: ${ctx.debugLogFile}` : "\nLog file will be created on next message.";
688
+ ctx.setMessages((prev) => [
689
+ ...prev,
690
+ {
691
+ role: "system",
692
+ content: `Debug mode: ${on ? "ON" : "OFF"}${on ? logLine : ""}`
693
+ }
694
+ ]);
695
+ };
696
+ if (args === "on") return set(true);
697
+ if (args === "off") return set(false);
698
+ set(!ctx.debug);
699
+ }
700
+ };
701
+
702
+ // src/cli-agent/chat/slash-commands/mcp.ts
703
+ var mcpCommand = {
704
+ name: "mcp",
705
+ description: "Manage MCP servers: list, add, remove",
706
+ execute: async (args, ctx) => {
707
+ const parts = args.trim().split(/\s+/);
708
+ const sub = parts[0];
709
+ if (!sub) {
710
+ return showStatus(ctx);
711
+ }
712
+ if (sub === "add") {
713
+ const name = parts[1];
714
+ const command = parts[2];
715
+ if (!name || !command) {
716
+ ctx.setMessages((prev) => [
717
+ ...prev,
718
+ { role: "system", content: "Usage: /mcp add <name> <command> [args...]" }
719
+ ]);
720
+ return;
721
+ }
722
+ const cmdArgs = parts.slice(3);
723
+ const config = { command, args: cmdArgs.length > 0 ? cmdArgs : void 0 };
724
+ const workspaceRoot = await readWorkspacePath();
725
+ await saveMcpServerEntry(workspaceRoot, name, config);
726
+ ctx.toolClient.addExtraServer(name, config);
727
+ ctx.setMessages((prev) => [
728
+ ...prev,
729
+ {
730
+ role: "system",
731
+ content: `Added MCP server "${name}" \u2192 ${command} ${cmdArgs.join(" ")}
732
+ Connecting\u2026`
733
+ }
734
+ ]);
735
+ ctx.setBusy(true);
736
+ try {
737
+ await ctx.reinitHarness?.();
738
+ } catch (err) {
739
+ ctx.setBusy(false);
740
+ const msg = err instanceof Error ? err.message : String(err);
741
+ ctx.toolClient.removeExtraServer(name);
742
+ await removeMcpServerEntry(workspaceRoot, name).catch(() => {
743
+ });
744
+ ctx.setMessages((prev) => [
745
+ ...prev,
746
+ { role: "system", content: `Failed to initialize harness: ${msg.split("\n")[0]}` }
747
+ ]);
748
+ return;
749
+ }
750
+ const summary = await getMastraMcpStatusSummary();
751
+ ctx.setBusy(false);
752
+ const serverStatus = summary?.statuses.find((s) => s.name === name);
753
+ const skipped = summary?.skipped.find((s) => s.name === name);
754
+ if (!serverStatus?.connected || skipped) {
755
+ const errMsg = serverStatus?.error ?? skipped?.reason ?? "unknown error";
756
+ const isNotFound = errMsg.includes("ENOENT") || errMsg.includes("not found");
757
+ const hint = isNotFound ? `
758
+
759
+ Tip: "${command}" was not found on PATH. Try using npx:
760
+ /mcp add ${name} npx ${[command, ...cmdArgs].join(" ")}` : "";
761
+ ctx.toolClient.removeExtraServer(name);
762
+ await removeMcpServerEntry(workspaceRoot, name).catch(() => {
763
+ });
764
+ ctx.setMessages((prev) => [
765
+ ...prev,
766
+ { role: "system", content: `Failed to connect MCP server "${name}": ${errMsg.split("\n")[0]}${hint}` }
767
+ ]);
768
+ ctx.reinitHarness?.().catch(() => {
769
+ });
770
+ } else {
771
+ ctx.setMessages((prev) => [
772
+ ...prev,
773
+ { role: "system", content: `MCP server "${name}" connected (${serverStatus.toolCount} tools).` }
774
+ ]);
775
+ }
776
+ return;
777
+ }
778
+ if (sub === "remove") {
779
+ const name = parts[1];
780
+ if (!name) {
781
+ ctx.setMessages((prev) => [
782
+ ...prev,
783
+ { role: "system", content: "Usage: /mcp remove <name>" }
784
+ ]);
785
+ return;
786
+ }
787
+ if (name === "codemap") {
788
+ ctx.setMessages((prev) => [
789
+ ...prev,
790
+ { role: "system", content: "Cannot remove the default codemap server." }
791
+ ]);
792
+ return;
793
+ }
794
+ const workspaceRoot = await readWorkspacePath();
795
+ const removed = await removeMcpServerEntry(workspaceRoot, name);
796
+ if (!removed) {
797
+ ctx.setMessages((prev) => [
798
+ ...prev,
799
+ { role: "system", content: `MCP server "${name}" not found in config.` }
800
+ ]);
801
+ return;
802
+ }
803
+ ctx.toolClient.removeExtraServer(name);
804
+ ctx.setMessages((prev) => [
805
+ ...prev,
806
+ { role: "system", content: `Removed MCP server "${name}". Reconnecting\u2026` }
807
+ ]);
808
+ ctx.setBusy(true);
809
+ await ctx.reinitHarness?.();
810
+ ctx.setBusy(false);
811
+ ctx.setMessages((prev) => [
812
+ ...prev,
813
+ { role: "system", content: `Harness reinitialized without "${name}".` }
814
+ ]);
815
+ return;
816
+ }
817
+ ctx.setMessages((prev) => [
818
+ ...prev,
819
+ {
820
+ role: "system",
821
+ content: "Unknown subcommand. Usage:\n/mcp \u2014 list servers\n/mcp add <name> <command> [args...]\n/mcp remove <name>"
822
+ }
823
+ ]);
824
+ }
825
+ };
826
+ async function showStatus(ctx) {
827
+ const summary = await getMastraMcpStatusSummary();
828
+ const lines = ["MCP Servers:", ""];
829
+ if (!summary) {
830
+ lines.push(" Mastra harness is not initialized yet.");
831
+ lines.push(" Send a chat message first, then run /mcp again.");
832
+ lines.push("");
833
+ lines.push("Add external servers: /mcp add <name> <command> [args...]");
834
+ lines.push("Or configure in .codemap/mcp.json \u2192 mcpServers");
835
+ ctx.setMessages((prev) => [
836
+ ...prev,
837
+ { role: "system", content: lines.join("\n") }
838
+ ]);
839
+ return;
840
+ }
841
+ const { statuses, skipped } = summary;
842
+ if (!summary.hasServers) {
843
+ lines.push(" No MCP servers configured in Mastra.");
844
+ }
845
+ for (const s of statuses) {
846
+ const icon = s.connected ? "\u25CF" : s.connecting ? "\u25D0" : "\u2717";
847
+ const state = s.connected ? `connected via ${s.transport}` : s.connecting ? `connecting via ${s.transport}` : `error: ${s.error ?? "unknown"}`;
848
+ const toolCount = s.toolCount > 0 ? ` (${s.toolCount} tools)` : "";
849
+ const toolNames = s.toolNames.length > 0 ? `
850
+ ${s.toolNames.slice(0, 8).join(", ")}${s.toolNames.length > 8 ? ", \u2026" : ""}` : "";
851
+ lines.push(` ${icon} ${s.name} \u2014 ${state}${toolCount}${toolNames}`);
852
+ }
853
+ for (const s of skipped) {
854
+ lines.push(` - ${s.name} \u2014 skipped: ${s.reason}`);
855
+ }
856
+ if (summary.hasServers && statuses.length === 0 && skipped.length === 0) {
857
+ lines.push(" Mastra MCP manager has configured servers, but no runtime status yet.");
858
+ lines.push(" Try sending one chat message or restart the chat if this persists.");
859
+ }
860
+ if (statuses.length + skipped.length <= 1) {
861
+ lines.push("");
862
+ lines.push("Add external servers: /mcp add <name> <command> [args...]");
863
+ lines.push("Or configure in .codemap/mcp.json \u2192 mcpServers");
864
+ }
865
+ ctx.setMessages((prev) => [
866
+ ...prev,
867
+ { role: "system", content: lines.join("\n") }
868
+ ]);
869
+ }
870
+
871
+ // src/cli-agent/chat/slash-commands/git-commit.ts
872
+ var COMMIT_MSG_PROMPT = `You are a git commit message generator. Given a diff, write a concise commit message.
873
+
874
+ Rules:
875
+ - First line: max 72 chars, imperative mood ("Add feature" not "Added feature")
876
+ - No period at end of subject line
877
+ - If needed, blank line then bullet points explaining WHY (not what the diff shows)
878
+ - Output ONLY the commit message, nothing else`;
879
+ var REVIEW_PROMPT = `You are a senior code reviewer. Analyze this git diff for issues.
880
+
881
+ Focus on:
882
+ - Security: hardcoded secrets, SQL injection, XSS, unsafe eval, missing auth checks
883
+ - Bugs: logic errors, null/undefined risks, race conditions, off-by-one
884
+ - Performance: N+1 queries, unnecessary re-renders, memory leaks
885
+ - Best practices: error handling, type safety, naming
886
+
887
+ Output format (strict JSON array, nothing else):
888
+ [{"severity":"critical|high|medium|low","category":"security|bugs|performance|style","message":"short description","file":"path","line":42}]
889
+
890
+ If no issues found, return: []
891
+
892
+ Diff:`;
893
+ function parseReviewResponse(text) {
894
+ const jsonMatch = /\[[\s\S]*\]/.exec(text);
895
+ if (!jsonMatch) return [];
896
+ try {
897
+ return JSON.parse(jsonMatch[0]);
898
+ } catch {
899
+ return [];
900
+ }
901
+ }
902
+ function formatReviewResult(issues) {
903
+ if (issues.length === 0) return "\u2705 Code review passed \u2014 no issues found";
904
+ const critical = issues.filter((i) => i.severity === "critical");
905
+ const high = issues.filter((i) => i.severity === "high");
906
+ const medium = issues.filter((i) => i.severity === "medium");
907
+ const low = issues.filter((i) => i.severity === "low");
908
+ const lines = ["\u26A0\uFE0F **Code Review:**\n"];
909
+ if (critical.length > 0) {
910
+ lines.push(`\u{1F534} **Critical (${critical.length}):**`);
911
+ for (const issue of critical) {
912
+ const loc = issue.file ? ` \`${issue.file}:${issue.line ?? ""}\`` : "";
913
+ lines.push(` - ${issue.message}${loc}`);
914
+ }
915
+ }
916
+ if (high.length > 0) {
917
+ lines.push(`\u{1F7E0} **High (${high.length}):**`);
918
+ for (const issue of high) {
919
+ const loc = issue.file ? ` \`${issue.file}:${issue.line ?? ""}\`` : "";
920
+ lines.push(` - ${issue.message}${loc}`);
921
+ }
922
+ }
923
+ if (medium.length > 0) {
924
+ lines.push(`\u{1F7E1} **Medium (${medium.length}):**`);
925
+ for (const issue of medium.slice(0, 3)) {
926
+ const loc = issue.file ? ` \`${issue.file}:${issue.line ?? ""}\`` : "";
927
+ lines.push(` - ${issue.message}${loc}`);
928
+ }
929
+ }
930
+ if (low.length > 0) {
931
+ lines.push(`\u26AA **Low (${low.length}):**`);
932
+ for (const issue of low.slice(0, 3)) {
933
+ const loc = issue.file ? ` \`${issue.file}:${issue.line ?? ""}\`` : "";
934
+ lines.push(` - ${issue.message}${loc}`);
935
+ }
936
+ }
937
+ return lines.join("\n");
938
+ }
939
+ var gitCommitCommand = {
940
+ name: "commit",
941
+ description: "AI-generated commit message [--review: run code review first] [--confirm: force commit despite review issues]",
942
+ execute: async (args, ctx) => {
943
+ ctx.setBusy(true);
944
+ ctx.startSubprocess("git commit");
945
+ const append = (content) => ctx.setMessages((prev) => [...prev, { role: "system", content }]);
946
+ const shouldReview = args.includes("--review");
947
+ const forceConfirm = args.includes("--confirm");
948
+ const cleanArgs = args.replace("--review", "").replace("--confirm", "").trim();
949
+ let committed = false;
950
+ try {
951
+ const manualMsg = cleanArgs;
952
+ if (manualMsg) {
953
+ ctx.logSubprocess("Committing with provided message\u2026");
954
+ const output = await runShell(`git add -A && git commit -m ${JSON.stringify(manualMsg)}`);
955
+ committed = true;
956
+ append(output || "Committed.");
957
+ return;
958
+ }
959
+ ctx.logSubprocess("Checking status\u2026");
960
+ const status = await runShell("git status --short");
961
+ if (!status.trim()) {
962
+ append("Nothing to commit \u2014 working tree clean.");
963
+ return;
964
+ }
965
+ ctx.logSubprocess("Reading diff\u2026");
966
+ const diffOutput = await runShell("git diff HEAD --stat && echo '---' && git diff HEAD -- . ':(exclude)package-lock.json' ':(exclude)*.lock'");
967
+ const diff = diffOutput.slice(0, 6e3);
968
+ let reviewIssues = [];
969
+ if (shouldReview) {
970
+ ctx.logSubprocess("AI reviewing diff\u2026");
971
+ let reviewResponse = "";
972
+ for await (const chunk of ctx.provider.stream({
973
+ model: ctx.currentModel,
974
+ messages: [{ role: "user", content: `${REVIEW_PROMPT}
975
+
976
+ ${diff}` }]
977
+ })) {
978
+ if (chunk.text) reviewResponse += chunk.text;
979
+ }
980
+ reviewIssues = parseReviewResponse(reviewResponse);
981
+ if (reviewIssues.length > 0) {
982
+ ctx.logSubprocess(`Found ${reviewIssues.length} issue(s)`);
983
+ const reviewResult = formatReviewResult(reviewIssues);
984
+ append(reviewResult);
985
+ const hasBlockingIssues = reviewIssues.some(
986
+ (i) => i.severity === "critical" || i.severity === "high"
987
+ );
988
+ if (hasBlockingIssues && !forceConfirm) {
989
+ append(
990
+ "\n\u274C **Commit blocked** \u2014 critical/high issues found.\nFix issues and run `/commit --review` again, or use `/commit --confirm` to force commit."
991
+ );
992
+ return;
993
+ }
994
+ } else {
995
+ ctx.logSubprocess("No issues found");
996
+ }
997
+ }
998
+ ctx.logSubprocess("Generating commit message\u2026");
999
+ let commitMsg = "";
1000
+ for await (const chunk of ctx.provider.stream({
1001
+ model: ctx.currentModel,
1002
+ messages: [{
1003
+ role: "user",
1004
+ content: `${COMMIT_MSG_PROMPT}
1005
+
1006
+ Diff:
1007
+ ${diff}`
1008
+ }]
1009
+ })) {
1010
+ if (chunk.text) commitMsg += chunk.text;
1011
+ }
1012
+ commitMsg = commitMsg.trim();
1013
+ if (!commitMsg) {
1014
+ append("Failed to generate commit message. Use `/commit <message>` instead.");
1015
+ return;
1016
+ }
1017
+ ctx.logSubprocess(`\u2192 ${commitMsg.split("\n")[0]}`);
1018
+ const commitOutput = await runShell(`git add -A && git commit -m ${JSON.stringify(commitMsg)}`);
1019
+ committed = true;
1020
+ let resultMsg = `\`\`\`
1021
+ ${commitMsg}
1022
+ \`\`\`
1023
+ ${commitOutput}
1024
+
1025
+ _Undo: \`git reset --soft HEAD~1\`_`;
1026
+ if (shouldReview) {
1027
+ if (reviewIssues.length > 0) {
1028
+ resultMsg += forceConfirm ? `
1029
+
1030
+ \u26A0\uFE0F Force committed with --confirm (${reviewIssues.length} review issue(s))` : `
1031
+
1032
+ \u26A0\uFE0F Committed with ${reviewIssues.length} review issue(s)`;
1033
+ } else {
1034
+ resultMsg += "\n\n\u2705 Code review passed \u2014 no issues found";
1035
+ }
1036
+ }
1037
+ append(resultMsg);
1038
+ } catch (err) {
1039
+ append(`Error: ${err instanceof Error ? err.message : String(err)}`);
1040
+ } finally {
1041
+ if (committed) {
1042
+ ctx.logSubprocess("Refreshing local/cloud commit status\u2026");
1043
+ await ctx.refreshWorkspaceCommits?.();
1044
+ }
1045
+ ctx.endSubprocess();
1046
+ ctx.setBusy(false);
1047
+ }
1048
+ }
1049
+ };
1050
+
1051
+ // src/cli-agent/chat/slash-commands/git-push.ts
1052
+ var gitPushCommand = {
1053
+ name: "push",
1054
+ description: "Push current branch to remote (sets upstream if needed)",
1055
+ execute: async (_args, ctx) => {
1056
+ ctx.setBusy(true);
1057
+ ctx.startSubprocess("git push");
1058
+ const append = (content) => ctx.setMessages((prev) => [...prev, { role: "system", content }]);
1059
+ try {
1060
+ ctx.logSubprocess("Reading branch\u2026");
1061
+ const branchOutput = await runShell("git rev-parse --abbrev-ref HEAD");
1062
+ const branch = branchOutput.trim();
1063
+ if (!branch || branch === "HEAD") {
1064
+ append("Not on a branch \u2014 cannot push.");
1065
+ return;
1066
+ }
1067
+ ctx.logSubprocess(`Pushing \`${branch}\`\u2026`);
1068
+ const pushOutput = await runShell(`git push 2>&1 || git push -u origin ${branch} 2>&1`);
1069
+ const output = pushOutput.trim();
1070
+ if (output) ctx.logSubprocess(output.split("\n").at(-1) ?? output);
1071
+ ctx.logSubprocess("Refreshing local/cloud commit status\u2026");
1072
+ await ctx.refreshWorkspaceCommits?.();
1073
+ append(output || `Pushed \`${branch}\`.`);
1074
+ } catch (err) {
1075
+ append(`Error: ${err instanceof Error ? err.message : String(err)}`);
1076
+ } finally {
1077
+ ctx.endSubprocess();
1078
+ ctx.setBusy(false);
1079
+ }
1080
+ }
1081
+ };
1082
+
1083
+ // src/cli-agent/chat/slash-commands/git-pr.ts
1084
+ var PR_PROMPT = `You are a pull request description writer. Given commits and a diff summary, write a PR title and description.
1085
+
1086
+ Output format (exactly):
1087
+ TITLE: <title under 70 chars>
1088
+
1089
+ ## Summary
1090
+ - <bullet>
1091
+ - <bullet>
1092
+
1093
+ ## Changes
1094
+ - <key file or area changed>
1095
+
1096
+ ## Test plan
1097
+ - <what to verify>
1098
+
1099
+ Rules:
1100
+ - Output ONLY the title and body in this exact format, nothing else
1101
+ - Title: imperative mood, under 70 chars
1102
+ - Be concise \u2014 8-12 lines total`;
1103
+ var gitPrCommand = {
1104
+ name: "pr",
1105
+ description: "Create a pull request with AI-generated title and description",
1106
+ execute: async (args, ctx) => {
1107
+ ctx.setBusy(true);
1108
+ ctx.startSubprocess("gh pr create");
1109
+ const append = (content) => ctx.setMessages((prev) => [...prev, { role: "system", content }]);
1110
+ try {
1111
+ ctx.logSubprocess("Checking gh CLI\u2026");
1112
+ const ghCheck = await runShell("which gh 2>&1");
1113
+ if (!ghCheck.trim().startsWith("/")) {
1114
+ append("`gh` CLI not found. Install it from https://cli.github.com then run `/pr` again.");
1115
+ return;
1116
+ }
1117
+ ctx.logSubprocess("Reading commits\u2026");
1118
+ const baseOutput = await runShell("git remote show origin 2>/dev/null | grep 'HEAD branch' | awk '{print $NF}' || echo 'main'");
1119
+ const base = baseOutput.trim() || "main";
1120
+ const logOutput = await runShell(`git log ${base}..HEAD --oneline 2>/dev/null | head -30`);
1121
+ if (!logOutput.trim()) {
1122
+ append(`No commits ahead of \`${base}\`. Nothing to PR.`);
1123
+ return;
1124
+ }
1125
+ const diffStat = await runShell(`git diff ${base}..HEAD --stat 2>/dev/null | tail -20`);
1126
+ const context = `Commits:
1127
+ ${logOutput}
1128
+
1129
+ Changed files:
1130
+ ${diffStat}`;
1131
+ const extra = args.trim() ? `
1132
+ Extra context: ${args}` : "";
1133
+ ctx.logSubprocess("Generating PR description\u2026");
1134
+ let raw = "";
1135
+ const vanillaModel = ctx.availableModels?.find((m) => m.startsWith("cc/") || m.startsWith("kr/")) ?? ctx.currentModel;
1136
+ for await (const chunk of ctx.provider.stream({
1137
+ model: vanillaModel,
1138
+ messages: [{
1139
+ role: "user",
1140
+ content: `${PR_PROMPT}
1141
+
1142
+ ${context}${extra}`
1143
+ }]
1144
+ })) {
1145
+ if (chunk.text) raw += chunk.text;
1146
+ }
1147
+ raw = raw.trim();
1148
+ const titleMatch = raw.match(/^TITLE:\s*(.+)/m);
1149
+ const title = titleMatch?.[1]?.trim() ?? "Update";
1150
+ const body = raw.replace(/^TITLE:.*\n?/m, "").trim();
1151
+ if (!title) {
1152
+ append("Failed to generate PR description.");
1153
+ return;
1154
+ }
1155
+ ctx.logSubprocess(`\u2192 ${title}`);
1156
+ ctx.logSubprocess("Creating PR\u2026");
1157
+ const prOutput = await runShell(`gh pr create --base ${base} --title ${JSON.stringify(title)} --body ${JSON.stringify(body)} 2>&1`);
1158
+ const prUrl = prOutput.match(/https:\/\/github\.com\/\S+/)?.[0] ?? "";
1159
+ if (prUrl) ctx.logSubprocess(prUrl);
1160
+ append(`**PR created**
1161
+
1162
+ **${title}**
1163
+
1164
+ ${body}
1165
+
1166
+ ---
1167
+ ${prOutput}`);
1168
+ } catch (err) {
1169
+ append(`Error: ${err instanceof Error ? err.message : String(err)}`);
1170
+ } finally {
1171
+ ctx.endSubprocess();
1172
+ ctx.setBusy(false);
1173
+ }
1174
+ }
1175
+ };
1176
+
1177
+ // src/cli-agent/chat/slash-commands/conventions.ts
1178
+ var conventionsCommand = {
1179
+ name: "conventions",
1180
+ description: "Show synthesized conventions/rules/skills. Use 'refresh' to re-synthesize.",
1181
+ execute: async (args, ctx) => {
1182
+ const append = (content) => ctx.setMessages((prev) => [...prev, { role: "system", content }]);
1183
+ const sub = args.trim().toLowerCase();
1184
+ const model = ctx.currentModel;
1185
+ if (sub === "refresh") {
1186
+ ctx.setBusy(true);
1187
+ append("Re-synthesizing conventions, rules and skills in parallel\u2026");
1188
+ try {
1189
+ const result = await refreshAll(ctx.provider, model);
1190
+ if (!result) {
1191
+ append("No convention/rule/skill files found in workspace.");
1192
+ } else {
1193
+ const parts = [];
1194
+ if (result.conventions) parts.push(`### Conventions
1195
+ ${result.conventions}`);
1196
+ if (result.rules) parts.push(`### Rules
1197
+ ${result.rules}`);
1198
+ if (result.skills) parts.push(`### Skills
1199
+ ${result.skills}`);
1200
+ append(`**Refreshed** (3 synthesis runs in parallel)
1201
+
1202
+ ${parts.join("\n\n---\n\n")}`);
1203
+ }
1204
+ } catch (err) {
1205
+ append(`Error: ${err instanceof Error ? err.message : String(err)}`);
1206
+ }
1207
+ ctx.setBusy(false);
1208
+ return;
1209
+ }
1210
+ ctx.setBusy(true);
1211
+ try {
1212
+ let cached = await getCachedContext();
1213
+ const hasCache = cached.conventions || cached.rules || cached.skills;
1214
+ if (!hasCache) {
1215
+ append("No cache found. Synthesizing now (running 3 streams in parallel)\u2026");
1216
+ const result = await loadOrSynthesizeAll(ctx.provider, model);
1217
+ if (!result) {
1218
+ append("No convention/rule/skill files found in workspace.");
1219
+ ctx.setBusy(false);
1220
+ return;
1221
+ }
1222
+ cached = result;
1223
+ }
1224
+ const parts = [];
1225
+ if (cached.conventions) parts.push(`### Conventions
1226
+ ${cached.conventions}`);
1227
+ if (cached.rules) parts.push(`### Rules
1228
+ ${cached.rules}`);
1229
+ if (cached.skills) parts.push(`### Skills & Workflows
1230
+ ${cached.skills}`);
1231
+ append(`**Project context** (3 synthesized files)
1232
+
1233
+ ${parts.join("\n\n---\n\n")}
1234
+
1235
+ ---
1236
+ _/conventions refresh \u2014 re-synthesize from source files_`);
1237
+ } catch (err) {
1238
+ append(`Error: ${err instanceof Error ? err.message : String(err)}`);
1239
+ }
1240
+ ctx.setBusy(false);
1241
+ }
1242
+ };
1243
+
1244
+ // src/cli-agent/chat/ui/pi-tui/clipboard.ts
1245
+ import { spawnSync as spawnSync2 } from "child_process";
1246
+ function copyToClipboard(text) {
1247
+ try {
1248
+ if (process.platform === "darwin") {
1249
+ return spawnSync2("pbcopy", { input: text, encoding: "utf8" }).status === 0;
1250
+ }
1251
+ if (process.platform === "linux") {
1252
+ if (spawnSync2("xclip", ["-selection", "clipboard"], { input: text, encoding: "utf8" }).status === 0) return true;
1253
+ if (spawnSync2("xsel", ["--clipboard", "--input"], { input: text, encoding: "utf8" }).status === 0) return true;
1254
+ if (spawnSync2("wl-copy", [], { input: text, encoding: "utf8" }).status === 0) return true;
1255
+ }
1256
+ if (process.platform === "win32") {
1257
+ return spawnSync2("clip", { input: text, encoding: "utf8" }).status === 0;
1258
+ }
1259
+ } catch {
1260
+ }
1261
+ return false;
1262
+ }
1263
+
1264
+ // src/cli-agent/chat/slash-commands/copy-cmd.ts
1265
+ var copyCommand = {
1266
+ name: "copy",
1267
+ description: "Copy last assistant response to clipboard",
1268
+ execute: (_args, ctx) => {
1269
+ const msgs = ctx.getMessages?.() ?? [];
1270
+ const last = [...msgs].reverse().find((m) => m.role === "assistant" && m.content?.trim());
1271
+ if (!last) {
1272
+ ctx.appendMessage({ role: "system", content: "Nothing to copy \u2014 no assistant response found." });
1273
+ return;
1274
+ }
1275
+ const ok = copyToClipboard(last.content);
1276
+ ctx.appendMessage({
1277
+ role: "system",
1278
+ content: ok ? `Copied to clipboard (${last.content.length.toLocaleString()} chars).` : "Copy failed \u2014 pbcopy/xclip/xsel not available."
1279
+ });
1280
+ }
1281
+ };
1282
+
1283
+ // src/cli-agent/chat/slash-commands/login.ts
1284
+ var loginCommand = {
1285
+ name: "login",
1286
+ description: "Log in to CodeMap (opens browser for authorization)",
1287
+ execute: async (_args, ctx) => {
1288
+ ctx.setBusy(true);
1289
+ try {
1290
+ const config = await loadConfig();
1291
+ if (config.apiToken) {
1292
+ const lines2 = ["Already logged in."];
1293
+ if (config.user?.email) lines2.push(`Account: ${config.user.email}`);
1294
+ if (config.user?.name) lines2.push(`Name: ${config.user.name}`);
1295
+ lines2.push(`API: ${config.apiUrl}`);
1296
+ lines2.push(`
1297
+ Use /logout first if you want to log in with a different account.`);
1298
+ ctx.setMessages((prev) => [
1299
+ ...prev,
1300
+ { role: "system", content: lines2.join("\n") }
1301
+ ]);
1302
+ return;
1303
+ }
1304
+ ctx.setMessages((prev) => [
1305
+ ...prev,
1306
+ { role: "system", content: "Starting CodeMap login..." }
1307
+ ]);
1308
+ const client = createCodeMapClient(config);
1309
+ const startResponse = await startMcpLogin(client);
1310
+ const openedBrowser = await tryOpenLoginBrowser(startResponse.authorizeUrl);
1311
+ const urlMsg = openedBrowser ? "Browser opened for CodeMap authorization.\nWaiting for you to approve access..." : `Open this URL to log in:
1312
+ ${startResponse.authorizeUrl}
1313
+
1314
+ Waiting for authorization...`;
1315
+ ctx.setMessages((prev) => [...prev, { role: "system", content: urlMsg }]);
1316
+ const result = await waitForLoginAuthorization(config, startResponse);
1317
+ const lines = ["Logged in successfully."];
1318
+ if (result.user?.email) lines.push(`Account: ${result.user.email}`);
1319
+ if (result.user?.name) lines.push(`Name: ${result.user.name}`);
1320
+ lines.push(`API: ${result.apiUrl || config.apiUrl}`);
1321
+ ctx.setMessages((prev) => [...prev, { role: "system", content: lines.join("\n") }]);
1322
+ await ctx.reinitHarness?.();
1323
+ } catch (err) {
1324
+ ctx.setMessages((prev) => [
1325
+ ...prev,
1326
+ {
1327
+ role: "system",
1328
+ content: `Login failed: ${err instanceof Error ? err.message : String(err)}`
1329
+ }
1330
+ ]);
1331
+ } finally {
1332
+ ctx.setBusy(false);
1333
+ }
1334
+ }
1335
+ };
1336
+
1337
+ // src/cli-agent/chat/slash-commands/logout.ts
1338
+ var logoutCommand = {
1339
+ name: "logout",
1340
+ description: "Log out of CodeMap and clear stored credentials",
1341
+ execute: async (_args, ctx) => {
1342
+ ctx.setBusy(true);
1343
+ try {
1344
+ const config = await loadConfig();
1345
+ if (!config.apiToken) {
1346
+ ctx.setMessages((prev) => [
1347
+ ...prev,
1348
+ { role: "system", content: "Not logged in." }
1349
+ ]);
1350
+ return;
1351
+ }
1352
+ const client = createCodeMapClient(config);
1353
+ await client.request("/settings/api-keys/revoke-current", {
1354
+ method: "POST",
1355
+ authRequired: true
1356
+ }).catch(() => {
1357
+ });
1358
+ await clearGlobalAuthConfig(config);
1359
+ ctx.setMessages((prev) => [
1360
+ ...prev,
1361
+ { role: "system", content: "Logged out. API key revoked and credentials cleared." }
1362
+ ]);
1363
+ } catch (err) {
1364
+ ctx.setMessages((prev) => [
1365
+ ...prev,
1366
+ {
1367
+ role: "system",
1368
+ content: `Logout error: ${err instanceof Error ? err.message : String(err)}`
1369
+ }
1370
+ ]);
1371
+ } finally {
1372
+ ctx.setBusy(false);
1373
+ }
1374
+ }
1375
+ };
1376
+
1377
+ // src/cli-agent/chat/slash-commands/projects.ts
1378
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "importing", "ready", "failed", "archived"]);
1379
+ var projectsCommand = {
1380
+ name: "projects",
1381
+ description: "List CodeMap cloud projects. Usage: /projects [--status <status>]",
1382
+ execute: async (args, ctx) => {
1383
+ ctx.setBusy(true);
1384
+ try {
1385
+ const parts = args.trim().split(/\s+/).filter(Boolean);
1386
+ const toolArgs = {};
1387
+ const statusIdx = parts.indexOf("--status");
1388
+ if (statusIdx >= 0 && parts[statusIdx + 1]) {
1389
+ const status = parts[statusIdx + 1];
1390
+ if (!VALID_STATUSES.has(status)) {
1391
+ ctx.setMessages((prev) => [
1392
+ ...prev,
1393
+ {
1394
+ role: "system",
1395
+ content: `Invalid status "${status}". Valid: draft, importing, ready, failed, archived`
1396
+ }
1397
+ ]);
1398
+ return;
1399
+ }
1400
+ toolArgs.status = status;
1401
+ }
1402
+ const result = await ctx.toolClient.callTool("list_projects", toolArgs);
1403
+ ctx.setMessages((prev) => [...prev, { role: "system", content: result.content }]);
1404
+ } catch (err) {
1405
+ ctx.setMessages((prev) => [
1406
+ ...prev,
1407
+ {
1408
+ role: "system",
1409
+ content: `Error: ${err instanceof Error ? err.message : String(err)}`
1410
+ }
1411
+ ]);
1412
+ } finally {
1413
+ ctx.setBusy(false);
1414
+ }
1415
+ }
1416
+ };
1417
+
1418
+ // src/cli-agent/chat/slash-commands/link.ts
1419
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1420
+ var linkCommand = {
1421
+ name: "link",
1422
+ description: "Link this workspace to a CodeMap project. Usage: /link [<project-id>] [--confirm] [--update-repo]",
1423
+ execute: async (args, ctx) => {
1424
+ ctx.setBusy(true);
1425
+ try {
1426
+ const parts = args.trim().split(/\s+/).filter(Boolean);
1427
+ const toolArgs = {};
1428
+ for (const part of parts) {
1429
+ if (UUID_RE.test(part)) toolArgs.project_id = part;
1430
+ else if (part === "--confirm") toolArgs.confirm = true;
1431
+ else if (part === "--update-repo") toolArgs.update_repo = true;
1432
+ }
1433
+ const result = await ctx.toolClient.callTool("link_project", toolArgs);
1434
+ ctx.setMessages((prev) => [...prev, { role: "system", content: result.content }]);
1435
+ } catch (err) {
1436
+ ctx.setMessages((prev) => [
1437
+ ...prev,
1438
+ {
1439
+ role: "system",
1440
+ content: `Error: ${err instanceof Error ? err.message : String(err)}`
1441
+ }
1442
+ ]);
1443
+ } finally {
1444
+ ctx.setBusy(false);
1445
+ }
1446
+ }
1447
+ };
1448
+
1449
+ // src/cli-agent/chat/slash-commands/create.ts
1450
+ var createCommand = {
1451
+ name: "create",
1452
+ description: "Create a CodeMap project from this workspace. Usage: /create [--upload] [github <url>] [gitlab <url> [--token <tok>]]",
1453
+ execute: async (args, ctx) => {
1454
+ ctx.setBusy(true);
1455
+ try {
1456
+ const parts = args.trim().split(/\s+/).filter(Boolean);
1457
+ let toolName = "create_project";
1458
+ const toolArgs = {};
1459
+ if ((parts[0] === "github" || parts[0] === "gitlab") && parts[1]) {
1460
+ toolArgs.repository_url = parts[1];
1461
+ const tokenIdx = parts.indexOf("--token");
1462
+ if (tokenIdx >= 0 && parts[tokenIdx + 1]) {
1463
+ toolArgs.access_token = parts[tokenIdx + 1];
1464
+ }
1465
+ } else if (parts.includes("--upload")) {
1466
+ toolArgs.upload_confirmed = true;
1467
+ }
1468
+ const result = await ctx.toolClient.callTool(toolName, toolArgs);
1469
+ ctx.setMessages((prev) => [...prev, { role: "system", content: result.content }]);
1470
+ } catch (err) {
1471
+ ctx.setMessages((prev) => [
1472
+ ...prev,
1473
+ {
1474
+ role: "system",
1475
+ content: `Error: ${err instanceof Error ? err.message : String(err)}`
1476
+ }
1477
+ ]);
1478
+ } finally {
1479
+ ctx.setBusy(false);
1480
+ }
1481
+ }
1482
+ };
1483
+
1484
+ // src/cli-agent/chat/slash-commands/import.ts
1485
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1486
+ var importCommand = {
1487
+ name: "import",
1488
+ description: "Trigger a CodeMap reimport and wait for it to finish. Usage: /import [<project-id>]",
1489
+ execute: async (args, ctx) => {
1490
+ ctx.setBusy(true);
1491
+ try {
1492
+ const parts = args.trim().split(/\s+/).filter(Boolean);
1493
+ const toolArgs = {};
1494
+ const uuid = parts.find((p) => UUID_RE2.test(p));
1495
+ if (uuid) toolArgs.project_id = uuid;
1496
+ ctx.setMessages((prev) => [
1497
+ ...prev,
1498
+ { role: "system", content: "Triggering reimport..." }
1499
+ ]);
1500
+ const MAX_POLLS = 12;
1501
+ let result = await ctx.toolClient.callTool("reimport", {
1502
+ ...toolArgs,
1503
+ action: "trigger_and_wait"
1504
+ });
1505
+ for (let i = 0; i < MAX_POLLS; i++) {
1506
+ if (result.isError) break;
1507
+ const text = result.content ?? "";
1508
+ if (!text.includes("still in progress") && !text.includes("timedOut")) break;
1509
+ result = await ctx.toolClient.callTool("reimport", {
1510
+ ...toolArgs,
1511
+ action: "wait"
1512
+ });
1513
+ }
1514
+ ctx.setMessages((prev) => [...prev, { role: "system", content: result.content }]);
1515
+ } catch (err) {
1516
+ ctx.setMessages((prev) => [
1517
+ ...prev,
1518
+ {
1519
+ role: "system",
1520
+ content: `Error: ${err instanceof Error ? err.message : String(err)}`
1521
+ }
1522
+ ]);
1523
+ } finally {
1524
+ ctx.setBusy(false);
1525
+ }
1526
+ }
1527
+ };
1528
+
1529
+ // src/cli-agent/chat/slash-commands/index.ts
1530
+ var commands = [
1531
+ helpCommand,
1532
+ statusCommand,
1533
+ loginCommand,
1534
+ logoutCommand,
1535
+ projectsCommand,
1536
+ linkCommand,
1537
+ createCommand,
1538
+ importCommand,
1539
+ modelsCommand,
1540
+ toolsCommand,
1541
+ diffCommand,
1542
+ gitCommitCommand,
1543
+ gitPushCommand,
1544
+ gitPrCommand,
1545
+ sessionsCommand,
1546
+ conventionsCommand,
1547
+ copyCommand,
1548
+ clearCommand,
1549
+ historyCommand,
1550
+ debugCommand,
1551
+ mcpCommand,
1552
+ exitCommand
1553
+ ];
1554
+ function getCommandList() {
1555
+ return commands;
1556
+ }
1557
+ function executeCommand(text, ctx) {
1558
+ const [cmd] = text.split(/\s+/);
1559
+ const command = commands.find((c) => `/${c.name}` === cmd);
1560
+ if (!command) return false;
1561
+ command.execute(text.slice(cmd.length).trim(), ctx);
1562
+ return true;
1563
+ }
1564
+
1565
+ // src/cli-agent/core/file-search.ts
1566
+ var import_code_index = __toESM(require_dist(), 1);
1567
+ import path2 from "path";
1568
+ var cachedWorkspaceFiles = null;
1569
+ var workspaceFilesPromise = null;
1570
+ async function warmupFileSearch() {
1571
+ await searchIndexedFiles("").catch(() => {
1572
+ });
1573
+ }
1574
+ async function searchIndexedFiles(query) {
1575
+ const normalized = query.trim();
1576
+ debugFileSearch("start", { query: normalized });
1577
+ const localResults = await searchLocalIndex(normalized);
1578
+ debugFileSearch("local results", { count: localResults.length });
1579
+ if (localResults.length > 0) return localResults;
1580
+ const workspaceResults = await searchWorkspaceFiles(normalized);
1581
+ debugFileSearch("workspace results", { count: workspaceResults.length });
1582
+ return workspaceResults;
1583
+ }
1584
+ var cachedLocalStore = null;
1585
+ async function searchLocalIndex(query) {
1586
+ try {
1587
+ if (!cachedLocalStore) {
1588
+ const { store: store2 } = await ensureLocalIndexWithSummary();
1589
+ cachedLocalStore = store2;
1590
+ debugFileSearch("local index opened", { dbPath: store2.dbPath });
1591
+ }
1592
+ const store = cachedLocalStore;
1593
+ const pathMatches = rankFiles(
1594
+ store.listFiles(5e3).filter((file) => isSelectablePath(file.path)),
1595
+ query
1596
+ ).map((file) => ({
1597
+ path: file.path,
1598
+ label: file.path,
1599
+ hint: formatHint(file.language, file.parseStatus, "local index")
1600
+ }));
1601
+ if (!query || pathMatches.length > 0) return pathMatches;
1602
+ const searchMatches = uniqueByPath(
1603
+ store.search(query, null).files.filter((file) => isSelectablePath(file.path)).slice(0, 50).map((file) => ({
1604
+ path: file.path,
1605
+ label: file.path,
1606
+ hint: formatHint(file.language, void 0, "local index")
1607
+ }))
1608
+ );
1609
+ debugFileSearch("local full-text results", { count: searchMatches.length });
1610
+ return searchMatches;
1611
+ } catch (error) {
1612
+ debugFileSearch("local failed", error);
1613
+ cachedLocalStore = null;
1614
+ return [];
1615
+ }
1616
+ }
1617
+ async function searchWorkspaceFiles(query) {
1618
+ try {
1619
+ if (!cachedWorkspaceFiles) {
1620
+ if (!workspaceFilesPromise) {
1621
+ workspaceFilesPromise = (async () => {
1622
+ const workspacePath = await readWorkspacePath();
1623
+ debugFileSearch("workspace scan", { workspacePath });
1624
+ return (0, import_code_index.collectWorkspaceFiles)(workspacePath);
1625
+ })();
1626
+ }
1627
+ cachedWorkspaceFiles = await workspaceFilesPromise;
1628
+ }
1629
+ return rankFiles(
1630
+ cachedWorkspaceFiles.filter(
1631
+ (file) => file.isText && isSelectablePath(file.path)
1632
+ ),
1633
+ query
1634
+ ).map((file) => ({
1635
+ path: file.path,
1636
+ label: file.path,
1637
+ hint: formatHint(file.language, file.parseStatus, "workspace")
1638
+ }));
1639
+ } catch (error) {
1640
+ debugFileSearch("workspace failed", error);
1641
+ cachedWorkspaceFiles = null;
1642
+ workspaceFilesPromise = null;
1643
+ return [];
1644
+ }
1645
+ }
1646
+ function rankFiles(files, query) {
1647
+ return files.map((file) => ({
1648
+ file,
1649
+ score: scorePath(file.path, query)
1650
+ })).filter((item) => !query || item.score < Number.MAX_SAFE_INTEGER).sort(
1651
+ (left, right) => left.score - right.score || left.file.path.localeCompare(right.file.path)
1652
+ ).slice(0, 50).map((item) => item.file);
1653
+ }
1654
+ function uniqueByPath(items) {
1655
+ const seen = /* @__PURE__ */ new Set();
1656
+ return items.filter((item) => {
1657
+ if (seen.has(item.path)) return false;
1658
+ seen.add(item.path);
1659
+ return true;
1660
+ });
1661
+ }
1662
+ function scorePath(filePath, query) {
1663
+ if (!query) return sourcePreferencePenalty(filePath);
1664
+ const normalizedPath = filePath.toLowerCase();
1665
+ const normalizedName = path2.posix.basename(normalizedPath);
1666
+ const normalizedQuery = query.toLowerCase();
1667
+ const queryTokens = splitTerms(normalizedQuery);
1668
+ const basePenalty = sourcePreferencePenalty(filePath);
1669
+ if (normalizedName === normalizedQuery) return basePenalty;
1670
+ if (stripExtension(normalizedName) === normalizedQuery)
1671
+ return basePenalty + 1;
1672
+ if (normalizedName.includes(normalizedQuery)) return basePenalty + 10;
1673
+ if (normalizedPath.includes(normalizedQuery)) return basePenalty + 20;
1674
+ if (queryTokens.length > 0 && queryTokens.every((term) => normalizedPath.includes(term))) {
1675
+ return basePenalty + 30;
1676
+ }
1677
+ if (isSubsequence(normalizedQuery, normalizedPath)) return basePenalty + 45;
1678
+ return Number.MAX_SAFE_INTEGER;
1679
+ }
1680
+ function sourcePreferencePenalty(filePath) {
1681
+ const lower = filePath.toLowerCase();
1682
+ let score = 0;
1683
+ if (/\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|php|dart|css|scss|md|json)$/.test(
1684
+ lower
1685
+ )) {
1686
+ score -= 5;
1687
+ }
1688
+ if (lower.includes("/src/") || lower.startsWith("src/")) score -= 4;
1689
+ if (lower.includes("/features/") || lower.includes("/lib/")) score -= 2;
1690
+ if (lower.includes("/dist/") || lower.includes("/build/") || lower.includes("/coverage/"))
1691
+ score += 50;
1692
+ if (lower.includes("/node_modules/") || lower.includes("/.next/"))
1693
+ score += 100;
1694
+ if (lower.includes("/.pnpm-store/")) score += 100;
1695
+ if (lower.startsWith(".") || lower.includes("/.")) score += 20;
1696
+ return score;
1697
+ }
1698
+ function isSelectablePath(filePath) {
1699
+ const lower = filePath.toLowerCase();
1700
+ if (lower.endsWith(".ds_store")) return false;
1701
+ if (lower.includes("/node_modules/") || lower.includes("/.git/"))
1702
+ return false;
1703
+ if (lower.includes(".pnpm-store/")) return false;
1704
+ if (lower.includes(".codemap-storage/")) return false;
1705
+ if (lower.includes("/dist/") || lower.includes("/build/") || lower.includes("/coverage/"))
1706
+ return false;
1707
+ return true;
1708
+ }
1709
+ function splitTerms(query) {
1710
+ return query.split(/[\s\-_/.,:]+/).map((term) => term.trim()).filter((term) => term.length > 0);
1711
+ }
1712
+ function stripExtension(name) {
1713
+ return name.replace(/\.[^.]+$/, "");
1714
+ }
1715
+ function isSubsequence(query, value) {
1716
+ let queryIndex = 0;
1717
+ for (const char of value) {
1718
+ if (char === query[queryIndex]) queryIndex += 1;
1719
+ if (queryIndex === query.length) return true;
1720
+ }
1721
+ return false;
1722
+ }
1723
+ function formatHint(language, parseStatus, source) {
1724
+ return [language, parseStatus, source].filter(Boolean).join(" \xB7 ");
1725
+ }
1726
+ function debugFileSearch(message, details) {
1727
+ if (process.env.CODEMAP_DEBUG_FILE_SEARCH !== "1") return;
1728
+ const suffix = details === void 0 ? "" : ` ${details instanceof Error ? details.stack ?? details.message : JSON.stringify(details)}`;
1729
+ process.stderr.write(`[codemap:file-search] ${message}${suffix}
1730
+ `);
1731
+ }
1732
+
1733
+ export {
1734
+ loadOrSynthesizeAll,
1735
+ getCachedContext,
1736
+ runShell,
1737
+ loadThreadIntoUI,
1738
+ getCommandList,
1739
+ executeCommand,
1740
+ warmupFileSearch,
1741
+ searchIndexedFiles
1742
+ };