ctxman 3.1.1 → 3.2.8
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/.ctxman/llm-profiles.json +6 -6
- package/README.md +16 -10
- package/bin/cli.js +109 -10
- package/bin/cm-ask.js +64 -6
- package/bin/cm-audit.js +76 -0
- package/bin/cm-count.js +45 -6
- package/bin/cm-diagnose.js +199 -0
- package/bin/cm-focus.js +322 -0
- package/bin/cm-map.js +71 -18
- package/bin/cm-pack.js +36 -5
- package/bin/cm-policy.js +107 -2
- package/bin/cm-pr.js +18 -3
- package/bin/cm-search.js +84 -19
- package/bin/cm-task.js +192 -0
- package/bin/cm-who.js +119 -0
- package/bin/cm-why.js +41 -5
- package/index.d.ts +940 -1
- package/index.js +105 -1
- package/lib/analyzers/token-calculator.js +76 -4
- package/lib/api/mcp/server.js +843 -29
- package/lib/cache/CacheManager.js +161 -47
- package/lib/core/Analyzer.js +61 -5
- package/lib/count/index.js +74 -14
- package/lib/diagnose/index.js +712 -0
- package/lib/focus/index.js +790 -0
- package/lib/map/call-graph.js +570 -0
- package/lib/map/graph.js +30 -5
- package/lib/map/index.js +12 -4
- package/lib/map/rank.js +45 -7
- package/lib/map/render.js +89 -28
- package/lib/pack/index.js +20 -1
- package/lib/pack/select.js +8 -1
- package/lib/policy/audit.js +3 -1
- package/lib/policy/index.js +5 -0
- package/lib/policy/workspace.js +254 -0
- package/lib/pr/pr-context.js +292 -31
- package/lib/pr/pr-renderer.js +25 -10
- package/lib/rag/EmbeddingProvider.js +139 -2
- package/lib/rag/VectorStoreFactory.js +10 -4
- package/lib/rag/ask.js +186 -17
- package/lib/rag/stores/SqliteVectorStore.js +595 -0
- package/lib/search/hybrid.js +324 -0
- package/lib/search/symbols.js +75 -11
- package/lib/task/index.js +630 -0
- package/lib/utils/file-utils.js +1 -0
- package/lib/utils/multi-repo-manager.js +4 -1
- package/lib/watch/FileWatcher.js +121 -31
- package/lib/watch/IncrementalAnalyzer.js +964 -16
- package/lib/who/index.js +893 -0
- package/lib/why/analyzer-bridge.js +223 -0
- package/lib/why/discussions.js +314 -0
- package/lib/why/index.js +356 -30
- package/lib/why/render.js +154 -9
- package/lib/workspace/index.js +1274 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "4.0.0",
|
|
3
|
-
"lastUpdated": "2026-10-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
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-
|
|
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,7 @@ 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`, `check_policy`, `audit_log`, `ask_codebase`.
|
|
331
|
+
`count_tokens`, `explain_history`, `pack_context`, `focus_context`, `check_policy`, `audit_log`, `ask_codebase`, `task_context`, `diagnose_failure`, `find_experts`, `query_knowledge_graph`.
|
|
326
332
|
Details:
|
|
327
333
|
[docs/commands/mcp.md](docs/commands/mcp.md).
|
|
328
334
|
|
|
@@ -333,8 +339,8 @@ A policy in the repository (`.ctxman/policy.json`), or one an organisation point
|
|
|
333
339
|
|
|
334
340
|
```bash
|
|
335
341
|
ctxman policy init # starter policy: deny .env, keys and secrets/, require redaction
|
|
336
|
-
ctxman policy check # validate and show the effective policy
|
|
337
|
-
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)
|
|
338
344
|
ctxman --cli --respect-ignore-files .cursorignore,.aiexclude # honour other AI tools' ignore files
|
|
339
345
|
```
|
|
340
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(
|
|
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
|
-
console.log(' count [path|-] [--model ID] [--api]');
|
|
767
|
-
console.log(' Count tokens for a model
|
|
813
|
+
console.log(' count [path|-] [--model ID] [--repo DIR] [--api]');
|
|
814
|
+
console.log(' Count tokens for a model or multi-repo workspace');
|
|
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(
|
|
771
|
-
|
|
772
|
-
|
|
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
|
|
975
|
-
|
|
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,18 +1,70 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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
|
+
};
|
|
3
26
|
|
|
4
27
|
async function main() {
|
|
5
|
-
const
|
|
6
|
-
|
|
28
|
+
const rawArgs = process.argv.slice(2);
|
|
29
|
+
let values = {};
|
|
30
|
+
let positionals = rawArgs;
|
|
31
|
+
|
|
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;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (values.help) {
|
|
42
|
+
process.stdout.write(HELP);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const query = positionals.join(' ').trim();
|
|
7
47
|
|
|
8
48
|
if (!query) {
|
|
9
|
-
console.error('Usage: ctxman ask "Your query here"');
|
|
49
|
+
console.error('Usage: ctxman ask "Your query here" [--repo <dir>] [--limit <n>] [--json]');
|
|
10
50
|
process.exit(1);
|
|
11
51
|
}
|
|
12
52
|
|
|
13
|
-
|
|
14
|
-
const
|
|
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;
|
|
58
|
+
|
|
59
|
+
if (!values.json) {
|
|
60
|
+
console.log(`🔍 Searching for: "${query}"...`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = await askCodebase(rootDir, query, {
|
|
64
|
+
repos: extraRepos,
|
|
65
|
+
limit,
|
|
15
66
|
onProgress: (event) => {
|
|
67
|
+
if (values.json) return;
|
|
16
68
|
if (event === 'indexing') {
|
|
17
69
|
console.log('📚 Index not found. Auto-indexing current project...');
|
|
18
70
|
console.log('⏳ Indexing files (this may take a moment)...');
|
|
@@ -22,6 +74,12 @@ async function main() {
|
|
|
22
74
|
},
|
|
23
75
|
});
|
|
24
76
|
|
|
77
|
+
if (values.json) {
|
|
78
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { results, removedByPolicy } = result;
|
|
25
83
|
if (removedByPolicy > 0) {
|
|
26
84
|
console.log(`🛡️ Removed ${removedByPolicy} results denied by the export policy`);
|
|
27
85
|
}
|
package/bin/cm-audit.js
CHANGED
|
@@ -8,6 +8,7 @@ import path from 'path';
|
|
|
8
8
|
import {
|
|
9
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,6 +114,48 @@ function main() {
|
|
|
82
114
|
}
|
|
83
115
|
}
|
|
84
116
|
|
|
117
|
+
function runWorkspaceAudit({ repos, since, limit, json }) {
|
|
118
|
+
let result;
|
|
119
|
+
try {
|
|
120
|
+
result = readWorkspaceAuditLogs(process.cwd(), repos, { since, limit });
|
|
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
|
+
|
|
85
159
|
// The policy's audit logs; the default location when no policy names one
|
|
86
160
|
function auditFiles() {
|
|
87
161
|
const { files, error } = auditLogFiles(process.cwd());
|
|
@@ -106,6 +180,8 @@ function printHelp() {
|
|
|
106
180
|
console.log('ctxman audit - show the export audit log\n');
|
|
107
181
|
console.log('Usage: ctxman audit [options]\n');
|
|
108
182
|
console.log('Options:');
|
|
183
|
+
console.log(' --repo DIR Include another repository in the merged audit log');
|
|
184
|
+
console.log(' (repeatable)');
|
|
109
185
|
console.log(' --since DATE Only exports at or after DATE: a date (2026-10-01), an ISO');
|
|
110
186
|
console.log(' time, or an age such as 30m, 12h, 7d, 2w');
|
|
111
187
|
console.log(` --limit N Show the last N exports (default: ${DEFAULT_TABLE_LIMIT};`);
|
package/bin/cm-count.js
CHANGED
|
@@ -20,11 +20,12 @@ getLogger().level = 'warn';
|
|
|
20
20
|
|
|
21
21
|
const HELP = `Usage: ctxman count [path | -] [options]
|
|
22
22
|
|
|
23
|
-
Count the tokens of a project
|
|
24
|
-
and check them against the model's context window.
|
|
23
|
+
Count the tokens of a project, multi-repository workspace, or text piped to stdin with "-"
|
|
24
|
+
for a target model, and check them against the model's context window.
|
|
25
25
|
|
|
26
26
|
Options:
|
|
27
27
|
--model, -m ID Target model (default: ${LLMDetector.getDefaultModelId()}; see ctxman --list-llms)
|
|
28
|
+
--repo DIR Additional repository root to count in a workspace (repeatable)
|
|
28
29
|
--api Claude models: count exactly with Anthropic's count_tokens API.
|
|
29
30
|
Sends the (secret-redacted) text to Anthropic; needs ANTHROPIC_API_KEY
|
|
30
31
|
and the @anthropic-ai/sdk package. Counting is free and generates nothing.
|
|
@@ -39,26 +40,46 @@ their counts are approximate.
|
|
|
39
40
|
Examples:
|
|
40
41
|
ctxman count --model gpt-6.1-sol
|
|
41
42
|
ctxman count --model claude-opus-5-5 --api
|
|
43
|
+
ctxman count ./api --repo ../shared --repo ../web --model claude-sonnet-5-5
|
|
42
44
|
ctxman pack | ctxman count - --model claude-sonnet-5-5 --api`;
|
|
43
45
|
|
|
44
46
|
function parseArgs(argv) {
|
|
45
|
-
const options = { top: 5 };
|
|
47
|
+
const options = { top: 5, repos: [] };
|
|
46
48
|
for (let i = 0; i < argv.length; i++) {
|
|
47
49
|
const arg = argv[i];
|
|
48
50
|
if (arg === '--model' || arg === '-m') options.model = argv[++i];
|
|
49
|
-
else if (arg
|
|
51
|
+
else if (arg.startsWith('--model=')) options.model = arg.slice('--model='.length);
|
|
52
|
+
else if (arg === '--repo') {
|
|
53
|
+
const repoDir = argv[++i];
|
|
54
|
+
if (!repoDir || repoDir.startsWith('-')) {
|
|
55
|
+
throw new Error('--repo needs a directory path');
|
|
56
|
+
}
|
|
57
|
+
options.repos.push(repoDir);
|
|
58
|
+
} else if (arg.startsWith('--repo=')) {
|
|
59
|
+
const repoDir = arg.slice('--repo='.length);
|
|
60
|
+
if (!repoDir) throw new Error('--repo needs a directory path');
|
|
61
|
+
options.repos.push(repoDir);
|
|
62
|
+
} else if (arg === '--api') options.api = true;
|
|
50
63
|
else if (arg === '--json') options.json = true;
|
|
51
64
|
else if (arg === '--top') options.top = Number.parseInt(argv[++i], 10);
|
|
65
|
+
else if (arg.startsWith('--top='))
|
|
66
|
+
options.top = Number.parseInt(arg.slice('--top='.length), 10);
|
|
52
67
|
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
53
68
|
else if (arg === '-' || !arg.startsWith('-')) options.target = arg;
|
|
54
69
|
else throw new Error(`Unknown option: ${arg}`);
|
|
55
70
|
}
|
|
56
|
-
if (
|
|
71
|
+
if (
|
|
72
|
+
(options.model === undefined || options.model === '') &&
|
|
73
|
+
argv.some((a) => a === '--model' || a === '-m' || a === '--model=')
|
|
74
|
+
) {
|
|
57
75
|
throw new Error('--model needs a model id');
|
|
58
76
|
}
|
|
59
77
|
if (!Number.isInteger(options.top) || options.top < 0) {
|
|
60
78
|
throw new Error('--top needs a number of files');
|
|
61
79
|
}
|
|
80
|
+
if (options.target === '-' && options.repos.length > 0) {
|
|
81
|
+
throw new Error('Cannot combine --repo with stdin ("-")');
|
|
82
|
+
}
|
|
62
83
|
return options;
|
|
63
84
|
}
|
|
64
85
|
|
|
@@ -89,8 +110,17 @@ function printResult(result, top) {
|
|
|
89
110
|
print(` For an exact count: ctxman count --model ${result.model} --api`);
|
|
90
111
|
}
|
|
91
112
|
|
|
113
|
+
if (result.repos?.length > 1) {
|
|
114
|
+
print('\nRepositories:');
|
|
115
|
+
for (const repo of result.repos) {
|
|
116
|
+
print(
|
|
117
|
+
` ${n(repo.tokens).padStart(9)} ${repo.alias} (${n(repo.files)} file${repo.files === 1 ? '' : 's'}, ${repo.percentOfWindow.toFixed(1)}%)`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
92
122
|
const largest = result.largestFiles.slice(0, top);
|
|
93
|
-
if (largest.length > 1) {
|
|
123
|
+
if (largest.length > 1 || (result.repos?.length > 1 && largest.length > 0)) {
|
|
94
124
|
const scale = result.method === 'anthropic-api' && result.ratio ? result.ratio : 1;
|
|
95
125
|
print(`\nLargest files${scale !== 1 ? ' (≈, scaled to the API count)' : ''}:`);
|
|
96
126
|
for (const file of largest) {
|
|
@@ -119,6 +149,14 @@ async function main() {
|
|
|
119
149
|
process.exit(1);
|
|
120
150
|
}
|
|
121
151
|
|
|
152
|
+
const repos = options.repos.map((r) => path.resolve(r));
|
|
153
|
+
for (const repoPath of repos) {
|
|
154
|
+
if (!fs.statSync(repoPath, { throwIfNoEntry: false })?.isDirectory()) {
|
|
155
|
+
console.error(`❌ Not a directory: ${repoPath}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
122
160
|
const model = options.model || LLMDetector.getDefaultModelId();
|
|
123
161
|
if (options.api && LLMDetector.getProfile(model).vendor === 'Anthropic') {
|
|
124
162
|
console.error('Sending the redacted text to the Anthropic API to count tokens...');
|
|
@@ -129,6 +167,7 @@ async function main() {
|
|
|
129
167
|
model,
|
|
130
168
|
api: options.api,
|
|
131
169
|
text,
|
|
170
|
+
repos,
|
|
132
171
|
onProgress: (done, total) => {
|
|
133
172
|
if (total > 1) process.stderr.write(`\r request ${done}/${total}`);
|
|
134
173
|
if (total > 1 && done === total) process.stderr.write('\n');
|