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.
- package/.claude/agents/code-simplifier.md +52 -0
- package/.claude/agents/curator.md +186 -246
- package/.claude/agents/documentation-taxonomist.md +255 -0
- package/.claude/agents/documentation-writer.md +366 -0
- package/.claude/agents/planner.md +123 -166
- package/.claude/agents/researcher.md +58 -41
- package/.claude/agents/surveyor.md +81 -0
- package/.claude/agents/worker.md +74 -0
- package/.claude/commands/continue.md +122 -0
- package/.claude/commands/create-skill.md +107 -0
- package/.claude/commands/create-specialist.md +111 -0
- package/.claude/commands/curator-audit.md +4 -0
- package/.claude/commands/debug.md +183 -0
- package/.claude/commands/docs-adjust.md +214 -0
- package/.claude/commands/docs-audit.md +172 -0
- package/.claude/commands/docs-init.md +210 -0
- package/.claude/commands/plan.md +199 -102
- package/.claude/commands/validate.md +11 -0
- package/.claude/commands/whats-next.md +106 -134
- package/.claude/envoy/README.md +5 -5
- package/.claude/envoy/envoy +11 -14
- package/.claude/envoy/package-lock.json +1594 -0
- package/.claude/envoy/package.json +38 -0
- package/.claude/envoy/src/cli.ts +126 -0
- package/.claude/envoy/src/commands/base.ts +216 -0
- package/.claude/envoy/src/commands/docs.ts +881 -0
- package/.claude/envoy/src/commands/gemini.ts +999 -0
- package/.claude/envoy/src/commands/git.ts +639 -0
- package/.claude/envoy/src/commands/index.ts +73 -0
- package/.claude/envoy/src/commands/knowledge.ts +178 -0
- package/.claude/envoy/src/commands/perplexity.ts +129 -0
- package/.claude/envoy/src/commands/plan/core.ts +134 -0
- package/.claude/envoy/src/commands/plan/findings.ts +446 -0
- package/.claude/envoy/src/commands/plan/gates.ts +672 -0
- package/.claude/envoy/src/commands/plan/index.ts +135 -0
- package/.claude/envoy/src/commands/plan/lifecycle.ts +648 -0
- package/.claude/envoy/src/commands/plan/plan-file.ts +138 -0
- package/.claude/envoy/src/commands/plan/prompts.ts +285 -0
- package/.claude/envoy/src/commands/plan/protocols.ts +166 -0
- package/.claude/envoy/src/commands/repomix.ts +99 -0
- package/.claude/envoy/src/commands/tavily.ts +220 -0
- package/.claude/envoy/src/commands/xai.ts +168 -0
- package/.claude/envoy/src/lib/ast-queries.ts +261 -0
- package/.claude/envoy/src/lib/design.ts +41 -0
- package/.claude/envoy/src/lib/feedback-schemas.ts +154 -0
- package/.claude/envoy/src/lib/findings.ts +215 -0
- package/.claude/envoy/src/lib/gates.ts +572 -0
- package/.claude/envoy/src/lib/git.ts +132 -0
- package/.claude/envoy/src/lib/index.ts +188 -0
- package/.claude/envoy/src/lib/knowledge.ts +646 -0
- package/.claude/envoy/src/lib/markdown.ts +75 -0
- package/.claude/envoy/src/lib/observability.ts +262 -0
- package/.claude/envoy/src/lib/paths.ts +130 -0
- package/.claude/envoy/src/lib/plan-io.ts +117 -0
- package/.claude/envoy/src/lib/prompts.ts +231 -0
- package/.claude/envoy/src/lib/protocols.ts +314 -0
- package/.claude/envoy/src/lib/repomix.ts +133 -0
- package/.claude/envoy/src/lib/retry.ts +138 -0
- package/.claude/envoy/src/lib/tree-sitter-utils.ts +301 -0
- package/.claude/envoy/src/lib/watcher.ts +167 -0
- package/.claude/envoy/src/types/tree-sitter.d.ts +76 -0
- package/.claude/envoy/tsconfig.json +21 -0
- package/.claude/hooks/scripts/enforce_research_fetch.py +1 -1
- package/.claude/hooks/scripts/scan_agents.py +62 -0
- package/.claude/hooks/scripts/scan_commands.py +50 -0
- package/.claude/hooks/scripts/scan_skills.py +46 -70
- package/.claude/hooks/scripts/validate_artifacts.py +128 -0
- package/.claude/hooks/startup.sh +26 -24
- package/.claude/protocols/bug-discovery.yaml +55 -0
- package/.claude/protocols/debugging.yaml +51 -0
- package/.claude/protocols/discovery.yaml +53 -0
- package/.claude/protocols/implementation.yaml +84 -0
- package/.claude/settings.json +38 -97
- package/.claude/skills/brainstorming/SKILL.md +54 -0
- package/.claude/skills/commands-development/SKILL.md +630 -0
- package/.claude/skills/commands-development/references/arguments.md +252 -0
- package/.claude/skills/commands-development/references/patterns.md +796 -0
- package/.claude/skills/commands-development/references/tool-restrictions.md +376 -0
- package/.claude/skills/discovery-mode/SKILL.md +108 -0
- package/.claude/skills/documentation-taxonomy/SKILL.md +287 -0
- package/.claude/skills/hooks-development/SKILL.md +332 -0
- package/.claude/skills/hooks-development/references/command-vs-prompt.md +269 -0
- package/.claude/skills/hooks-development/references/examples.md +658 -0
- package/.claude/skills/hooks-development/references/hook-types.md +463 -0
- package/.claude/skills/hooks-development/references/input-output-schemas.md +469 -0
- package/.claude/skills/hooks-development/references/matchers.md +470 -0
- package/.claude/skills/hooks-development/references/troubleshooting.md +587 -0
- package/.claude/skills/implementation-mode/SKILL.md +171 -0
- package/.claude/skills/knowledge-discovery/SKILL.md +178 -0
- package/.claude/skills/research-tools/SKILL.md +35 -33
- package/.claude/skills/skills-development/SKILL.md +192 -0
- package/.claude/skills/skills-development/references/api-security.md +226 -0
- package/.claude/skills/skills-development/references/be-clear-and-direct.md +531 -0
- package/.claude/skills/skills-development/references/common-patterns.md +595 -0
- package/.claude/skills/skills-development/references/core-principles.md +437 -0
- package/.claude/skills/skills-development/references/executable-code.md +175 -0
- package/.claude/skills/skills-development/references/iteration-and-testing.md +474 -0
- package/.claude/skills/skills-development/references/recommended-structure.md +168 -0
- package/.claude/skills/skills-development/references/skill-structure.md +372 -0
- package/.claude/skills/skills-development/references/use-xml-tags.md +466 -0
- package/.claude/skills/skills-development/references/using-scripts.md +113 -0
- package/.claude/skills/skills-development/references/using-templates.md +112 -0
- package/.claude/skills/skills-development/references/workflows-and-validation.md +510 -0
- package/.claude/skills/skills-development/templates/router-skill.md +73 -0
- package/.claude/skills/skills-development/templates/simple-skill.md +33 -0
- package/.claude/skills/skills-development/workflows/add-reference.md +96 -0
- package/.claude/skills/skills-development/workflows/add-script.md +93 -0
- package/.claude/skills/skills-development/workflows/add-template.md +74 -0
- package/.claude/skills/skills-development/workflows/add-workflow.md +120 -0
- package/.claude/skills/skills-development/workflows/audit-skill.md +138 -0
- package/.claude/skills/skills-development/workflows/create-domain-expertise-skill.md +605 -0
- package/.claude/skills/skills-development/workflows/create-new-skill.md +191 -0
- package/.claude/skills/skills-development/workflows/get-guidance.md +121 -0
- package/.claude/skills/skills-development/workflows/upgrade-to-router.md +161 -0
- package/.claude/skills/skills-development/workflows/verify-skill.md +204 -0
- package/.claude/skills/subagents-development/SKILL.md +325 -0
- package/.claude/skills/subagents-development/references/context-management.md +567 -0
- package/.claude/skills/subagents-development/references/debugging-agents.md +714 -0
- package/.claude/skills/subagents-development/references/error-handling-and-recovery.md +502 -0
- package/.claude/skills/subagents-development/references/evaluation-and-testing.md +374 -0
- package/.claude/skills/subagents-development/references/orchestration-patterns.md +591 -0
- package/.claude/skills/subagents-development/references/subagents.md +508 -0
- package/.claude/skills/subagents-development/references/writing-subagent-prompts.md +517 -0
- package/.claude/statusline.sh +24 -0
- package/bin/cli.js +150 -72
- package/package.json +1 -1
- package/.claude/agents/explorer.md +0 -62
- package/.claude/agents/parallel-worker.md +0 -121
- package/.claude/commands/curation-fix.md +0 -92
- package/.claude/commands/new-branch.md +0 -36
- package/.claude/commands/parallel-discovery.md +0 -69
- package/.claude/commands/parallel-orchestration.md +0 -99
- package/.claude/commands/plan-checkpoint.md +0 -37
- package/.claude/envoy/commands/__init__.py +0 -1
- package/.claude/envoy/commands/base.py +0 -95
- package/.claude/envoy/commands/parallel.py +0 -439
- package/.claude/envoy/commands/perplexity.py +0 -86
- package/.claude/envoy/commands/plans.py +0 -451
- package/.claude/envoy/commands/tavily.py +0 -156
- package/.claude/envoy/commands/vertex.py +0 -358
- package/.claude/envoy/commands/xai.py +0 -124
- package/.claude/envoy/envoy.py +0 -122
- package/.claude/envoy/pyrightconfig.json +0 -4
- package/.claude/envoy/requirements.txt +0 -2
- package/.claude/hooks/capture-queries.sh +0 -3
- package/.claude/hooks/scripts/enforce_planning.py +0 -118
- package/.claude/hooks/scripts/enforce_rg.py +0 -34
- package/.claude/hooks/scripts/validate_skill.py +0 -81
- package/.claude/skills/claude-envoy-curation/SKILL.md +0 -162
- package/.claude/skills/claude-envoy-usage/SKILL.md +0 -46
- package/.claude/skills/command-development/SKILL.md +0 -206
- package/.claude/skills/command-development/examples/simple-commands.md +0 -212
- package/.claude/skills/command-development/references/frontmatter-reference.md +0 -221
- package/.claude/skills/hook-development/SKILL.md +0 -127
- package/.claude/skills/hook-development/examples/command-hooks.md +0 -301
- package/.claude/skills/hook-development/examples/prompt-hooks.md +0 -114
- package/.claude/skills/hook-development/references/event-reference.md +0 -226
- package/.claude/skills/repomix-extraction/SKILL.md +0 -91
- package/.claude/skills/skill-development/SKILL.md +0 -168
- package/.claude/skills/skill-development/examples/complete-skill-examples.md +0 -281
- package/.claude/skills/skill-development/references/progressive-disclosure.md +0 -141
- package/.claude/skills/skill-development/references/writing-style.md +0 -180
- package/.claude/skills/skill-development/scripts/validate-skill.sh +0 -144
- package/.claude/skills/specialist-builder/SKILL.md +0 -327
- package/.claude/skills/specialist-builder/docs/agent-catalog.md +0 -28
- package/.claude/skills/specialist-builder/examples/complete-agent-examples.md +0 -206
- package/.claude/skills/specialist-builder/references/system-prompt-patterns.md +0 -281
- package/.claude/skills/specialist-builder/references/triggering-examples.md +0 -162
- package/.claude/skills/specialist-builder/scripts/validate-agent.sh +0 -137
- /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
|
+
}
|