@mars167/git-ai 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +364 -0
  3. package/README.zh-CN.md +361 -0
  4. package/assets/hooks/post-checkout +28 -0
  5. package/assets/hooks/post-merge +28 -0
  6. package/assets/hooks/pre-commit +17 -0
  7. package/assets/hooks/pre-push +29 -0
  8. package/dist/bin/git-ai.js +62 -0
  9. package/dist/src/commands/ai.js +30 -0
  10. package/dist/src/commands/checkIndex.js +19 -0
  11. package/dist/src/commands/dsr.js +156 -0
  12. package/dist/src/commands/graph.js +203 -0
  13. package/dist/src/commands/hooks.js +125 -0
  14. package/dist/src/commands/index.js +92 -0
  15. package/dist/src/commands/pack.js +31 -0
  16. package/dist/src/commands/query.js +139 -0
  17. package/dist/src/commands/semantic.js +134 -0
  18. package/dist/src/commands/serve.js +14 -0
  19. package/dist/src/commands/status.js +78 -0
  20. package/dist/src/commands/trae.js +75 -0
  21. package/dist/src/commands/unpack.js +28 -0
  22. package/dist/src/core/archive.js +91 -0
  23. package/dist/src/core/astGraph.js +127 -0
  24. package/dist/src/core/astGraphQuery.js +142 -0
  25. package/dist/src/core/cozo.js +266 -0
  26. package/dist/src/core/cpg/astLayer.js +56 -0
  27. package/dist/src/core/cpg/callGraph.js +483 -0
  28. package/dist/src/core/cpg/cfgLayer.js +490 -0
  29. package/dist/src/core/cpg/dfgLayer.js +237 -0
  30. package/dist/src/core/cpg/index.js +80 -0
  31. package/dist/src/core/cpg/types.js +108 -0
  32. package/dist/src/core/crypto.js +10 -0
  33. package/dist/src/core/dsr/generate.js +308 -0
  34. package/dist/src/core/dsr/gitContext.js +74 -0
  35. package/dist/src/core/dsr/indexMaterialize.js +106 -0
  36. package/dist/src/core/dsr/paths.js +26 -0
  37. package/dist/src/core/dsr/query.js +73 -0
  38. package/dist/src/core/dsr/snapshotParser.js +73 -0
  39. package/dist/src/core/dsr/state.js +27 -0
  40. package/dist/src/core/dsr/types.js +2 -0
  41. package/dist/src/core/embedding/fusion.js +52 -0
  42. package/dist/src/core/embedding/index.js +43 -0
  43. package/dist/src/core/embedding/parser.js +14 -0
  44. package/dist/src/core/embedding/semantic.js +254 -0
  45. package/dist/src/core/embedding/structural.js +97 -0
  46. package/dist/src/core/embedding/symbolic.js +117 -0
  47. package/dist/src/core/embedding/tokenizer.js +91 -0
  48. package/dist/src/core/embedding/types.js +2 -0
  49. package/dist/src/core/embedding.js +36 -0
  50. package/dist/src/core/git.js +49 -0
  51. package/dist/src/core/gitDiff.js +73 -0
  52. package/dist/src/core/indexCheck.js +131 -0
  53. package/dist/src/core/indexer.js +185 -0
  54. package/dist/src/core/indexerIncremental.js +303 -0
  55. package/dist/src/core/indexing/config.js +51 -0
  56. package/dist/src/core/indexing/hnsw.js +568 -0
  57. package/dist/src/core/indexing/index.js +17 -0
  58. package/dist/src/core/indexing/monitor.js +82 -0
  59. package/dist/src/core/indexing/parallel.js +252 -0
  60. package/dist/src/core/lancedb.js +111 -0
  61. package/dist/src/core/lfs.js +27 -0
  62. package/dist/src/core/log.js +62 -0
  63. package/dist/src/core/manifest.js +88 -0
  64. package/dist/src/core/parser/adapter.js +2 -0
  65. package/dist/src/core/parser/c.js +93 -0
  66. package/dist/src/core/parser/chunkRelations.js +178 -0
  67. package/dist/src/core/parser/chunker.js +274 -0
  68. package/dist/src/core/parser/go.js +98 -0
  69. package/dist/src/core/parser/java.js +80 -0
  70. package/dist/src/core/parser/markdown.js +76 -0
  71. package/dist/src/core/parser/python.js +81 -0
  72. package/dist/src/core/parser/rust.js +103 -0
  73. package/dist/src/core/parser/typescript.js +98 -0
  74. package/dist/src/core/parser/utils.js +62 -0
  75. package/dist/src/core/parser/yaml.js +53 -0
  76. package/dist/src/core/parser.js +75 -0
  77. package/dist/src/core/paths.js +10 -0
  78. package/dist/src/core/repoMap.js +164 -0
  79. package/dist/src/core/retrieval/cache.js +31 -0
  80. package/dist/src/core/retrieval/classifier.js +74 -0
  81. package/dist/src/core/retrieval/expander.js +80 -0
  82. package/dist/src/core/retrieval/fuser.js +40 -0
  83. package/dist/src/core/retrieval/index.js +32 -0
  84. package/dist/src/core/retrieval/reranker.js +304 -0
  85. package/dist/src/core/retrieval/types.js +2 -0
  86. package/dist/src/core/retrieval/weights.js +42 -0
  87. package/dist/src/core/search.js +41 -0
  88. package/dist/src/core/sq8.js +65 -0
  89. package/dist/src/core/symbolSearch.js +143 -0
  90. package/dist/src/core/types.js +2 -0
  91. package/dist/src/core/workspace.js +116 -0
  92. package/dist/src/mcp/server.js +794 -0
  93. package/docs/README.md +44 -0
  94. package/docs/cross-encoder.md +157 -0
  95. package/docs/embedding.md +158 -0
  96. package/docs/logo.png +0 -0
  97. package/docs/windows-setup.md +67 -0
  98. package/docs/zh-CN/DESIGN.md +102 -0
  99. package/docs/zh-CN/README.md +46 -0
  100. package/docs/zh-CN/advanced.md +26 -0
  101. package/docs/zh-CN/architecture_explained.md +116 -0
  102. package/docs/zh-CN/cli.md +109 -0
  103. package/docs/zh-CN/dsr.md +91 -0
  104. package/docs/zh-CN/graph_scenarios.md +173 -0
  105. package/docs/zh-CN/hooks.md +14 -0
  106. package/docs/zh-CN/manifests.md +136 -0
  107. package/docs/zh-CN/mcp.md +205 -0
  108. package/docs/zh-CN/quickstart.md +35 -0
  109. package/docs/zh-CN/rules.md +7 -0
  110. package/docs/zh-CN/technical-details.md +454 -0
  111. package/docs/zh-CN/troubleshooting.md +19 -0
  112. package/docs/zh-CN/windows-setup.md +67 -0
  113. package/install.sh +183 -0
  114. package/package.json +97 -0
  115. package/skills/git-ai-mcp/SKILL.md +86 -0
  116. package/skills/git-ai-mcp/references/constraints.md +143 -0
  117. package/skills/git-ai-mcp/references/tools.md +263 -0
  118. package/templates/agents/common/documents/Fix EISDIR error and enable multi-language indexing.md +14 -0
  119. package/templates/agents/common/documents/Fix git-ai index error in CodaGraph directory.md +13 -0
  120. package/templates/agents/common/skills/git-ai-mcp/SKILL.md +86 -0
  121. package/templates/agents/common/skills/git-ai-mcp/references/constraints.md +143 -0
  122. package/templates/agents/common/skills/git-ai-mcp/references/tools.md +263 -0
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.dsrCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ const gitContext_1 = require("../core/dsr/gitContext");
10
+ const generate_1 = require("../core/dsr/generate");
11
+ const indexMaterialize_1 = require("../core/dsr/indexMaterialize");
12
+ const query_1 = require("../core/dsr/query");
13
+ const state_1 = require("../core/dsr/state");
14
+ exports.dsrCommand = new commander_1.Command('dsr')
15
+ .description('Deterministic Semantic Record (per-commit, immutable, Git-addressable)');
16
+ exports.dsrCommand
17
+ .command('context')
18
+ .description('Discover repository root, HEAD, branch, and DSR directory state')
19
+ .option('-p, --path <path>', 'Path inside the repository', '.')
20
+ .option('--json', 'Output machine-readable JSON', false)
21
+ .action(async (options) => {
22
+ const start = path_1.default.resolve(options.path);
23
+ const ctx = await (0, gitContext_1.detectRepoGitContext)(start);
24
+ const state = await (0, state_1.getDsrDirectoryState)(ctx.repo_root);
25
+ const out = {
26
+ commit_hash: ctx.head_commit,
27
+ repo_root: ctx.repo_root,
28
+ branch: ctx.branch,
29
+ detached: ctx.detached,
30
+ dsr_directory_state: state,
31
+ };
32
+ if (options.json) {
33
+ console.log(JSON.stringify(out, null, 2));
34
+ process.exit(0);
35
+ }
36
+ const lines = [];
37
+ lines.push(`repo: ${out.repo_root}`);
38
+ lines.push(`head: ${out.commit_hash}`);
39
+ lines.push(`branch: ${out.detached ? '(detached)' : out.branch}`);
40
+ lines.push(`dsrCacheRoot: ${out.dsr_directory_state.cache_root} (${out.dsr_directory_state.cache_root_exists ? 'exists' : 'missing'})`);
41
+ lines.push(`dsrDir: ${out.dsr_directory_state.dsr_dir} (${out.dsr_directory_state.dsr_dir_exists ? 'exists' : 'missing'})`);
42
+ lines.push(`dsrFiles: ${String(out.dsr_directory_state.dsr_file_count)}`);
43
+ console.log(lines.join('\n'));
44
+ process.exit(0);
45
+ });
46
+ exports.dsrCommand
47
+ .command('generate')
48
+ .description('Generate DSR for exactly one commit')
49
+ .argument('<commit>', 'Commit hash (any rev that resolves to a commit)')
50
+ .option('-p, --path <path>', 'Path inside the repository', '.')
51
+ .option('--json', 'Output machine-readable JSON', false)
52
+ .action(async (commit, options) => {
53
+ const start = path_1.default.resolve(options.path);
54
+ const ctx = await (0, gitContext_1.detectRepoGitContext)(start);
55
+ const res = await (0, generate_1.generateDsrForCommit)(ctx.repo_root, String(commit));
56
+ const out = {
57
+ commit_hash: res.dsr.commit_hash,
58
+ file_path: res.file_path,
59
+ existed: res.existed,
60
+ counts: {
61
+ affected_symbols: res.dsr.affected_symbols.length,
62
+ ast_operations: res.dsr.ast_operations.length,
63
+ },
64
+ semantic_change_type: res.dsr.semantic_change_type,
65
+ risk_level: res.dsr.risk_level,
66
+ };
67
+ if (options.json) {
68
+ console.log(JSON.stringify(out, null, 2));
69
+ process.exit(0);
70
+ }
71
+ const lines = [];
72
+ lines.push(`commit: ${out.commit_hash}`);
73
+ lines.push(`dsr: ${out.file_path}`);
74
+ lines.push(`status: ${out.existed ? 'exists' : 'generated'}`);
75
+ lines.push(`ops: ${String(out.counts.ast_operations)}`);
76
+ lines.push(`affected_symbols: ${String(out.counts.affected_symbols)}`);
77
+ lines.push(`semantic_change_type: ${out.semantic_change_type}`);
78
+ lines.push(`risk_level: ${out.risk_level ?? 'unknown'}`);
79
+ console.log(lines.join('\n'));
80
+ process.exit(0);
81
+ });
82
+ exports.dsrCommand
83
+ .command('rebuild-index')
84
+ .description('Rebuild performance-oriented DSR index from DSR files')
85
+ .option('-p, --path <path>', 'Path inside the repository', '.')
86
+ .option('--json', 'Output machine-readable JSON', false)
87
+ .action(async (options) => {
88
+ const start = path_1.default.resolve(options.path);
89
+ const ctx = await (0, gitContext_1.detectRepoGitContext)(start);
90
+ const res = await (0, indexMaterialize_1.materializeDsrIndex)(ctx.repo_root);
91
+ if (options.json) {
92
+ console.log(JSON.stringify({ repo_root: ctx.repo_root, ...res }, null, 2));
93
+ process.exit(res.enabled ? 0 : 2);
94
+ }
95
+ if (!res.enabled) {
96
+ console.error(res.skippedReason ?? 'rebuild-index skipped');
97
+ process.exit(2);
98
+ }
99
+ const lines = [];
100
+ lines.push(`repo: ${ctx.repo_root}`);
101
+ lines.push(`engine: ${res.engine}`);
102
+ if (res.dbPath)
103
+ lines.push(`db: ${res.dbPath}`);
104
+ if (res.exportPath)
105
+ lines.push(`export: ${res.exportPath}`);
106
+ if (res.counts) {
107
+ lines.push(`commits: ${String(res.counts.commits)}`);
108
+ lines.push(`affected_symbols: ${String(res.counts.affected_symbols)}`);
109
+ lines.push(`ast_operations: ${String(res.counts.ast_operations)}`);
110
+ }
111
+ console.log(lines.join('\n'));
112
+ process.exit(0);
113
+ });
114
+ const queryCommand = new commander_1.Command('query').description('Read-only semantic queries over Git DAG + DSR');
115
+ queryCommand
116
+ .command('symbol-evolution')
117
+ .description('List commits where a symbol changed (requires DSR per traversed commit)')
118
+ .argument('<symbol>', 'Symbol name')
119
+ .option('-p, --path <path>', 'Path inside the repository', '.')
120
+ .option('--all', 'Traverse all refs (default: from HEAD)', false)
121
+ .option('--start <commit>', 'Start commit (default: HEAD)')
122
+ .option('--limit <n>', 'Max commits to traverse', (v) => Number(v), 200)
123
+ .option('--contains', 'Match by substring instead of exact match', false)
124
+ .option('--json', 'Output machine-readable JSON', false)
125
+ .action(async (symbol, options) => {
126
+ const startDir = path_1.default.resolve(options.path);
127
+ const ctx = await (0, gitContext_1.detectRepoGitContext)(startDir);
128
+ const res = await (0, query_1.symbolEvolution)(ctx.repo_root, String(symbol), {
129
+ all: Boolean(options.all),
130
+ start: options.start ? String(options.start) : undefined,
131
+ limit: Number(options.limit),
132
+ contains: Boolean(options.contains),
133
+ });
134
+ if (options.json) {
135
+ console.log(JSON.stringify({ repo_root: ctx.repo_root, symbol, ...res }, null, 2));
136
+ process.exit(res.ok ? 0 : 2);
137
+ }
138
+ if (!res.ok) {
139
+ console.error(`missing DSR for commit: ${res.missing_dsrs?.[0] ?? 'unknown'}`);
140
+ process.exit(2);
141
+ }
142
+ const hits = res.hits ?? [];
143
+ const lines = [];
144
+ lines.push(`repo: ${ctx.repo_root}`);
145
+ lines.push(`symbol: ${symbol}`);
146
+ lines.push(`hits: ${String(hits.length)}`);
147
+ for (const h of hits.slice(0, 50)) {
148
+ const opKinds = Array.from(new Set(h.operations.map((o) => o.op))).sort().join(',');
149
+ lines.push(`${h.commit_hash} ${h.semantic_change_type} ${h.risk_level ?? ''} ops=${String(h.operations.length)} kinds=${opKinds} ${h.summary ?? ''}`.trim());
150
+ }
151
+ if (hits.length > 50)
152
+ lines.push(`... (${hits.length - 50} more)`);
153
+ console.log(lines.join('\n'));
154
+ process.exit(0);
155
+ });
156
+ exports.dsrCommand.addCommand(queryCommand);
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.graphCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ const git_1 = require("../core/git");
10
+ const crypto_1 = require("../core/crypto");
11
+ const astGraphQuery_1 = require("../core/astGraphQuery");
12
+ const paths_1 = require("../core/paths");
13
+ const log_1 = require("../core/log");
14
+ const indexCheck_1 = require("../core/indexCheck");
15
+ exports.graphCommand = new commander_1.Command('graph')
16
+ .description('AST graph search powered by CozoDB')
17
+ .addCommand(new commander_1.Command('query')
18
+ .description('Run a CozoScript query against the AST graph database')
19
+ .argument('<script...>', 'CozoScript query')
20
+ .option('-p, --path <path>', 'Path inside the repository', '.')
21
+ .option('--params <json>', 'JSON params object', '{}')
22
+ .action(async (scriptParts, options) => {
23
+ const log = (0, log_1.createLogger)({ component: 'cli', cmd: 'ai graph query' });
24
+ const startedAt = Date.now();
25
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
26
+ const query = Array.isArray(scriptParts) ? scriptParts.join(' ') : String(scriptParts ?? '');
27
+ const params = JSON.parse(String(options.params ?? '{}'));
28
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, query, params);
29
+ log.info('ast_graph_query', { ok: true, repoRoot, duration_ms: Date.now() - startedAt });
30
+ console.log(JSON.stringify({ repoRoot, result }, null, 2));
31
+ }))
32
+ .addCommand(new commander_1.Command('find')
33
+ .description('Find symbols by name prefix')
34
+ .argument('<prefix>', 'Name prefix (case-insensitive)')
35
+ .option('-p, --path <path>', 'Path inside the repository', '.')
36
+ .option('--lang <lang>', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto')
37
+ .action(async (prefix, options) => {
38
+ const log = (0, log_1.createLogger)({ component: 'cli', cmd: 'ai graph find' });
39
+ const startedAt = Date.now();
40
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
41
+ const status = await (0, indexCheck_1.checkIndex)(repoRoot);
42
+ if (!status.ok) {
43
+ process.stderr.write(JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) + '\n');
44
+ process.exit(2);
45
+ return;
46
+ }
47
+ const langSel = String(options.lang ?? 'auto');
48
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
49
+ const allRows = [];
50
+ for (const lang of langs) {
51
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, (0, astGraphQuery_1.buildFindSymbolsQuery)(lang), { prefix: String(prefix), lang });
52
+ const rows = Array.isArray(result?.rows) ? result.rows : [];
53
+ for (const r of rows)
54
+ allRows.push(r);
55
+ }
56
+ const result = { headers: ['ref_id', 'file', 'lang', 'name', 'kind', 'signature', 'start_line', 'end_line'], rows: allRows };
57
+ log.info('ast_graph_find', { ok: true, repoRoot, prefix: String(prefix), lang: langSel, langs, rows: allRows.length, duration_ms: Date.now() - startedAt });
58
+ console.log(JSON.stringify({ repoRoot, lang: langSel, result }, null, 2));
59
+ }))
60
+ .addCommand(new commander_1.Command('children')
61
+ .description('List direct children in the AST containment graph')
62
+ .argument('<id>', 'Parent id (ref_id or file_id)')
63
+ .option('-p, --path <path>', 'Path inside the repository', '.')
64
+ .option('--as-file', 'Treat <id> as a repository-relative file path and hash it to file_id', false)
65
+ .action(async (id, options) => {
66
+ const log = (0, log_1.createLogger)({ component: 'cli', cmd: 'ai graph children' });
67
+ const startedAt = Date.now();
68
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
69
+ const parentId = options.asFile ? (0, crypto_1.sha256Hex)(`file:${(0, paths_1.toPosixPath)(String(id))}`) : String(id);
70
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, (0, astGraphQuery_1.buildChildrenQuery)(), { parent_id: parentId });
71
+ log.info('ast_graph_children', { ok: true, repoRoot, parent_id: parentId, rows: Array.isArray(result?.rows) ? result.rows.length : 0, duration_ms: Date.now() - startedAt });
72
+ console.log(JSON.stringify({ repoRoot, parent_id: parentId, result }, null, 2));
73
+ }))
74
+ .addCommand(new commander_1.Command('refs')
75
+ .description('Find reference locations by name (calls/new/type)')
76
+ .argument('<name>', 'Symbol name')
77
+ .option('-p, --path <path>', 'Path inside the repository', '.')
78
+ .option('--limit <n>', 'Limit results', '200')
79
+ .option('--lang <lang>', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto')
80
+ .action(async (name, options) => {
81
+ const log = (0, log_1.createLogger)({ component: 'cli', cmd: 'ai graph refs' });
82
+ const startedAt = Date.now();
83
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
84
+ const status = await (0, indexCheck_1.checkIndex)(repoRoot);
85
+ if (!status.ok) {
86
+ process.stderr.write(JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) + '\n');
87
+ process.exit(2);
88
+ return;
89
+ }
90
+ const limit = Number(options.limit ?? 200);
91
+ const langSel = String(options.lang ?? 'auto');
92
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
93
+ const allRows = [];
94
+ for (const lang of langs) {
95
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, (0, astGraphQuery_1.buildFindReferencesQuery)(lang), { name: String(name), lang });
96
+ const rows = Array.isArray(result?.rows) ? result.rows : [];
97
+ for (const r of rows)
98
+ allRows.push(r);
99
+ }
100
+ const rows = allRows.slice(0, limit);
101
+ log.info('ast_graph_refs', { ok: true, repoRoot, name: String(name), lang: langSel, langs, rows: rows.length, duration_ms: Date.now() - startedAt });
102
+ console.log(JSON.stringify({ repoRoot, name: String(name), lang: langSel, result: { headers: ['file', 'line', 'col', 'ref_kind', 'from_id', 'from_kind', 'from_name', 'from_lang'], rows } }, null, 2));
103
+ }))
104
+ .addCommand(new commander_1.Command('callers')
105
+ .description('Find callers by callee name')
106
+ .argument('<name>', 'Callee name')
107
+ .option('-p, --path <path>', 'Path inside the repository', '.')
108
+ .option('--limit <n>', 'Limit results', '200')
109
+ .option('--lang <lang>', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto')
110
+ .action(async (name, options) => {
111
+ const log = (0, log_1.createLogger)({ component: 'cli', cmd: 'ai graph callers' });
112
+ const startedAt = Date.now();
113
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
114
+ const status = await (0, indexCheck_1.checkIndex)(repoRoot);
115
+ if (!status.ok) {
116
+ process.stderr.write(JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) + '\n');
117
+ process.exit(2);
118
+ return;
119
+ }
120
+ const limit = Number(options.limit ?? 200);
121
+ const langSel = String(options.lang ?? 'auto');
122
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
123
+ const allRows = [];
124
+ for (const lang of langs) {
125
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, (0, astGraphQuery_1.buildCallersByNameQuery)(lang), { name: String(name), lang });
126
+ const rows = Array.isArray(result?.rows) ? result.rows : [];
127
+ for (const r of rows)
128
+ allRows.push(r);
129
+ }
130
+ const rows = allRows.slice(0, limit);
131
+ log.info('ast_graph_callers', { ok: true, repoRoot, name: String(name), lang: langSel, langs, rows: rows.length, duration_ms: Date.now() - startedAt });
132
+ console.log(JSON.stringify({ repoRoot, name: String(name), lang: langSel, result: { headers: ['caller_id', 'caller_kind', 'caller_name', 'file', 'line', 'col', 'caller_lang'], rows } }, null, 2));
133
+ }))
134
+ .addCommand(new commander_1.Command('callees')
135
+ .description('Find callees by caller name (resolved by exact callee name match in graph)')
136
+ .argument('<name>', 'Caller name')
137
+ .option('-p, --path <path>', 'Path inside the repository', '.')
138
+ .option('--limit <n>', 'Limit results', '200')
139
+ .option('--lang <lang>', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto')
140
+ .action(async (name, options) => {
141
+ const log = (0, log_1.createLogger)({ component: 'cli', cmd: 'ai graph callees' });
142
+ const startedAt = Date.now();
143
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
144
+ const status = await (0, indexCheck_1.checkIndex)(repoRoot);
145
+ if (!status.ok) {
146
+ process.stderr.write(JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) + '\n');
147
+ process.exit(2);
148
+ return;
149
+ }
150
+ const limit = Number(options.limit ?? 200);
151
+ const langSel = String(options.lang ?? 'auto');
152
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
153
+ const allRows = [];
154
+ for (const lang of langs) {
155
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, (0, astGraphQuery_1.buildCalleesByNameQuery)(lang), { name: String(name), lang });
156
+ const rows = Array.isArray(result?.rows) ? result.rows : [];
157
+ for (const r of rows)
158
+ allRows.push(r);
159
+ }
160
+ const rows = allRows.slice(0, limit);
161
+ log.info('ast_graph_callees', { ok: true, repoRoot, name: String(name), lang: langSel, langs, rows: rows.length, duration_ms: Date.now() - startedAt });
162
+ console.log(JSON.stringify({ repoRoot, name: String(name), lang: langSel, result: { headers: ['caller_id', 'caller_lang', 'callee_id', 'callee_file', 'callee_name', 'callee_kind', 'file', 'line', 'col'], rows } }, null, 2));
163
+ }))
164
+ .addCommand(new commander_1.Command('chain')
165
+ .description('Compute call chain by symbol name (heuristic, name-based)')
166
+ .argument('<name>', 'Start symbol name')
167
+ .option('-p, --path <path>', 'Path inside the repository', '.')
168
+ .option('--direction <direction>', 'Direction: downstream|upstream', 'downstream')
169
+ .option('--depth <n>', 'Max depth', '3')
170
+ .option('--limit <n>', 'Limit results', '500')
171
+ .option('--min-name-len <n>', 'Filter out edges with very short names (default: 1)', '1')
172
+ .option('--lang <lang>', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto')
173
+ .action(async (name, options) => {
174
+ const log = (0, log_1.createLogger)({ component: 'cli', cmd: 'ai graph chain' });
175
+ const startedAt = Date.now();
176
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
177
+ const status = await (0, indexCheck_1.checkIndex)(repoRoot);
178
+ if (!status.ok) {
179
+ process.stderr.write(JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) + '\n');
180
+ process.exit(2);
181
+ return;
182
+ }
183
+ const direction = String(options.direction ?? 'downstream');
184
+ const maxDepth = Number(options.depth ?? 3);
185
+ const limit = Number(options.limit ?? 500);
186
+ const minNameLen = Math.max(1, Number(options.minNameLen ?? 1));
187
+ const langSel = String(options.lang ?? 'auto');
188
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
189
+ const query = direction === 'upstream' ? (0, astGraphQuery_1.buildCallChainUpstreamByNameQuery)() : (0, astGraphQuery_1.buildCallChainDownstreamByNameQuery)();
190
+ const allRows = [];
191
+ for (const lang of langs) {
192
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, query, { name: String(name), max_depth: maxDepth, lang });
193
+ const rawRows = Array.isArray(result?.rows) ? result.rows : [];
194
+ for (const r of rawRows)
195
+ allRows.push(r);
196
+ }
197
+ const filtered = minNameLen > 1
198
+ ? allRows.filter((r) => String(r?.[3] ?? '').length >= minNameLen && String(r?.[4] ?? '').length >= minNameLen)
199
+ : allRows;
200
+ const rows = filtered.slice(0, limit);
201
+ log.info('ast_graph_chain', { ok: true, repoRoot, name: String(name), lang: langSel, langs, direction, max_depth: maxDepth, rows: rows.length, min_name_len: minNameLen, duration_ms: Date.now() - startedAt });
202
+ console.log(JSON.stringify({ repoRoot, name: String(name), lang: langSel, direction, max_depth: maxDepth, min_name_len: minNameLen, result: { headers: ['caller_id', 'callee_id', 'depth', 'caller_name', 'callee_name', 'lang'], rows } }, null, 2));
203
+ }));
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.hooksCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
10
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
+ const git_1 = require("../core/git");
12
+ function runGit(cwd, args) {
13
+ const res = (0, child_process_1.spawnSync)('git', args, { cwd, stdio: 'inherit' });
14
+ if (res.status !== 0)
15
+ throw new Error(`git ${args.join(' ')} failed`);
16
+ }
17
+ function getGitConfig(cwd, key) {
18
+ const res = (0, child_process_1.spawnSync)('git', ['config', '--get', key], { cwd, stdio: 'pipe', encoding: 'utf-8' });
19
+ if (res.status !== 0)
20
+ return null;
21
+ const out = String(res.stdout ?? '').trim();
22
+ return out.length > 0 ? out : null;
23
+ }
24
+ function isGitRepo(cwd) {
25
+ const res = (0, child_process_1.spawnSync)('git', ['rev-parse', '--show-toplevel'], { cwd, stdio: 'ignore' });
26
+ return res.status === 0;
27
+ }
28
+ async function ensureHooksExecutable(repoRoot) {
29
+ if (process.platform === 'win32')
30
+ return { changed: 0, skipped: true };
31
+ const hooksDir = path_1.default.join(repoRoot, '.githooks');
32
+ if (!await fs_extra_1.default.pathExists(hooksDir))
33
+ return { changed: 0, skipped: false };
34
+ const names = new Set(await fs_extra_1.default.readdir(hooksDir));
35
+ const targets = ['pre-commit', 'pre-push', 'post-checkout', 'post-merge'].filter(n => names.has(n));
36
+ let changed = 0;
37
+ for (const n of targets) {
38
+ const p = path_1.default.join(hooksDir, n);
39
+ const stat = await fs_extra_1.default.stat(p);
40
+ if (!stat.isFile())
41
+ continue;
42
+ await fs_extra_1.default.chmod(p, 0o755);
43
+ changed++;
44
+ }
45
+ return { changed, skipped: false };
46
+ }
47
+ async function findPackageRoot(startDir) {
48
+ let cur = path_1.default.resolve(startDir);
49
+ for (let i = 0; i < 8; i++) {
50
+ const pj = path_1.default.join(cur, 'package.json');
51
+ if (await fs_extra_1.default.pathExists(pj))
52
+ return cur;
53
+ const parent = path_1.default.dirname(cur);
54
+ if (parent === cur)
55
+ break;
56
+ cur = parent;
57
+ }
58
+ return path_1.default.resolve(startDir);
59
+ }
60
+ async function installHookTemplates(repoRoot) {
61
+ const hooksDir = path_1.default.join(repoRoot, '.githooks');
62
+ await fs_extra_1.default.ensureDir(hooksDir);
63
+ const packageRoot = await findPackageRoot(__dirname);
64
+ const templateDir = path_1.default.join(packageRoot, 'assets', 'hooks');
65
+ const names = await fs_extra_1.default.readdir(templateDir);
66
+ const installed = [];
67
+ for (const n of names) {
68
+ const src = path_1.default.join(templateDir, n);
69
+ const stat = await fs_extra_1.default.stat(src);
70
+ if (!stat.isFile())
71
+ continue;
72
+ const dst = path_1.default.join(hooksDir, n);
73
+ await fs_extra_1.default.copyFile(src, dst);
74
+ installed.push(n);
75
+ }
76
+ return { installed };
77
+ }
78
+ const install = new commander_1.Command('install')
79
+ .description('Install git hooks (write .githooks/* and set core.hooksPath=.githooks)')
80
+ .option('-p, --path <path>', 'Path inside the repository', '.')
81
+ .action(async (options) => {
82
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
83
+ if (!isGitRepo(repoRoot)) {
84
+ console.log(JSON.stringify({ ok: false, error: 'not_a_git_repo', repoRoot }, null, 2));
85
+ process.exitCode = 1;
86
+ return;
87
+ }
88
+ const templates = await installHookTemplates(repoRoot);
89
+ const exec = await ensureHooksExecutable(repoRoot);
90
+ runGit(repoRoot, ['config', 'core.hooksPath', '.githooks']);
91
+ const hooksPath = getGitConfig(repoRoot, 'core.hooksPath');
92
+ console.log(JSON.stringify({ ok: true, repoRoot, hooksPath, templates, executable: exec }, null, 2));
93
+ });
94
+ const uninstall = new commander_1.Command('uninstall')
95
+ .description('Uninstall git hooks (unset core.hooksPath)')
96
+ .option('-p, --path <path>', 'Path inside the repository', '.')
97
+ .action(async (options) => {
98
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
99
+ if (!isGitRepo(repoRoot)) {
100
+ console.log(JSON.stringify({ ok: false, error: 'not_a_git_repo', repoRoot }, null, 2));
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ (0, child_process_1.spawnSync)('git', ['config', '--unset', 'core.hooksPath'], { cwd: repoRoot, stdio: 'ignore' });
105
+ const hooksPath = getGitConfig(repoRoot, 'core.hooksPath');
106
+ console.log(JSON.stringify({ ok: true, repoRoot, hooksPath }, null, 2));
107
+ });
108
+ const status = new commander_1.Command('status')
109
+ .description('Show current hooks configuration')
110
+ .option('-p, --path <path>', 'Path inside the repository', '.')
111
+ .action(async (options) => {
112
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
113
+ if (!isGitRepo(repoRoot)) {
114
+ console.log(JSON.stringify({ ok: false, error: 'not_a_git_repo', repoRoot }, null, 2));
115
+ process.exitCode = 1;
116
+ return;
117
+ }
118
+ const hooksPath = getGitConfig(repoRoot, 'core.hooksPath');
119
+ console.log(JSON.stringify({ ok: true, repoRoot, hooksPath, expected: '.githooks', installed: hooksPath === '.githooks' }, null, 2));
120
+ });
121
+ exports.hooksCommand = new commander_1.Command('hooks')
122
+ .description('Manage git hooks integration')
123
+ .addCommand(install)
124
+ .addCommand(uninstall)
125
+ .addCommand(status);
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.indexCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const git_1 = require("../core/git");
11
+ const indexer_1 = require("../core/indexer");
12
+ const log_1 = require("../core/log");
13
+ const gitDiff_1 = require("../core/gitDiff");
14
+ const indexerIncremental_1 = require("../core/indexerIncremental");
15
+ exports.indexCommand = new commander_1.Command('index')
16
+ .description('Build LanceDB+SQ8 index for the current repository (HEAD working tree)')
17
+ .option('-p, --path <path>', 'Path inside the repository', '.')
18
+ .option('-d, --dim <dim>', 'Embedding dimension', '256')
19
+ .option('--overwrite', 'Overwrite existing tables', false)
20
+ .option('--incremental', 'Incremental indexing (only changed files)', false)
21
+ .option('--staged', 'Read changed file contents from Git index (staged)', false)
22
+ .action(async (options) => {
23
+ const log = (0, log_1.createLogger)({ component: 'cli', cmd: 'ai index' });
24
+ const startedAt = Date.now();
25
+ try {
26
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
27
+ const scanRoot = (0, git_1.inferScanRoot)(repoRoot);
28
+ const requestedDim = Number(options.dim);
29
+ const overwrite = Boolean(options.overwrite);
30
+ const incremental = Boolean(options.incremental ?? false);
31
+ const staged = Boolean(options.staged ?? false);
32
+ const metaPath = path_1.default.join(repoRoot, '.git-ai', 'meta.json');
33
+ const meta = await fs_extra_1.default.readJSON(metaPath).catch(() => null);
34
+ const dim = typeof meta?.dim === 'number' ? meta.dim : requestedDim;
35
+ const isTTY = Boolean(process.stderr.isTTY) && !process.env.CI;
36
+ let renderedInTTY = false;
37
+ let finishedInTTY = false;
38
+ const renderProgress = (p) => {
39
+ const total = Math.max(0, p.totalFiles);
40
+ const done = Math.max(0, Math.min(total, p.processedFiles));
41
+ if (!isTTY) {
42
+ if (done === 0) {
43
+ process.stderr.write(`Indexing ${total} files...\n`);
44
+ }
45
+ else if (done === total || done % 200 === 0) {
46
+ process.stderr.write((`[${done}/${total}] ${p.currentFile ?? ''}`.trim()) + '\n');
47
+ }
48
+ return;
49
+ }
50
+ const columns = Math.max(40, Number(process.stderr.columns ?? 100));
51
+ const prefix = `${done}/${total}`;
52
+ const percent = total > 0 ? Math.floor((done / total) * 100) : 100;
53
+ const reserved = prefix.length + 1 + 6 + 1 + 1;
54
+ const barWidth = Math.max(10, Math.min(30, columns - reserved - 20));
55
+ const filled = total > 0 ? Math.round((done / total) * barWidth) : barWidth;
56
+ const bar = `${'='.repeat(filled)}${'-'.repeat(Math.max(0, barWidth - filled))}`;
57
+ const fileSuffix = p.currentFile ? ` ${p.currentFile}` : '';
58
+ const line = `[${bar}] ${String(percent).padStart(3)}% ${prefix}${fileSuffix}`;
59
+ const clipped = line.length >= columns ? line.slice(0, columns - 1) : line;
60
+ process.stderr.write(`\r${clipped.padEnd(columns - 1, ' ')}`);
61
+ renderedInTTY = true;
62
+ if (done === total && !finishedInTTY) {
63
+ process.stderr.write('\n');
64
+ finishedInTTY = true;
65
+ }
66
+ };
67
+ if (incremental) {
68
+ const changes = staged ? await (0, gitDiff_1.getStagedNameStatus)(repoRoot) : await (0, gitDiff_1.getWorktreeNameStatus)(repoRoot);
69
+ const indexer = new indexerIncremental_1.IncrementalIndexerV2({
70
+ repoRoot,
71
+ scanRoot,
72
+ dim,
73
+ source: staged ? 'staged' : 'worktree',
74
+ changes,
75
+ onProgress: renderProgress,
76
+ });
77
+ await indexer.run();
78
+ }
79
+ else {
80
+ const indexer = new indexer_1.IndexerV2({ repoRoot, scanRoot, dim, overwrite, onProgress: renderProgress });
81
+ await indexer.run();
82
+ }
83
+ if (renderedInTTY && !finishedInTTY)
84
+ process.stderr.write('\n');
85
+ log.info('index_repo', { ok: true, repoRoot, scanRoot, dim, overwrite, duration_ms: Date.now() - startedAt });
86
+ console.log(JSON.stringify({ ok: true, repoRoot, scanRoot, dim, overwrite, incremental, staged }, null, 2));
87
+ }
88
+ catch (e) {
89
+ log.error('index_repo', { ok: false, duration_ms: Date.now() - startedAt, err: e instanceof Error ? { name: e.name, message: e.message, stack: e.stack } : { message: String(e) } });
90
+ process.exit(1);
91
+ }
92
+ });
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.packCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ const git_1 = require("../core/git");
10
+ const archive_1 = require("../core/archive");
11
+ const lfs_1 = require("../core/lfs");
12
+ const log_1 = require("../core/log");
13
+ exports.packCommand = new commander_1.Command('pack')
14
+ .description('Pack .git-ai/lancedb into .git-ai/lancedb.tar.gz')
15
+ .option('-p, --path <path>', 'Path inside the repository', '.')
16
+ .option('--lfs', 'Run git lfs track for .git-ai/lancedb.tar.gz', false)
17
+ .action(async (options) => {
18
+ const log = (0, log_1.createLogger)({ component: 'cli', cmd: 'ai pack' });
19
+ const startedAt = Date.now();
20
+ try {
21
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(options.path));
22
+ const packed = await (0, archive_1.packLanceDb)(repoRoot);
23
+ const lfs = options.lfs ? (0, lfs_1.ensureLfsTracking)(repoRoot, '.git-ai/lancedb.tar.gz') : { tracked: false };
24
+ log.info('pack_index', { ok: true, repoRoot, packed: packed.packed, lfs: Boolean(lfs.tracked), duration_ms: Date.now() - startedAt });
25
+ console.log(JSON.stringify({ repoRoot, ...packed, lfs }, null, 2));
26
+ }
27
+ catch (e) {
28
+ log.error('pack_index', { ok: false, duration_ms: Date.now() - startedAt, err: e instanceof Error ? { name: e.name, message: e.message, stack: e.stack } : { message: String(e) } });
29
+ process.exit(1);
30
+ }
31
+ });