claude-all-hands 1.0.1 → 1.0.3

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 (170) hide show
  1. package/.claude/agents/code-simplifier.md +52 -0
  2. package/.claude/agents/curator.md +186 -246
  3. package/.claude/agents/documentation-taxonomist.md +255 -0
  4. package/.claude/agents/documentation-writer.md +366 -0
  5. package/.claude/agents/planner.md +123 -166
  6. package/.claude/agents/researcher.md +58 -41
  7. package/.claude/agents/surveyor.md +81 -0
  8. package/.claude/agents/worker.md +74 -0
  9. package/.claude/commands/continue.md +122 -0
  10. package/.claude/commands/create-skill.md +107 -0
  11. package/.claude/commands/create-specialist.md +111 -0
  12. package/.claude/commands/curator-audit.md +4 -0
  13. package/.claude/commands/debug.md +183 -0
  14. package/.claude/commands/docs-adjust.md +214 -0
  15. package/.claude/commands/docs-audit.md +172 -0
  16. package/.claude/commands/docs-init.md +210 -0
  17. package/.claude/commands/plan.md +199 -102
  18. package/.claude/commands/validate.md +11 -0
  19. package/.claude/commands/whats-next.md +106 -134
  20. package/.claude/envoy/README.md +5 -5
  21. package/.claude/envoy/envoy +11 -14
  22. package/.claude/envoy/package-lock.json +1594 -0
  23. package/.claude/envoy/package.json +38 -0
  24. package/.claude/envoy/src/cli.ts +126 -0
  25. package/.claude/envoy/src/commands/base.ts +216 -0
  26. package/.claude/envoy/src/commands/docs.ts +881 -0
  27. package/.claude/envoy/src/commands/gemini.ts +999 -0
  28. package/.claude/envoy/src/commands/git.ts +639 -0
  29. package/.claude/envoy/src/commands/index.ts +73 -0
  30. package/.claude/envoy/src/commands/knowledge.ts +178 -0
  31. package/.claude/envoy/src/commands/perplexity.ts +129 -0
  32. package/.claude/envoy/src/commands/plan/core.ts +134 -0
  33. package/.claude/envoy/src/commands/plan/findings.ts +446 -0
  34. package/.claude/envoy/src/commands/plan/gates.ts +672 -0
  35. package/.claude/envoy/src/commands/plan/index.ts +135 -0
  36. package/.claude/envoy/src/commands/plan/lifecycle.ts +648 -0
  37. package/.claude/envoy/src/commands/plan/plan-file.ts +138 -0
  38. package/.claude/envoy/src/commands/plan/prompts.ts +285 -0
  39. package/.claude/envoy/src/commands/plan/protocols.ts +166 -0
  40. package/.claude/envoy/src/commands/repomix.ts +99 -0
  41. package/.claude/envoy/src/commands/tavily.ts +220 -0
  42. package/.claude/envoy/src/commands/xai.ts +168 -0
  43. package/.claude/envoy/src/lib/ast-queries.ts +261 -0
  44. package/.claude/envoy/src/lib/design.ts +41 -0
  45. package/.claude/envoy/src/lib/feedback-schemas.ts +154 -0
  46. package/.claude/envoy/src/lib/findings.ts +215 -0
  47. package/.claude/envoy/src/lib/gates.ts +572 -0
  48. package/.claude/envoy/src/lib/git.ts +132 -0
  49. package/.claude/envoy/src/lib/index.ts +188 -0
  50. package/.claude/envoy/src/lib/knowledge.ts +646 -0
  51. package/.claude/envoy/src/lib/markdown.ts +75 -0
  52. package/.claude/envoy/src/lib/observability.ts +262 -0
  53. package/.claude/envoy/src/lib/paths.ts +130 -0
  54. package/.claude/envoy/src/lib/plan-io.ts +117 -0
  55. package/.claude/envoy/src/lib/prompts.ts +231 -0
  56. package/.claude/envoy/src/lib/protocols.ts +314 -0
  57. package/.claude/envoy/src/lib/repomix.ts +133 -0
  58. package/.claude/envoy/src/lib/retry.ts +138 -0
  59. package/.claude/envoy/src/lib/tree-sitter-utils.ts +301 -0
  60. package/.claude/envoy/src/lib/watcher.ts +167 -0
  61. package/.claude/envoy/src/types/tree-sitter.d.ts +76 -0
  62. package/.claude/envoy/tsconfig.json +21 -0
  63. package/.claude/hooks/scripts/enforce_research_fetch.py +1 -1
  64. package/.claude/hooks/scripts/scan_agents.py +62 -0
  65. package/.claude/hooks/scripts/scan_commands.py +50 -0
  66. package/.claude/hooks/scripts/scan_skills.py +46 -70
  67. package/.claude/hooks/scripts/validate_artifacts.py +128 -0
  68. package/.claude/hooks/startup.sh +26 -24
  69. package/.claude/protocols/bug-discovery.yaml +55 -0
  70. package/.claude/protocols/debugging.yaml +51 -0
  71. package/.claude/protocols/discovery.yaml +53 -0
  72. package/.claude/protocols/implementation.yaml +84 -0
  73. package/.claude/settings.json +38 -97
  74. package/.claude/skills/brainstorming/SKILL.md +54 -0
  75. package/.claude/skills/commands-development/SKILL.md +630 -0
  76. package/.claude/skills/commands-development/references/arguments.md +252 -0
  77. package/.claude/skills/commands-development/references/patterns.md +796 -0
  78. package/.claude/skills/commands-development/references/tool-restrictions.md +376 -0
  79. package/.claude/skills/discovery-mode/SKILL.md +108 -0
  80. package/.claude/skills/documentation-taxonomy/SKILL.md +287 -0
  81. package/.claude/skills/hooks-development/SKILL.md +332 -0
  82. package/.claude/skills/hooks-development/references/command-vs-prompt.md +269 -0
  83. package/.claude/skills/hooks-development/references/examples.md +658 -0
  84. package/.claude/skills/hooks-development/references/hook-types.md +463 -0
  85. package/.claude/skills/hooks-development/references/input-output-schemas.md +469 -0
  86. package/.claude/skills/hooks-development/references/matchers.md +470 -0
  87. package/.claude/skills/hooks-development/references/troubleshooting.md +587 -0
  88. package/.claude/skills/implementation-mode/SKILL.md +171 -0
  89. package/.claude/skills/knowledge-discovery/SKILL.md +178 -0
  90. package/.claude/skills/research-tools/SKILL.md +35 -33
  91. package/.claude/skills/skills-development/SKILL.md +192 -0
  92. package/.claude/skills/skills-development/references/api-security.md +226 -0
  93. package/.claude/skills/skills-development/references/be-clear-and-direct.md +531 -0
  94. package/.claude/skills/skills-development/references/common-patterns.md +595 -0
  95. package/.claude/skills/skills-development/references/core-principles.md +437 -0
  96. package/.claude/skills/skills-development/references/executable-code.md +175 -0
  97. package/.claude/skills/skills-development/references/iteration-and-testing.md +474 -0
  98. package/.claude/skills/skills-development/references/recommended-structure.md +168 -0
  99. package/.claude/skills/skills-development/references/skill-structure.md +372 -0
  100. package/.claude/skills/skills-development/references/use-xml-tags.md +466 -0
  101. package/.claude/skills/skills-development/references/using-scripts.md +113 -0
  102. package/.claude/skills/skills-development/references/using-templates.md +112 -0
  103. package/.claude/skills/skills-development/references/workflows-and-validation.md +510 -0
  104. package/.claude/skills/skills-development/templates/router-skill.md +73 -0
  105. package/.claude/skills/skills-development/templates/simple-skill.md +33 -0
  106. package/.claude/skills/skills-development/workflows/add-reference.md +96 -0
  107. package/.claude/skills/skills-development/workflows/add-script.md +93 -0
  108. package/.claude/skills/skills-development/workflows/add-template.md +74 -0
  109. package/.claude/skills/skills-development/workflows/add-workflow.md +120 -0
  110. package/.claude/skills/skills-development/workflows/audit-skill.md +138 -0
  111. package/.claude/skills/skills-development/workflows/create-domain-expertise-skill.md +605 -0
  112. package/.claude/skills/skills-development/workflows/create-new-skill.md +191 -0
  113. package/.claude/skills/skills-development/workflows/get-guidance.md +121 -0
  114. package/.claude/skills/skills-development/workflows/upgrade-to-router.md +161 -0
  115. package/.claude/skills/skills-development/workflows/verify-skill.md +204 -0
  116. package/.claude/skills/subagents-development/SKILL.md +325 -0
  117. package/.claude/skills/subagents-development/references/context-management.md +567 -0
  118. package/.claude/skills/subagents-development/references/debugging-agents.md +714 -0
  119. package/.claude/skills/subagents-development/references/error-handling-and-recovery.md +502 -0
  120. package/.claude/skills/subagents-development/references/evaluation-and-testing.md +374 -0
  121. package/.claude/skills/subagents-development/references/orchestration-patterns.md +591 -0
  122. package/.claude/skills/subagents-development/references/subagents.md +508 -0
  123. package/.claude/skills/subagents-development/references/writing-subagent-prompts.md +517 -0
  124. package/.claude/statusline.sh +24 -0
  125. package/bin/cli.js +150 -72
  126. package/package.json +1 -1
  127. package/.claude/agents/explorer.md +0 -62
  128. package/.claude/agents/parallel-worker.md +0 -121
  129. package/.claude/commands/curation-fix.md +0 -92
  130. package/.claude/commands/new-branch.md +0 -36
  131. package/.claude/commands/parallel-discovery.md +0 -69
  132. package/.claude/commands/parallel-orchestration.md +0 -99
  133. package/.claude/commands/plan-checkpoint.md +0 -37
  134. package/.claude/envoy/commands/__init__.py +0 -1
  135. package/.claude/envoy/commands/base.py +0 -95
  136. package/.claude/envoy/commands/parallel.py +0 -439
  137. package/.claude/envoy/commands/perplexity.py +0 -86
  138. package/.claude/envoy/commands/plans.py +0 -451
  139. package/.claude/envoy/commands/tavily.py +0 -156
  140. package/.claude/envoy/commands/vertex.py +0 -358
  141. package/.claude/envoy/commands/xai.py +0 -124
  142. package/.claude/envoy/envoy.py +0 -122
  143. package/.claude/envoy/pyrightconfig.json +0 -4
  144. package/.claude/envoy/requirements.txt +0 -2
  145. package/.claude/hooks/capture-queries.sh +0 -3
  146. package/.claude/hooks/scripts/enforce_planning.py +0 -118
  147. package/.claude/hooks/scripts/enforce_rg.py +0 -34
  148. package/.claude/hooks/scripts/validate_skill.py +0 -81
  149. package/.claude/skills/claude-envoy-curation/SKILL.md +0 -162
  150. package/.claude/skills/claude-envoy-usage/SKILL.md +0 -46
  151. package/.claude/skills/command-development/SKILL.md +0 -206
  152. package/.claude/skills/command-development/examples/simple-commands.md +0 -212
  153. package/.claude/skills/command-development/references/frontmatter-reference.md +0 -221
  154. package/.claude/skills/hook-development/SKILL.md +0 -127
  155. package/.claude/skills/hook-development/examples/command-hooks.md +0 -301
  156. package/.claude/skills/hook-development/examples/prompt-hooks.md +0 -114
  157. package/.claude/skills/hook-development/references/event-reference.md +0 -226
  158. package/.claude/skills/repomix-extraction/SKILL.md +0 -91
  159. package/.claude/skills/skill-development/SKILL.md +0 -168
  160. package/.claude/skills/skill-development/examples/complete-skill-examples.md +0 -281
  161. package/.claude/skills/skill-development/references/progressive-disclosure.md +0 -141
  162. package/.claude/skills/skill-development/references/writing-style.md +0 -180
  163. package/.claude/skills/skill-development/scripts/validate-skill.sh +0 -144
  164. package/.claude/skills/specialist-builder/SKILL.md +0 -327
  165. package/.claude/skills/specialist-builder/docs/agent-catalog.md +0 -28
  166. package/.claude/skills/specialist-builder/examples/complete-agent-examples.md +0 -206
  167. package/.claude/skills/specialist-builder/references/system-prompt-patterns.md +0 -281
  168. package/.claude/skills/specialist-builder/references/triggering-examples.md +0 -162
  169. package/.claude/skills/specialist-builder/scripts/validate-agent.sh +0 -137
  170. /package/.claude/{envoy/claude-envoy.py → skills/claude-envoy-patterns/SKILL.md} +0 -0
@@ -0,0 +1,646 @@
1
+ /**
2
+ * KnowledgeService - USearch-based semantic search for documentation.
3
+ * Uses @visheratin/web-ai-node for embeddings and usearch for HNSW indexing.
4
+ */
5
+
6
+ import { createHash } from "crypto";
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
8
+ import matter from "gray-matter";
9
+ import { createRequire } from "module";
10
+ import { basename, extname, join, relative } from "path";
11
+ import { Index, MetricKind, ScalarKind } from "usearch";
12
+
13
+ // Create require function for ESM compatibility (needed for fetch caching)
14
+ const require = createRequire(import.meta.url);
15
+
16
+ // Types
17
+ interface DocumentMeta {
18
+ description: string;
19
+ relevant_files: string[];
20
+ token_count: number;
21
+ }
22
+
23
+ interface IndexMetadata {
24
+ id_to_path: Record<string, string>;
25
+ path_to_id: Record<string, string>;
26
+ documents: Record<string, DocumentMeta>;
27
+ next_id: number;
28
+ lastUpdated: string;
29
+ }
30
+
31
+ interface SearchResult {
32
+ resource_path: string;
33
+ similarity: number;
34
+ token_count: number;
35
+ description: string;
36
+ relevant_files: string[];
37
+ full_resource_context?: string;
38
+ }
39
+
40
+ interface ReindexResult {
41
+ files_indexed: number;
42
+ total_tokens: number;
43
+ }
44
+
45
+ interface FileChange {
46
+ path: string;
47
+ added?: boolean;
48
+ deleted?: boolean;
49
+ modified?: boolean;
50
+ }
51
+
52
+ interface IndexConfig {
53
+ name: string;
54
+ paths: string[];
55
+ extensions: string[];
56
+ description: string;
57
+ /** Whether this index expects front-matter with description/relevant_files */
58
+ hasFrontmatter: boolean;
59
+ }
60
+
61
+ // Docs index configuration (only supported index)
62
+ const DOCS_CONFIG: IndexConfig = {
63
+ name: "docs",
64
+ paths: ["docs/"],
65
+ extensions: [".md"],
66
+ description: "Project documentation",
67
+ hasFrontmatter: true,
68
+ };
69
+
70
+ // File reference patterns for auto-populating relevant_files
71
+ const FILE_REF_PATTERNS = [
72
+ /`([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)`/g,
73
+ /\[.*?\]\(([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)\)/g,
74
+ /(?:src|lib|components|utils|hooks|services)\/[a-zA-Z0-9_\-./]+\.[a-zA-Z]+/g,
75
+ ];
76
+
77
+ // Environment config with defaults
78
+ const SEARCH_SIMILARITY_THRESHOLD = parseFloat(
79
+ process.env.SEARCH_SIMILARITY_THRESHOLD ?? "0.65"
80
+ );
81
+ const SEARCH_CONTEXT_TOKEN_LIMIT = parseInt(
82
+ process.env.SEARCH_CONTEXT_TOKEN_LIMIT ?? "5000",
83
+ 10
84
+ );
85
+ const SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD = parseFloat(
86
+ process.env.SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD ?? "0.82"
87
+ );
88
+
89
+ export class KnowledgeService {
90
+ private model: unknown = null;
91
+ private readonly knowledgeDir: string;
92
+ private readonly projectRoot: string;
93
+
94
+ constructor(projectRoot: string) {
95
+ this.projectRoot = projectRoot;
96
+ this.knowledgeDir = join(projectRoot, ".claude", "envoy", ".knowledge");
97
+ }
98
+
99
+ /**
100
+ * Ensure .knowledge/ directory exists
101
+ */
102
+ ensureDir(): void {
103
+ if (!existsSync(this.knowledgeDir)) {
104
+ mkdirSync(this.knowledgeDir, { recursive: true });
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Get model cache directory
110
+ */
111
+ private getModelCacheDir(): string {
112
+ return join(this.knowledgeDir, "models");
113
+ }
114
+
115
+ /**
116
+ * Install caching wrapper for node-fetch (must call before importing web-ai-node)
117
+ */
118
+ private installFetchCache(): void {
119
+ const cacheDir = this.getModelCacheDir();
120
+ if (!existsSync(cacheDir)) {
121
+ mkdirSync(cacheDir, { recursive: true });
122
+ }
123
+
124
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
125
+ const nodeFetchModule = require("node-fetch");
126
+ const originalFetch = nodeFetchModule.default || nodeFetchModule;
127
+
128
+ // Skip if already patched
129
+ if ((originalFetch as { __cached?: boolean }).__cached) return;
130
+
131
+ const self = this;
132
+ const cachedFetch = async function (url: string, init?: RequestInit) {
133
+ // Only cache model files
134
+ if (!url.includes("web-ai-models.org") && !url.includes(".onnx")) {
135
+ return originalFetch(url, init);
136
+ }
137
+
138
+ const urlHash = createHash("md5").update(url).digest("hex").slice(0, 8);
139
+ const fileName = `${urlHash}-${basename(url)}`;
140
+ const cachePath = join(self.getModelCacheDir(), fileName);
141
+
142
+ if (existsSync(cachePath)) {
143
+ console.error(`[knowledge] Using cached model: ${fileName}`);
144
+ const data = readFileSync(cachePath);
145
+ // Return a mock response with arrayBuffer method
146
+ return {
147
+ ok: true,
148
+ status: 200,
149
+ arrayBuffer: async () => data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength),
150
+ };
151
+ }
152
+
153
+ console.error(`[knowledge] Downloading model: ${basename(url)}`);
154
+ const response = await originalFetch(url, init);
155
+ if (!response.ok) {
156
+ return response;
157
+ }
158
+
159
+ const arrayBuffer = await response.arrayBuffer();
160
+ const buffer = Buffer.from(arrayBuffer);
161
+ writeFileSync(cachePath, buffer);
162
+ console.error(`[knowledge] Cached model: ${fileName} (${(buffer.length / 1024 / 1024).toFixed(1)}MB)`);
163
+
164
+ // Return a mock response since we consumed the original
165
+ return {
166
+ ok: true,
167
+ status: 200,
168
+ arrayBuffer: async () => arrayBuffer,
169
+ };
170
+ };
171
+
172
+ (cachedFetch as { __cached?: boolean }).__cached = true;
173
+
174
+ // Patch the module's default export
175
+ if (nodeFetchModule.default) {
176
+ nodeFetchModule.default = cachedFetch;
177
+ }
178
+ // Also patch require.cache
179
+ const cacheKey = require.resolve("node-fetch");
180
+ if (require.cache[cacheKey]) {
181
+ require.cache[cacheKey]!.exports = cachedFetch;
182
+ require.cache[cacheKey]!.exports.default = cachedFetch;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Lazy-load embedding model with local caching
188
+ */
189
+ async getModel(): Promise<unknown> {
190
+ if (this.model) return this.model;
191
+
192
+ this.ensureDir();
193
+ console.error("[knowledge] Loading embedding model...");
194
+ const startTime = Date.now();
195
+
196
+ // Install caching before importing the library
197
+ this.installFetchCache();
198
+
199
+ const { TextModel } = await import("@visheratin/web-ai-node/text");
200
+ const modelResult = await TextModel.create("gtr-t5-quant");
201
+ this.model = modelResult.model;
202
+
203
+ console.error(`[knowledge] Model loaded in ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
204
+ return this.model;
205
+ }
206
+
207
+ /**
208
+ * Generate embedding for text
209
+ */
210
+ async embed(text: string): Promise<Float32Array> {
211
+ const model = await this.getModel();
212
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
+ const result = await (model as any).process(text);
214
+ return new Float32Array(result.result);
215
+ }
216
+
217
+
218
+ /**
219
+ * Convert cosine distance to similarity (0-1 scale)
220
+ * Cosine distance: 0 = identical, 1 = orthogonal, 2 = opposite
221
+ */
222
+ distanceToSimilarity(distance: number): number {
223
+ return 1 - distance / 2;
224
+ }
225
+
226
+ /**
227
+ * Get index file paths
228
+ */
229
+ private getIndexPaths(): { index: string; meta: string } {
230
+ return {
231
+ index: join(this.knowledgeDir, "docs.usearch"),
232
+ meta: join(this.knowledgeDir, "docs.meta.json"),
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Create empty index metadata
238
+ */
239
+ private createEmptyMetadata(): IndexMetadata {
240
+ return {
241
+ id_to_path: {},
242
+ path_to_id: {},
243
+ documents: {},
244
+ next_id: 0,
245
+ lastUpdated: new Date().toISOString(),
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Create a new USearch index
251
+ */
252
+ private createIndex(): Index {
253
+ return new Index(
254
+ 768, // dimensions
255
+ MetricKind.Cos, // metric
256
+ ScalarKind.F32, // quantization
257
+ 16 // connectivity
258
+ );
259
+ }
260
+
261
+ /**
262
+ * Load index + metadata from disk
263
+ */
264
+ async loadIndex(): Promise<{ index: Index; meta: IndexMetadata }> {
265
+ const paths = this.getIndexPaths();
266
+
267
+ if (!existsSync(paths.index) || !existsSync(paths.meta)) {
268
+ return {
269
+ index: this.createIndex(),
270
+ meta: this.createEmptyMetadata(),
271
+ };
272
+ }
273
+
274
+ const index = this.createIndex();
275
+ index.load(paths.index);
276
+
277
+ const meta: IndexMetadata = JSON.parse(readFileSync(paths.meta, "utf-8"));
278
+ return { index, meta };
279
+ }
280
+
281
+ /**
282
+ * Save index + metadata to disk
283
+ */
284
+ async saveIndex(index: Index, meta: IndexMetadata): Promise<void> {
285
+ this.ensureDir();
286
+ const paths = this.getIndexPaths();
287
+
288
+ meta.lastUpdated = new Date().toISOString();
289
+ index.save(paths.index);
290
+ writeFileSync(paths.meta, JSON.stringify(meta, null, 2));
291
+ }
292
+
293
+ /**
294
+ * Estimate token count (rough approximation: 1 token ≈ 4 chars)
295
+ */
296
+ private estimateTokens(text: string): number {
297
+ return Math.ceil(text.length / 4);
298
+ }
299
+
300
+ /**
301
+ * Discover files for an index based on config
302
+ */
303
+ private discoverFiles(config: IndexConfig): string[] {
304
+ const files: string[] = [];
305
+
306
+ for (const configPath of config.paths) {
307
+ const fullPath = join(this.projectRoot, configPath);
308
+
309
+ if (!existsSync(fullPath)) continue;
310
+
311
+ const stat = statSync(fullPath);
312
+ if (stat.isFile()) {
313
+ if (config.extensions.includes(extname(fullPath))) {
314
+ files.push(configPath);
315
+ }
316
+ } else if (stat.isDirectory()) {
317
+ this.walkDir(fullPath, config.extensions, files, this.projectRoot);
318
+ }
319
+ }
320
+
321
+ return files;
322
+ }
323
+
324
+ /**
325
+ * Recursively walk directory and collect files
326
+ */
327
+ private walkDir(
328
+ dir: string,
329
+ extensions: string[],
330
+ files: string[],
331
+ projectRoot: string
332
+ ): void {
333
+ const entries = readdirSync(dir, { withFileTypes: true });
334
+
335
+ for (const entry of entries) {
336
+ const fullPath = join(dir, entry.name);
337
+
338
+ if (entry.isDirectory()) {
339
+ // Skip node_modules and hidden dirs (except .claude)
340
+ if (entry.name === "node_modules" || (entry.name.startsWith(".") && entry.name !== ".claude")) {
341
+ continue;
342
+ }
343
+ this.walkDir(fullPath, extensions, files, projectRoot);
344
+ } else if (entry.isFile() && extensions.includes(extname(entry.name))) {
345
+ files.push(relative(projectRoot, fullPath));
346
+ }
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Extract file references from document content
352
+ */
353
+ private extractFileReferences(content: string): string[] {
354
+ const refs = new Set<string>();
355
+
356
+ for (const pattern of FILE_REF_PATTERNS) {
357
+ const regex = new RegExp(pattern.source, pattern.flags);
358
+ let match;
359
+ while ((match = regex.exec(content)) !== null) {
360
+ const ref = match[1] || match[0];
361
+ if (ref && !ref.startsWith("http") && !ref.startsWith("#")) {
362
+ refs.add(ref);
363
+ }
364
+ }
365
+ }
366
+
367
+ return Array.from(refs);
368
+ }
369
+
370
+ /**
371
+ * Validate file references exist
372
+ */
373
+ private validateFileReferences(refs: string[]): { valid: string[]; missing: string[] } {
374
+ const valid: string[] = [];
375
+ const missing: string[] = [];
376
+
377
+ for (const ref of refs) {
378
+ const fullPath = join(this.projectRoot, ref);
379
+ if (existsSync(fullPath)) {
380
+ valid.push(ref);
381
+ } else {
382
+ missing.push(ref);
383
+ }
384
+ }
385
+
386
+ return { valid, missing };
387
+ }
388
+
389
+ /**
390
+ * Index a single document
391
+ */
392
+ async indexDocument(
393
+ index: Index,
394
+ meta: IndexMetadata,
395
+ path: string,
396
+ content: string,
397
+ frontMatterData: Record<string, unknown>
398
+ ): Promise<bigint> {
399
+ // Assign or reuse ID
400
+ let id: bigint;
401
+ if (meta.path_to_id[path]) {
402
+ id = BigInt(meta.path_to_id[path]);
403
+ } else {
404
+ id = BigInt(meta.next_id++);
405
+ meta.id_to_path[id.toString()] = path;
406
+ meta.path_to_id[path] = id.toString();
407
+ }
408
+
409
+ // Generate embedding
410
+ const embedding = await this.embed(content);
411
+
412
+ // Add to index
413
+ index.add(id, embedding);
414
+
415
+ // Store metadata
416
+ meta.documents[path] = {
417
+ description: (frontMatterData.description as string) || "",
418
+ relevant_files: (frontMatterData.relevant_files as string[]) || [],
419
+ token_count: this.estimateTokens(content),
420
+ };
421
+
422
+ return id;
423
+ }
424
+
425
+ /**
426
+ * Search docs index with similarity computation
427
+ * @param metadataOnly - If true, only return file paths and descriptions (no full_resource_context)
428
+ */
429
+ async search(query: string, k: number = 50, metadataOnly: boolean = false): Promise<SearchResult[]> {
430
+ const { index, meta } = await this.loadIndex();
431
+
432
+ if (Object.keys(meta.documents).length === 0) {
433
+ return [];
434
+ }
435
+
436
+ // Generate query embedding
437
+ const queryEmbedding = await this.embed(query);
438
+
439
+ // Search (1 thread for CLI usage)
440
+ const searchResult = index.search(queryEmbedding, k, 1);
441
+ const keys = searchResult.keys;
442
+ const distances = searchResult.distances;
443
+
444
+ // Convert to results
445
+ const results: SearchResult[] = [];
446
+ let totalTokens = 0;
447
+
448
+ for (let i = 0; i < keys.length; i++) {
449
+ const id = keys[i].toString();
450
+ const distance = distances[i];
451
+ const similarity = this.distanceToSimilarity(distance);
452
+
453
+ // Filter by threshold
454
+ if (similarity < SEARCH_SIMILARITY_THRESHOLD) continue;
455
+
456
+ const path = meta.id_to_path[id];
457
+ if (!path) continue;
458
+
459
+ const docMeta = meta.documents[path];
460
+ if (!docMeta) continue;
461
+
462
+ // Check token limit
463
+ if (totalTokens + docMeta.token_count > SEARCH_CONTEXT_TOKEN_LIMIT) continue;
464
+ totalTokens += docMeta.token_count;
465
+
466
+ const result: SearchResult = {
467
+ resource_path: path,
468
+ similarity,
469
+ token_count: docMeta.token_count,
470
+ description: docMeta.description,
471
+ relevant_files: docMeta.relevant_files,
472
+ };
473
+
474
+ // Include full context for high-similarity results (unless metadata-only mode)
475
+ if (!metadataOnly && similarity >= SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD) {
476
+ const fullPath = join(this.projectRoot, path);
477
+ if (existsSync(fullPath)) {
478
+ result.full_resource_context = readFileSync(fullPath, "utf-8");
479
+ }
480
+ }
481
+
482
+ results.push(result);
483
+ }
484
+
485
+ return results;
486
+ }
487
+
488
+ /**
489
+ * Full reindex of docs
490
+ */
491
+ async reindexAll(): Promise<ReindexResult> {
492
+ this.ensureDir();
493
+ const startTime = Date.now();
494
+ console.error("[knowledge] Reindexing docs...");
495
+
496
+ // Create fresh index
497
+ const index = this.createIndex();
498
+ const meta = this.createEmptyMetadata();
499
+
500
+ // Discover and index files
501
+ const files = this.discoverFiles(DOCS_CONFIG);
502
+ console.error(`[knowledge] Found ${files.length} files`);
503
+ let totalTokens = 0;
504
+
505
+ for (let i = 0; i < files.length; i++) {
506
+ const filePath = files[i];
507
+ const fullPath = join(this.projectRoot, filePath);
508
+ const content = readFileSync(fullPath, "utf-8");
509
+
510
+ // Parse front-matter
511
+ let frontMatter: Record<string, unknown> = {};
512
+ if (filePath.endsWith(".md")) {
513
+ try {
514
+ const parsed = matter(content);
515
+ frontMatter = parsed.data;
516
+ } catch {
517
+ // Skip files with invalid front-matter
518
+ }
519
+ }
520
+
521
+ console.error(`[knowledge] Embedding ${i + 1}/${files.length}: ${filePath}`);
522
+ await this.indexDocument(index, meta, filePath, content, frontMatter);
523
+ totalTokens += meta.documents[filePath].token_count;
524
+ }
525
+
526
+ // Save
527
+ await this.saveIndex(index, meta);
528
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
529
+ console.error(`[knowledge] Reindex complete: ${files.length} files, ${totalTokens} tokens in ${duration}s`);
530
+
531
+ return {
532
+ files_indexed: files.length,
533
+ total_tokens: totalTokens,
534
+ };
535
+ }
536
+
537
+ /**
538
+ * Incremental reindex from changed files
539
+ */
540
+ async reindexFromChanges(changes: FileChange[]): Promise<{
541
+ success: boolean;
542
+ message: string;
543
+ missing_references?: { doc_path: string; missing_files: string[] }[];
544
+ files: { path: string; action: string }[];
545
+ }> {
546
+ console.error(`[knowledge] Incremental reindex: ${changes.length} change(s)`);
547
+ const startTime = Date.now();
548
+
549
+ const { index, meta } = await this.loadIndex();
550
+ const processedFiles: { path: string; action: string }[] = [];
551
+ const missingReferences: { doc_path: string; missing_files: string[] }[] = [];
552
+
553
+ for (const change of changes) {
554
+ const { path, added, deleted, modified } = change;
555
+
556
+ // Check if file matches docs config
557
+ const matchesConfig = DOCS_CONFIG.paths.some((p: string) => path.startsWith(p)) &&
558
+ DOCS_CONFIG.extensions.includes(extname(path));
559
+
560
+ if (!matchesConfig) continue;
561
+
562
+ if (deleted) {
563
+ // Remove from index
564
+ const id = meta.path_to_id[path];
565
+ if (id) {
566
+ // Note: USearch doesn't have a remove method in basic API
567
+ // We mark as deleted in metadata
568
+ delete meta.id_to_path[id];
569
+ delete meta.path_to_id[path];
570
+ delete meta.documents[path];
571
+ processedFiles.push({ path, action: "deleted" });
572
+ console.error(`[knowledge] Deleted: ${path}`);
573
+ }
574
+ } else if (added || modified) {
575
+ const fullPath = join(this.projectRoot, path);
576
+ if (!existsSync(fullPath)) continue;
577
+
578
+ const content = readFileSync(fullPath, "utf-8");
579
+ let frontMatter: Record<string, unknown> = {};
580
+
581
+ // Process front-matter and file references
582
+ if (path.endsWith(".md")) {
583
+ try {
584
+ const parsed = matter(content);
585
+ frontMatter = parsed.data;
586
+
587
+ // Extract and validate file references
588
+ const refs = this.extractFileReferences(parsed.content);
589
+ const { valid, missing } = this.validateFileReferences(refs);
590
+
591
+ if (missing.length > 0) {
592
+ missingReferences.push({ doc_path: path, missing_files: missing });
593
+ }
594
+
595
+ // Auto-populate relevant_files
596
+ if (valid.length > 0) {
597
+ frontMatter.relevant_files = valid;
598
+ // Write back with updated front-matter
599
+ const newContent = matter.stringify(parsed.content, frontMatter);
600
+ writeFileSync(fullPath, newContent);
601
+ }
602
+ } catch {
603
+ // Skip files with invalid front-matter
604
+ }
605
+ }
606
+
607
+ // Index document
608
+ const action = added ? "added" : "modified";
609
+ console.error(`[knowledge] Embedding (${action}): ${path}`);
610
+ await this.indexDocument(index, meta, path, content, frontMatter);
611
+ processedFiles.push({ path, action });
612
+ }
613
+ }
614
+
615
+ // Save updated index
616
+ await this.saveIndex(index, meta);
617
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
618
+ console.error(`[knowledge] Incremental reindex complete: ${processedFiles.length} file(s) in ${duration}s`);
619
+
620
+ if (missingReferences.length > 0) {
621
+ return {
622
+ success: false,
623
+ message: "Documents contain references to missing files",
624
+ missing_references: missingReferences,
625
+ files: processedFiles,
626
+ };
627
+ }
628
+
629
+ return {
630
+ success: true,
631
+ message: "Index updated successfully",
632
+ files: processedFiles,
633
+ };
634
+ }
635
+
636
+ /**
637
+ * Check if docs index exists
638
+ */
639
+ async checkIndex(): Promise<{ exists: boolean }> {
640
+ const paths = this.getIndexPaths();
641
+ return { exists: existsSync(paths.index) && existsSync(paths.meta) };
642
+ }
643
+ }
644
+
645
+ export type { DocumentMeta, FileChange, IndexMetadata, ReindexResult, SearchResult };
646
+
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Markdown with YAML front matter parsing utilities.
3
+ */
4
+
5
+ import { existsSync, readFileSync, writeFileSync } from "fs";
6
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
7
+
8
+ /**
9
+ * Parse a markdown file with YAML front matter.
10
+ * Returns { frontMatter: object, content: string }
11
+ */
12
+ export function parseMarkdownWithFrontMatter(text: string): {
13
+ frontMatter: Record<string, unknown>;
14
+ content: string;
15
+ } {
16
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
17
+ if (!match) {
18
+ return { frontMatter: {}, content: text };
19
+ }
20
+ try {
21
+ const frontMatter = parseYaml(match[1]) as Record<string, unknown>;
22
+ return { frontMatter, content: match[2] };
23
+ } catch {
24
+ return { frontMatter: {}, content: text };
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Write a markdown file with YAML front matter.
30
+ */
31
+ export function writeMarkdownWithFrontMatter(
32
+ filePath: string,
33
+ frontMatter: Record<string, unknown>,
34
+ content: string
35
+ ): void {
36
+ const yaml = stringifyYaml(frontMatter, { lineWidth: 0 });
37
+ const fileContent = `---\n${yaml}---\n\n${content}`;
38
+ writeFileSync(filePath, fileContent, "utf-8");
39
+ }
40
+
41
+ /**
42
+ * Read a markdown file and parse front matter.
43
+ * Returns null if file doesn't exist.
44
+ */
45
+ export function readMarkdownFile(filePath: string): {
46
+ frontMatter: Record<string, unknown>;
47
+ content: string;
48
+ } | null {
49
+ if (!existsSync(filePath)) {
50
+ return null;
51
+ }
52
+ const text = readFileSync(filePath, "utf-8");
53
+ return parseMarkdownWithFrontMatter(text);
54
+ }
55
+
56
+ /**
57
+ * Log file placeholder patterns - these are stripped when reading if no other content.
58
+ */
59
+ const LOG_PLACEHOLDER_PATTERNS = [
60
+ /^<!--\s*Paste\s+(test|debug)\s+logs\s+here\s*-->\s*$/i,
61
+ /^<!--\s*ENVOY_LOG_PLACEHOLDER\s*-->\s*$/i,
62
+ ];
63
+
64
+ /**
65
+ * Strip log placeholder if file only contains placeholder text.
66
+ */
67
+ export function stripLogPlaceholder(content: string): string {
68
+ const trimmed = content.trim();
69
+ for (const pattern of LOG_PLACEHOLDER_PATTERNS) {
70
+ if (pattern.test(trimmed)) {
71
+ return "";
72
+ }
73
+ }
74
+ return content;
75
+ }