clast-ai 0.1.2 → 0.2.1

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 (294) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +11 -9
  3. package/README.md +468 -398
  4. package/bench/ab/onair-concept.json +22 -0
  5. package/bench/ab/onair-deadcode.json +7 -0
  6. package/bench/ab/onair-hard.json +32 -0
  7. package/bench/ab/onair-impact.json +17 -0
  8. package/bench/ab/onair-refactors.json +18 -0
  9. package/bench/ab/onair-survey.json +17 -0
  10. package/bench/ab/onair-tasks.json +20 -0
  11. package/bench/ab/onair.json +52 -0
  12. package/bench/ab/vscode-concept.json +22 -0
  13. package/bench/ab/vscode-hard.json +32 -0
  14. package/bench/ab/vscode-impact.json +17 -0
  15. package/bench/ab/vscode-refactors.json +18 -0
  16. package/bench/ab/vscode-tasks.json +20 -0
  17. package/bench/ab/vscode.json +52 -0
  18. package/bench/ab.mjs +213 -0
  19. package/bench/bench.mjs +98 -0
  20. package/bench/resolution.mjs +75 -0
  21. package/dist/cli.d.ts +2 -0
  22. package/dist/cli.js +266 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/config/defaults.d.ts +12 -2
  25. package/dist/config/defaults.js +38 -31
  26. package/dist/config/defaults.js.map +1 -1
  27. package/dist/config/index.d.ts +1 -1
  28. package/dist/config/index.js +26 -14
  29. package/dist/config/index.js.map +1 -1
  30. package/dist/db/index.d.ts +73 -13
  31. package/dist/db/index.js +481 -39
  32. package/dist/db/index.js.map +1 -1
  33. package/dist/engine.d.ts +15 -0
  34. package/dist/engine.js +28 -0
  35. package/dist/engine.js.map +1 -0
  36. package/dist/indexer/imports.d.ts +6 -0
  37. package/dist/indexer/imports.js +254 -0
  38. package/dist/indexer/imports.js.map +1 -0
  39. package/dist/indexer/index.d.ts +63 -0
  40. package/dist/indexer/index.js +360 -0
  41. package/dist/indexer/index.js.map +1 -0
  42. package/dist/indexer/process.d.ts +22 -0
  43. package/dist/indexer/process.js +42 -0
  44. package/dist/indexer/process.js.map +1 -0
  45. package/dist/indexer/scan.d.ts +43 -0
  46. package/dist/indexer/scan.js +223 -0
  47. package/dist/indexer/scan.js.map +1 -0
  48. package/dist/indexer/worker.d.ts +1 -0
  49. package/dist/indexer/worker.js +23 -0
  50. package/dist/indexer/worker.js.map +1 -0
  51. package/dist/llm/comment-generator.d.ts +17 -19
  52. package/dist/llm/comment-generator.js +46 -80
  53. package/dist/llm/comment-generator.js.map +1 -1
  54. package/dist/llm/index.d.ts +0 -1
  55. package/dist/llm/index.js.map +1 -1
  56. package/dist/llm/prompts.d.ts +0 -1
  57. package/dist/llm/prompts.js +16 -16
  58. package/dist/migrate.d.ts +35 -0
  59. package/dist/migrate.js +194 -0
  60. package/dist/migrate.js.map +1 -0
  61. package/dist/parser/extractor.d.ts +21 -16
  62. package/dist/parser/extractor.js +700 -228
  63. package/dist/parser/extractor.js.map +1 -1
  64. package/dist/parser/index.d.ts +17 -21
  65. package/dist/parser/index.js +115 -171
  66. package/dist/parser/index.js.map +1 -1
  67. package/dist/parser/langs/c.d.ts +3 -0
  68. package/dist/parser/langs/c.js +879 -0
  69. package/dist/parser/langs/c.js.map +1 -0
  70. package/dist/parser/langs/csharp.d.ts +2 -0
  71. package/dist/parser/langs/csharp.js +548 -0
  72. package/dist/parser/langs/csharp.js.map +1 -0
  73. package/dist/parser/langs/dart.d.ts +2 -0
  74. package/dist/parser/langs/dart.js +893 -0
  75. package/dist/parser/langs/dart.js.map +1 -0
  76. package/dist/parser/langs/go.d.ts +2 -0
  77. package/dist/parser/langs/go.js +455 -0
  78. package/dist/parser/langs/go.js.map +1 -0
  79. package/dist/parser/langs/java.d.ts +2 -0
  80. package/dist/parser/langs/java.js +390 -0
  81. package/dist/parser/langs/java.js.map +1 -0
  82. package/dist/parser/langs/php.d.ts +2 -0
  83. package/dist/parser/langs/php.js +486 -0
  84. package/dist/parser/langs/php.js.map +1 -0
  85. package/dist/parser/langs/python.d.ts +2 -0
  86. package/dist/parser/langs/python.js +216 -0
  87. package/dist/parser/langs/python.js.map +1 -0
  88. package/dist/parser/langs/ruby.d.ts +2 -0
  89. package/dist/parser/langs/ruby.js +602 -0
  90. package/dist/parser/langs/ruby.js.map +1 -0
  91. package/dist/parser/langs/rust.d.ts +2 -0
  92. package/dist/parser/langs/rust.js +931 -0
  93. package/dist/parser/langs/rust.js.map +1 -0
  94. package/dist/parser/langs/swift.d.ts +2 -0
  95. package/dist/parser/langs/swift.js +616 -0
  96. package/dist/parser/langs/swift.js.map +1 -0
  97. package/dist/parser/langs/typescript.d.ts +4 -0
  98. package/dist/parser/langs/typescript.js +535 -0
  99. package/dist/parser/langs/typescript.js.map +1 -0
  100. package/dist/parser/registry.d.ts +16 -0
  101. package/dist/parser/registry.js +87 -0
  102. package/dist/parser/registry.js.map +1 -0
  103. package/dist/parser/returns.d.ts +9 -0
  104. package/dist/parser/returns.js +152 -0
  105. package/dist/parser/returns.js.map +1 -0
  106. package/dist/parser/spec.d.ts +165 -0
  107. package/dist/parser/spec.js +188 -0
  108. package/dist/parser/spec.js.map +1 -0
  109. package/dist/query/arity.d.ts +27 -0
  110. package/dist/query/arity.js +187 -0
  111. package/dist/query/arity.js.map +1 -0
  112. package/dist/query/callgraph.d.ts +113 -0
  113. package/dist/query/callgraph.js +332 -0
  114. package/dist/query/callgraph.js.map +1 -0
  115. package/dist/query/format.d.ts +36 -0
  116. package/dist/query/format.js +136 -0
  117. package/dist/query/format.js.map +1 -0
  118. package/dist/query/hierarchy.d.ts +44 -0
  119. package/dist/query/hierarchy.js +441 -0
  120. package/dist/query/hierarchy.js.map +1 -0
  121. package/dist/query/resolve.d.ts +265 -0
  122. package/dist/query/resolve.js +1957 -0
  123. package/dist/query/resolve.js.map +1 -0
  124. package/dist/query/search.d.ts +35 -0
  125. package/dist/query/search.js +379 -0
  126. package/dist/query/search.js.map +1 -0
  127. package/dist/query/words.d.ts +30 -0
  128. package/dist/query/words.js +201 -0
  129. package/dist/query/words.js.map +1 -0
  130. package/dist/server/index.d.ts +1 -3
  131. package/dist/server/index.js +313 -73
  132. package/dist/server/index.js.map +1 -1
  133. package/dist/tools/context.d.ts +35 -0
  134. package/dist/tools/context.js +79 -0
  135. package/dist/tools/context.js.map +1 -0
  136. package/dist/tools/explore.d.ts +51 -0
  137. package/dist/tools/explore.js +779 -0
  138. package/dist/tools/explore.js.map +1 -0
  139. package/dist/tools/impact.d.ts +21 -0
  140. package/dist/tools/impact.js +743 -0
  141. package/dist/tools/impact.js.map +1 -0
  142. package/dist/tools/search.d.ts +59 -0
  143. package/dist/tools/search.js +843 -0
  144. package/dist/tools/search.js.map +1 -0
  145. package/dist/tools/stats.d.ts +26 -0
  146. package/dist/tools/stats.js +604 -0
  147. package/dist/tools/stats.js.map +1 -0
  148. package/dist/types.d.ts +95 -40
  149. package/dist/types.js +9 -1
  150. package/dist/types.js.map +1 -1
  151. package/dist/version.d.ts +2 -0
  152. package/dist/version.js +3 -0
  153. package/dist/version.js.map +1 -0
  154. package/dist/watcher/index.d.ts +18 -16
  155. package/dist/watcher/index.js +94 -77
  156. package/dist/watcher/index.js.map +1 -1
  157. package/package.json +21 -24
  158. package/plugin/server.mjs +72 -0
  159. package/skills/audit/SKILL.md +37 -0
  160. package/skills/deadcode/SKILL.md +33 -0
  161. package/skills/explore/SKILL.md +54 -0
  162. package/skills/impact/SKILL.md +24 -0
  163. package/skills/status/SKILL.md +11 -0
  164. package/src/cli.ts +265 -0
  165. package/src/config/defaults.ts +50 -32
  166. package/src/config/index.ts +23 -14
  167. package/src/db/index.ts +540 -43
  168. package/src/engine.ts +30 -0
  169. package/src/indexer/imports.ts +241 -0
  170. package/src/indexer/index.ts +369 -0
  171. package/src/indexer/process.ts +63 -0
  172. package/src/indexer/scan.ts +226 -0
  173. package/src/indexer/worker.ts +24 -0
  174. package/src/llm/comment-generator.ts +52 -103
  175. package/src/llm/index.ts +80 -80
  176. package/src/llm/prompts.ts +95 -95
  177. package/src/migrate.ts +213 -0
  178. package/src/parser/extractor.ts +643 -256
  179. package/src/parser/index.ts +105 -205
  180. package/src/parser/langs/c.ts +860 -0
  181. package/src/parser/langs/csharp.ts +519 -0
  182. package/src/parser/langs/dart.ts +820 -0
  183. package/src/parser/langs/go.ts +437 -0
  184. package/src/parser/langs/java.ts +361 -0
  185. package/src/parser/langs/php.ts +450 -0
  186. package/src/parser/langs/python.ts +206 -0
  187. package/src/parser/langs/ruby.ts +554 -0
  188. package/src/parser/langs/rust.ts +904 -0
  189. package/src/parser/langs/swift.ts +590 -0
  190. package/src/parser/langs/typescript.ts +499 -0
  191. package/src/parser/registry.ts +88 -0
  192. package/src/parser/returns.ts +137 -0
  193. package/src/parser/spec.ts +309 -0
  194. package/src/query/arity.ts +173 -0
  195. package/src/query/callgraph.ts +473 -0
  196. package/src/query/format.ts +136 -0
  197. package/src/query/hierarchy.ts +447 -0
  198. package/src/query/resolve.ts +1972 -0
  199. package/src/query/search.ts +430 -0
  200. package/src/query/words.ts +183 -0
  201. package/src/server/index.ts +390 -79
  202. package/src/tools/context.ts +82 -0
  203. package/src/tools/explore.ts +860 -0
  204. package/src/tools/impact.ts +748 -0
  205. package/src/tools/search.ts +886 -0
  206. package/src/tools/stats.ts +633 -0
  207. package/src/types.ts +113 -45
  208. package/src/version.ts +2 -0
  209. package/src/watcher/index.ts +82 -88
  210. package/tsconfig.json +6 -3
  211. package/.mcp.json +0 -8
  212. package/commands/clast-status.md +0 -9
  213. package/dist/config/defaults.d.ts.map +0 -1
  214. package/dist/config/index.d.ts.map +0 -1
  215. package/dist/db/index.d.ts.map +0 -1
  216. package/dist/db/queries.d.ts +0 -44
  217. package/dist/db/queries.d.ts.map +0 -1
  218. package/dist/db/queries.js +0 -251
  219. package/dist/db/queries.js.map +0 -1
  220. package/dist/db/schema.d.ts +0 -3
  221. package/dist/db/schema.d.ts.map +0 -1
  222. package/dist/db/schema.js +0 -94
  223. package/dist/db/schema.js.map +0 -1
  224. package/dist/db/types.d.ts +0 -44
  225. package/dist/db/types.d.ts.map +0 -1
  226. package/dist/db/types.js +0 -2
  227. package/dist/db/types.js.map +0 -1
  228. package/dist/llm/comment-generator.d.ts.map +0 -1
  229. package/dist/llm/index.d.ts.map +0 -1
  230. package/dist/llm/prompts.d.ts.map +0 -1
  231. package/dist/parser/call-resolver.d.ts +0 -15
  232. package/dist/parser/call-resolver.d.ts.map +0 -1
  233. package/dist/parser/call-resolver.js +0 -16
  234. package/dist/parser/call-resolver.js.map +0 -1
  235. package/dist/parser/extractor.d.ts.map +0 -1
  236. package/dist/parser/index.d.ts.map +0 -1
  237. package/dist/parser/languages.d.ts +0 -11
  238. package/dist/parser/languages.d.ts.map +0 -1
  239. package/dist/parser/languages.js +0 -74
  240. package/dist/parser/languages.js.map +0 -1
  241. package/dist/server/index.d.ts.map +0 -1
  242. package/dist/server/register-tools.d.ts +0 -7
  243. package/dist/server/register-tools.d.ts.map +0 -1
  244. package/dist/server/register-tools.js +0 -17
  245. package/dist/server/register-tools.js.map +0 -1
  246. package/dist/server/tools/clast-call-graph.d.ts +0 -4
  247. package/dist/server/tools/clast-call-graph.d.ts.map +0 -1
  248. package/dist/server/tools/clast-call-graph.js +0 -70
  249. package/dist/server/tools/clast-call-graph.js.map +0 -1
  250. package/dist/server/tools/clast-comment.d.ts +0 -5
  251. package/dist/server/tools/clast-comment.d.ts.map +0 -1
  252. package/dist/server/tools/clast-comment.js +0 -64
  253. package/dist/server/tools/clast-comment.js.map +0 -1
  254. package/dist/server/tools/clast-file-summary.d.ts +0 -4
  255. package/dist/server/tools/clast-file-summary.d.ts.map +0 -1
  256. package/dist/server/tools/clast-file-summary.js +0 -42
  257. package/dist/server/tools/clast-file-summary.js.map +0 -1
  258. package/dist/server/tools/clast-get-context.d.ts +0 -4
  259. package/dist/server/tools/clast-get-context.d.ts.map +0 -1
  260. package/dist/server/tools/clast-get-context.js +0 -80
  261. package/dist/server/tools/clast-get-context.js.map +0 -1
  262. package/dist/server/tools/clast-reindex.d.ts +0 -5
  263. package/dist/server/tools/clast-reindex.d.ts.map +0 -1
  264. package/dist/server/tools/clast-reindex.js +0 -36
  265. package/dist/server/tools/clast-reindex.js.map +0 -1
  266. package/dist/server/tools/clast-search.d.ts +0 -4
  267. package/dist/server/tools/clast-search.d.ts.map +0 -1
  268. package/dist/server/tools/clast-search.js +0 -29
  269. package/dist/server/tools/clast-search.js.map +0 -1
  270. package/dist/server/tools/clast-status.d.ts +0 -5
  271. package/dist/server/tools/clast-status.d.ts.map +0 -1
  272. package/dist/server/tools/clast-status.js +0 -23
  273. package/dist/server/tools/clast-status.js.map +0 -1
  274. package/dist/types.d.ts.map +0 -1
  275. package/dist/watcher/gitignore.d.ts +0 -9
  276. package/dist/watcher/gitignore.d.ts.map +0 -1
  277. package/dist/watcher/gitignore.js +0 -38
  278. package/dist/watcher/gitignore.js.map +0 -1
  279. package/dist/watcher/index.d.ts.map +0 -1
  280. package/skills/clast-explore/SKILL.md +0 -41
  281. package/src/db/queries.ts +0 -332
  282. package/src/db/schema.ts +0 -106
  283. package/src/db/types.ts +0 -47
  284. package/src/parser/call-resolver.ts +0 -17
  285. package/src/parser/languages.ts +0 -86
  286. package/src/server/register-tools.ts +0 -30
  287. package/src/server/tools/clast-call-graph.ts +0 -82
  288. package/src/server/tools/clast-comment.ts +0 -76
  289. package/src/server/tools/clast-file-summary.ts +0 -59
  290. package/src/server/tools/clast-get-context.ts +0 -97
  291. package/src/server/tools/clast-reindex.ts +0 -52
  292. package/src/server/tools/clast-search.ts +0 -38
  293. package/src/server/tools/clast-status.ts +0 -37
  294. package/src/watcher/gitignore.ts +0 -42
@@ -0,0 +1,226 @@
1
+ import ignoreFactory, { type Ignore } from 'ignore';
2
+ import { readdirSync, readFileSync, existsSync } from 'node:fs';
3
+ import { stat } from 'node:fs/promises';
4
+ import { extname, join } from 'node:path';
5
+ import type { ClastConfig } from '../config/index.js';
6
+ import { supportedExtensions } from '../parser/registry.js';
7
+
8
+ // `ignore` is CJS; under NodeNext the default import may be the module object.
9
+ const createIgnore: () => Ignore =
10
+ (ignoreFactory as unknown as { default?: () => Ignore }).default ?? (ignoreFactory as unknown as () => Ignore);
11
+
12
+ const BINARY_EXTENSIONS = new Set([
13
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.tiff', '.psd', '.svgz', '.pdf', '.zip', '.gz',
14
+ '.tgz', '.bz2', '.xz', '.7z', '.rar', '.tar', '.jar', '.war', '.class', '.exe', '.dll', '.so', '.dylib', '.a',
15
+ '.o', '.obj', '.lib', '.pdb', '.wasm', '.woff', '.woff2', '.ttf', '.otf', '.eot', '.mp3', '.mp4', '.mov',
16
+ '.avi', '.wav', '.ogg', '.flac', '.webm', '.bin', '.dat', '.db', '.sqlite', '.pyc', '.pyo', '.node', '.snap',
17
+ '.map', '.min.js', '.icns', '.cur', '.keystore', '.p12', '.pfx', '.der', '.nupkg', '.dmg', '.iso', '.blend',
18
+ ]);
19
+ const LOCK_FILES = new Set([
20
+ 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'Cargo.lock', 'composer.lock', 'Gemfile.lock', 'poetry.lock',
21
+ 'go.sum', 'bun.lockb', 'npm-shrinkwrap.json',
22
+ ]);
23
+
24
+ export interface ScannedFile {
25
+ abs: string;
26
+ /** Project-relative path with forward slashes. */
27
+ rel: string;
28
+ mtime: number;
29
+ size: number;
30
+ }
31
+
32
+ interface Rule {
33
+ /** Directory prefix this rule set is relative to ('' or 'a/b/'). */
34
+ base: string;
35
+ ig: Ignore;
36
+ }
37
+
38
+ /**
39
+ * Ignore decisions shared by the scanner and the watcher: config patterns plus
40
+ * (optionally) every .gitignore from the project root down to the file.
41
+ */
42
+ export class IgnoreRules {
43
+ private readonly root: string;
44
+ private readonly useGitignore: boolean;
45
+ private readonly rootRule: Rule;
46
+ private readonly dirRules = new Map<string, Rule | null>();
47
+ readonly extensions: Set<string>;
48
+ readonly maxFileSize: number;
49
+
50
+ constructor(root: string, config: ClastConfig) {
51
+ this.root = root;
52
+ this.useGitignore = config.respectGitignore;
53
+ this.rootRule = { base: '', ig: createIgnore().add(config.ignoredPaths) };
54
+ this.extensions = supportedExtensions(config.languages);
55
+ this.maxFileSize = config.maxFileSize;
56
+ this.maxFiles = config.maxFiles;
57
+ }
58
+
59
+ /** Forget cached .gitignore contents (call when a .gitignore changes). */
60
+ invalidate(): void {
61
+ this.dirRules.clear();
62
+ }
63
+
64
+ /** Rule for the .gitignore inside `dirRel` ('' or 'a/b/'), if any. */
65
+ private ruleFor(dirRel: string): Rule | null {
66
+ if (!this.useGitignore) return null;
67
+ let rule = this.dirRules.get(dirRel);
68
+ if (rule !== undefined) return rule;
69
+ rule = null;
70
+ const file = join(this.root, dirRel, '.gitignore');
71
+ try {
72
+ if (existsSync(file)) {
73
+ rule = { base: dirRel, ig: createIgnore().add(readFileSync(file, 'utf-8')) };
74
+ }
75
+ } catch {
76
+ rule = null;
77
+ }
78
+ this.dirRules.set(dirRel, rule);
79
+ return rule;
80
+ }
81
+
82
+ private static test(rules: Rule[], rel: string, isDir: boolean): boolean {
83
+ let ignored = false;
84
+ for (const r of rules) {
85
+ const sub = rel.slice(r.base.length) + (isDir ? '/' : '');
86
+ if (!sub) continue;
87
+ const t = r.ig.test(sub);
88
+ if (t.ignored) ignored = true;
89
+ else if (t.unignored) ignored = false;
90
+ }
91
+ return ignored;
92
+ }
93
+
94
+ /** Full check for an arbitrary project-relative path (used by the watcher). */
95
+ isIgnored(rel: string): boolean {
96
+ const parts = rel.split('/');
97
+ const rules: Rule[] = [this.rootRule];
98
+ const rootGit = this.ruleFor('');
99
+ if (rootGit) rules.push(rootGit);
100
+ let prefix = '';
101
+ for (let i = 0; i < parts.length; i++) {
102
+ const name = parts[i]!;
103
+ const isDir = i < parts.length - 1;
104
+ if (isDir && name.startsWith('.')) return true;
105
+ const path = prefix + name;
106
+ if (IgnoreRules.test(rules, path, isDir)) return true;
107
+ if (isDir) {
108
+ prefix = path + '/';
109
+ const r = this.ruleFor(prefix);
110
+ if (r) rules.push(r);
111
+ }
112
+ }
113
+ return false;
114
+ }
115
+
116
+ isSupported(rel: string): boolean {
117
+ return this.extensions.has(extname(rel).toLowerCase());
118
+ }
119
+
120
+ /** Files skipped by the last scan because they exceed maxFileSize. */
121
+ skippedLarge = 0;
122
+ /** True when the last scan stopped at the maxFiles cap. */
123
+ truncated = false;
124
+ private readonly maxFiles: number;
125
+
126
+ /**
127
+ * Project-relative paths of text-like files that are NOT indexed (config,
128
+ * docs, data, unsupported languages) but pass the ignore rules. Used by
129
+ * text search so it can replace a plain grep.
130
+ */
131
+ otherTextFiles(limit = 60000): string[] {
132
+ const out: string[] = [];
133
+ const visit = (dirRel: string, parentRules: Rule[], depth: number): void => {
134
+ if (depth > 40 || out.length >= limit) return;
135
+ const rule = this.ruleFor(dirRel);
136
+ const rules = rule ? [...parentRules, rule] : parentRules;
137
+ let entries;
138
+ try {
139
+ entries = readdirSync(join(this.root, dirRel), { withFileTypes: true });
140
+ } catch {
141
+ return;
142
+ }
143
+ for (const e of entries) {
144
+ const name = e.name;
145
+ if (e.isDirectory()) {
146
+ if (name.startsWith('.') && name !== '.github') continue;
147
+ const rel = dirRel + name;
148
+ if (IgnoreRules.test(rules, rel, true)) continue;
149
+ visit(rel + '/', rules, depth + 1);
150
+ } else if (e.isFile()) {
151
+ const ext = extname(name).toLowerCase();
152
+ if (this.extensions.has(ext) || BINARY_EXTENSIONS.has(ext) || LOCK_FILES.has(name)) continue;
153
+ const rel = dirRel + name;
154
+ if (IgnoreRules.test(rules, rel, false)) continue;
155
+ out.push(rel);
156
+ if (out.length >= limit) return;
157
+ }
158
+ }
159
+ };
160
+ visit('', [this.rootRule], 0);
161
+ return out;
162
+ }
163
+
164
+ /** Walk the project and return every indexable file. */
165
+ async scan(): Promise<ScannedFile[]> {
166
+ const candidates: string[] = [];
167
+ this.skippedLarge = 0;
168
+ this.truncated = false;
169
+ const rules: Rule[] = [this.rootRule];
170
+ this.walk('', rules, candidates, 0);
171
+
172
+ // stat() dominates a warm start (especially on Windows); run it on the
173
+ // libuv thread pool instead of serially on the main thread.
174
+ const out: ScannedFile[] = new Array(candidates.length);
175
+ let next = 0;
176
+ const worker = async (): Promise<void> => {
177
+ while (next < candidates.length) {
178
+ const i = next++;
179
+ const rel = candidates[i]!;
180
+ const abs = join(this.root, rel);
181
+ try {
182
+ const st = await stat(abs);
183
+ if (st.size <= this.maxFileSize) out[i] = { abs, rel, mtime: st.mtimeMs, size: st.size };
184
+ else this.skippedLarge++;
185
+ } catch {
186
+ // vanished between readdir and stat
187
+ }
188
+ }
189
+ };
190
+ await Promise.all(Array.from({ length: Math.min(32, candidates.length) }, worker));
191
+ return out.filter(Boolean);
192
+ }
193
+
194
+ private walk(dirRel: string, parentRules: Rule[], out: string[], depth: number): void {
195
+ if (depth > 40) return;
196
+ if (out.length >= this.maxFiles) {
197
+ this.truncated = true;
198
+ return;
199
+ }
200
+ const rule = this.ruleFor(dirRel);
201
+ const rules = rule ? [...parentRules, rule] : parentRules;
202
+
203
+ let entries;
204
+ try {
205
+ entries = readdirSync(join(this.root, dirRel), { withFileTypes: true });
206
+ } catch {
207
+ return;
208
+ }
209
+
210
+ for (const e of entries) {
211
+ const name = e.name;
212
+ if (e.isDirectory()) {
213
+ if (name.startsWith('.')) continue;
214
+ const rel = dirRel + name;
215
+ if (IgnoreRules.test(rules, rel, true)) continue;
216
+ this.walk(rel + '/', rules, out, depth + 1);
217
+ } else if (e.isFile()) {
218
+ if (!this.extensions.has(extname(name).toLowerCase())) continue;
219
+ const rel = dirRel + name;
220
+ if (IgnoreRules.test(rules, rel, false)) continue;
221
+ out.push(rel);
222
+ }
223
+ // Symlinks are skipped: they cause cycles and duplicate symbols.
224
+ }
225
+ }
226
+ }
@@ -0,0 +1,24 @@
1
+ import { parentPort, workerData } from 'node:worker_threads';
2
+ import { SourceParser } from '../parser/index.js';
3
+ import { processFile, type FileResult, type FileTask } from './process.js';
4
+
5
+ /**
6
+ * Indexing worker: receives batches of files, reads + parses them, and posts
7
+ * the extraction results back. The main thread owns the database.
8
+ */
9
+ const { maxFileSize } = workerData as { maxFileSize: number };
10
+ const parser = new SourceParser();
11
+
12
+ if (parentPort) {
13
+ const port = parentPort;
14
+ port.on('message', async (tasks: FileTask[] | null) => {
15
+ if (tasks === null) {
16
+ process.exit(0);
17
+ }
18
+ const results: FileResult[] = [];
19
+ for (const task of tasks) {
20
+ results.push(await processFile(parser, task, maxFileSize));
21
+ }
22
+ port.postMessage(results);
23
+ });
24
+ }
@@ -1,131 +1,80 @@
1
1
  import { LlmClient } from './index.js';
2
2
  import { buildFunctionCommentPrompt, buildClassCommentPrompt } from './prompts.js';
3
- import type { Queries } from '../db/queries.js';
4
- import type { AstNodeRow } from '../db/types.js';
3
+ import type { ClastDb } from '../db/index.js';
5
4
  import type { ClastConfig } from '../config/index.js';
6
-
5
+ import { CONTAINER_KINDS, type SymbolRow } from '../types.js';
6
+ import type { LineCache } from '../query/format.js';
7
+
8
+ /**
9
+ * Generates documentation for undocumented symbols with an OpenAI-compatible
10
+ * endpoint. Generated text is stored by (path, qualified name) so it survives
11
+ * re-indexing of the file.
12
+ */
7
13
  export class CommentGenerator {
8
14
  private client: LlmClient;
9
- private queries: Queries;
10
- private config: ClastConfig;
11
15
 
12
- constructor(client: LlmClient, queries: Queries, config: ClastConfig) {
13
- this.client = client;
14
- this.queries = queries;
15
- this.config = config;
16
+ constructor(
17
+ private readonly db: ClastDb,
18
+ private readonly config: ClastConfig,
19
+ private readonly lines: LineCache
20
+ ) {
21
+ this.client = new LlmClient(config.llm.endpoint, config.llm.model, config.llm.apiKey);
16
22
  }
17
23
 
18
24
  get isAvailable(): boolean {
19
25
  return this.client.isConfigured;
20
26
  }
21
27
 
28
+ /** Previously generated doc for a symbol, if any. */
29
+ stored(sym: SymbolRow): string | null {
30
+ const row = this.db.get<{ text: string }>(
31
+ `SELECT text FROM gen_docs WHERE path = ? AND qname = ?`, sym.path, sym.qname
32
+ );
33
+ return row?.text ?? null;
34
+ }
35
+
22
36
  /**
23
- * Generate a comment for a specific node.
24
- * Returns the generated comment text, or null if generation is disabled/failed.
37
+ * Returns the doc for a symbol: the original comment when present (unless
38
+ * alwaysGenerate), else a stored or freshly generated one. Null on failure.
25
39
  */
26
- async generateForNode(nodeId: number): Promise<string | null> {
40
+ async generate(sym: SymbolRow, force = false): Promise<string | null> {
41
+ if (sym.doc && !this.config.llm.alwaysGenerate && !force) return sym.doc;
42
+ if (!force) {
43
+ const existing = this.stored(sym);
44
+ if (existing) return existing;
45
+ }
27
46
  if (!this.client.isConfigured) return null;
28
47
 
29
- const node = this.queries.getNodeById(nodeId);
30
- if (!node) return null;
31
-
32
- // Check if original comment exists
33
- const existingComments = this.queries.getCommentsForNode(nodeId);
34
- const hasOriginal = existingComments.some(c => c.source === 'original');
35
-
36
- if (hasOriginal && !this.config.llm.alwaysGenerate) {
37
- // Original comment exists and we're not forcing generation
38
- return existingComments.find(c => c.source === 'original')?.text ?? null;
48
+ let prompt: { system: string; user: string };
49
+ if (CONTAINER_KINDS.has(sym.kind)) {
50
+ const members = this.db
51
+ .all<{ name: string }>(
52
+ `SELECT name FROM symbols WHERE parent_id = ? AND kind IN ('method', 'function') LIMIT 60`, sym.id
53
+ )
54
+ .map(m => m.name);
55
+ prompt = buildClassCommentPrompt(sym.name, sym.signature, members, sym.lang);
56
+ } else {
57
+ const src = this.lines.lines(sym.path);
58
+ const body = src ? src.slice(sym.start_line - 1, Math.min(sym.end_line, sym.start_line + 80)).join('\n') : '';
59
+ prompt = buildFunctionCommentPrompt(sym.signature, body, sym.lang);
39
60
  }
40
61
 
41
62
  try {
42
- const prompt = this.buildPrompt(node);
43
63
  const response = await this.client.complete([
44
64
  { role: 'system', content: prompt.system },
45
65
  { role: 'user', content: prompt.user },
46
66
  ]);
47
-
48
- const comment = response.content.trim();
49
- if (!comment) return null;
50
-
51
- // Remove existing generated comment if any
52
- this.queries.deleteCommentsBySource(nodeId, 'generated');
53
-
54
- // Store the generated comment
55
- this.queries.insertComment(nodeId, comment, 'generated');
56
-
57
- return comment;
67
+ const text = response.content.trim();
68
+ if (!text) return null;
69
+ this.db.run(
70
+ `INSERT INTO gen_docs (path, qname, text, created_at) VALUES (?, ?, ?, ?)
71
+ ON CONFLICT(path, qname) DO UPDATE SET text = excluded.text, created_at = excluded.created_at`,
72
+ sym.path, sym.qname, text, Date.now()
73
+ );
74
+ return text;
58
75
  } catch (err) {
59
- console.error(`[clast] Failed to generate comment for node ${nodeId}:`, err);
76
+ console.error(`[clast] comment generation failed for ${sym.qname}:`, err instanceof Error ? err.message : err);
60
77
  return null;
61
78
  }
62
79
  }
63
-
64
- /**
65
- * Batch generate comments for nodes without any comments.
66
- * Returns counts of successes and failures.
67
- */
68
- async batchGenerate(
69
- limit: number = 100,
70
- onProgress?: (current: number, total: number) => void
71
- ): Promise<{ success: number; failed: number; skipped: number }> {
72
- if (!this.client.isConfigured) {
73
- return { success: 0, failed: 0, skipped: 0 };
74
- }
75
-
76
- const nodes = this.queries.getNodesWithoutComments(limit);
77
- let success = 0;
78
- let failed = 0;
79
- let skipped = 0;
80
-
81
- // Process with concurrency limit
82
- const concurrency = this.config.llm.maxConcurrent;
83
- const chunks = chunkArray(nodes, concurrency);
84
-
85
- let processed = 0;
86
- for (const chunk of chunks) {
87
- const results = await Promise.allSettled(
88
- chunk.map(node => this.generateForNode(node.id))
89
- );
90
-
91
- for (const result of results) {
92
- processed++;
93
- onProgress?.(processed, nodes.length);
94
-
95
- if (result.status === 'fulfilled') {
96
- if (result.value) {
97
- success++;
98
- } else {
99
- skipped++;
100
- }
101
- } else {
102
- failed++;
103
- }
104
- }
105
- }
106
-
107
- return { success, failed, skipped };
108
- }
109
-
110
- private buildPrompt(node: AstNodeRow): { system: string; user: string } {
111
- if (node.type === 'class' || node.type === 'interface' || node.type === 'struct' || node.type === 'trait') {
112
- // Get method names for context
113
- const children = this.queries.searchNodes(node.name, 'method', 50);
114
- const methodNames = children
115
- .filter(c => c.file_path === node.file_path)
116
- .map(c => c.name);
117
-
118
- return buildClassCommentPrompt(node.name, node.signature, methodNames, node.language);
119
- }
120
-
121
- return buildFunctionCommentPrompt(node.signature, node.body_text, node.language);
122
- }
123
- }
124
-
125
- function chunkArray<T>(arr: T[], size: number): T[][] {
126
- const chunks: T[][] = [];
127
- for (let i = 0; i < arr.length; i += size) {
128
- chunks.push(arr.slice(i, i + size));
129
- }
130
- return chunks;
131
80
  }
package/src/llm/index.ts CHANGED
@@ -1,80 +1,80 @@
1
- export interface ChatMessage {
2
- role: 'system' | 'user' | 'assistant';
3
- content: string;
4
- }
5
-
6
- export interface LlmResponse {
7
- content: string;
8
- model: string;
9
- usage?: { prompt_tokens: number; completion_tokens: number };
10
- }
11
-
12
- export class LlmClient {
13
- private endpoint: string;
14
- private model: string;
15
- private apiKey: string;
16
-
17
- constructor(endpoint: string, model: string, apiKey: string = '') {
18
- // Ensure endpoint doesn't have trailing slash
19
- this.endpoint = endpoint.replace(/\/$/, '');
20
- this.model = model;
21
- this.apiKey = apiKey;
22
- }
23
-
24
- get isConfigured(): boolean {
25
- return this.model.length > 0;
26
- }
27
-
28
- async complete(
29
- messages: ChatMessage[],
30
- options: { temperature?: number; maxTokens?: number } = {}
31
- ): Promise<LlmResponse> {
32
- const { temperature = 0.3, maxTokens = 500 } = options;
33
-
34
- const url = `${this.endpoint}/chat/completions`;
35
- const headers: Record<string, string> = {
36
- 'Content-Type': 'application/json',
37
- };
38
-
39
- if (this.apiKey) {
40
- headers['Authorization'] = `Bearer ${this.apiKey}`;
41
- }
42
-
43
- const controller = new AbortController();
44
- const timeout = setTimeout(() => controller.abort(), 30000);
45
-
46
- try {
47
- const response = await fetch(url, {
48
- method: 'POST',
49
- headers,
50
- body: JSON.stringify({
51
- model: this.model,
52
- messages,
53
- temperature,
54
- max_tokens: maxTokens,
55
- }),
56
- signal: controller.signal,
57
- });
58
-
59
- if (!response.ok) {
60
- const body = await response.text().catch(() => '');
61
- throw new Error(`LLM API error ${response.status}: ${body}`);
62
- }
63
-
64
- const data = await response.json() as {
65
- choices: Array<{ message: { content: string } }>;
66
- model: string;
67
- usage?: { prompt_tokens: number; completion_tokens: number };
68
- };
69
-
70
- const content = data.choices?.[0]?.message?.content ?? '';
71
- return {
72
- content,
73
- model: data.model ?? this.model,
74
- usage: data.usage,
75
- };
76
- } finally {
77
- clearTimeout(timeout);
78
- }
79
- }
80
- }
1
+ export interface ChatMessage {
2
+ role: 'system' | 'user' | 'assistant';
3
+ content: string;
4
+ }
5
+
6
+ export interface LlmResponse {
7
+ content: string;
8
+ model: string;
9
+ usage?: { prompt_tokens: number; completion_tokens: number };
10
+ }
11
+
12
+ export class LlmClient {
13
+ private endpoint: string;
14
+ private model: string;
15
+ private apiKey: string;
16
+
17
+ constructor(endpoint: string, model: string, apiKey: string = '') {
18
+ // Ensure endpoint doesn't have trailing slash
19
+ this.endpoint = endpoint.replace(/\/$/, '');
20
+ this.model = model;
21
+ this.apiKey = apiKey;
22
+ }
23
+
24
+ get isConfigured(): boolean {
25
+ return this.model.length > 0;
26
+ }
27
+
28
+ async complete(
29
+ messages: ChatMessage[],
30
+ options: { temperature?: number; maxTokens?: number } = {}
31
+ ): Promise<LlmResponse> {
32
+ const { temperature = 0.3, maxTokens = 500 } = options;
33
+
34
+ const url = `${this.endpoint}/chat/completions`;
35
+ const headers: Record<string, string> = {
36
+ 'Content-Type': 'application/json',
37
+ };
38
+
39
+ if (this.apiKey) {
40
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
41
+ }
42
+
43
+ const controller = new AbortController();
44
+ const timeout = setTimeout(() => controller.abort(), 30000);
45
+
46
+ try {
47
+ const response = await fetch(url, {
48
+ method: 'POST',
49
+ headers,
50
+ body: JSON.stringify({
51
+ model: this.model,
52
+ messages,
53
+ temperature,
54
+ max_tokens: maxTokens,
55
+ }),
56
+ signal: controller.signal,
57
+ });
58
+
59
+ if (!response.ok) {
60
+ const body = await response.text().catch(() => '');
61
+ throw new Error(`LLM API error ${response.status}: ${body}`);
62
+ }
63
+
64
+ const data = await response.json() as {
65
+ choices: Array<{ message: { content: string } }>;
66
+ model: string;
67
+ usage?: { prompt_tokens: number; completion_tokens: number };
68
+ };
69
+
70
+ const content = data.choices?.[0]?.message?.content ?? '';
71
+ return {
72
+ content,
73
+ model: data.model ?? this.model,
74
+ usage: data.usage,
75
+ };
76
+ } finally {
77
+ clearTimeout(timeout);
78
+ }
79
+ }
80
+ }