@mgamil/mapx 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Local code graph memory for LLMs.** Scan your codebase once — instantly query symbols, trace dependencies, analyze impact, and generate structured summaries without re-reading files.
4
4
 
5
- MapX uses [tree-sitter](https://tree-sitter.github.io/) to parse source files across **22 languages**, builds a PageRank-weighted dependency graph, and persists everything to a local SQLite database. Works as a standalone CLI or as an [MCP server](https://modelcontextprotocol.io/) with **25 tools** for Claude Desktop, Cursor, VS Code, and any other MCP-compatible client.
5
+ MapXGraph _-also known as MapX-_ uses [tree-sitter](https://tree-sitter.github.io/) to parse source files across **22 languages**, builds a PageRank-weighted dependency graph, and persists everything to a local SQLite database. Works as a standalone CLI or as an [MCP server](https://modelcontextprotocol.io/) with **25 tools** for Claude Desktop, Cursor, VS Code, and any other MCP-compatible client.
6
6
 
7
7
  ---
8
8
 
@@ -26,7 +26,27 @@ MapX uses [tree-sitter](https://tree-sitter.github.io/) to parse source files ac
26
26
 
27
27
  ## Installation
28
28
 
29
- ### Pre-built binary (recommended)
29
+ ### From npm (Global Installation)
30
+
31
+ Install MapX globally (recommended):
32
+
33
+ ```bash
34
+ npm install -g @mgamil/mapx
35
+ ```
36
+
37
+ ### Zero Installation (via npx)
38
+
39
+ Run MapX directly without installing it globally:
40
+
41
+ ```bash
42
+ # Initialize project
43
+ npx @mgamil/mapx init
44
+
45
+ # Scan files
46
+ npx @mgamil/mapx scan
47
+ ```
48
+
49
+ ### Pre-built binary
30
50
 
31
51
  Download the latest release for your platform from the [Releases](../../releases) page and place it on your `PATH`:
32
52
 
@@ -47,12 +67,6 @@ cd mapx-<version>
47
67
  ./install.sh --system # installs to /usr/local/bin (needs sudo)
48
68
  ```
49
69
 
50
- ### From npm
51
-
52
- ```bash
53
- npm install -g mapx
54
- ```
55
-
56
70
  ### From source
57
71
 
58
72
  Requires [Node.js](https://nodejs.org/) ≥ 20 or [Bun](https://bun.sh/).
@@ -68,6 +82,9 @@ npx tsx src/main.ts --help
68
82
 
69
83
  ## Quick Start
70
84
 
85
+ > [!TIP]
86
+ > If using the zero-installation method, replace `mapx` with `npx @mgamil/mapx` in the commands below (e.g. `npx @mgamil/mapx init`, `npx @mgamil/mapx scan`).
87
+
71
88
  ```bash
72
89
  # 1. Initialize mapx in your project (auto-adds .mapx/ to .gitignore)
73
90
  cd /path/to/your/project
@@ -469,20 +486,6 @@ make install-local
469
486
 
470
487
  ---
471
488
 
472
- ## Publishing to npm
473
-
474
- To publish new releases of the npm package:
475
-
476
- 1. Create a tag matching the version in `package.json` and push it:
477
- ```bash
478
- git tag v0.1.7
479
- git push origin v0.1.7
480
- ```
481
- 2. The GitHub Actions publish workflow will automatically run, verify version synchronization, build WASM grammars, compile the TypeScript code using `tsup`, and publish to the npm registry with provenance.
482
- 3. **Important**: The workflow requires a repository secret named `NPM_TOKEN`. This token must be generated on `npmjs.com` as an **Automation** access token.
483
-
484
- ---
485
-
486
489
  ## License
487
490
 
488
491
  Apache 2.0 — see [LICENSE](LICENSE).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.4
1
+ 0.2.6
package/dist/cli.js CHANGED
@@ -3,7 +3,6 @@ import { resolve, join, dirname, relative, basename } from "node:path";
3
3
  import { existsSync, readFileSync, writeFileSync, readdirSync, rmSync } from "node:fs";
4
4
  import { createRequire } from "node:module";
5
5
  import { fileURLToPath } from "node:url";
6
- import * as readline from "node:readline";
7
6
  import { Store } from "./core/store.js";
8
7
  import { MapxGraph } from "./core/graph.js";
9
8
  import { Scanner, buildMatcher } from "./core/scanner.js";
@@ -21,6 +20,7 @@ import { getChangedFiles, isGitRepo } from "./core/git-tracker.js";
21
20
  import { getBuiltinLanguages } from "./languages/registry.js";
22
21
  import { isLanguageInstalled, installLanguage, uninstallLanguage } from "./languages/installer.js";
23
22
  import { RouteRegistry } from "./frameworks/route-registry.js";
23
+ import * as clack from "@clack/prompts";
24
24
  const dynamicRequire = createRequire(import.meta.url);
25
25
  function readVersion() {
26
26
  const base = dirname(fileURLToPath(import.meta.url));
@@ -44,49 +44,81 @@ const PHASE_LABELS = {
44
44
  detect: { active: "Detecting changes", done: "Detected changes" },
45
45
  cluster: { active: "Detecting clusters", done: "Detected clusters" }
46
46
  };
47
- const SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2E26", "\u28BC", "\u28F4", "\u28F7", "\u28EF", "\u28DF", "\u287F", "\u28BF", "\u28FB", "\u28FD", "\u28FE"];
48
- let spinnerIdx = 0;
47
+ function truncatePath(path, maxLength) {
48
+ if (path.length <= maxLength) return path;
49
+ if (maxLength <= 3) return "...";
50
+ return "..." + path.slice(path.length - maxLength + 3);
51
+ }
49
52
  function createProgressRenderer() {
50
53
  let lastPhase = null;
51
- let lastLineLen = 0;
52
- const writeLine = (line) => {
53
- const clear = lastLineLen > 0 ? "\r" + " ".repeat(lastLineLen) + "\r" : "\r";
54
- process.stderr.write(clear + line);
55
- lastLineLen = line.length;
56
- };
57
- const renderBar = (current, total, width = 20) => {
58
- if (total === 0) {
59
- const frame = SPINNER_FRAMES[spinnerIdx++ % SPINNER_FRAMES.length];
60
- return `${frame} `;
61
- }
62
- const filled = Math.min(width, Math.max(0, Math.round(current / total * width)));
63
- const empty = width - filled;
64
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
65
- const pct = Math.round(current / total * 100);
66
- return `${bar} ${pct}%`;
67
- };
68
- return (progress) => {
69
- const { phase, current, total, file } = progress;
54
+ let p = null;
55
+ let s = null;
56
+ let lastCurrent = 0;
57
+ const callback = (progressData) => {
58
+ const { phase, current, total, file } = progressData;
70
59
  const label = PHASE_LABELS[phase];
60
+ if (!label) return;
71
61
  const isNewPhase = phase !== lastPhase;
72
- if (isNewPhase && lastPhase !== null) {
73
- const prevLabel = PHASE_LABELS[lastPhase];
74
- writeLine(` \u2714 ${prevLabel.done}
75
- `);
62
+ if (isNewPhase) {
63
+ if (s) {
64
+ const prevLabel = lastPhase ? PHASE_LABELS[lastPhase] : null;
65
+ s.stop(prevLabel ? `\u2714 ${prevLabel.done}` : "\u2714 Done");
66
+ s = null;
67
+ }
68
+ if (p) {
69
+ const prevLabel = lastPhase ? PHASE_LABELS[lastPhase] : null;
70
+ p.stop(prevLabel ? `\u2714 ${prevLabel.done}` : "\u2714 Done");
71
+ p = null;
72
+ }
73
+ lastPhase = phase;
74
+ lastCurrent = 0;
75
+ if (total > 0) {
76
+ p = clack.progress({
77
+ style: "heavy",
78
+ max: total,
79
+ size: 40
80
+ });
81
+ p.start(label.active);
82
+ } else {
83
+ s = clack.spinner();
84
+ s.start(label.active);
85
+ }
76
86
  }
77
- lastPhase = phase;
78
- const bar = renderBar(current, total);
79
- const counter = total > 0 ? `${current}/${total}` : `${current}`;
80
- let line = ` ${label.active} ${bar} ${counter}`;
87
+ const cols = process.stdout.columns || 80;
88
+ let fileLabel = "";
81
89
  if (file) {
82
- const maxFileLen = Math.max(0, 60 - line.length);
83
- const displayFile = file.length > maxFileLen && maxFileLen > 3 ? "\u2026" + file.slice(-(maxFileLen - 1)) : file.length <= maxFileLen ? file : "";
84
- if (displayFile) {
85
- line += ` ${displayFile}`;
90
+ const prefixText = p ? `${label.active} (${current}/${total}) - ` : `${label.active} (${current}) - `;
91
+ const clackDecorationLength = p ? 45 : 5;
92
+ const reserved = prefixText.length + clackDecorationLength;
93
+ const maxFileLen = Math.max(10, cols - reserved - 3);
94
+ fileLabel = ` - ${truncatePath(file, maxFileLen)}`;
95
+ }
96
+ if (p) {
97
+ const diff = current - lastCurrent;
98
+ const msg = `${label.active} (${current}/${total})${fileLabel}`;
99
+ if (diff > 0) {
100
+ p.advance(diff, msg);
101
+ lastCurrent = current;
102
+ } else {
103
+ p.message(msg);
86
104
  }
105
+ } else if (s) {
106
+ s.message(`${label.active} (${current})${fileLabel}`);
107
+ }
108
+ };
109
+ callback.stop = (title) => {
110
+ if (s) {
111
+ const prevLabel = lastPhase ? PHASE_LABELS[lastPhase] : null;
112
+ s.stop(title || (prevLabel ? `\u2714 ${prevLabel.done}` : "\u2714 Done"));
113
+ s = null;
114
+ }
115
+ if (p) {
116
+ const prevLabel = lastPhase ? PHASE_LABELS[lastPhase] : null;
117
+ p.stop(title || (prevLabel ? `\u2714 ${prevLabel.done}` : "\u2714 Done"));
118
+ p = null;
87
119
  }
88
- writeLine(line);
89
120
  };
121
+ return callback;
90
122
  }
91
123
  const MAPX_MARKER_START = "<!-- mapx -->";
92
124
  const MAPX_MARKER_END = "<!-- /mapx -->";
@@ -177,47 +209,20 @@ function replaceBetweenMarkers(existing, block) {
177
209
  }
178
210
  return existing.slice(0, startIdx) + block + existing.slice(endIdx + MAPX_MARKER_END.length);
179
211
  }
180
- function prompt(question, options) {
181
- return new Promise((res) => {
182
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
183
- const labels = options.map((o, i) => ` ${i + 1}) ${o}`);
184
- process.stderr.write(question + "\n" + labels.join("\n") + "\n> ");
185
- rl.question("", (answer) => {
186
- rl.close();
187
- const num = parseInt(answer.trim(), 10);
188
- res(num >= 1 && num <= options.length ? num - 1 : options.length - 1);
189
- });
190
- });
191
- }
192
- function askQuestion(query) {
193
- const rl = readline.createInterface({
194
- input: process.stdin,
195
- output: process.stdout
196
- });
197
- return new Promise((resolve2) => rl.question(query, (ans) => {
198
- rl.close();
199
- resolve2(ans);
200
- }));
201
- }
202
212
  async function selectProvidersInteractive() {
203
213
  const generator = new AgentGenerator();
204
214
  const providers = generator.listProviders();
205
- console.log("\nWhich LLM/agent tools do you use in this project?");
206
- console.log('Enter numbers separated by commas (e.g. 1,3), type "all" for all, or press Enter for default [1 (generic)]:');
207
- providers.forEach((p, idx) => {
208
- console.log(` [${idx + 1}] ${p}`);
215
+ const selected = await clack.multiselect({
216
+ message: "Which LLM/agent tools do you use in this project?",
217
+ options: providers.map((p) => ({ value: p, label: p })),
218
+ required: false
209
219
  });
210
- const answer = await askQuestion("\nSelection: ");
211
- const input = answer.trim().toLowerCase();
212
- if (!input) {
213
- return ["generic"];
214
- }
215
- if (input === "all") {
216
- return providers;
220
+ if (clack.isCancel(selected)) {
221
+ clack.cancel("Operation cancelled.");
222
+ process.exit(0);
217
223
  }
218
- const parts = input.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n) && n >= 1 && n <= providers.length);
219
- const selected = parts.map((n) => providers[n - 1]);
220
- return selected.length === 0 ? ["generic"] : selected;
224
+ const result = selected;
225
+ return result.length === 0 ? ["generic"] : result;
221
226
  }
222
227
  function buildCLI() {
223
228
  const program = new Command();
@@ -260,8 +265,15 @@ function buildCLI() {
260
265
  console.log(" \u2713 public/** (web assets)");
261
266
  console.log(" \u2713 resources/views/** (Blade templates \u2014 not yet supported)");
262
267
  console.log(" \u2713 **/*.blade.php (Blade files)");
263
- const answer = await askQuestion("\nAdd these to .mapx/config.json? [Y/n] ");
264
- return answer.toLowerCase() !== "n";
268
+ const answer = await clack.confirm({
269
+ message: "Add these to .mapx/config.json?",
270
+ initialValue: true
271
+ });
272
+ if (clack.isCancel(answer)) {
273
+ clack.cancel("Operation cancelled.");
274
+ process.exit(0);
275
+ }
276
+ return answer;
265
277
  }
266
278
  program.command("init").description("Initialize mapx for a project").argument("[path]", "Target directory").option("--name <name>", "Repository name").option("--no-agents", "Skip AGENTS.md creation").option("--no-suggestions", "Skip interactive framework suggestions").option("--no-mcp-configs", "Skip auto-generating MCP config files for detected agent tools").action(async (path, opts) => {
267
279
  const dir = path ? resolve(path) : resolveDir(opts, program.opts());
@@ -274,19 +286,19 @@ function buildCLI() {
274
286
  if (opts.agents !== false) {
275
287
  if (process.stdin.isTTY && opts.suggestions !== false) {
276
288
  const selected = await selectProvidersInteractive();
277
- console.log(`Generating integration files for: ${selected.join(", ")}...`);
289
+ clack.log.step(`Generating integration files for: ${selected.join(", ")}...`);
278
290
  const generator = new AgentGenerator();
279
291
  const actions = generator.plan(selected, { dir });
280
292
  for (const action of actions) {
281
293
  generator.execute(action);
282
- console.log(` \u2713 Generated ${action.filename} (${action.status})`);
294
+ clack.log.success(`Generated ${action.filename} (${action.status})`);
283
295
  }
284
296
  } else {
285
297
  const generator = new AgentGenerator();
286
298
  const actions = generator.plan(["generic"], { dir });
287
299
  for (const action of actions) {
288
300
  generator.execute(action);
289
- console.log(` \u2713 Generated ${action.filename} (${action.status})`);
301
+ clack.log.success(`Generated ${action.filename} (${action.status})`);
290
302
  }
291
303
  }
292
304
  }
@@ -299,7 +311,7 @@ function buildCLI() {
299
311
  if (action.status === "up_to_date") continue;
300
312
  generator.executeMcpConfig(action);
301
313
  const verb = action.status === "merge" ? "merged into" : action.status === "create" ? "created" : "updated";
302
- console.log(` \u2713 MCP config ${verb} ${action.filename} (${action.tool})`);
314
+ clack.log.success(`MCP config ${verb} ${action.filename} (${action.tool})`);
303
315
  }
304
316
  }
305
317
  }
@@ -312,19 +324,22 @@ function buildCLI() {
312
324
  if (!lines.includes(".mapx/") && !lines.includes(".mapx")) {
313
325
  const entry = content.length > 0 && !content.endsWith("\n") ? "\n.mapx/\n" : ".mapx/\n";
314
326
  writeFileSync(gitignorePath, content + entry);
315
- console.log(` \u2713 Added .mapx/ to .gitignore`);
327
+ clack.log.success(`Added .mapx/ to .gitignore`);
316
328
  }
317
329
  }
318
- console.log(`Initialized mapx in ${dir}/.mapx/`);
319
- console.log(`Repo: ${config.repo.name}`);
330
+ clack.log.success(`Initialized mapx in ${dir}/.mapx/`);
331
+ clack.log.info(`Repo: ${config.repo.name}`);
320
332
  });
321
333
  program.command("uninit").description("Remove .mapx/ directory and reverse project integration changes").argument("[path]", "Target directory").option("-f, --force", "Skip confirmation prompt").action(async (path, opts) => {
322
334
  const dir = path ? resolve(path) : resolveDir(opts, program.opts());
323
335
  const hasMapx = existsSync(join(dir, ".mapx"));
324
336
  if (!opts.force && process.stdin.isTTY) {
325
- const answer = await askQuestion(`Are you sure you want to remove .mapx/ and reverse all mapx integrations in ${dir}? [y/N] `);
326
- if (answer.trim().toLowerCase() !== "y") {
327
- console.log("Aborted.");
337
+ const answer = await clack.confirm({
338
+ message: `Are you sure you want to remove .mapx/ and reverse all mapx integrations in ${dir}?`,
339
+ initialValue: false
340
+ });
341
+ if (clack.isCancel(answer) || !answer) {
342
+ clack.cancel("Aborted.");
328
343
  return;
329
344
  }
330
345
  }
@@ -347,21 +362,21 @@ function buildCLI() {
347
362
  });
348
363
  if (removed) {
349
364
  writeFileSync(gitignorePath, filteredLines.join("\n"), "utf-8");
350
- console.log(` \u2713 Removed .mapx/ from .gitignore`);
365
+ clack.log.success(`Removed .mapx/ from .gitignore`);
351
366
  }
352
367
  } catch (err) {
353
- console.error(` \u2717 Failed to update .gitignore: ${err.message}`);
368
+ clack.log.error(`Failed to update .gitignore: ${err.message}`);
354
369
  }
355
370
  }
356
371
  if (hasMapx) {
357
372
  try {
358
373
  rmSync(join(dir, ".mapx"), { recursive: true, force: true });
359
- console.log(` \u2713 Removed .mapx/ directory`);
374
+ clack.log.success(`Removed .mapx/ directory`);
360
375
  } catch (err) {
361
- console.error(` \u2717 Failed to remove .mapx/ directory: ${err.message}`);
376
+ clack.log.error(`Failed to remove .mapx/ directory: ${err.message}`);
362
377
  }
363
378
  }
364
- console.log(`Successfully uninitialized mapx for project: ${dir}`);
379
+ clack.log.success(`Successfully uninitialized mapx for project: ${dir}`);
365
380
  });
366
381
  program.command("scan").description("Full scan: parse all files, build graph").argument("[path]", "Target directory").option("--exclude <glob>", "Exclude glob pattern(s)", collectPatterns, []).option("--include <glob>", "Include glob pattern(s)", collectPatterns, []).option("--repo <name>", "Scan only a specific registered repository").option("--all", "Scan all registered repositories").option("--force", "Force re-parsing of all files (bypass cache)", false).action(async (path, opts) => {
367
382
  const dir = path ? resolve(path) : resolveDir({}, program.opts());
@@ -373,6 +388,7 @@ function buildCLI() {
373
388
  });
374
389
  const onSigInt = () => {
375
390
  scanner.abort();
391
+ onProgress.stop("Canceled");
376
392
  process.stderr.write("\n");
377
393
  };
378
394
  process.once("SIGINT", onSigInt);
@@ -383,21 +399,22 @@ function buildCLI() {
383
399
  repoNames = ["all"];
384
400
  }
385
401
  const result = await scanner.scanFull(repoNames, { force: !!opts.force }).catch((err) => {
402
+ onProgress.stop();
386
403
  if (err.message.includes("Another scan is already running")) {
387
- console.error(`Error: ${err.message}`);
404
+ clack.log.error(`Error: ${err.message}`);
388
405
  process.exit(1);
389
406
  }
390
407
  throw err;
391
408
  });
392
409
  process.removeListener("SIGINT", onSigInt);
393
- process.stderr.write("\r" + " ".repeat(80) + "\r");
410
+ onProgress.stop();
394
411
  if (result.interrupted) {
395
- console.log(`Scan interrupted after ${result.filesScanned}/${result.totalFiles} files. Progress saved \u2014 run \`scan\` again to resume.`);
412
+ clack.log.warn(`Scan interrupted after ${result.filesScanned}/${result.totalFiles} files. Progress saved \u2014 run \`scan\` again to resume.`);
396
413
  } else {
397
- console.log(`Scanned ${result.filesScanned} files in ${result.durationMs}ms`);
414
+ clack.log.success(`Scanned ${result.filesScanned} files in ${result.durationMs}ms`);
398
415
  }
399
- console.log(`Languages: ${Object.entries(result.languageBreakdown).map(([l, c]) => `${l}: ${c}`).join(", ")}`);
400
- console.log(`Found ${result.symbolsFound} symbols, ${result.edgesFound} edges`);
416
+ clack.log.info(`Languages: ${Object.entries(result.languageBreakdown).map(([l, c]) => `${l}: ${c}`).join(", ")}`);
417
+ clack.log.info(`Found ${result.symbolsFound} symbols, ${result.edgesFound} edges`);
401
418
  });
402
419
  program.command("update").alias("sync").description("Incremental scan: re-scan only changed files").argument("[path]", "Target directory").option("--exclude <glob>", "Exclude glob pattern(s)", collectPatterns, []).option("--include <glob>", "Include glob pattern(s)", collectPatterns, []).option("--repo <name>", "Update only a specific registered repository").option("--all", "Update all registered repositories").action(async (path, opts) => {
403
420
  const dir = path ? resolve(path) : resolveDir({}, program.opts());
@@ -420,14 +437,23 @@ function buildCLI() {
420
437
  excludes: opts.exclude,
421
438
  includes: opts.include
422
439
  });
423
- process.once("SIGINT", () => scanner.abort());
424
- const result = await scanner.scanIncremental(repoNames).catch(handleLockError);
425
- process.stderr.write("\r" + " ".repeat(80) + "\r");
440
+ const onSigInt = () => {
441
+ scanner.abort();
442
+ onProgress.stop("Canceled");
443
+ };
444
+ process.once("SIGINT", onSigInt);
445
+ const result = await scanner.scanIncremental(repoNames).catch((err) => {
446
+ onProgress.stop();
447
+ handleLockError(err);
448
+ throw err;
449
+ });
450
+ process.removeListener("SIGINT", onSigInt);
451
+ onProgress.stop();
426
452
  if (result.interrupted) {
427
- console.log(`Update interrupted after ${result.filesScanned} files.`);
453
+ clack.log.warn(`Update interrupted after ${result.filesScanned} files.`);
428
454
  } else {
429
- console.log(`Updated ${result.filesScanned} files in ${result.durationMs}ms`);
430
- console.log(`${result.symbolsFound} symbols updated, ${result.edgesFound} edges updated`);
455
+ clack.log.success(`Updated ${result.filesScanned} files in ${result.durationMs}ms`);
456
+ clack.log.info(`${result.symbolsFound} symbols updated, ${result.edgesFound} edges updated`);
431
457
  }
432
458
  });
433
459
  program.command("status").description("Show scan status, collected metrics, and changed files").argument("[path]", "Target directory").option("--exclude <glob>", "Exclude glob pattern(s)", collectPatterns, []).option("--include <glob>", "Include glob pattern(s)", collectPatterns, []).action(async (path, opts) => {
@@ -1165,21 +1191,21 @@ Trace: ${start} ${dirSymbol} depth\u2264${parsedDepth}`);
1165
1191
  });
1166
1192
  langCmd.command("install <lang>").description("Install grammar and query files for an installable language").action(async (lang) => {
1167
1193
  try {
1168
- console.log(`Installing language '${lang}'...`);
1194
+ clack.log.step(`Installing language '${lang}'...`);
1169
1195
  await installLanguage(lang);
1170
- console.log(`Successfully installed language '${lang}'.`);
1196
+ clack.log.success(`Successfully installed language '${lang}'.`);
1171
1197
  } catch (err) {
1172
- console.error(`Error installing language '${lang}':`, err.message);
1198
+ clack.log.error(`Error installing language '${lang}': ${err.message}`);
1173
1199
  process.exit(1);
1174
1200
  }
1175
1201
  });
1176
1202
  langCmd.command("uninstall <lang>").description("Uninstall grammar and query files for an installable language").action(async (lang) => {
1177
1203
  try {
1178
- console.log(`Uninstalling language '${lang}'...`);
1204
+ clack.log.step(`Uninstalling language '${lang}'...`);
1179
1205
  await uninstallLanguage(lang);
1180
- console.log(`Successfully uninstalled language '${lang}'.`);
1206
+ clack.log.success(`Successfully uninstalled language '${lang}'.`);
1181
1207
  } catch (err) {
1182
- console.error(`Error uninstalling language '${lang}':`, err.message);
1208
+ clack.log.error(`Error uninstalling language '${lang}': ${err.message}`);
1183
1209
  process.exit(1);
1184
1210
  }
1185
1211
  });
@@ -1450,34 +1476,36 @@ Detected Hooks (${hooks.length}):`);
1450
1476
  }
1451
1477
  }
1452
1478
  if (targets.length === 0) {
1453
- console.error("No valid providers specified.");
1479
+ clack.log.error("No valid providers specified.");
1454
1480
  process.exit(1);
1455
1481
  }
1456
1482
  const actions = generator.plan(targets, { dir, mcpPort: parseInt(opts.mcpPort, 10) });
1457
1483
  for (const action of actions) {
1458
1484
  if (action.status === "up_to_date") {
1459
- console.log(` - ${action.filename}: Up to date. Skipping.`);
1485
+ clack.log.info(`${action.filename}: Up to date. Skipping.`);
1460
1486
  continue;
1461
1487
  }
1462
1488
  if (action.status === "update_conflict" || action.status === "no_sentinel") {
1463
- console.log(`
1464
- \u26A0\uFE0F Conflict/Modification detected in ${action.filename}:`);
1489
+ clack.log.warn(`Conflict/Modification detected in ${action.filename}:`);
1465
1490
  if (action.diff) {
1466
1491
  console.log(action.diff);
1467
1492
  }
1468
1493
  if (!opts.force) {
1469
- const confirm = await askQuestion(`Overwrite ${action.filename}? [y/N] `);
1470
- if (confirm.trim().toLowerCase() !== "y") {
1471
- console.log(`Skipped ${action.filename}.`);
1494
+ const confirm = await clack.confirm({
1495
+ message: `Overwrite ${action.filename}?`,
1496
+ initialValue: false
1497
+ });
1498
+ if (clack.isCancel(confirm) || !confirm) {
1499
+ clack.log.warn(`Skipped ${action.filename}.`);
1472
1500
  continue;
1473
1501
  }
1474
1502
  }
1475
1503
  }
1476
1504
  if (opts.dryRun) {
1477
- console.log(`[DRY RUN] Would write to ${action.filepath} (status: ${action.status})`);
1505
+ clack.log.info(`[DRY RUN] Would write to ${action.filepath} (status: ${action.status})`);
1478
1506
  } else {
1479
1507
  generator.execute(action);
1480
- console.log(`\u2713 Wrote to ${action.filename} (status: ${action.status})`);
1508
+ clack.log.success(`Wrote to ${action.filename} (status: ${action.status})`);
1481
1509
  }
1482
1510
  }
1483
1511
  });
@@ -1490,7 +1518,7 @@ Detected Hooks (${hooks.length}):`);
1490
1518
  return temp && existsSync(join(dir, temp.filename));
1491
1519
  });
1492
1520
  if (existingProviders.length === 0) {
1493
- console.log("No existing LLM integration files found to update.");
1521
+ clack.log.info("No existing LLM integration files found to update.");
1494
1522
  return;
1495
1523
  }
1496
1524
  const actions = generator.plan(existingProviders, { dir, mcpPort: parseInt(opts.mcpPort, 10) });
@@ -1500,29 +1528,31 @@ Detected Hooks (${hooks.length}):`);
1500
1528
  continue;
1501
1529
  }
1502
1530
  if (action.status === "update_conflict") {
1503
- console.log(`
1504
- \u26A0\uFE0F Customized content detected in ${action.filename}:`);
1531
+ clack.log.warn(`Customized content detected in ${action.filename}:`);
1505
1532
  if (action.diff) {
1506
1533
  console.log(action.diff);
1507
1534
  }
1508
1535
  if (!opts.force) {
1509
- const confirm = await askQuestion(`Overwrite customizations in ${action.filename}? [y/N] `);
1510
- if (confirm.trim().toLowerCase() !== "y") {
1511
- console.log(`Skipped ${action.filename}.`);
1536
+ const confirm = await clack.confirm({
1537
+ message: `Overwrite customizations in ${action.filename}?`,
1538
+ initialValue: false
1539
+ });
1540
+ if (clack.isCancel(confirm) || !confirm) {
1541
+ clack.log.warn(`Skipped ${action.filename}.`);
1512
1542
  continue;
1513
1543
  }
1514
1544
  }
1515
1545
  }
1516
1546
  if (opts.dryRun) {
1517
- console.log(`[DRY RUN] Would update ${action.filepath}`);
1547
+ clack.log.info(`[DRY RUN] Would update ${action.filepath}`);
1518
1548
  } else {
1519
1549
  generator.execute(action);
1520
- console.log(`\u2713 Updated ${action.filename}`);
1550
+ clack.log.success(`Updated ${action.filename}`);
1521
1551
  updatedCount++;
1522
1552
  }
1523
1553
  }
1524
1554
  if (updatedCount === 0 && !opts.dryRun) {
1525
- console.log("All integration files are already up to date.");
1555
+ clack.log.success("All integration files are already up to date.");
1526
1556
  }
1527
1557
  });
1528
1558
  agentsCmd.command("mcp").description("Auto-detect agent tools and generate/update MCP config files").option("--tools <list>", "Comma-separated list of tools to generate configs for (opencode, gemini-cli, cursor-mcp, vscode-mcp)").option("--all", "Generate MCP configs for all supported tools").option("--detect", "Only detect agent tools without writing files").option("--dry-run", "Show actions without writing files").action(async (opts) => {
@@ -1540,39 +1570,37 @@ Detected Hooks (${hooks.length}):`);
1540
1570
  }
1541
1571
  if (opts.detect) {
1542
1572
  if (targets.length === 0) {
1543
- console.log("No agent tools detected in this project.");
1573
+ clack.log.info("No agent tools detected in this project.");
1544
1574
  } else {
1545
- console.log(`
1546
- Detected agent tools (${targets.length}):`);
1575
+ clack.log.info(`Detected agent tools (${targets.length}):`);
1547
1576
  for (const t of targets) {
1548
- console.log(` \u2713 ${t.name.padEnd(15)} \u2192 ${t.filename}`);
1577
+ clack.log.success(`${t.name.padEnd(15)} \u2192 ${t.filename}`);
1549
1578
  }
1550
1579
  }
1551
- console.log(`
1552
- All available targets:`);
1580
+ clack.log.info(`All available targets:`);
1553
1581
  for (const c of allConfigs) {
1554
1582
  const detected = targets.includes(c);
1555
1583
  const icon = detected ? "\u2713" : "\xB7";
1556
- console.log(` ${icon} ${c.name.padEnd(15)} \u2192 ${c.filename}`);
1584
+ clack.log.info(`${icon} ${c.name.padEnd(15)} \u2192 ${c.filename}`);
1557
1585
  }
1558
1586
  return;
1559
1587
  }
1560
1588
  if (targets.length === 0) {
1561
- console.log("No agent tools detected. Use --all or --tools to specify targets.");
1589
+ clack.log.warn("No agent tools detected. Use --all or --tools to specify targets.");
1562
1590
  return;
1563
1591
  }
1564
1592
  const actions = generator.generateMcpConfigs(targets, { dir });
1565
1593
  for (const action of actions) {
1566
1594
  if (action.status === "up_to_date") {
1567
- console.log(` - ${action.filename}: Up to date.`);
1595
+ clack.log.info(`${action.filename}: Up to date.`);
1568
1596
  continue;
1569
1597
  }
1570
1598
  if (opts.dryRun) {
1571
- console.log(`[DRY RUN] Would ${action.status} ${action.filename} (${action.tool})`);
1599
+ clack.log.info(`[DRY RUN] Would ${action.status} ${action.filename} (${action.tool})`);
1572
1600
  } else {
1573
1601
  generator.executeMcpConfig(action);
1574
1602
  const verb = action.status === "merge" ? "merged into" : action.status === "create" ? "created" : "updated";
1575
- console.log(` \u2713 MCP config ${verb} ${action.filename} (${action.tool})`);
1603
+ clack.log.success(`MCP config ${verb} ${action.filename} (${action.tool})`);
1576
1604
  }
1577
1605
  }
1578
1606
  });
@@ -1623,43 +1651,53 @@ All available targets:`);
1623
1651
  const { config, store, graph } = await loadContext(dir);
1624
1652
  const absPath = resolve(dir, repoPath);
1625
1653
  if (!existsSync(absPath)) {
1626
- console.error(`Error: Path ${repoPath} does not exist.`);
1654
+ clack.log.error(`Path ${repoPath} does not exist.`);
1627
1655
  process.exit(1);
1628
1656
  }
1629
1657
  if (!isGitRepo(absPath)) {
1630
- console.error(`Error: Path ${repoPath} is not a git repository.`);
1658
+ clack.log.error(`Path ${repoPath} is not a git repository.`);
1631
1659
  process.exit(1);
1632
1660
  }
1633
1661
  const relPath = relative(dir, absPath);
1634
1662
  const name = opts.name || basename(absPath);
1635
1663
  if (config.repos.some((r) => r.name === name || r.path === relPath)) {
1636
- console.log(`Repository already registered: ${name} (${relPath})`);
1664
+ clack.log.warn(`Repository already registered: ${name} (${relPath})`);
1637
1665
  return;
1638
1666
  }
1639
1667
  config.addRepo(name, relPath);
1640
1668
  await config.save();
1641
- console.log(`Registered repository: ${name} -> ${relPath}`);
1642
- console.log("Running initial full scan for the new repository...");
1669
+ clack.log.success(`Registered repository: ${name} -> ${relPath}`);
1670
+ clack.log.step("Running initial full scan for the new repository...");
1643
1671
  const onProgress = createProgressRenderer();
1644
1672
  const scanner = new Scanner(store, config, graph, onProgress);
1645
- const result = await scanner.scanFull([name]);
1646
- console.log(`Scanned ${result.filesScanned} files, ${result.symbolsFound} symbols, ${result.edgesFound} edges in ${result.durationMs}ms`);
1673
+ const onSigInt = () => {
1674
+ scanner.abort();
1675
+ onProgress.stop("Canceled");
1676
+ };
1677
+ process.once("SIGINT", onSigInt);
1678
+ const result = await scanner.scanFull([name]).catch((err) => {
1679
+ onProgress.stop();
1680
+ throw err;
1681
+ });
1682
+ process.removeListener("SIGINT", onSigInt);
1683
+ onProgress.stop();
1684
+ clack.log.success(`Scanned ${result.filesScanned} files, ${result.symbolsFound} symbols, ${result.edgesFound} edges in ${result.durationMs}ms`);
1647
1685
  });
1648
1686
  workspacesCmd.command("remove <name>").description("Unregister a repository by name or path").action(async (name) => {
1649
1687
  const dir = resolveDir({}, program.opts());
1650
1688
  const { config, store } = await loadContext(dir);
1651
1689
  const repo = config.repos.find((r) => r.name === name || r.path === name);
1652
1690
  if (!repo) {
1653
- console.error(`Error: Repository ${name} is not registered.`);
1691
+ clack.log.error(`Repository ${name} is not registered.`);
1654
1692
  process.exit(1);
1655
1693
  }
1656
1694
  const repoName = repo.name;
1657
1695
  config.removeRepo(name);
1658
1696
  await config.save();
1659
- console.log(`Unregistered repository: ${repoName}`);
1660
- console.log(`Cleaning up stored data for repository: ${repoName}...`);
1697
+ clack.log.success(`Unregistered repository: ${repoName}`);
1698
+ clack.log.step(`Cleaning up stored data for repository: ${repoName}...`);
1661
1699
  store.deleteRepo(repoName);
1662
- console.log(`Done.`);
1700
+ clack.log.success(`Done.`);
1663
1701
  });
1664
1702
  workspacesCmd.command("discover").description("Discover unregistered submodules, peer repos, and VS Code workspace folders (read-only)").action(async () => {
1665
1703
  const dir = resolveDir({}, program.opts());
@@ -1750,20 +1788,31 @@ ${found} unregistered repositor${found === 1 ? "y" : "ies"} discovered. Use \`ma
1750
1788
  }
1751
1789
  }
1752
1790
  if (toAdd.length === 0) {
1753
- console.log("No new repositories discovered to sync.");
1791
+ clack.log.info("No new repositories discovered to sync.");
1754
1792
  return;
1755
1793
  }
1756
- console.log(`Syncing ${toAdd.length} newly discovered repositories:`);
1757
- const scanner = new Scanner(store, config, graph, createProgressRenderer());
1794
+ clack.log.step(`Syncing ${toAdd.length} newly discovered repositories:`);
1758
1795
  for (const item of toAdd) {
1759
1796
  config.addRepo(item.name, item.path);
1760
- console.log(` + Registered: ${item.name} -> ${item.path}`);
1797
+ clack.log.success(`Registered: ${item.name} -> ${item.path}`);
1761
1798
  }
1762
1799
  await config.save();
1763
- console.log("\nRunning initial full scan for new repositories...");
1800
+ clack.log.step("Running initial full scan for new repositories...");
1764
1801
  const newNames = toAdd.map((item) => item.name);
1765
- const result = await scanner.scanFull(newNames);
1766
- console.log(`Scanned ${result.filesScanned} files, ${result.symbolsFound} symbols, ${result.edgesFound} edges in ${result.durationMs}ms`);
1802
+ const onProgress = createProgressRenderer();
1803
+ const scanner = new Scanner(store, config, graph, onProgress);
1804
+ const onSigInt = () => {
1805
+ scanner.abort();
1806
+ onProgress.stop("Canceled");
1807
+ };
1808
+ process.once("SIGINT", onSigInt);
1809
+ const result = await scanner.scanFull(newNames).catch((err) => {
1810
+ onProgress.stop();
1811
+ throw err;
1812
+ });
1813
+ process.removeListener("SIGINT", onSigInt);
1814
+ onProgress.stop();
1815
+ clack.log.success(`Scanned ${result.filesScanned} files, ${result.symbolsFound} symbols, ${result.edgesFound} edges in ${result.durationMs}ms`);
1767
1816
  });
1768
1817
  return program;
1769
1818
  }
@@ -9,15 +9,63 @@ const DEFAULT_CONFIG = {
9
9
  settings: {
10
10
  maxTokenBudget: 16384,
11
11
  excludePatterns: [
12
- "node_modules/**",
13
- "vendor/**",
14
12
  ".git/**",
15
- "dist/**",
16
13
  ".mapx/**",
14
+ "node_modules/**",
15
+ "**/node_modules/**",
16
+ "dist/**",
17
+ "**/dist/**",
18
+ "vendor/**",
19
+ "**/vendor/**",
20
+ "Vendor/**",
21
+ "**/Vendor/**",
22
+ "__pycache__/**",
23
+ "**/__pycache__/**",
24
+ ".venv/**",
25
+ ".next/**",
26
+ "res/**",
27
+ "**/res/**",
28
+ "gradle/**",
29
+ "**/gradle/**",
30
+ "build/**",
31
+ "**/build/**",
17
32
  "*.min.js",
18
- "*.min.css",
33
+ "monaco/vs/**/*.js",
34
+ "bootstrap-*/bootstrap.js",
35
+ "bootstrap-*/bootstrap.min.js",
36
+ "bootstrap-*/bootstrap.bundle.js",
37
+ "bootstrap-*/bootstrap.bundle.min.js",
38
+ "vue.global.js",
39
+ "vue.esm-browser.js",
40
+ "vue.runtime.esm-browser.js",
41
+ "vue.runtime.esm-browser.prod.js",
42
+ "react.development.js",
43
+ "react.production.min.js",
44
+ "react-dom.development.js",
45
+ "react-dom.production.min.js",
46
+ "package.json",
19
47
  "package-lock.json",
20
- "composer.lock"
48
+ "yarn.lock",
49
+ "pnpm-lock.yaml",
50
+ "composer.json",
51
+ "composer.lock",
52
+ "pyproject.toml",
53
+ "Pipfile",
54
+ "Pipfile.lock",
55
+ "poetry.lock",
56
+ "go.mod",
57
+ "go.sum",
58
+ "Cargo.toml",
59
+ "Cargo.lock",
60
+ "bun.lock",
61
+ "AndroidManifest.xml",
62
+ "AndroidManifest.xml",
63
+ "**/*.log",
64
+ "**/*.lock",
65
+ "**/*.tmp",
66
+ "**/*.swp",
67
+ "**/*.swo",
68
+ "**/*.DS_Store"
21
69
  ],
22
70
  includePatterns: []
23
71
  }
@@ -245,7 +245,21 @@ class Scanner {
245
245
  const activeDetectors = await registry.detectActiveFrameworks(repoRoot, filePaths);
246
246
  const activeFrameworks = activeDetectors.map((d) => d.name);
247
247
  const ignoredSymbols = buildIgnoredSymbols(activeFrameworks);
248
- const parseResults = await this.parseFilesParallel(toParse, workspaceRoot, ignoredSymbols);
248
+ let parsedFilesCount = 0;
249
+ const parseResults = await this.parseFilesParallel(
250
+ toParse,
251
+ workspaceRoot,
252
+ ignoredSymbols,
253
+ (relPath) => {
254
+ parsedFilesCount++;
255
+ this.onProgress?.({
256
+ phase: "parse",
257
+ current: unchangedFiles.length + resumedCompleted.size + parsedFilesCount,
258
+ total: discovered.length,
259
+ file: relPath
260
+ });
261
+ }
262
+ );
249
263
  const fileMap = this.workspaceFileMap.size > 0 ? this.workspaceFileMap : (() => {
250
264
  const allTrackedFiles = this.store.getAllFiles();
251
265
  const map = /* @__PURE__ */ new Map();
@@ -266,12 +280,6 @@ class Scanner {
266
280
  totalEdges += parseResults[i].references.length;
267
281
  completed.add(toParse[i].relativePath);
268
282
  }
269
- this.onProgress?.({
270
- phase: "parse",
271
- current: completed.size,
272
- total: discovered.length,
273
- file: toParse[batchEnd - 1].relativePath
274
- });
275
283
  this.saveResumeState(repo.name, {
276
284
  totalFiles: discovered.length,
277
285
  completedFiles: [...completed],
@@ -441,10 +449,20 @@ class Scanner {
441
449
  const activeDetectors = await registry.detectActiveFrameworks(repoRoot, allFilePaths);
442
450
  const activeFrameworks = activeDetectors.map((d) => d.name);
443
451
  const ignoredSymbols = buildIgnoredSymbols(activeFrameworks);
452
+ let parsedFilesCount = 0;
444
453
  const parseResults = await this.parseFilesParallel(
445
454
  toReindex.map((r) => r.fileInfo),
446
455
  workspaceRoot,
447
- ignoredSymbols
456
+ ignoredSymbols,
457
+ (relPath) => {
458
+ parsedFilesCount++;
459
+ this.onProgress?.({
460
+ phase: "parse",
461
+ current: parsedFilesCount,
462
+ total: toReindex.length,
463
+ file: relPath
464
+ });
465
+ }
448
466
  );
449
467
  let totalSymbols = 0;
450
468
  let totalEdges = 0;
@@ -456,12 +474,6 @@ class Scanner {
456
474
  totalSymbols += result.symbols.length;
457
475
  totalEdges += result.references.length;
458
476
  langBreakdown[fileInfo.language] = (langBreakdown[fileInfo.language] || 0) + 1;
459
- this.onProgress?.({
460
- phase: "parse",
461
- current: i + 1,
462
- total: changes.length,
463
- file: relPath
464
- });
465
477
  }
466
478
  if (!this.aborted) {
467
479
  const commitSha = getCurrentCommitSha(repoRoot);
@@ -564,14 +576,14 @@ class Scanner {
564
576
  }
565
577
  return deleted;
566
578
  }
567
- async parseFilesParallel(files, workspaceRoot, ignoredSymbols) {
579
+ async parseFilesParallel(files, workspaceRoot, ignoredSymbols, onFileParsed) {
568
580
  if (files.length === 0) return [];
569
- return this.parseOnMainThread(files, workspaceRoot, ignoredSymbols);
581
+ return this.parseOnMainThread(files, workspaceRoot, ignoredSymbols, onFileParsed);
570
582
  }
571
- async parseWithWorkers(files, workspaceRoot, ignoredSymbols) {
572
- return this.parseOnMainThread(files, workspaceRoot, ignoredSymbols);
583
+ async parseWithWorkers(files, workspaceRoot, ignoredSymbols, onFileParsed) {
584
+ return this.parseOnMainThread(files, workspaceRoot, ignoredSymbols, onFileParsed);
573
585
  }
574
- async parseOnMainThread(files, workspaceRoot, ignoredSymbols) {
586
+ async parseOnMainThread(files, workspaceRoot, ignoredSymbols, onFileParsed) {
575
587
  const results = new Array(files.length);
576
588
  const sources = await Promise.all(
577
589
  files.map(async (f) => {
@@ -592,6 +604,8 @@ class Scanner {
592
604
  const relPath = relative(workspaceRoot, fileInfo.absolutePath).replace(/\\/g, "/");
593
605
  if (sources[i] === null) {
594
606
  results[i] = { symbols: [], references: [], errors: [{ message: `Failed to read ${relPath}` }] };
607
+ onFileParsed?.(relPath);
608
+ await new Promise((resolve2) => setImmediate(resolve2));
595
609
  continue;
596
610
  }
597
611
  try {
@@ -603,6 +617,8 @@ class Scanner {
603
617
  } catch {
604
618
  results[i] = { symbols: [], references: [], errors: [{ message: `Failed to parse ${relPath}` }] };
605
619
  }
620
+ onFileParsed?.(relPath);
621
+ await new Promise((resolve2) => setImmediate(resolve2));
606
622
  }
607
623
  };
608
624
  await Promise.all(Array.from({ length: Math.min(CONCURRENCY, files.length) }, runWorker));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgamil/mapx",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Multi-language code graph memory system for LLMs",
6
6
  "author": {
@@ -94,6 +94,8 @@
94
94
  "test": "tsx src/main.ts scan && tsx src/main.ts export"
95
95
  },
96
96
  "dependencies": {
97
+ "@clack/prompts": "^1.4.0",
98
+ "@modelcontextprotocol/sdk": "^1.29.0",
97
99
  "better-sqlite3": "^12.10.0",
98
100
  "commander": "^13.1.0",
99
101
  "cytoscape": "^3.33.4",
@@ -110,10 +112,8 @@
110
112
  "tree-sitter-typescript": "^0.23.2",
111
113
  "uplot": "^1.6.32",
112
114
  "web-tree-sitter": "^0.26.9",
113
- "zod": "^3.25.0",
114
- "@modelcontextprotocol/sdk": "^1.29.0"
115
+ "zod": "^3.25.0"
115
116
  },
116
- "optionalDependencies": {},
117
117
  "devDependencies": {
118
118
  "@types/better-sqlite3": "^7.6.13",
119
119
  "@types/cytoscape": "^3.21.9",