ctxman 3.0.0 ā 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +380 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -13
- package/bin/cm-audit.js +124 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +683 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +675 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +113 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +294 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +52 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/sources/FileDataSource.js +6 -6
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +243 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +255 -0
- package/lib/wizards/project-detector.js +571 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
package/bin/cm-ask.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { VectorStoreFactory } from '../lib/rag/VectorStoreFactory.js';
|
|
3
|
-
import {
|
|
3
|
+
import { TransformersEmbeddingProvider } from '../lib/rag/EmbeddingProvider.js';
|
|
4
4
|
import { Indexer } from '../lib/rag/Indexer.js';
|
|
5
5
|
import { FileDataSource } from '../lib/rag/sources/FileDataSource.js';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import fs from 'fs/promises';
|
|
8
|
-
import { TokenizerManager } from '../lib/utils/tokenizer-adapter.js';
|
|
9
8
|
|
|
10
9
|
async function main() {
|
|
11
10
|
const args = process.argv.slice(2);
|
|
@@ -33,7 +32,7 @@ async function main() {
|
|
|
33
32
|
|
|
34
33
|
// 2. Check if indexed
|
|
35
34
|
const isIndexed = await checkIndexExists(storePath);
|
|
36
|
-
|
|
35
|
+
|
|
37
36
|
if (!isIndexed) {
|
|
38
37
|
console.log('š Index not found. Auto-indexing current project...');
|
|
39
38
|
await indexProject(store);
|
|
@@ -50,16 +49,16 @@ async function main() {
|
|
|
50
49
|
|
|
51
50
|
// 4. Display Results (or feed to LLM if we had one configured)
|
|
52
51
|
console.log(`\nFound ${results.length} relevant snippets:\n`);
|
|
53
|
-
|
|
52
|
+
|
|
54
53
|
results.forEach((r, i) => {
|
|
55
|
-
console.log(`[${i+1}] ${r.metadata.filename} (Score: ${r.score?.toFixed(4)})`);
|
|
54
|
+
console.log(`[${i + 1}] ${r.metadata.filename} (Score: ${r.score?.toFixed(4)})`);
|
|
56
55
|
console.log('ā'.repeat(40));
|
|
57
56
|
// Show a small snippet around the match if possible, but we stored full text.
|
|
58
57
|
// Let's just show the first 200 chars or so.
|
|
59
58
|
console.log(r.text.substring(0, 300).replace(/\n/g, ' ') + '...');
|
|
60
59
|
console.log('\n');
|
|
61
60
|
});
|
|
62
|
-
|
|
61
|
+
|
|
63
62
|
// Future: Pass `results` to an LLM API to generate a real answer.
|
|
64
63
|
// For now, this validates the retrieval pipeline works from CLI.
|
|
65
64
|
}
|
|
@@ -81,15 +80,21 @@ async function indexProject(store) {
|
|
|
81
80
|
indexer.registerSource(fileSource);
|
|
82
81
|
|
|
83
82
|
console.log('ā³ Indexing files (this may take a moment)...');
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
}
|
|
89
93
|
console.log('ā
Indexing complete.');
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
main().catch(err => {
|
|
93
|
-
|
|
96
|
+
main().catch((err) => {
|
|
97
|
+
// Message only: missing optional dependencies surface here with an install hint
|
|
98
|
+
console.error('ā Error:', err.message);
|
|
94
99
|
process.exit(1);
|
|
95
100
|
});
|
package/bin/cm-audit.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Audit Command
|
|
4
|
+
* Print the export audit log written while a policy enables `audit`
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import {
|
|
9
|
+
loadPolicy,
|
|
10
|
+
readAuditLog,
|
|
11
|
+
filterAuditEvents,
|
|
12
|
+
formatAuditTable,
|
|
13
|
+
parseSince,
|
|
14
|
+
DEFAULT_AUDIT_PATH,
|
|
15
|
+
POLICY_FILE,
|
|
16
|
+
} from '../lib/policy/index.js';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_TABLE_LIMIT = 50;
|
|
19
|
+
|
|
20
|
+
function main() {
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
|
|
23
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
24
|
+
printHelp();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const json = args.includes('--json');
|
|
29
|
+
let since = null;
|
|
30
|
+
let limit = json ? 0 : DEFAULT_TABLE_LIMIT;
|
|
31
|
+
let file = null;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const sinceValue = valueOf(args, '--since');
|
|
35
|
+
if (sinceValue !== undefined) since = parseSince(sinceValue);
|
|
36
|
+
|
|
37
|
+
const limitValue = valueOf(args, '--limit');
|
|
38
|
+
if (limitValue !== undefined) {
|
|
39
|
+
if (!/^\d+$/.test(limitValue)) {
|
|
40
|
+
throw new Error(`Invalid --limit value ${JSON.stringify(limitValue)}: expected 0 or more`);
|
|
41
|
+
}
|
|
42
|
+
limit = Number(limitValue);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
file = valueOf(args, '--file') ?? null;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`ā ${error.message}`);
|
|
48
|
+
process.exitCode = 2;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const files = file ? [path.resolve(file)] : auditFiles();
|
|
53
|
+
const { events, skipped, missing } = readAuditLog(files);
|
|
54
|
+
const selected = filterAuditEvents(events, { since, limit });
|
|
55
|
+
|
|
56
|
+
if (json) {
|
|
57
|
+
console.log(JSON.stringify(selected, null, 2));
|
|
58
|
+
} else if (missing.length === files.length) {
|
|
59
|
+
console.log(`No audit log found (${files.join(', ')}).`);
|
|
60
|
+
console.log(
|
|
61
|
+
`Exports are logged while ${POLICY_FILE} has "audit": {"enabled": true} (ctxman policy init).`
|
|
62
|
+
);
|
|
63
|
+
} else if (selected.length === 0) {
|
|
64
|
+
console.log(
|
|
65
|
+
since ? `No exports recorded since ${since.toISOString()}.` : 'No exports recorded.'
|
|
66
|
+
);
|
|
67
|
+
} else {
|
|
68
|
+
console.log(formatAuditTable(selected));
|
|
69
|
+
console.log();
|
|
70
|
+
const matching = filterAuditEvents(events, { since }).length;
|
|
71
|
+
const shown =
|
|
72
|
+
selected.length < matching
|
|
73
|
+
? `Showing the last ${selected.length} of ${matching} events (--limit 0 shows all)`
|
|
74
|
+
: `${selected.length} event${selected.length === 1 ? '' : 's'}`;
|
|
75
|
+
console.log(`${shown} from ${files.filter((f) => !missing.includes(f)).join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (skipped > 0) {
|
|
79
|
+
console.error(
|
|
80
|
+
`ā ļø Skipped ${skipped} line${skipped === 1 ? '' : 's'} that are not JSON events`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The policy's audit logs; the default location when no policy names one
|
|
86
|
+
function auditFiles() {
|
|
87
|
+
const root = process.cwd();
|
|
88
|
+
try {
|
|
89
|
+
const policy = loadPolicy(root);
|
|
90
|
+
if (policy.audit.paths.length > 0) return policy.audit.paths;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(`ā ļø ${error.message}`);
|
|
93
|
+
console.error(` Reading the default log ${DEFAULT_AUDIT_PATH} instead.`);
|
|
94
|
+
}
|
|
95
|
+
return [path.join(root, DEFAULT_AUDIT_PATH)];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function valueOf(args, flag) {
|
|
99
|
+
const index = args.indexOf(flag);
|
|
100
|
+
if (index === -1) return undefined;
|
|
101
|
+
const value = args[index + 1];
|
|
102
|
+
if (value === undefined || value.startsWith('--')) {
|
|
103
|
+
throw new Error(`${flag} needs a value`);
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function printHelp() {
|
|
109
|
+
console.log('ctxman audit - show the export audit log\n');
|
|
110
|
+
console.log('Usage: ctxman audit [options]\n');
|
|
111
|
+
console.log('Options:');
|
|
112
|
+
console.log(' --since DATE Only exports at or after DATE: a date (2026-10-01), an ISO');
|
|
113
|
+
console.log(' time, or an age such as 30m, 12h, 7d, 2w');
|
|
114
|
+
console.log(` --limit N Show the last N exports (default: ${DEFAULT_TABLE_LIMIT};`);
|
|
115
|
+
console.log(' with --json: all; 0 = all)');
|
|
116
|
+
console.log(' --json Print the events as a JSON array');
|
|
117
|
+
console.log(" --file PATH Read this log instead of the policy's audit log(s)");
|
|
118
|
+
console.log(' -h, --help Show this help\n');
|
|
119
|
+
console.log(`The log is written while ${POLICY_FILE} (or $CTXMAN_POLICY) enables audit;`);
|
|
120
|
+
console.log(`default location: ${DEFAULT_AUDIT_PATH}. One JSON object per line; each export`);
|
|
121
|
+
console.log('records who, when, where to, which files and a SHA-256 of the exported text.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main();
|
package/bin/cm-count.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ctxman count ā token count for a target model, exact for Claude through the API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { countForModel } from '../lib/count/index.js';
|
|
9
|
+
import { LLMDetector } from '../lib/utils/llm-detector.js';
|
|
10
|
+
import { getLogger } from '../lib/utils/logger.js';
|
|
11
|
+
import { exitOnBrokenPipe } from '../lib/utils/cli-output.js';
|
|
12
|
+
|
|
13
|
+
// `ctxman ... | head` closes stdout early; stop quietly instead of throwing EPIPE
|
|
14
|
+
exitOnBrokenPipe();
|
|
15
|
+
|
|
16
|
+
// stdout carries only the result (it may be JSON); progress, notices and warnings go to stderr
|
|
17
|
+
const print = (line = '') => process.stdout.write(`${line}\n`);
|
|
18
|
+
console.log = console.error;
|
|
19
|
+
getLogger().level = 'warn';
|
|
20
|
+
|
|
21
|
+
const HELP = `Usage: ctxman count [path | -] [options]
|
|
22
|
+
|
|
23
|
+
Count the tokens of a project (or of text piped to stdin with "-") for a target model,
|
|
24
|
+
and check them against the model's context window.
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--model, -m ID Target model (default: ${LLMDetector.getDefaultModelId()}; see ctxman --list-llms)
|
|
28
|
+
--api Claude models: count exactly with Anthropic's count_tokens API.
|
|
29
|
+
Sends the (secret-redacted) text to Anthropic; needs ANTHROPIC_API_KEY
|
|
30
|
+
and the @anthropic-ai/sdk package. Counting is free and generates nothing.
|
|
31
|
+
--top N Show the N largest files (default 5, 0 to hide)
|
|
32
|
+
--json Print the result as JSON
|
|
33
|
+
-h, --help Show this help
|
|
34
|
+
|
|
35
|
+
Without --api, OpenAI models are counted exactly with their tiktoken encoding
|
|
36
|
+
(o200k_base or cl100k_base); other vendors' models use their own tokenizers, so
|
|
37
|
+
their counts are approximate.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
ctxman count --model gpt-6.1-sol
|
|
41
|
+
ctxman count --model claude-opus-5-5 --api
|
|
42
|
+
ctxman pack | ctxman count - --model claude-sonnet-5-5 --api`;
|
|
43
|
+
|
|
44
|
+
function parseArgs(argv) {
|
|
45
|
+
const options = { top: 5 };
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const arg = argv[i];
|
|
48
|
+
if (arg === '--model' || arg === '-m') options.model = argv[++i];
|
|
49
|
+
else if (arg === '--api') options.api = true;
|
|
50
|
+
else if (arg === '--json') options.json = true;
|
|
51
|
+
else if (arg === '--top') options.top = Number.parseInt(argv[++i], 10);
|
|
52
|
+
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
53
|
+
else if (arg === '-' || !arg.startsWith('-')) options.target = arg;
|
|
54
|
+
else throw new Error(`Unknown option: ${arg}`);
|
|
55
|
+
}
|
|
56
|
+
if (options.model === undefined && argv.includes('--model')) {
|
|
57
|
+
throw new Error('--model needs a model id');
|
|
58
|
+
}
|
|
59
|
+
if (!Number.isInteger(options.top) || options.top < 0) {
|
|
60
|
+
throw new Error('--top needs a number of files');
|
|
61
|
+
}
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const n = (value) => value.toLocaleString('en-US');
|
|
66
|
+
|
|
67
|
+
function printResult(result, top) {
|
|
68
|
+
const label =
|
|
69
|
+
result.method === 'anthropic-api'
|
|
70
|
+
? `exact, Anthropic count_tokens API (${result.requests} request${result.requests === 1 ? '' : 's'})`
|
|
71
|
+
: result.encoding === 'estimate'
|
|
72
|
+
? 'estimate (install tiktoken for exact counts)'
|
|
73
|
+
: result.exact
|
|
74
|
+
? `exact, tiktoken ${result.encoding}`
|
|
75
|
+
: `approximate, tiktoken ${result.encoding}; ${result.vendor} models use their own tokenizer`;
|
|
76
|
+
|
|
77
|
+
print(`Model: ${result.modelName} (${result.model})`);
|
|
78
|
+
print(`Files: ${n(result.files)}`);
|
|
79
|
+
print(`Tokens: ${n(result.tokens)} [${label}]`);
|
|
80
|
+
if (result.method === 'anthropic-api' && result.ratio) {
|
|
81
|
+
print(
|
|
82
|
+
` ${n(result.localTokens)} with cl100k_base; Claude counts ${result.ratio.toFixed(2)}x as many`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
print(
|
|
86
|
+
`Window: ${n(result.contextWindow)} tokens ā ${result.percentOfWindow.toFixed(1)}% used, ${result.fits ? 'fits' : "doesn't fit"}`
|
|
87
|
+
);
|
|
88
|
+
if (result.method === 'local' && !result.exact && result.vendor === 'Anthropic') {
|
|
89
|
+
print(` For an exact count: ctxman count --model ${result.model} --api`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const largest = result.largestFiles.slice(0, top);
|
|
93
|
+
if (largest.length > 1) {
|
|
94
|
+
const scale = result.method === 'anthropic-api' && result.ratio ? result.ratio : 1;
|
|
95
|
+
print(`\nLargest files${scale !== 1 ? ' (ā, scaled to the API count)' : ''}:`);
|
|
96
|
+
for (const file of largest) {
|
|
97
|
+
print(` ${n(Math.round(file.tokens * scale)).padStart(9)} ${file.path}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function main() {
|
|
103
|
+
let options;
|
|
104
|
+
try {
|
|
105
|
+
options = parseArgs(process.argv.slice(2));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(`ā ${error.message}\n\n${HELP}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
if (options.help) {
|
|
111
|
+
print(HELP);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const root = path.resolve(options.target && options.target !== '-' ? options.target : '.');
|
|
116
|
+
const text = options.target === '-' ? fs.readFileSync(0, 'utf8') : undefined;
|
|
117
|
+
if (text === undefined && !fs.statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
|
|
118
|
+
console.error(`ā Not a directory: ${root}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const model = options.model || LLMDetector.getDefaultModelId();
|
|
123
|
+
if (options.api && LLMDetector.getProfile(model).vendor === 'Anthropic') {
|
|
124
|
+
console.error('Sending the redacted text to the Anthropic API to count tokens...');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const result = await countForModel(root, {
|
|
129
|
+
model,
|
|
130
|
+
api: options.api,
|
|
131
|
+
text,
|
|
132
|
+
onProgress: (done, total) => {
|
|
133
|
+
if (total > 1) process.stderr.write(`\r request ${done}/${total}`);
|
|
134
|
+
if (total > 1 && done === total) process.stderr.write('\n');
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
if (options.json) {
|
|
138
|
+
const { largestFiles, ...summary } = result;
|
|
139
|
+
print(
|
|
140
|
+
JSON.stringify({ ...summary, largestFiles: largestFiles.slice(0, options.top) }, null, 2)
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
printResult(result, options.top);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(`ā ${error.message}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
main();
|
package/bin/cm-gitingest.js
CHANGED
|
@@ -4,131 +4,144 @@
|
|
|
4
4
|
* Generate GitIngest digest from GitHub repositories
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import GitUtils from '../lib/utils/git-utils.js';
|
|
8
|
+
import { getLogger } from '../lib/utils/logger.js';
|
|
9
|
+
import { loadPolicy } from '../lib/policy/policy.js';
|
|
9
10
|
|
|
10
11
|
const logger = getLogger({ level: 'info' });
|
|
11
12
|
|
|
12
13
|
async function main() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
|
|
16
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
17
|
+
printHelp();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const url = args[0];
|
|
22
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
23
|
+
const keepClone = args.includes('--keep-clone');
|
|
24
|
+
const shallow = !args.includes('--full-clone');
|
|
25
|
+
|
|
26
|
+
// Parse options
|
|
27
|
+
const outputIndex = args.findIndex((arg) => arg === '--output' || arg === '-o');
|
|
28
|
+
const outputFile = outputIndex !== -1 ? args[outputIndex + 1] : null;
|
|
29
|
+
|
|
30
|
+
const branchIndex = args.findIndex((arg) => arg === '--branch' || arg === '-b');
|
|
31
|
+
const branch = branchIndex !== -1 ? args[branchIndex + 1] : null;
|
|
32
|
+
|
|
33
|
+
const chunkSize = args.includes('--chunk-size')
|
|
34
|
+
? parseInt(args[args.findIndex((arg) => arg === '--chunk-size') + 1])
|
|
35
|
+
: null;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
39
|
+
console.log('ā GitHub GitIngest Generator v2.3.6 ā');
|
|
40
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
41
|
+
|
|
42
|
+
const gitUtils = new GitUtils({
|
|
43
|
+
verbose,
|
|
44
|
+
outputDir: 'docs',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Repository info is informational: an API error (rate limit, renamed repo) must not
|
|
48
|
+
// stop the clone
|
|
49
|
+
console.log('š Fetching repository information...');
|
|
50
|
+
const repoInfo = gitUtils.parseGitHubURL(url);
|
|
36
51
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Get repository info first
|
|
47
|
-
console.log('š Fetching repository information...');
|
|
48
|
-
const repoInfo = gitUtils.parseGitHubURL(url);
|
|
49
|
-
repoInfo.branch = branch; // Override branch if specified
|
|
50
|
-
|
|
51
|
-
const repoData = await gitUtils.getRepositoryInfo(repoInfo);
|
|
52
|
-
console.log(`\nš Repository Info:`);
|
|
53
|
-
console.log(` Name: ${repoData.fullName}`);
|
|
54
|
-
console.log(` Description: ${repoData.description || 'N/A'}`);
|
|
55
|
-
console.log(` Stars: ā ${repoData.stars.toLocaleString()}`);
|
|
56
|
-
console.log(` Language: ${repoData.language || 'Multiple'}`);
|
|
57
|
-
console.log(` Default Branch: ${repoData.defaultBranch}`);
|
|
58
|
-
|
|
59
|
-
// Generate GitIngest
|
|
60
|
-
const options = {
|
|
61
|
-
outputFile,
|
|
62
|
-
cleanup: !keepClone,
|
|
63
|
-
shallow,
|
|
64
|
-
formatterOptions: chunkSize ? {
|
|
65
|
-
chunking: {
|
|
66
|
-
enabled: true,
|
|
67
|
-
maxTokensPerChunk: chunkSize
|
|
68
|
-
}
|
|
69
|
-
} : {}
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const stats = await gitUtils.generateFromGitHub(url, options);
|
|
73
|
-
|
|
74
|
-
// Log to file
|
|
75
|
-
logger.info('GitHub GitIngest generated', {
|
|
76
|
-
repository: stats.repository,
|
|
77
|
-
outputFile: stats.outputFile,
|
|
78
|
-
files: stats.files,
|
|
79
|
-
tokens: stats.tokens
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
console.log('\nā
Complete!\n');
|
|
83
|
-
|
|
84
|
-
// Show next steps
|
|
85
|
-
console.log('š Next steps:');
|
|
86
|
-
console.log(` 1. View digest: cat "${stats.outputFile}"`);
|
|
87
|
-
console.log(` 2. Use in LLM: Copy to clipboard`);
|
|
88
|
-
console.log(` 3. Cleanup: npm run clean\n`);
|
|
89
|
-
|
|
52
|
+
const repoData = await gitUtils.getRepositoryInfo(repoInfo);
|
|
53
|
+
console.log(`\nš Repository Info:`);
|
|
54
|
+
console.log(` Name: ${repoData.fullName}`);
|
|
55
|
+
console.log(` Description: ${repoData.description || 'N/A'}`);
|
|
56
|
+
console.log(` Stars: ā ${(repoData.stars ?? 0).toLocaleString()}`);
|
|
57
|
+
console.log(` Language: ${repoData.language || 'Multiple'}`);
|
|
58
|
+
console.log(` Default Branch: ${repoData.defaultBranch}`);
|
|
90
59
|
} catch (error) {
|
|
91
|
-
|
|
92
|
-
logger.error('GitHub GitIngest failed', { error: error.message });
|
|
93
|
-
process.exit(1);
|
|
60
|
+
console.log(`ā ļø Could not fetch repository info (${error.message}); continuing`);
|
|
94
61
|
}
|
|
62
|
+
|
|
63
|
+
// Generate GitIngest
|
|
64
|
+
const options = {
|
|
65
|
+
outputFile,
|
|
66
|
+
branch,
|
|
67
|
+
cleanup: !keepClone,
|
|
68
|
+
shallow,
|
|
69
|
+
// Policy of the current directory and CTXMAN_POLICY (deny rules apply to the clone)
|
|
70
|
+
policy: loadPolicy(process.cwd()),
|
|
71
|
+
formatterOptions: chunkSize
|
|
72
|
+
? {
|
|
73
|
+
chunking: {
|
|
74
|
+
enabled: true,
|
|
75
|
+
maxTokensPerChunk: chunkSize,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
: {},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const stats = await gitUtils.generateFromGitHub(url, options);
|
|
82
|
+
|
|
83
|
+
// Log to file
|
|
84
|
+
logger.info('GitHub GitIngest generated', {
|
|
85
|
+
repository: stats.repository,
|
|
86
|
+
outputFile: stats.outputFile,
|
|
87
|
+
files: stats.files,
|
|
88
|
+
tokens: stats.tokens,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log('\nā
Complete!\n');
|
|
92
|
+
|
|
93
|
+
// Show next steps
|
|
94
|
+
console.log('š Next steps:');
|
|
95
|
+
console.log(` 1. View digest: cat "${stats.outputFile}"`);
|
|
96
|
+
console.log(` 2. Use in LLM: Copy to clipboard`);
|
|
97
|
+
console.log(` 3. Cleanup: npm run clean\n`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('\nā Error:', error.message);
|
|
100
|
+
logger.error('GitHub GitIngest failed', { error: error.message });
|
|
101
|
+
// 2: invalid policy file, 3: export refused by the policy
|
|
102
|
+
process.exit(error.exitCode ?? 1);
|
|
103
|
+
}
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
function printHelp() {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
107
|
+
console.log('GitHub GitIngest Generator - Generate digest from GitHub repositories\n');
|
|
108
|
+
console.log('Usage: ctxman github <url> [options]\n');
|
|
109
|
+
console.log('Arguments:');
|
|
110
|
+
console.log(' url GitHub repository URL or owner/repo\n');
|
|
111
|
+
console.log('Options:');
|
|
112
|
+
console.log(
|
|
113
|
+
' -o, --output FILE Output file path (default: docs/{repo}-gitingest-{hash}.txt)'
|
|
114
|
+
);
|
|
115
|
+
console.log(
|
|
116
|
+
" -b, --branch BRANCH Branch to clone (default: the repository's default branch)"
|
|
117
|
+
);
|
|
118
|
+
console.log(' -v, --verbose Verbose output');
|
|
119
|
+
console.log(" --keep-clone Keep cloned repository (don't cleanup)");
|
|
120
|
+
console.log(' --full-clone Full clone (default: shallow)');
|
|
121
|
+
console.log(' --chunk-size TOKENS Enable chunking with max tokens per chunk');
|
|
122
|
+
console.log(' -h, --help Show this help\n');
|
|
123
|
+
console.log('URL Formats:');
|
|
124
|
+
console.log(' https://github.com/owner/repo');
|
|
125
|
+
console.log(' https://github.com/owner/repo.git');
|
|
126
|
+
console.log(' git@github.com:owner/repo.git');
|
|
127
|
+
console.log(' github.com/owner/repo');
|
|
128
|
+
console.log(' owner/repo\n');
|
|
129
|
+
console.log('Examples:');
|
|
130
|
+
console.log(' # Basic usage');
|
|
131
|
+
console.log(' ctxman github https://github.com/facebook/react\n');
|
|
132
|
+
console.log(' # Short format');
|
|
133
|
+
console.log(' ctxman github facebook/react\n');
|
|
134
|
+
console.log(' # Specific branch');
|
|
135
|
+
console.log(' ctxman github vercel/next.js --branch canary\n');
|
|
136
|
+
console.log(' # Custom output location');
|
|
137
|
+
console.log(' ctxman github angular/angular -o docs/angular-digest.txt\n');
|
|
138
|
+
console.log(' # With chunking');
|
|
139
|
+
console.log(' ctxman github microsoft/vscode --chunk-size 100000\n');
|
|
140
|
+
console.log(' # Keep clone for inspection');
|
|
141
|
+
console.log(' ctxman github nodejs/node --keep-clone --verbose\n');
|
|
142
|
+
console.log('Output:');
|
|
143
|
+
console.log(' Generated digest file in docs/ directory');
|
|
144
|
+
console.log(' Automatic cleanup of temporary clone (unless --keep-clone)\n');
|
|
132
145
|
}
|
|
133
146
|
|
|
134
147
|
main();
|