claude-all-hands 1.0.2 → 1.0.4

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 (35) hide show
  1. package/.claude/agents/curator.md +1 -5
  2. package/.claude/agents/documentation-taxonomist.md +255 -0
  3. package/.claude/agents/documentation-writer.md +366 -0
  4. package/.claude/agents/surveyor.md +1 -1
  5. package/.claude/commands/continue.md +12 -10
  6. package/.claude/commands/create-skill.md +2 -2
  7. package/.claude/commands/create-specialist.md +3 -3
  8. package/.claude/commands/debug.md +5 -5
  9. package/.claude/commands/docs-adjust.md +214 -0
  10. package/.claude/commands/docs-audit.md +172 -0
  11. package/.claude/commands/docs-init.md +210 -0
  12. package/.claude/commands/plan.md +6 -6
  13. package/.claude/commands/whats-next.md +2 -2
  14. package/.claude/envoy/README.md +5 -5
  15. package/.claude/envoy/package-lock.json +216 -10
  16. package/.claude/envoy/package.json +9 -0
  17. package/.claude/envoy/src/commands/docs.ts +881 -0
  18. package/.claude/envoy/src/commands/knowledge.ts +33 -42
  19. package/.claude/envoy/src/lib/ast-queries.ts +261 -0
  20. package/.claude/envoy/src/lib/knowledge.ts +176 -124
  21. package/.claude/envoy/src/lib/tree-sitter-utils.ts +301 -0
  22. package/.claude/envoy/src/types/tree-sitter.d.ts +76 -0
  23. package/.claude/hooks/scripts/enforce_research_fetch.py +1 -1
  24. package/.claude/protocols/bug-discovery.yaml +1 -1
  25. package/.claude/protocols/discovery.yaml +1 -1
  26. package/.claude/settings.json +4 -3
  27. package/.claude/skills/discovery-mode/SKILL.md +7 -7
  28. package/.claude/skills/documentation-taxonomy/SKILL.md +287 -0
  29. package/.claude/skills/implementation-mode/SKILL.md +7 -7
  30. package/.claude/skills/knowledge-discovery/SKILL.md +178 -0
  31. package/bin/cli.js +41 -1
  32. package/package.json +1 -1
  33. package/.claude/agents/documentor.md +0 -147
  34. package/.claude/commands/audit-docs.md +0 -94
  35. package/.claude/commands/create-docs.md +0 -100
@@ -3,10 +3,15 @@
3
3
  * Uses @visheratin/web-ai-node for embeddings and usearch for HNSW indexing.
4
4
  */
5
5
 
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from "fs";
7
- import { join, relative, extname } from "path";
8
- import { Index, MetricKind, ScalarKind, type Matches } from "usearch";
6
+ import { createHash } from "crypto";
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
9
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);
10
15
 
11
16
  // Types
12
17
  interface DocumentMeta {
@@ -53,32 +58,13 @@ interface IndexConfig {
53
58
  hasFrontmatter: boolean;
54
59
  }
55
60
 
56
- // Configuration
57
- const INDEX_CONFIGS: Record<string, IndexConfig> = {
58
- docs: {
59
- name: "docs",
60
- paths: ["docs/"],
61
- extensions: [".md"],
62
- description: "Project documentation for all agents",
63
- hasFrontmatter: true,
64
- },
65
- curator: {
66
- name: "curator",
67
- paths: [
68
- ".claude/agents/",
69
- ".claude/hooks/",
70
- ".claude/skills/",
71
- ".claude/commands/",
72
- ".claude/output-styles/",
73
- ".claude/envoy/README.md",
74
- ".claude/settings.json",
75
- ".claude/envoy/src/",
76
- ".claude/envoy/package.json",
77
- ],
78
- extensions: [".md", ".yaml", ".yml", ".ts", ".json"],
79
- description: ".claude/ files for curator agent",
80
- hasFrontmatter: false,
81
- },
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,
82
68
  };
83
69
 
84
70
  // File reference patterns for auto-populating relevant_files
@@ -90,14 +76,14 @@ const FILE_REF_PATTERNS = [
90
76
 
91
77
  // Environment config with defaults
92
78
  const SEARCH_SIMILARITY_THRESHOLD = parseFloat(
93
- process.env.SEARCH_SIMILARITY_THRESHOLD ?? "0.64"
79
+ process.env.SEARCH_SIMILARITY_THRESHOLD ?? "0.65"
94
80
  );
95
81
  const SEARCH_CONTEXT_TOKEN_LIMIT = parseInt(
96
82
  process.env.SEARCH_CONTEXT_TOKEN_LIMIT ?? "5000",
97
83
  10
98
84
  );
99
85
  const SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD = parseFloat(
100
- process.env.SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD ?? "0.72"
86
+ process.env.SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD ?? "0.82"
101
87
  );
102
88
 
103
89
  export class KnowledgeService {
@@ -120,14 +106,101 @@ export class KnowledgeService {
120
106
  }
121
107
 
122
108
  /**
123
- * Lazy-load embedding model (first use downloads ~100-400MB)
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
124
188
  */
125
189
  async getModel(): Promise<unknown> {
126
190
  if (this.model) return this.model;
127
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
+
128
199
  const { TextModel } = await import("@visheratin/web-ai-node/text");
129
200
  const modelResult = await TextModel.create("gtr-t5-quant");
130
201
  this.model = modelResult.model;
202
+
203
+ console.error(`[knowledge] Model loaded in ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
131
204
  return this.model;
132
205
  }
133
206
 
@@ -153,10 +226,10 @@ export class KnowledgeService {
153
226
  /**
154
227
  * Get index file paths
155
228
  */
156
- private getIndexPaths(indexName: string): { index: string; meta: string } {
229
+ private getIndexPaths(): { index: string; meta: string } {
157
230
  return {
158
- index: join(this.knowledgeDir, `${indexName}.usearch`),
159
- meta: join(this.knowledgeDir, `${indexName}.meta.json`),
231
+ index: join(this.knowledgeDir, "docs.usearch"),
232
+ meta: join(this.knowledgeDir, "docs.meta.json"),
160
233
  };
161
234
  }
162
235
 
@@ -188,10 +261,8 @@ export class KnowledgeService {
188
261
  /**
189
262
  * Load index + metadata from disk
190
263
  */
191
- async loadIndex(
192
- indexName: string
193
- ): Promise<{ index: Index; meta: IndexMetadata }> {
194
- const paths = this.getIndexPaths(indexName);
264
+ async loadIndex(): Promise<{ index: Index; meta: IndexMetadata }> {
265
+ const paths = this.getIndexPaths();
195
266
 
196
267
  if (!existsSync(paths.index) || !existsSync(paths.meta)) {
197
268
  return {
@@ -210,13 +281,9 @@ export class KnowledgeService {
210
281
  /**
211
282
  * Save index + metadata to disk
212
283
  */
213
- async saveIndex(
214
- indexName: string,
215
- index: Index,
216
- meta: IndexMetadata
217
- ): Promise<void> {
284
+ async saveIndex(index: Index, meta: IndexMetadata): Promise<void> {
218
285
  this.ensureDir();
219
- const paths = this.getIndexPaths(indexName);
286
+ const paths = this.getIndexPaths();
220
287
 
221
288
  meta.lastUpdated = new Date().toISOString();
222
289
  index.save(paths.index);
@@ -356,10 +423,11 @@ export class KnowledgeService {
356
423
  }
357
424
 
358
425
  /**
359
- * Search with similarity computation
426
+ * Search docs index with similarity computation
427
+ * @param metadataOnly - If true, only return file paths and descriptions (no full_resource_context)
360
428
  */
361
- async search(indexName: string, query: string, k: number = 50): Promise<SearchResult[]> {
362
- const { index, meta } = await this.loadIndex(indexName);
429
+ async search(query: string, k: number = 50, metadataOnly: boolean = false): Promise<SearchResult[]> {
430
+ const { index, meta } = await this.loadIndex();
363
431
 
364
432
  if (Object.keys(meta.documents).length === 0) {
365
433
  return [];
@@ -403,8 +471,8 @@ export class KnowledgeService {
403
471
  relevant_files: docMeta.relevant_files,
404
472
  };
405
473
 
406
- // Include full context for high-similarity results
407
- if (similarity >= SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD) {
474
+ // Include full context for high-similarity results (unless metadata-only mode)
475
+ if (!metadataOnly && similarity >= SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD) {
408
476
  const fullPath = join(this.projectRoot, path);
409
477
  if (existsSync(fullPath)) {
410
478
  result.full_resource_context = readFileSync(fullPath, "utf-8");
@@ -418,86 +486,76 @@ export class KnowledgeService {
418
486
  }
419
487
 
420
488
  /**
421
- * Full reindex for an index
489
+ * Full reindex of docs
422
490
  */
423
- async reindexAll(indexName?: string): Promise<Record<string, ReindexResult>> {
491
+ async reindexAll(): Promise<ReindexResult> {
424
492
  this.ensureDir();
425
- const results: Record<string, ReindexResult> = {};
493
+ const startTime = Date.now();
494
+ console.error("[knowledge] Reindexing docs...");
426
495
 
427
- const indexes = indexName ? [indexName] : Object.keys(INDEX_CONFIGS);
428
-
429
- for (const name of indexes) {
430
- const config = INDEX_CONFIGS[name];
431
- if (!config) {
432
- throw new Error(`Unknown index: ${name}`);
433
- }
434
-
435
- // Create fresh index
436
- const index = this.createIndex();
437
- const meta = this.createEmptyMetadata();
438
-
439
- // Discover and index files
440
- const files = this.discoverFiles(config);
441
- let totalTokens = 0;
496
+ // Create fresh index
497
+ const index = this.createIndex();
498
+ const meta = this.createEmptyMetadata();
442
499
 
443
- for (const filePath of files) {
444
- const fullPath = join(this.projectRoot, filePath);
445
- const content = readFileSync(fullPath, "utf-8");
500
+ // Discover and index files
501
+ const files = this.discoverFiles(DOCS_CONFIG);
502
+ console.error(`[knowledge] Found ${files.length} files`);
503
+ let totalTokens = 0;
446
504
 
447
- // Parse front-matter only for indexes that support it
448
- let frontMatter: Record<string, unknown> = {};
449
- if (config.hasFrontmatter && filePath.endsWith(".md")) {
450
- try {
451
- const parsed = matter(content);
452
- frontMatter = parsed.data;
453
- } catch {
454
- // Skip files with invalid front-matter
455
- }
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
456
518
  }
457
-
458
- await this.indexDocument(index, meta, filePath, content, frontMatter);
459
- totalTokens += meta.documents[filePath].token_count;
460
519
  }
461
520
 
462
- // Save
463
- await this.saveIndex(name, index, meta);
464
-
465
- results[name] = {
466
- files_indexed: files.length,
467
- total_tokens: totalTokens,
468
- };
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;
469
524
  }
470
525
 
471
- return results;
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
+ };
472
535
  }
473
536
 
474
537
  /**
475
538
  * Incremental reindex from changed files
476
539
  */
477
- async reindexFromChanges(
478
- indexName: string,
479
- changes: FileChange[]
480
- ): Promise<{
540
+ async reindexFromChanges(changes: FileChange[]): Promise<{
481
541
  success: boolean;
482
542
  message: string;
483
543
  missing_references?: { doc_path: string; missing_files: string[] }[];
484
544
  files: { path: string; action: string }[];
485
545
  }> {
486
- const config = INDEX_CONFIGS[indexName];
487
- if (!config) {
488
- throw new Error(`Unknown index: ${indexName}`);
489
- }
546
+ console.error(`[knowledge] Incremental reindex: ${changes.length} change(s)`);
547
+ const startTime = Date.now();
490
548
 
491
- const { index, meta } = await this.loadIndex(indexName);
549
+ const { index, meta } = await this.loadIndex();
492
550
  const processedFiles: { path: string; action: string }[] = [];
493
551
  const missingReferences: { doc_path: string; missing_files: string[] }[] = [];
494
552
 
495
553
  for (const change of changes) {
496
554
  const { path, added, deleted, modified } = change;
497
555
 
498
- // Check if file matches index config
499
- const matchesConfig = config.paths.some((p) => path.startsWith(p)) &&
500
- config.extensions.includes(extname(path));
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));
501
559
 
502
560
  if (!matchesConfig) continue;
503
561
 
@@ -511,6 +569,7 @@ export class KnowledgeService {
511
569
  delete meta.path_to_id[path];
512
570
  delete meta.documents[path];
513
571
  processedFiles.push({ path, action: "deleted" });
572
+ console.error(`[knowledge] Deleted: ${path}`);
514
573
  }
515
574
  } else if (added || modified) {
516
575
  const fullPath = join(this.projectRoot, path);
@@ -519,8 +578,8 @@ export class KnowledgeService {
519
578
  const content = readFileSync(fullPath, "utf-8");
520
579
  let frontMatter: Record<string, unknown> = {};
521
580
 
522
- // Only process front-matter and file references for indexes that support it
523
- if (config.hasFrontmatter && path.endsWith(".md")) {
581
+ // Process front-matter and file references
582
+ if (path.endsWith(".md")) {
524
583
  try {
525
584
  const parsed = matter(content);
526
585
  frontMatter = parsed.data;
@@ -546,13 +605,17 @@ export class KnowledgeService {
546
605
  }
547
606
 
548
607
  // Index document
608
+ const action = added ? "added" : "modified";
609
+ console.error(`[knowledge] Embedding (${action}): ${path}`);
549
610
  await this.indexDocument(index, meta, path, content, frontMatter);
550
- processedFiles.push({ path, action: added ? "added" : "modified" });
611
+ processedFiles.push({ path, action });
551
612
  }
552
613
  }
553
614
 
554
615
  // Save updated index
555
- await this.saveIndex(indexName, index, meta);
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`);
556
619
 
557
620
  if (missingReferences.length > 0) {
558
621
  return {
@@ -571,24 +634,13 @@ export class KnowledgeService {
571
634
  }
572
635
 
573
636
  /**
574
- * Check if indexes exist and are valid
637
+ * Check if docs index exists
575
638
  */
576
- async checkIndexes(): Promise<{ valid: string[]; missing: string[] }> {
577
- const valid: string[] = [];
578
- const missing: string[] = [];
579
-
580
- for (const name of Object.keys(INDEX_CONFIGS)) {
581
- const paths = this.getIndexPaths(name);
582
- if (existsSync(paths.index) && existsSync(paths.meta)) {
583
- valid.push(name);
584
- } else {
585
- missing.push(name);
586
- }
587
- }
588
-
589
- return { valid, missing };
639
+ async checkIndex(): Promise<{ exists: boolean }> {
640
+ const paths = this.getIndexPaths();
641
+ return { exists: existsSync(paths.index) && existsSync(paths.meta) };
590
642
  }
591
643
  }
592
644
 
593
- export { INDEX_CONFIGS };
594
- export type { SearchResult, ReindexResult, FileChange, IndexMetadata, DocumentMeta };
645
+ export type { DocumentMeta, FileChange, IndexMetadata, ReindexResult, SearchResult };
646
+