ctxman 3.1.0 → 3.2.7

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 (60) hide show
  1. package/.ctxman/llm-profiles.json +6 -6
  2. package/README.md +17 -10
  3. package/bin/cli.js +107 -8
  4. package/bin/cm-ask.js +74 -70
  5. package/bin/cm-audit.js +80 -7
  6. package/bin/cm-diagnose.js +199 -0
  7. package/bin/cm-focus.js +322 -0
  8. package/bin/cm-map.js +71 -18
  9. package/bin/cm-pack.js +36 -5
  10. package/bin/cm-policy.js +107 -2
  11. package/bin/cm-pr.js +18 -3
  12. package/bin/cm-search.js +84 -19
  13. package/bin/cm-task.js +192 -0
  14. package/bin/cm-who.js +119 -0
  15. package/bin/cm-why.js +41 -5
  16. package/examples/github-actions/ctxman-check.yml +1 -1
  17. package/index.d.ts +931 -1
  18. package/index.js +105 -1
  19. package/lib/analyzers/performance-dashboard.js +7 -3
  20. package/lib/analyzers/token-calculator.js +76 -4
  21. package/lib/api/mcp/server.js +713 -14
  22. package/lib/cache/CacheManager.js +161 -47
  23. package/lib/core/Analyzer.js +61 -5
  24. package/lib/diagnose/index.js +712 -0
  25. package/lib/focus/index.js +790 -0
  26. package/lib/map/call-graph.js +570 -0
  27. package/lib/map/graph.js +30 -5
  28. package/lib/map/index.js +12 -4
  29. package/lib/map/rank.js +45 -7
  30. package/lib/map/render.js +89 -28
  31. package/lib/pack/index.js +20 -1
  32. package/lib/pack/select.js +8 -1
  33. package/lib/parsers/gitignore-parser.js +4 -1
  34. package/lib/policy/audit.js +22 -2
  35. package/lib/policy/index.js +6 -0
  36. package/lib/policy/workspace.js +254 -0
  37. package/lib/pr/pr-context.js +292 -31
  38. package/lib/pr/pr-renderer.js +25 -10
  39. package/lib/rag/EmbeddingProvider.js +139 -2
  40. package/lib/rag/VectorStoreFactory.js +10 -4
  41. package/lib/rag/ask.js +291 -0
  42. package/lib/rag/sources/FileDataSource.js +6 -3
  43. package/lib/rag/stores/SqliteVectorStore.js +595 -0
  44. package/lib/search/hybrid.js +324 -0
  45. package/lib/search/symbols.js +75 -11
  46. package/lib/task/index.js +630 -0
  47. package/lib/utils/file-utils.js +1 -0
  48. package/lib/utils/logger.js +5 -2
  49. package/lib/utils/multi-repo-manager.js +4 -1
  50. package/lib/watch/FileWatcher.js +121 -31
  51. package/lib/watch/IncrementalAnalyzer.js +964 -16
  52. package/lib/who/index.js +893 -0
  53. package/lib/why/analyzer-bridge.js +223 -0
  54. package/lib/why/discussions.js +314 -0
  55. package/lib/why/index.js +356 -30
  56. package/lib/why/render.js +154 -9
  57. package/lib/wizards/config-generator.js +20 -18
  58. package/lib/wizards/project-detector.js +2 -1
  59. package/lib/workspace/index.js +1274 -0
  60. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "4.0.0",
3
- "lastUpdated": "2026-10-09",
3
+ "lastUpdated": "2026-10-30",
4
4
  "description": "Context windows, output limits and list prices (USD per 1M tokens, standard tier) of current models. Check prices with the provider before budgeting.",
5
5
  "sources": {
6
6
  "Anthropic": [
@@ -390,7 +390,7 @@
390
390
  {
391
391
  "above": 272000,
392
392
  "input": 20,
393
- "output": 75.0
393
+ "output": 75
394
394
  }
395
395
  ]
396
396
  },
@@ -421,7 +421,7 @@
421
421
  {
422
422
  "above": 272000,
423
423
  "input": 4,
424
- "output": 15.0
424
+ "output": 15
425
425
  }
426
426
  ]
427
427
  },
@@ -485,7 +485,7 @@
485
485
  {
486
486
  "above": 272000,
487
487
  "input": 4,
488
- "output": 18.0
488
+ "output": 18
489
489
  }
490
490
  ]
491
491
  },
@@ -515,7 +515,7 @@
515
515
  {
516
516
  "above": 272000,
517
517
  "input": 10,
518
- "output": 45.0
518
+ "output": 45
519
519
  }
520
520
  ]
521
521
  },
@@ -606,7 +606,7 @@
606
606
  "supportedLanguages": "all",
607
607
  "strengths": ["json-mode", "function-calling"],
608
608
  "tier": "frontier",
609
- "status": "deprecated",
609
+ "status": "retired",
610
610
  "pricing": {
611
611
  "input": 10,
612
612
  "output": 30
package/README.md CHANGED
@@ -81,14 +81,18 @@ If you find this tool helpful, consider buying me a coffee! Your support helps m
81
81
 
82
82
  ### 🧰 Context Commands
83
83
 
84
- - 📦 **`ctxman pack`** - File contents in one LLM-ready document (Claude XML, Markdown, JSON) with signatures-only compression and a hard token budget
85
- - 🗺️ **`ctxman map`** - Ranked repository map (import graph + git history) within a token budget
86
- - 🔍 **`ctxman pr`** - Review context for a branch: log, changed methods, diff, related tests
87
- - 🧭 **`ctxman why`** - Why code looks as it does: commits, linked issues and PRs, owners, co-changed files, ADRs and tests from local git history
88
- - 🔎 **`ctxman search`** - Ranked (BM25) search over functions and code chunks
84
+ - 📦 **`ctxman pack`** - File contents in one LLM-ready document (Claude XML, Markdown, JSON) with signatures-only compression, a hard token budget, and multi-repo `--repo` packing
85
+ - 🎯 **`ctxman focus`** - Pack a file or `file:symbol` together with its `N`-hop import and AST symbol call neighborhood within a token budget (`--repo` for multi-repo; `docs/commands/focus.md`)
86
+ - 🧭 **`ctxman task`** - Task-driven auto context bundle: hybrid search seeds, 1-hop AST call & import code, 2-hop compressed signatures, and repo map (`--repo` for multi-repo; `docs/commands/task.md`)
87
+ - 🩺 **`ctxman diagnose`** - Stack-trace & failure root-cause bundle: implicated frames, enclosing methods, 1-hop AST call & import neighbors, and git history (`--repo` for multi-repo; `docs/commands/diagnose.md`)
88
+ - 👥 **`ctxman who`** - Rank domain experts via surviving `git blame -w` share, recency-decayed commits, and `CODEOWNERS` (`--repo` for multi-repo; `docs/commands/who.md`)
89
+ - 🗺️ **`ctxman map`** - Ranked repository map (import graph + git history) within a token budget (`--repo` for multi-repo)
90
+ - 🔍 **`ctxman pr`** - Review context for a branch: log, changed methods, diff, related tests (`--repo` for multi-repo; `docs/commands/pr.md`)
91
+ - 🧭 **`ctxman why`** - Why code looks as it does: commits, linked issues and PRs, optional GitHub/GitLab discussions (`--discussions`), owners, co-changed files, ADRs and tests (`--repo` for multi-repo; `docs/commands/why.md`)
92
+ - 🔎 **`ctxman search`** - Ranked (BM25) and hybrid BM25 + semantic vector RRF (`--hybrid`) search over functions and code chunks across one or more `--repo` roots
89
93
  - 🔢 **`ctxman count`** - Token count for one model, exact for Claude through Anthropic's API
90
94
  - 🔌 **`ctxman mcp`** - MCP server with one-command setup for Claude Desktop, Claude Code, Cursor, VS Code and Windsurf
91
- - 🛡️ **`ctxman policy` / `ctxman audit`** - Export policy (deny rules, required redaction, token limits) and an audit log
95
+ - 🛡️ **`ctxman policy` / `ctxman audit`** - Export policy (deny rules, required redaction, token limits) and an audit log (`--repo` for multi-repo workspace checks and merged audit trails)
92
96
  - 🤖 **GitHub Action and Docker image** - Token budgets and model fit as a CI check
93
97
 
94
98
  ### 🎨 User Interface
@@ -184,7 +188,7 @@ ctxman --cli --target-model claude-sonnet-5-5
184
188
  # ✅ PERFECT FIT! Your entire codebase fits in one context.
185
189
  ```
186
190
 
187
- Some of the supported models (run `ctxman --list-llms` for the full list, checked 2026-10-09):
191
+ Some of the supported models (run `ctxman --list-llms` for the full list, checked 2026-10-30):
188
192
 
189
193
  | Vendor | Model ID | Context | Output | $ in/out per 1M tokens |
190
194
  | --------- | ------------------------ | ------- | ------ | ---------------------- |
@@ -275,6 +279,7 @@ tests, with your question last.
275
279
  ctxman pr # HEAD against origin/HEAD, main or master
276
280
  ctxman pr --base release/2.x --budget 30k # drops the least important parts to fit
277
281
  ctxman pr --staged --ask "Is this safe to merge?" --copy
282
+ ctxman pr --base main --repo ../billing-api --repo ../checkout-web --budget 24k
278
283
  ctxman pr --style xml -o review.xml
279
284
  ```
280
285
 
@@ -289,6 +294,7 @@ repository record about a file, a function or a line range.
289
294
  ctxman why src/cart.js:total # function / method / class (Class.method)
290
295
  ctxman why src/cart.js#L40-L72 --patches # line range, with each commit's diff
291
296
  ctxman why lib/server.js --since "1 year ago" # whole file, following renames
297
+ ctxman why billing-api/src/invoice.js:createInvoice --repo ../billing-api --repo ../checkout-web
292
298
  ctxman why src/cart.js:total --ask "Why round here?" --budget 8k --copy
293
299
  ```
294
300
 
@@ -322,7 +328,8 @@ ctxman mcp # run the server (stdio)
322
328
  The installer merges into the client's configuration and keeps a `.bak` of the previous file.
323
329
  `--root` confines every tool to one directory. Tools: `analyze_codebase`, `generate_context`,
324
330
  `git_diff`, `search_code`, `list_methods`, `repo_map`, `pr_context`, `get_symbol`,
325
- `count_tokens`, `explain_history`, `pack_context`. Details:
331
+ `count_tokens`, `explain_history`, `pack_context`, `focus_context`, `check_policy`, `audit_log`, `ask_codebase`, `task_context`, `diagnose_failure`, `find_experts`, `query_knowledge_graph`.
332
+ Details:
326
333
  [docs/commands/mcp.md](docs/commands/mcp.md).
327
334
 
328
335
  ### 🛡️ Export Policy and Audit Log
@@ -332,8 +339,8 @@ A policy in the repository (`.ctxman/policy.json`), or one an organisation point
332
339
 
333
340
  ```bash
334
341
  ctxman policy init # starter policy: deny .env, keys and secrets/, require redaction
335
- ctxman policy check # validate and show the effective policy
336
- ctxman audit # what was exported, when, by whom, how many tokens
342
+ ctxman policy check # validate and show the effective policy (--repo DIR for multi-repo)
343
+ ctxman audit # what was exported, when, by whom, how many tokens (--repo DIR for multi-repo)
337
344
  ctxman --cli --respect-ignore-files .cursorignore,.aiexclude # honour other AI tools' ignore files
338
345
  ```
339
346
 
package/bin/cli.js CHANGED
@@ -94,11 +94,31 @@ async function main() {
94
94
  return;
95
95
  }
96
96
 
97
+ if (command === 'who') {
98
+ runSubcommand('./cm-who.js', args.slice(1));
99
+ return;
100
+ }
101
+
97
102
  if (command === 'pack') {
98
103
  runSubcommand('./cm-pack.js', args.slice(1));
99
104
  return;
100
105
  }
101
106
 
107
+ if (command === 'focus') {
108
+ runSubcommand('./cm-focus.js', args.slice(1));
109
+ return;
110
+ }
111
+
112
+ if (command === 'task') {
113
+ runSubcommand('./cm-task.js', args.slice(1));
114
+ return;
115
+ }
116
+
117
+ if (command === 'diagnose') {
118
+ runSubcommand('./cm-diagnose.js', args.slice(1));
119
+ return;
120
+ }
121
+
102
122
  if (args.includes('--help') || args.includes('-h')) {
103
123
  printHelp();
104
124
  return;
@@ -708,14 +728,33 @@ function printHelp() {
708
728
  console.log(' --changed-only Analyze only files with uncommitted changes');
709
729
  console.log(' --changed-since REF Analyze files changed since commit/branch');
710
730
  console.log();
731
+ console.log('Task-Driven Context (v3.2.0):');
732
+ console.log(' task "<goal>" [options] Build task-driven context bundle around a goal');
733
+ console.log(' --repo DIR Additional repository root for workspace (repeatable)');
734
+ console.log(' --budget TOKENS Token budget (default: 32000, supports 32k)');
735
+ console.log(' --model MODEL Optimize for specific target model');
736
+ console.log(' --format FORMAT markdown (default), json, xml');
737
+ console.log();
738
+ console.log('Failure Diagnosis (v3.2.0):');
739
+ console.log(' diagnose [file|text] Analyze stack traces and failure logs');
740
+ console.log(' --repo DIR Additional repository root for workspace (repeatable)');
741
+ console.log(' --stdin Read failure log or stack trace from stdin');
742
+ console.log(' --budget TOKENS Token budget (default: 24000)');
743
+ console.log(' --json Output in JSON format');
744
+ console.log();
711
745
  console.log('Platform Features (v3.0.0):');
712
746
  console.log(' serve [options] Start REST API server');
713
747
  console.log(' --port PORT Server port (default: 3000)');
714
748
  console.log(' --auth-token TOKEN API authentication token');
715
749
  console.log(' --host HOST Address to bind (default: localhost)');
716
750
  console.log(' --cors Send CORS headers (off by default)');
717
- console.log(' watch [options] Watch mode with auto-analysis');
751
+ console.log(' watch [options] Watch mode with auto-analysis & live RAG/graph sync');
752
+ console.log(' --repo DIR Additional repository root to watch (repeatable)');
718
753
  console.log(' --debounce MS Debounce delay (default: 1000ms)');
754
+ console.log(' --bootstrap Seed full SQLite vector & graph store on startup');
755
+ console.log(' --no-sync Disable incremental SQLite vector & graph sync');
756
+ console.log(' --no-sync-rag Disable incremental SQLite vector embedding sync');
757
+ console.log(' --no-sync-graph Disable incremental SQLite knowledge-graph sync');
719
758
  console.log();
720
759
  console.log('General Options:');
721
760
  console.log(' -h, --help Show this help');
@@ -754,22 +793,34 @@ function printHelp() {
754
793
  console.log(' pack [path] [options] File contents in one LLM-ready document (xml, markdown,');
755
794
  console.log(' plain, json); --compress, --budget, --split');
756
795
  console.log(' (ctxman pack --help for options)');
796
+ console.log(
797
+ ' focus FILE [path] Target file plus its imports and importers up to --depth N'
798
+ );
799
+ console.log(' (ctxman focus --help for options)');
757
800
  console.log(' map [path] [options] Ranked repository map within a token budget');
758
801
  console.log(' (ctxman map --help for options)');
759
802
  console.log(' pr [--base REF] [--head REF] [path]');
760
803
  console.log(' Review context for a branch: log, changed methods,');
761
- console.log(' diff, related tests (ctxman pr --help)');
804
+ console.log(' diff, related tests (--repo DIR, ctxman pr --help)');
762
805
  console.log(' why FILE[:SYMBOL|#L10-L40] [options]');
763
806
  console.log(' Why the code looks like this: commits, references,');
764
- console.log(' owners, co-changes, docs and tests (ctxman why --help)');
807
+ console.log(
808
+ ' owners, co-changes, docs and tests (--repo DIR, ctxman why --help)'
809
+ );
810
+ console.log(' who TARGET [options] Find code & domain experts across git blame, history,');
811
+ console.log(' and CODEOWNERS (--repo DIR for multi-repo workspaces)');
765
812
  console.log(' search QUERY [path] Ranked (BM25) search over methods and code chunks');
766
813
  console.log(' count [path|-] [--model ID] [--api]');
767
814
  console.log(' Count tokens for a model (exact for Claude with --api)');
768
815
  console.log(' mcp [install|uninstall|config]');
769
816
  console.log(' Start the MCP server or add it to an MCP client');
770
- console.log(' policy [check|init] Show or create the export policy (.ctxman/policy.json)');
771
- console.log(' audit [--since DATE] Show the export audit log written under a policy');
772
- console.log(' ask QUESTION Answer a question from the indexed codebase');
817
+ console.log(
818
+ ' policy [check|init] Show or create the export policy (--repo DIR for workspaces)'
819
+ );
820
+ console.log(' audit [--since DATE] Show the export audit log (--repo DIR for workspaces)');
821
+ console.log(
822
+ ' ask QUESTION [options] Answer a question from the indexed codebase (--repo DIR)'
823
+ );
773
824
  console.log(' update [check|install|rollback|channel NAME|info]');
774
825
  console.log(' Check for and manage ctxman updates');
775
826
  console.log();
@@ -971,8 +1022,39 @@ async function runWatchMode(args) {
971
1022
  ? parseInt(args[args.indexOf('--debounce') + 1], 10) || 1000
972
1023
  : 1000;
973
1024
 
974
- const watcher = new FileWatcher(projectRoot, { debounce });
975
- const analyzer = new IncrementalAnalyzer({ methodLevel: args.includes('-m') });
1025
+ const repos = [];
1026
+ for (let i = 0; i < args.length; i++) {
1027
+ if (args[i] === '--repo' && args[i + 1] && !args[i + 1].startsWith('-')) {
1028
+ repos.push(resolve(projectRoot, args[i + 1]));
1029
+ i++;
1030
+ }
1031
+ }
1032
+
1033
+ const noSync = args.includes('--no-sync');
1034
+ const syncRag = !noSync && !args.includes('--no-sync-rag');
1035
+ const syncGraph = !noSync && !args.includes('--no-sync-graph');
1036
+
1037
+ const watcher = new FileWatcher(projectRoot, { debounce, repos });
1038
+ const analyzer = new IncrementalAnalyzer({
1039
+ projectRoot,
1040
+ repos,
1041
+ methodLevel: args.includes('-m'),
1042
+ syncRag,
1043
+ syncGraph,
1044
+ });
1045
+
1046
+ if (repos.length > 0) {
1047
+ console.log(
1048
+ ` 🔗 Multi-repo workspace: ${watcher.repos.map((r) => `${r.alias} (${r.root})`).join(', ')}`
1049
+ );
1050
+ }
1051
+
1052
+ if (args.includes('--bootstrap')) {
1053
+ const boot = await analyzer.bootstrapSync(projectRoot, { repos });
1054
+ console.log(
1055
+ ` 🔄 Bootstrapped SQLite store: ${boot.ragUpserted} embedded (${boot.ragSkipped} unchanged), ${boot.graphNodes} graph nodes, ${boot.graphEdges} edges`
1056
+ );
1057
+ }
976
1058
 
977
1059
  // Handle file changes
978
1060
  watcher.on('file:changed', async (event) => {
@@ -984,6 +1066,17 @@ async function runWatchMode(args) {
984
1066
  analyzer.on('analysis:complete', (event) => {
985
1067
  console.log(` ✅ Analysis complete: ${event.analysis.tokens} tokens (${event.elapsed}ms)`);
986
1068
 
1069
+ if (event.sync?.rag) {
1070
+ console.log(
1071
+ ` 🧠 Vector store (${event.sync.rag.action}): ${event.sync.rag.totalDocuments} indexed files`
1072
+ );
1073
+ }
1074
+ if (event.sync?.graph) {
1075
+ console.log(
1076
+ ` 🕸️ Knowledge graph (${event.sync.graph.action}): ${event.sync.graph.totalNodes} nodes, ${event.sync.graph.totalEdges} edges`
1077
+ );
1078
+ }
1079
+
987
1080
  const stats = analyzer.getStats();
988
1081
  console.log(
989
1082
  ` 📊 Total: ${stats.totalFiles} files, ${stats.totalTokens.toLocaleString()} tokens`
@@ -993,6 +1086,11 @@ async function runWatchMode(args) {
993
1086
  // Handle file deletion
994
1087
  analyzer.on('file:deleted', (event) => {
995
1088
  console.log(` 🗑️ File deleted: ${event.file} (-${event.oldTokens} tokens)`);
1089
+ if (event.sync?.rag || event.sync?.graph) {
1090
+ console.log(
1091
+ ` 🧹 Synced deletion: ${event.sync.rag?.totalDocuments ?? 0} indexed docs, ${event.sync.graph?.totalNodes ?? 0} graph nodes`
1092
+ );
1093
+ }
996
1094
  });
997
1095
 
998
1096
  // Start watching
@@ -1005,6 +1103,7 @@ async function runWatchMode(args) {
1005
1103
  process.on('SIGINT', () => {
1006
1104
  console.log('\n\n🛑 Stopping watch mode...');
1007
1105
  watcher.stop();
1106
+ analyzer.close();
1008
1107
  process.exit(0);
1009
1108
  });
1010
1109
  }
package/bin/cm-ask.js CHANGED
@@ -1,100 +1,104 @@
1
1
  #!/usr/bin/env node
2
- import { VectorStoreFactory } from '../lib/rag/VectorStoreFactory.js';
3
- import { TransformersEmbeddingProvider } from '../lib/rag/EmbeddingProvider.js';
4
- import { Indexer } from '../lib/rag/Indexer.js';
5
- import { FileDataSource } from '../lib/rag/sources/FileDataSource.js';
6
- import path from 'path';
7
- import fs from 'fs/promises';
2
+ import { parseArgs } from 'node:util';
3
+ import path from 'node:path';
4
+ import { askCodebase, DEFAULT_ASK_LIMIT } from '../lib/rag/ask.js';
5
+
6
+ const HELP = `Usage: ctxman ask "<question>" [options]
7
+
8
+ Semantic search over the codebase (single-repo or multi-repo workspace) using the
9
+ local SQLite embedding index (.ctxman/self-knowledge/index.sqlite).
10
+
11
+ Options:
12
+ --root <dir> Primary project root (default: cwd)
13
+ --repo <dir> Additional repository root for multi-repo workspace (repeatable)
14
+ -n, --limit <n> Maximum snippets to return (default: ${DEFAULT_ASK_LIMIT})
15
+ --json Output result as JSON
16
+ -h, --help Show this help
17
+ `;
18
+
19
+ const OPTIONS = {
20
+ root: { type: 'string' },
21
+ repo: { type: 'string', multiple: true },
22
+ limit: { type: 'string', short: 'n' },
23
+ json: { type: 'boolean' },
24
+ help: { type: 'boolean', short: 'h' },
25
+ };
8
26
 
9
27
  async function main() {
10
- const args = process.argv.slice(2);
11
- const query = args.join(' ');
28
+ const rawArgs = process.argv.slice(2);
29
+ let values = {};
30
+ let positionals = rawArgs;
12
31
 
13
- if (!query) {
14
- console.error('Usage: ctxman ask "Your query here"');
15
- process.exit(1);
32
+ try {
33
+ const parsed = parseArgs({ args: rawArgs, options: OPTIONS, allowPositionals: true });
34
+ values = parsed.values;
35
+ positionals = parsed.positionals;
36
+ } catch {
37
+ // Allow unquoted queries that might contain dashes if parseArgs fails
38
+ positionals = rawArgs;
16
39
  }
17
40
 
18
- // 1. Initialize RAG Components
19
- let provider;
20
- try {
21
- // Try to load local transformers
22
- provider = new TransformersEmbeddingProvider();
23
- // Lazy init will handle model download on first use
24
- } catch (e) {
25
- console.error('⚠️ Failed to initialize embedding provider:', e.message);
26
- process.exit(1);
41
+ if (values.help) {
42
+ process.stdout.write(HELP);
43
+ return;
27
44
  }
28
45
 
29
- // Use LanceDB in .ctxman/self-knowledge
30
- const storePath = path.join(process.cwd(), '.ctxman', 'self-knowledge');
31
- const store = VectorStoreFactory.create('lancedb', provider, { path: storePath });
46
+ const query = positionals.join(' ').trim();
47
+
48
+ if (!query) {
49
+ console.error('Usage: ctxman ask "Your query here" [--repo <dir>] [--limit <n>] [--json]');
50
+ process.exit(1);
51
+ }
32
52
 
33
- // 2. Check if indexed
34
- const isIndexed = await checkIndexExists(storePath);
53
+ const rootDir = values.root ? path.resolve(process.cwd(), values.root) : process.cwd();
54
+ const extraRepos = Array.isArray(values.repo)
55
+ ? values.repo.map((r) => path.resolve(process.cwd(), r))
56
+ : undefined;
57
+ const limit = values.limit ? Number.parseInt(values.limit, 10) : undefined;
35
58
 
36
- if (!isIndexed) {
37
- console.log('📚 Index not found. Auto-indexing current project...');
38
- await indexProject(store);
59
+ if (!values.json) {
60
+ console.log(`🔍 Searching for: "${query}"...`);
39
61
  }
40
62
 
41
- // 3. Search
42
- console.log(`🔍 Searching for: "${query}"...`);
43
- const results = await store.search(query, 5); // Get top 5
63
+ const result = await askCodebase(rootDir, query, {
64
+ repos: extraRepos,
65
+ limit,
66
+ onProgress: (event) => {
67
+ if (values.json) return;
68
+ if (event === 'indexing') {
69
+ console.log('📚 Index not found. Auto-indexing current project...');
70
+ console.log('⏳ Indexing files (this may take a moment)...');
71
+ } else if (event === 'indexed') {
72
+ console.log('✅ Indexing complete.');
73
+ }
74
+ },
75
+ });
44
76
 
77
+ if (values.json) {
78
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
79
+ return;
80
+ }
81
+
82
+ const { results, removedByPolicy } = result;
83
+ if (removedByPolicy > 0) {
84
+ console.log(`🛡️ Removed ${removedByPolicy} results denied by the export policy`);
85
+ }
45
86
  if (results.length === 0) {
46
87
  console.log('❌ No relevant context found.');
47
88
  return;
48
89
  }
49
90
 
50
- // 4. Display Results (or feed to LLM if we had one configured)
51
91
  console.log(`\nFound ${results.length} relevant snippets:\n`);
52
-
53
92
  results.forEach((r, i) => {
54
- console.log(`[${i + 1}] ${r.metadata.filename} (Score: ${r.score?.toFixed(4)})`);
93
+ console.log(`[${i + 1}] ${r.file} (Score: ${r.score?.toFixed(4)})`);
55
94
  console.log('─'.repeat(40));
56
- // Show a small snippet around the match if possible, but we stored full text.
57
- // Let's just show the first 200 chars or so.
58
95
  console.log(r.text.substring(0, 300).replace(/\n/g, ' ') + '...');
59
96
  console.log('\n');
60
97
  });
61
-
62
- // Future: Pass `results` to an LLM API to generate a real answer.
63
- // For now, this validates the retrieval pipeline works from CLI.
64
- }
65
-
66
- async function checkIndexExists(storePath) {
67
- try {
68
- await fs.access(storePath);
69
- // Also check if it has tables (LanceDB creates folders)
70
- const files = await fs.readdir(storePath);
71
- return files.length > 0;
72
- } catch {
73
- return false;
74
- }
75
- }
76
-
77
- async function indexProject(store) {
78
- const indexer = new Indexer(store);
79
- const fileSource = new FileDataSource();
80
- indexer.registerSource(fileSource);
81
-
82
- console.log('⏳ Indexing files (this may take a moment)...');
83
- try {
84
- await indexer.index('local-files', {
85
- targetDir: process.cwd(),
86
- include: ['.js', '.md', '.ts', '.json'], // Configurable?
87
- exclude: ['node_modules', '.git', 'dist', 'coverage', '.ctxman'],
88
- });
89
- } catch (error) {
90
- console.error(`❌ ${error.message}`);
91
- process.exit(1);
92
- }
93
- console.log('✅ Indexing complete.');
94
98
  }
95
99
 
96
100
  main().catch((err) => {
97
- // Message only: missing optional dependencies surface here with an install hint
101
+ // Message only: missing optional dependencies and indexing failures surface here
98
102
  console.error('❌ Error:', err.message);
99
103
  process.exit(1);
100
104
  });
package/bin/cm-audit.js CHANGED
@@ -6,8 +6,9 @@
6
6
 
7
7
  import path from 'path';
8
8
  import {
9
- loadPolicy,
9
+ auditLogFiles,
10
10
  readAuditLog,
11
+ readWorkspaceAuditLogs,
11
12
  filterAuditEvents,
12
13
  formatAuditTable,
13
14
  parseSince,
@@ -17,6 +18,27 @@ import {
17
18
 
18
19
  const DEFAULT_TABLE_LIMIT = 50;
19
20
 
21
+ function collectRepos(args) {
22
+ const repos = [];
23
+ for (let i = 0; i < args.length; i++) {
24
+ const arg = args[i];
25
+ if (arg === '--repo') {
26
+ const value = args[++i];
27
+ if (value === undefined || value.startsWith('--')) {
28
+ throw new Error('--repo needs a value');
29
+ }
30
+ repos.push(value);
31
+ } else if (arg.startsWith('--repo=')) {
32
+ const value = arg.slice('--repo='.length);
33
+ if (!value) {
34
+ throw new Error('--repo needs a value');
35
+ }
36
+ repos.push(value);
37
+ }
38
+ }
39
+ return repos;
40
+ }
41
+
20
42
  function main() {
21
43
  const args = process.argv.slice(2);
22
44
 
@@ -29,6 +51,7 @@ function main() {
29
51
  let since = null;
30
52
  let limit = json ? 0 : DEFAULT_TABLE_LIMIT;
31
53
  let file = null;
54
+ let repos = [];
32
55
 
33
56
  try {
34
57
  const sinceValue = valueOf(args, '--since');
@@ -43,12 +66,21 @@ function main() {
43
66
  }
44
67
 
45
68
  file = valueOf(args, '--file') ?? null;
69
+ repos = collectRepos(args);
70
+ if (file && repos.length > 0) {
71
+ throw new Error('Cannot combine --file and --repo');
72
+ }
46
73
  } catch (error) {
47
74
  console.error(`❌ ${error.message}`);
48
75
  process.exitCode = 2;
49
76
  return;
50
77
  }
51
78
 
79
+ if (repos.length > 0) {
80
+ runWorkspaceAudit({ repos, since, limit, json });
81
+ return;
82
+ }
83
+
52
84
  const files = file ? [path.resolve(file)] : auditFiles();
53
85
  const { events, skipped, missing } = readAuditLog(files);
54
86
  const selected = filterAuditEvents(events, { since, limit });
@@ -82,17 +114,56 @@ function main() {
82
114
  }
83
115
  }
84
116
 
85
- // The policy's audit logs; the default location when no policy names one
86
- function auditFiles() {
87
- const root = process.cwd();
117
+ function runWorkspaceAudit({ repos, since, limit, json }) {
118
+ let result;
88
119
  try {
89
- const policy = loadPolicy(root);
90
- if (policy.audit.paths.length > 0) return policy.audit.paths;
120
+ result = readWorkspaceAuditLogs(process.cwd(), repos, { since, limit });
91
121
  } catch (error) {
122
+ console.error(`❌ ${error.message}`);
123
+ process.exitCode = 2;
124
+ return;
125
+ }
126
+
127
+ for (const err of result.policyErrors) {
128
+ console.error(`⚠️ [${err.repo}] ${err.message}`);
129
+ }
130
+
131
+ if (json) {
132
+ console.log(JSON.stringify(result.events, null, 2));
133
+ } else if (result.files.length === 0) {
134
+ console.log(`No audit logs found across ${result.repos.length} repositories.`);
135
+ console.log(
136
+ `Exports are logged while ${POLICY_FILE} has "audit": {"enabled": true} (ctxman policy init).`
137
+ );
138
+ } else if (result.events.length === 0) {
139
+ console.log(
140
+ since ? `No exports recorded since ${since.toISOString()}.` : 'No exports recorded.'
141
+ );
142
+ } else {
143
+ console.log(formatAuditTable(result.events));
144
+ console.log();
145
+ const shown =
146
+ result.events.length < result.total
147
+ ? `Showing the last ${result.events.length} of ${result.total} events (--limit 0 shows all)`
148
+ : `${result.events.length} event${result.events.length === 1 ? '' : 's'}`;
149
+ console.log(`${shown} across ${result.repos.length} repositories (${result.files.join(', ')})`);
150
+ }
151
+
152
+ if (result.skippedLines > 0) {
153
+ console.error(
154
+ `⚠️ Skipped ${result.skippedLines} line${result.skippedLines === 1 ? '' : 's'} that are not JSON events`
155
+ );
156
+ }
157
+ }
158
+
159
+ // The policy's audit logs; the default location when no policy names one
160
+ function auditFiles() {
161
+ const { files, error } = auditLogFiles(process.cwd());
162
+ if (error) {
92
163
  console.error(`⚠️ ${error.message}`);
93
164
  console.error(` Reading the default log ${DEFAULT_AUDIT_PATH} instead.`);
94
165
  }
95
- return [path.join(root, DEFAULT_AUDIT_PATH)];
166
+ return files;
96
167
  }
97
168
 
98
169
  function valueOf(args, flag) {
@@ -109,6 +180,8 @@ function printHelp() {
109
180
  console.log('ctxman audit - show the export audit log\n');
110
181
  console.log('Usage: ctxman audit [options]\n');
111
182
  console.log('Options:');
183
+ console.log(' --repo DIR Include another repository in the merged audit log');
184
+ console.log(' (repeatable)');
112
185
  console.log(' --since DATE Only exports at or after DATE: a date (2026-10-01), an ISO');
113
186
  console.log(' time, or an age such as 30m, 12h, 7d, 2w');
114
187
  console.log(` --limit N Show the last N exports (default: ${DEFAULT_TABLE_LIMIT};`);