all-hands-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.allhands/README.md +75 -0
- package/.allhands/agents/compounder.yaml +15 -0
- package/.allhands/agents/coordinator.yaml +17 -0
- package/.allhands/agents/documentor.yaml +15 -0
- package/.allhands/agents/e2e-test-planner.yaml +17 -0
- package/.allhands/agents/emergent.yaml +22 -0
- package/.allhands/agents/executor.yaml +14 -0
- package/.allhands/agents/ideation.yaml +11 -0
- package/.allhands/agents/initiative-steering.yaml +19 -0
- package/.allhands/agents/judge.yaml +13 -0
- package/.allhands/agents/planner.yaml +19 -0
- package/.allhands/agents/pr-reviewer.yaml +15 -0
- package/.allhands/docs.json +5 -0
- package/.allhands/docs.local.json +26 -0
- package/.allhands/flows/COMPOUNDING.md +203 -0
- package/.allhands/flows/COORDINATION.md +89 -0
- package/.allhands/flows/CORE.md +87 -0
- package/.allhands/flows/DOCUMENTATION.md +218 -0
- package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
- package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
- package/.allhands/flows/IDEATION_SCOPING.md +154 -0
- package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
- package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
- package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
- package/.allhands/flows/PR_REVIEWING.md +43 -0
- package/.allhands/flows/SPEC_PLANNING.md +216 -0
- package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
- package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
- package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
- package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
- package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
- package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
- package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
- package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
- package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
- package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
- package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
- package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
- package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
- package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
- package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
- package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
- package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
- package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
- package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
- package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
- package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
- package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
- package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
- package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
- package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
- package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
- package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
- package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
- package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
- package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
- package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
- package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
- package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
- package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
- package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
- package/.allhands/harness/ah +131 -0
- package/.allhands/harness/package-lock.json +5292 -0
- package/.allhands/harness/package.json +52 -0
- package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
- package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
- package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
- package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
- package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
- package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
- package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
- package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
- package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
- package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
- package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
- package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
- package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
- package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
- package/.allhands/harness/src/cli.ts +36 -0
- package/.allhands/harness/src/commands/complexity.ts +177 -0
- package/.allhands/harness/src/commands/context7.ts +202 -0
- package/.allhands/harness/src/commands/docs.ts +557 -0
- package/.allhands/harness/src/commands/hooks.ts +24 -0
- package/.allhands/harness/src/commands/index.ts +51 -0
- package/.allhands/harness/src/commands/knowledge.ts +382 -0
- package/.allhands/harness/src/commands/memories.ts +302 -0
- package/.allhands/harness/src/commands/notify.ts +61 -0
- package/.allhands/harness/src/commands/oracle.ts +158 -0
- package/.allhands/harness/src/commands/perplexity.ts +220 -0
- package/.allhands/harness/src/commands/planning.ts +245 -0
- package/.allhands/harness/src/commands/schema.ts +73 -0
- package/.allhands/harness/src/commands/skills.ts +128 -0
- package/.allhands/harness/src/commands/solutions.ts +353 -0
- package/.allhands/harness/src/commands/spawn.ts +158 -0
- package/.allhands/harness/src/commands/specs.ts +532 -0
- package/.allhands/harness/src/commands/tavily.ts +226 -0
- package/.allhands/harness/src/commands/tools.ts +579 -0
- package/.allhands/harness/src/commands/trace.ts +327 -0
- package/.allhands/harness/src/commands/tui.ts +960 -0
- package/.allhands/harness/src/commands/validate.ts +143 -0
- package/.allhands/harness/src/commands/validation-tools.ts +108 -0
- package/.allhands/harness/src/hooks/context.ts +1442 -0
- package/.allhands/harness/src/hooks/enforcement.ts +170 -0
- package/.allhands/harness/src/hooks/index.ts +54 -0
- package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
- package/.allhands/harness/src/hooks/notification.ts +104 -0
- package/.allhands/harness/src/hooks/observability.ts +551 -0
- package/.allhands/harness/src/hooks/session.ts +88 -0
- package/.allhands/harness/src/hooks/shared.ts +815 -0
- package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
- package/.allhands/harness/src/hooks/validation.ts +617 -0
- package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
- package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
- package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
- package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
- package/.allhands/harness/src/lib/base-command.ts +198 -0
- package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
- package/.allhands/harness/src/lib/compaction.ts +313 -0
- package/.allhands/harness/src/lib/ctags.ts +497 -0
- package/.allhands/harness/src/lib/docs-validation.ts +907 -0
- package/.allhands/harness/src/lib/event-loop.ts +662 -0
- package/.allhands/harness/src/lib/flows.ts +155 -0
- package/.allhands/harness/src/lib/git.ts +276 -0
- package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
- package/.allhands/harness/src/lib/knowledge.ts +810 -0
- package/.allhands/harness/src/lib/llm.ts +255 -0
- package/.allhands/harness/src/lib/mcp-client.ts +432 -0
- package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
- package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
- package/.allhands/harness/src/lib/notification.ts +115 -0
- package/.allhands/harness/src/lib/opencode/index.ts +70 -0
- package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
- package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
- package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
- package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
- package/.allhands/harness/src/lib/oracle.ts +926 -0
- package/.allhands/harness/src/lib/planning-utils.ts +150 -0
- package/.allhands/harness/src/lib/planning.ts +605 -0
- package/.allhands/harness/src/lib/pr-review.ts +225 -0
- package/.allhands/harness/src/lib/prompts.ts +522 -0
- package/.allhands/harness/src/lib/schema.ts +418 -0
- package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
- package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
- package/.allhands/harness/src/lib/session.ts +164 -0
- package/.allhands/harness/src/lib/specs.ts +348 -0
- package/.allhands/harness/src/lib/tldr.ts +829 -0
- package/.allhands/harness/src/lib/tmux.ts +1051 -0
- package/.allhands/harness/src/lib/trace-store.ts +714 -0
- package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
- package/.allhands/harness/src/mcp/_template.ts +47 -0
- package/.allhands/harness/src/mcp/filesystem.ts +33 -0
- package/.allhands/harness/src/mcp/index.ts +69 -0
- package/.allhands/harness/src/mcp/playwright.ts +34 -0
- package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
- package/.allhands/harness/src/schemas/docs.schema.json +44 -0
- package/.allhands/harness/src/schemas/settings.schema.json +214 -0
- package/.allhands/harness/src/tui/actions.ts +227 -0
- package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
- package/.allhands/harness/src/tui/index.ts +1574 -0
- package/.allhands/harness/src/tui/modal.ts +232 -0
- package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
- package/.allhands/harness/src/tui/status-pane.ts +434 -0
- package/.allhands/harness/tsconfig.json +22 -0
- package/.allhands/harness/vitest.config.ts +13 -0
- package/.allhands/pillars.md +33 -0
- package/.allhands/principles.md +88 -0
- package/.allhands/schemas/alignment.yaml +51 -0
- package/.allhands/schemas/documentation.yaml +10 -0
- package/.allhands/schemas/prompt.yaml +92 -0
- package/.allhands/schemas/skill.yaml +34 -0
- package/.allhands/schemas/solution.yaml +131 -0
- package/.allhands/schemas/spec.yaml +67 -0
- package/.allhands/schemas/validation-suite.yaml +49 -0
- package/.allhands/schemas/workflow.yaml +51 -0
- package/.allhands/settings.json +57 -0
- package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
- package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
- package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
- package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
- package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
- package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
- package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
- package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
- package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
- package/.allhands/validation/browser-automation.md +109 -0
- package/.allhands/validation/xcode-automation.md +195 -0
- package/.allhands/workflows/documentation.md +86 -0
- package/.allhands/workflows/investigation.md +81 -0
- package/.allhands/workflows/milestone.md +91 -0
- package/.allhands/workflows/optimization.md +85 -0
- package/.allhands/workflows/refactor.md +99 -0
- package/.allhands/workflows/triage.md +81 -0
- package/.claude/README.md +1 -0
- package/.claude/agents/explorer.md +10 -0
- package/.claude/agents/researcher.md +11 -0
- package/.claude/agents/task-runner.md +8 -0
- package/.claude/settings.json +231 -0
- package/.env.ai.example +7 -0
- package/.github/workflows/npm-publish.yml +69 -0
- package/.internal.json +45 -0
- package/.tldr/config.json +11 -0
- package/.tldrignore +90 -0
- package/CLAUDE.md +6 -0
- package/README.md +98 -0
- package/bin/sync-cli.js +7552 -0
- package/concerns.md +7 -0
- package/docs/README.md +41 -0
- package/docs/agents/README.md +24 -0
- package/docs/agents/agent-configuration-system.md +86 -0
- package/docs/agents/execution-agents.md +50 -0
- package/docs/agents/knowledge-agents.md +61 -0
- package/docs/agents/orchestration-agent.md +57 -0
- package/docs/agents/planning-agents.md +84 -0
- package/docs/agents/quality-review-agents.md +67 -0
- package/docs/agents/workflow-agent-orchestration.md +69 -0
- package/docs/flows/README.md +44 -0
- package/docs/flows/compounding.md +126 -0
- package/docs/flows/coordination.md +72 -0
- package/docs/flows/core-harness-integration.md +63 -0
- package/docs/flows/documentation-orchestration.md +98 -0
- package/docs/flows/e2e-test-plan-building.md +83 -0
- package/docs/flows/emergent-refinement.md +104 -0
- package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
- package/docs/flows/judge-reviewing.md +112 -0
- package/docs/flows/plan-deepening-and-research.md +107 -0
- package/docs/flows/plan-review-jury.md +114 -0
- package/docs/flows/pr-reviewing.md +54 -0
- package/docs/flows/prompt-task-execution.md +119 -0
- package/docs/flows/spec-planning.md +162 -0
- package/docs/flows/type-specific-scoping-flows.md +49 -0
- package/docs/flows/validation-and-skills-integration.md +145 -0
- package/docs/flows/wip/wip-flows.md +102 -0
- package/docs/harness/README.md +23 -0
- package/docs/harness/agent-profiles.md +84 -0
- package/docs/harness/cli/README.md +24 -0
- package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
- package/docs/harness/cli/docs-command.md +87 -0
- package/docs/harness/cli/knowledge-command.md +91 -0
- package/docs/harness/cli/minor-cli-commands.md +65 -0
- package/docs/harness/cli/oracle-command.md +113 -0
- package/docs/harness/cli/planning-command.md +95 -0
- package/docs/harness/cli/schema-and-validation-commands.md +154 -0
- package/docs/harness/cli/search-commands.md +97 -0
- package/docs/harness/cli/spawn-command.md +136 -0
- package/docs/harness/cli/specs-command.md +102 -0
- package/docs/harness/cli/tools-command.md +122 -0
- package/docs/harness/cli/trace-command.md +122 -0
- package/docs/harness/cli-daemon.md +92 -0
- package/docs/harness/event-loop.md +184 -0
- package/docs/harness/hooks/README.md +15 -0
- package/docs/harness/hooks/context-hooks.md +96 -0
- package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
- package/docs/harness/hooks/validation-hooks.md +97 -0
- package/docs/harness/test-harness.md +149 -0
- package/docs/harness/tui.md +176 -0
- package/docs/memories.md +20 -0
- package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
- package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
- package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
- package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
- package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
- package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
- package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
- package/docs/sync-cli/README.md +19 -0
- package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
- package/docs/sync-cli/commands/README.md +11 -0
- package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
- package/docs/sync-cli/commands/push-command.md +84 -0
- package/docs/sync-cli/commands/sync-command.md +71 -0
- package/docs/sync-cli/systems/README.md +14 -0
- package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
- package/docs/sync-cli/systems/interactive-ui.md +43 -0
- package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
- package/docs/sync-cli/systems/path-resolution.md +42 -0
- package/package.json +46 -0
- package/scripts/install-shim.sh +40 -0
- package/scripts/pre-pack.sh +25 -0
- package/specs/harness-maintenance-skill.spec.md +138 -0
- package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
- package/specs/sync-init-flag.spec.md +117 -0
- package/specs/unified-workflow-orchestration.spec.md +250 -0
- package/specs/validation-tooling-practice.spec.md +98 -0
- package/specs/workflow-domain-configuration.spec.md +265 -0
- package/src/commands/pull-manifest.ts +31 -0
- package/src/commands/push.ts +344 -0
- package/src/commands/sync.ts +289 -0
- package/src/lib/constants.ts +10 -0
- package/src/lib/dotfiles.ts +36 -0
- package/src/lib/fs-utils.ts +18 -0
- package/src/lib/gh.ts +40 -0
- package/src/lib/git.ts +63 -0
- package/src/lib/gitignore.ts +167 -0
- package/src/lib/manifest.ts +121 -0
- package/src/lib/marker-sync.ts +39 -0
- package/src/lib/paths.ts +38 -0
- package/src/lib/target-lines.ts +66 -0
- package/src/lib/ui.ts +78 -0
- package/src/sync-cli.ts +120 -0
- package/target-lines.json +23 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,810 @@
|
|
|
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 { spawn } from "child_process";
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
8
|
+
import matter from "gray-matter";
|
|
9
|
+
import { basename, extname, join, relative } from "path";
|
|
10
|
+
import { Index, MetricKind, ScalarKind } from "usearch";
|
|
11
|
+
import { loadProjectSettings } from "../hooks/shared.js";
|
|
12
|
+
|
|
13
|
+
// Types
|
|
14
|
+
interface DocumentMeta {
|
|
15
|
+
description: string;
|
|
16
|
+
relevant_files: string[];
|
|
17
|
+
token_count: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface IndexMetadata {
|
|
21
|
+
id_to_path: Record<string, string>;
|
|
22
|
+
path_to_id: Record<string, string>;
|
|
23
|
+
documents: Record<string, DocumentMeta>;
|
|
24
|
+
next_id: number;
|
|
25
|
+
lastUpdated: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SearchResult {
|
|
29
|
+
resource_path: string;
|
|
30
|
+
similarity: number;
|
|
31
|
+
token_count: number;
|
|
32
|
+
description: string;
|
|
33
|
+
relevant_files: string[];
|
|
34
|
+
full_resource_context?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ReindexResult {
|
|
38
|
+
files_indexed: number;
|
|
39
|
+
total_tokens: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface FileChange {
|
|
43
|
+
path: string;
|
|
44
|
+
added?: boolean;
|
|
45
|
+
deleted?: boolean;
|
|
46
|
+
modified?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface IndexConfig {
|
|
50
|
+
name: string;
|
|
51
|
+
paths: string[];
|
|
52
|
+
extensions: string[];
|
|
53
|
+
description: string;
|
|
54
|
+
/** Whether this index expects front-matter with description/relevant_files */
|
|
55
|
+
hasFrontmatter: boolean;
|
|
56
|
+
/** Whether to strip frontmatter from content before embedding */
|
|
57
|
+
stripFrontmatter?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Index configurations
|
|
61
|
+
const INDEX_CONFIGS: Record<string, IndexConfig> = {
|
|
62
|
+
docs: {
|
|
63
|
+
name: "docs",
|
|
64
|
+
paths: ["docs/"],
|
|
65
|
+
extensions: [".md"],
|
|
66
|
+
description: "Project documentation and specifications",
|
|
67
|
+
hasFrontmatter: true,
|
|
68
|
+
stripFrontmatter: true,
|
|
69
|
+
},
|
|
70
|
+
roadmap: {
|
|
71
|
+
name: "roadmap",
|
|
72
|
+
paths: ["specs/roadmap/"],
|
|
73
|
+
extensions: [".md"],
|
|
74
|
+
description: "Roadmap specifications (planned work)",
|
|
75
|
+
hasFrontmatter: true,
|
|
76
|
+
stripFrontmatter: true,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type IndexName = keyof typeof INDEX_CONFIGS;
|
|
81
|
+
|
|
82
|
+
// Config with settings override > default
|
|
83
|
+
const settings = loadProjectSettings();
|
|
84
|
+
const SEARCH_SIMILARITY_THRESHOLD = settings?.knowledge?.similarityThreshold ?? 0.65;
|
|
85
|
+
const SEARCH_CONTEXT_TOKEN_LIMIT = settings?.knowledge?.contextTokenLimit ?? 5000;
|
|
86
|
+
const SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD = settings?.knowledge?.fullContextSimilarityThreshold ?? 0.82;
|
|
87
|
+
|
|
88
|
+
export class KnowledgeService {
|
|
89
|
+
private model: unknown = null;
|
|
90
|
+
private readonly knowledgeDir: string;
|
|
91
|
+
private readonly projectRoot: string;
|
|
92
|
+
private readonly quiet: boolean;
|
|
93
|
+
|
|
94
|
+
constructor(projectRoot: string, options?: { quiet?: boolean }) {
|
|
95
|
+
this.projectRoot = projectRoot;
|
|
96
|
+
this.quiet = options?.quiet ?? false;
|
|
97
|
+
// Store knowledge index in .allhands/harness/.knowledge
|
|
98
|
+
this.knowledgeDir = join(projectRoot, ".allhands", "harness", ".knowledge");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private log(message: string): void {
|
|
102
|
+
if (!this.quiet) {
|
|
103
|
+
console.error(message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Ensure .knowledge/ directory exists
|
|
109
|
+
*/
|
|
110
|
+
ensureDir(): void {
|
|
111
|
+
if (!existsSync(this.knowledgeDir)) {
|
|
112
|
+
mkdirSync(this.knowledgeDir, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Lazy-load embedding model
|
|
118
|
+
* Note: web-ai-node handles its own model caching
|
|
119
|
+
*/
|
|
120
|
+
async getModel(): Promise<unknown> {
|
|
121
|
+
if (this.model) return this.model;
|
|
122
|
+
|
|
123
|
+
this.ensureDir();
|
|
124
|
+
this.log("[knowledge] Loading embedding model...");
|
|
125
|
+
const startTime = Date.now();
|
|
126
|
+
|
|
127
|
+
const { TextModel } = await import("@visheratin/web-ai-node/text");
|
|
128
|
+
const modelResult = await TextModel.create("gtr-t5-quant");
|
|
129
|
+
this.model = modelResult.model;
|
|
130
|
+
|
|
131
|
+
this.log(`[knowledge] Model loaded in ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
|
|
132
|
+
return this.model;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate embedding for text
|
|
137
|
+
*/
|
|
138
|
+
async embed(text: string): Promise<Float32Array> {
|
|
139
|
+
const model = await this.getModel();
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
141
|
+
const result = await (model as any).process(text);
|
|
142
|
+
return new Float32Array(result.result);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Convert cosine distance to similarity (0-1 scale)
|
|
148
|
+
* Cosine distance: 0 = identical, 1 = orthogonal, 2 = opposite
|
|
149
|
+
*/
|
|
150
|
+
distanceToSimilarity(distance: number): number {
|
|
151
|
+
return 1 - distance / 2;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get index file paths for a specific index
|
|
156
|
+
*/
|
|
157
|
+
private getIndexPaths(indexName: IndexName): { index: string; meta: string } {
|
|
158
|
+
return {
|
|
159
|
+
index: join(this.knowledgeDir, `${indexName}.usearch`),
|
|
160
|
+
meta: join(this.knowledgeDir, `${indexName}.meta.json`),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get config for a specific index
|
|
166
|
+
*/
|
|
167
|
+
private getIndexConfig(indexName: IndexName): IndexConfig {
|
|
168
|
+
const config = INDEX_CONFIGS[indexName];
|
|
169
|
+
if (!config) {
|
|
170
|
+
throw new Error(`Unknown index: ${indexName}. Available: ${Object.keys(INDEX_CONFIGS).join(", ")}`);
|
|
171
|
+
}
|
|
172
|
+
return config;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Create empty index metadata
|
|
177
|
+
*/
|
|
178
|
+
private createEmptyMetadata(): IndexMetadata {
|
|
179
|
+
return {
|
|
180
|
+
id_to_path: {},
|
|
181
|
+
path_to_id: {},
|
|
182
|
+
documents: {},
|
|
183
|
+
next_id: 0,
|
|
184
|
+
lastUpdated: new Date().toISOString(),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create a new USearch index
|
|
190
|
+
*/
|
|
191
|
+
private createIndex(): Index {
|
|
192
|
+
return new Index(
|
|
193
|
+
768, // dimensions
|
|
194
|
+
MetricKind.Cos, // metric
|
|
195
|
+
ScalarKind.F32, // quantization
|
|
196
|
+
16 // connectivity
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Load index + metadata from disk
|
|
202
|
+
*/
|
|
203
|
+
async loadIndex(indexName: IndexName): Promise<{ index: Index; meta: IndexMetadata }> {
|
|
204
|
+
const paths = this.getIndexPaths(indexName);
|
|
205
|
+
|
|
206
|
+
if (!existsSync(paths.index) || !existsSync(paths.meta)) {
|
|
207
|
+
return {
|
|
208
|
+
index: this.createIndex(),
|
|
209
|
+
meta: this.createEmptyMetadata(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const index = this.createIndex();
|
|
214
|
+
index.load(paths.index);
|
|
215
|
+
|
|
216
|
+
const meta: IndexMetadata = JSON.parse(readFileSync(paths.meta, "utf-8"));
|
|
217
|
+
return { index, meta };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Save index + metadata to disk
|
|
222
|
+
*/
|
|
223
|
+
async saveIndex(indexName: IndexName, index: Index, meta: IndexMetadata): Promise<void> {
|
|
224
|
+
this.ensureDir();
|
|
225
|
+
const paths = this.getIndexPaths(indexName);
|
|
226
|
+
|
|
227
|
+
meta.lastUpdated = new Date().toISOString();
|
|
228
|
+
index.save(paths.index);
|
|
229
|
+
writeFileSync(paths.meta, JSON.stringify(meta, null, 2));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Estimate token count (rough approximation: 1 token ≈ 4 chars)
|
|
234
|
+
*/
|
|
235
|
+
private estimateTokens(text: string): number {
|
|
236
|
+
return Math.ceil(text.length / 4);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Discover files for an index based on config
|
|
241
|
+
*/
|
|
242
|
+
private discoverFiles(config: IndexConfig): string[] {
|
|
243
|
+
const files: string[] = [];
|
|
244
|
+
|
|
245
|
+
for (const configPath of config.paths) {
|
|
246
|
+
const fullPath = join(this.projectRoot, configPath);
|
|
247
|
+
|
|
248
|
+
if (!existsSync(fullPath)) continue;
|
|
249
|
+
|
|
250
|
+
const stat = statSync(fullPath);
|
|
251
|
+
if (stat.isFile()) {
|
|
252
|
+
if (config.extensions.includes(extname(fullPath))) {
|
|
253
|
+
files.push(configPath);
|
|
254
|
+
}
|
|
255
|
+
} else if (stat.isDirectory()) {
|
|
256
|
+
this.walkDir(fullPath, config.extensions, files, this.projectRoot);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return files;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Recursively walk directory and collect files
|
|
265
|
+
*/
|
|
266
|
+
private walkDir(
|
|
267
|
+
dir: string,
|
|
268
|
+
extensions: string[],
|
|
269
|
+
files: string[],
|
|
270
|
+
projectRoot: string
|
|
271
|
+
): void {
|
|
272
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
273
|
+
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
const fullPath = join(dir, entry.name);
|
|
276
|
+
|
|
277
|
+
if (entry.isDirectory()) {
|
|
278
|
+
// Skip node_modules and hidden dirs (except .allhands)
|
|
279
|
+
if (entry.name === "node_modules" || (entry.name.startsWith(".") && entry.name !== ".allhands")) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
this.walkDir(fullPath, extensions, files, projectRoot);
|
|
283
|
+
} else if (entry.isFile() && extensions.includes(extname(entry.name))) {
|
|
284
|
+
// Exclude memories.md from docs indexing - project-specific learnings, not indexed for semantic search
|
|
285
|
+
if (entry.name === "memories.md") continue;
|
|
286
|
+
files.push(relative(projectRoot, fullPath));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Index a single document
|
|
293
|
+
*/
|
|
294
|
+
async indexDocument(
|
|
295
|
+
index: Index,
|
|
296
|
+
meta: IndexMetadata,
|
|
297
|
+
path: string,
|
|
298
|
+
content: string,
|
|
299
|
+
frontMatterData: Record<string, unknown>
|
|
300
|
+
): Promise<bigint> {
|
|
301
|
+
// Assign or reuse ID, removing old entry if exists
|
|
302
|
+
let id: bigint;
|
|
303
|
+
if (meta.path_to_id[path]) {
|
|
304
|
+
id = BigInt(meta.path_to_id[path]);
|
|
305
|
+
// Remove old entry before re-adding (usearch doesn't allow duplicate keys)
|
|
306
|
+
index.remove(id);
|
|
307
|
+
} else {
|
|
308
|
+
id = BigInt(meta.next_id++);
|
|
309
|
+
meta.id_to_path[id.toString()] = path;
|
|
310
|
+
meta.path_to_id[path] = id.toString();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Generate embedding
|
|
314
|
+
const embedding = await this.embed(content);
|
|
315
|
+
|
|
316
|
+
// Add to index
|
|
317
|
+
index.add(id, embedding);
|
|
318
|
+
|
|
319
|
+
// Store metadata
|
|
320
|
+
meta.documents[path] = {
|
|
321
|
+
description: (frontMatterData.description as string) || "",
|
|
322
|
+
relevant_files: (frontMatterData.relevant_files as string[]) || [],
|
|
323
|
+
token_count: this.estimateTokens(content),
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return id;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Search an index with similarity computation
|
|
331
|
+
* @param indexName - Which index to search (docs, specs)
|
|
332
|
+
* @param query - Search query
|
|
333
|
+
* @param k - Max results to return
|
|
334
|
+
* @param metadataOnly - If true, only return file paths and descriptions (no full_resource_context)
|
|
335
|
+
*/
|
|
336
|
+
async search(indexName: IndexName, query: string, k: number = 50, metadataOnly: boolean = false): Promise<SearchResult[]> {
|
|
337
|
+
const { index, meta } = await this.loadIndex(indexName);
|
|
338
|
+
|
|
339
|
+
if (Object.keys(meta.documents).length === 0) {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Generate query embedding
|
|
344
|
+
const queryEmbedding = await this.embed(query);
|
|
345
|
+
|
|
346
|
+
// Over-fetch to compensate for deleted entries that remain in vector index
|
|
347
|
+
// (USearch doesn't support deletion, so we filter in metadata post-search)
|
|
348
|
+
const searchK = Math.min(k * 2, index.size());
|
|
349
|
+
|
|
350
|
+
// Search (1 thread for CLI usage)
|
|
351
|
+
const searchResult = index.search(queryEmbedding, searchK, 1);
|
|
352
|
+
const keys = searchResult.keys;
|
|
353
|
+
const distances = searchResult.distances;
|
|
354
|
+
|
|
355
|
+
// Convert to results
|
|
356
|
+
const results: SearchResult[] = [];
|
|
357
|
+
let totalTokens = 0;
|
|
358
|
+
|
|
359
|
+
for (let i = 0; i < keys.length; i++) {
|
|
360
|
+
const id = keys[i].toString();
|
|
361
|
+
const distance = distances[i];
|
|
362
|
+
const similarity = this.distanceToSimilarity(distance);
|
|
363
|
+
|
|
364
|
+
// Filter by threshold
|
|
365
|
+
if (similarity < SEARCH_SIMILARITY_THRESHOLD) continue;
|
|
366
|
+
|
|
367
|
+
const path = meta.id_to_path[id];
|
|
368
|
+
if (!path) continue;
|
|
369
|
+
|
|
370
|
+
const docMeta = meta.documents[path];
|
|
371
|
+
if (!docMeta) continue;
|
|
372
|
+
|
|
373
|
+
// Check token limit
|
|
374
|
+
if (totalTokens + docMeta.token_count > SEARCH_CONTEXT_TOKEN_LIMIT) continue;
|
|
375
|
+
totalTokens += docMeta.token_count;
|
|
376
|
+
|
|
377
|
+
const result: SearchResult = {
|
|
378
|
+
resource_path: path,
|
|
379
|
+
similarity,
|
|
380
|
+
token_count: docMeta.token_count,
|
|
381
|
+
description: docMeta.description,
|
|
382
|
+
relevant_files: docMeta.relevant_files,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Include full context for high-similarity results (unless metadata-only mode)
|
|
386
|
+
if (!metadataOnly && similarity >= SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD) {
|
|
387
|
+
const fullPath = join(this.projectRoot, path);
|
|
388
|
+
if (existsSync(fullPath)) {
|
|
389
|
+
result.full_resource_context = readFileSync(fullPath, "utf-8");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
results.push(result);
|
|
394
|
+
|
|
395
|
+
// Stop once we have enough results (we over-fetched to handle deleted entries)
|
|
396
|
+
if (results.length >= k) break;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return results;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Full reindex of a specific index
|
|
404
|
+
*/
|
|
405
|
+
async reindexAll(indexName: IndexName): Promise<ReindexResult> {
|
|
406
|
+
this.ensureDir();
|
|
407
|
+
const config = this.getIndexConfig(indexName);
|
|
408
|
+
const startTime = Date.now();
|
|
409
|
+
this.log(`[knowledge] Reindexing ${indexName}...`);
|
|
410
|
+
|
|
411
|
+
// Create fresh index
|
|
412
|
+
const index = this.createIndex();
|
|
413
|
+
const meta = this.createEmptyMetadata();
|
|
414
|
+
|
|
415
|
+
// Discover and index files
|
|
416
|
+
const files = this.discoverFiles(config);
|
|
417
|
+
this.log(`[knowledge] Found ${files.length} files`);
|
|
418
|
+
let totalTokens = 0;
|
|
419
|
+
|
|
420
|
+
for (let i = 0; i < files.length; i++) {
|
|
421
|
+
const filePath = files[i];
|
|
422
|
+
const fullPath = join(this.projectRoot, filePath);
|
|
423
|
+
const rawContent = readFileSync(fullPath, "utf-8");
|
|
424
|
+
|
|
425
|
+
// Parse front-matter
|
|
426
|
+
let frontMatter: Record<string, unknown> = {};
|
|
427
|
+
let contentForEmbedding = rawContent;
|
|
428
|
+
|
|
429
|
+
if (filePath.endsWith(".md")) {
|
|
430
|
+
try {
|
|
431
|
+
const parsed = matter(rawContent);
|
|
432
|
+
frontMatter = parsed.data;
|
|
433
|
+
// Strip frontmatter from content for embedding if configured
|
|
434
|
+
if (config.stripFrontmatter) {
|
|
435
|
+
contentForEmbedding = parsed.content;
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
// Skip files with invalid front-matter
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.log(`[knowledge] Embedding ${i + 1}/${files.length}: ${filePath}`);
|
|
443
|
+
await this.indexDocument(index, meta, filePath, contentForEmbedding, frontMatter);
|
|
444
|
+
totalTokens += meta.documents[filePath].token_count;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Save
|
|
448
|
+
await this.saveIndex(indexName, index, meta);
|
|
449
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
450
|
+
this.log(`[knowledge] Reindex complete: ${files.length} files, ${totalTokens} tokens in ${duration}s`);
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
files_indexed: files.length,
|
|
454
|
+
total_tokens: totalTokens,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Reindex all configured indexes
|
|
460
|
+
*/
|
|
461
|
+
async reindexAllIndexes(): Promise<Record<string, ReindexResult>> {
|
|
462
|
+
const results: Record<string, ReindexResult> = {};
|
|
463
|
+
for (const indexName of Object.keys(INDEX_CONFIGS) as IndexName[]) {
|
|
464
|
+
results[indexName] = await this.reindexAll(indexName);
|
|
465
|
+
}
|
|
466
|
+
return results;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Incremental reindex from changed files for a specific index
|
|
471
|
+
*/
|
|
472
|
+
async reindexFromChanges(indexName: IndexName, changes: FileChange[]): Promise<{
|
|
473
|
+
success: boolean;
|
|
474
|
+
message: string;
|
|
475
|
+
files: { path: string; action: string }[];
|
|
476
|
+
}> {
|
|
477
|
+
const config = this.getIndexConfig(indexName);
|
|
478
|
+
this.log(`[knowledge] Incremental reindex (${indexName}): ${changes.length} change(s)`);
|
|
479
|
+
const startTime = Date.now();
|
|
480
|
+
|
|
481
|
+
const { index, meta } = await this.loadIndex(indexName);
|
|
482
|
+
const processedFiles: { path: string; action: string }[] = [];
|
|
483
|
+
|
|
484
|
+
for (const change of changes) {
|
|
485
|
+
const { path, added, deleted, modified } = change;
|
|
486
|
+
|
|
487
|
+
// Check if file matches config (excluding memories.md)
|
|
488
|
+
const matchesConfig = config.paths.some((p: string) => path.startsWith(p)) &&
|
|
489
|
+
config.extensions.includes(extname(path)) &&
|
|
490
|
+
basename(path) !== "memories.md";
|
|
491
|
+
|
|
492
|
+
if (!matchesConfig) continue;
|
|
493
|
+
|
|
494
|
+
if (deleted) {
|
|
495
|
+
// Remove from index
|
|
496
|
+
const id = meta.path_to_id[path];
|
|
497
|
+
if (id) {
|
|
498
|
+
// Note: USearch doesn't have a remove method in basic API
|
|
499
|
+
// We mark as deleted in metadata
|
|
500
|
+
delete meta.id_to_path[id];
|
|
501
|
+
delete meta.path_to_id[path];
|
|
502
|
+
delete meta.documents[path];
|
|
503
|
+
processedFiles.push({ path, action: "deleted" });
|
|
504
|
+
this.log(`[knowledge] Deleted: ${path}`);
|
|
505
|
+
}
|
|
506
|
+
} else if (added || modified) {
|
|
507
|
+
const fullPath = join(this.projectRoot, path);
|
|
508
|
+
if (!existsSync(fullPath)) continue;
|
|
509
|
+
|
|
510
|
+
const rawContent = readFileSync(fullPath, "utf-8");
|
|
511
|
+
let frontMatter: Record<string, unknown> = {};
|
|
512
|
+
let contentForEmbedding = rawContent;
|
|
513
|
+
|
|
514
|
+
// Process front-matter
|
|
515
|
+
if (path.endsWith(".md")) {
|
|
516
|
+
try {
|
|
517
|
+
const parsed = matter(rawContent);
|
|
518
|
+
frontMatter = parsed.data;
|
|
519
|
+
// Strip frontmatter from content for embedding if configured
|
|
520
|
+
if (config.stripFrontmatter) {
|
|
521
|
+
contentForEmbedding = parsed.content;
|
|
522
|
+
}
|
|
523
|
+
} catch {
|
|
524
|
+
// Skip files with invalid front-matter
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Index document
|
|
529
|
+
const action = added ? "added" : "modified";
|
|
530
|
+
this.log(`[knowledge] Embedding (${action}): ${path}`);
|
|
531
|
+
await this.indexDocument(index, meta, path, contentForEmbedding, frontMatter);
|
|
532
|
+
processedFiles.push({ path, action });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Save updated index
|
|
537
|
+
await this.saveIndex(indexName, index, meta);
|
|
538
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
539
|
+
this.log(`[knowledge] Incremental reindex complete: ${processedFiles.length} file(s) in ${duration}s`);
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
success: true,
|
|
543
|
+
message: "Index updated successfully",
|
|
544
|
+
files: processedFiles,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Check if a specific index exists
|
|
550
|
+
*/
|
|
551
|
+
indexExists(indexName: IndexName): boolean {
|
|
552
|
+
const paths = this.getIndexPaths(indexName);
|
|
553
|
+
return existsSync(paths.index) && existsSync(paths.meta);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Check if a specific index exists (async wrapper for backward compat)
|
|
558
|
+
*/
|
|
559
|
+
async checkIndex(indexName: IndexName): Promise<{ exists: boolean }> {
|
|
560
|
+
return { exists: this.indexExists(indexName) };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Get file changes since last index update for a specific index.
|
|
565
|
+
* Compares file modification times against last index update timestamp.
|
|
566
|
+
*/
|
|
567
|
+
getChangesFromGit(indexName: IndexName): FileChange[] {
|
|
568
|
+
const config = this.getIndexConfig(indexName);
|
|
569
|
+
const paths = this.getIndexPaths(indexName);
|
|
570
|
+
|
|
571
|
+
// If no metadata file, can't do incremental - need full reindex
|
|
572
|
+
if (!existsSync(paths.meta)) {
|
|
573
|
+
return [];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const meta: IndexMetadata = JSON.parse(readFileSync(paths.meta, "utf-8"));
|
|
577
|
+
const lastUpdated = meta.lastUpdated;
|
|
578
|
+
|
|
579
|
+
if (!lastUpdated) {
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const lastUpdateTime = new Date(lastUpdated).getTime();
|
|
584
|
+
const changes: FileChange[] = [];
|
|
585
|
+
const indexedPaths = new Set(Object.keys(meta.path_to_id));
|
|
586
|
+
|
|
587
|
+
// Check each configured path for changes
|
|
588
|
+
for (const configPath of config.paths) {
|
|
589
|
+
const fullConfigPath = join(this.projectRoot, configPath);
|
|
590
|
+
if (!existsSync(fullConfigPath)) continue;
|
|
591
|
+
|
|
592
|
+
// Find all matching files and check their modification times
|
|
593
|
+
this.findFilesRecursive(fullConfigPath, config.extensions).forEach(filePath => {
|
|
594
|
+
const relativePath = relative(this.projectRoot, filePath);
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
const stats = statSync(filePath);
|
|
598
|
+
const fileModTime = stats.mtimeMs;
|
|
599
|
+
|
|
600
|
+
if (!indexedPaths.has(relativePath)) {
|
|
601
|
+
// New file not in index
|
|
602
|
+
changes.push({ path: relativePath, added: true });
|
|
603
|
+
} else if (fileModTime > lastUpdateTime) {
|
|
604
|
+
// File modified since last index
|
|
605
|
+
changes.push({ path: relativePath, modified: true });
|
|
606
|
+
}
|
|
607
|
+
} catch {
|
|
608
|
+
// File access error - skip
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Check for deleted files (in index but not on disk)
|
|
614
|
+
Array.from(indexedPaths).forEach(indexedPath => {
|
|
615
|
+
const fullPath = join(this.projectRoot, indexedPath);
|
|
616
|
+
if (!existsSync(fullPath)) {
|
|
617
|
+
changes.push({ path: indexedPath, deleted: true });
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
return changes;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Find files recursively matching extensions
|
|
626
|
+
*/
|
|
627
|
+
private findFilesRecursive(dir: string, extensions: string[]): string[] {
|
|
628
|
+
const files: string[] = [];
|
|
629
|
+
if (!existsSync(dir)) return files;
|
|
630
|
+
|
|
631
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
632
|
+
const fullPath = join(dir, entry.name);
|
|
633
|
+
if (entry.isDirectory()) {
|
|
634
|
+
files.push(...this.findFilesRecursive(fullPath, extensions));
|
|
635
|
+
} else if (extensions.some(ext => entry.name.endsWith(ext)) && entry.name !== "memories.md") {
|
|
636
|
+
files.push(fullPath);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return files;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Check status of all indexes
|
|
644
|
+
*/
|
|
645
|
+
async checkAllIndexes(): Promise<Record<string, { exists: boolean }>> {
|
|
646
|
+
const results: Record<string, { exists: boolean }> = {};
|
|
647
|
+
for (const indexName of Object.keys(INDEX_CONFIGS) as IndexName[]) {
|
|
648
|
+
results[indexName] = await this.checkIndex(indexName);
|
|
649
|
+
}
|
|
650
|
+
return results;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Get available index names
|
|
655
|
+
*/
|
|
656
|
+
static getIndexNames(): string[] {
|
|
657
|
+
return Object.keys(INDEX_CONFIGS);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Spawn a child process to run reindexAll in an isolated worker.
|
|
663
|
+
* The ONNX model loads and dies entirely within the child process.
|
|
664
|
+
*/
|
|
665
|
+
export async function reindexAllInWorker(
|
|
666
|
+
projectRoot: string,
|
|
667
|
+
indexName: IndexName,
|
|
668
|
+
onProgress?: (message: string) => void,
|
|
669
|
+
): Promise<ReindexResult> {
|
|
670
|
+
const workerPath = join(import.meta.dirname, "knowledge-worker.ts");
|
|
671
|
+
|
|
672
|
+
return new Promise((resolve) => {
|
|
673
|
+
const child = spawn("npx", ["tsx", workerPath, "reindexAll", indexName, projectRoot], {
|
|
674
|
+
cwd: join(projectRoot, ".allhands", "harness"),
|
|
675
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
let stdoutBuffer = "";
|
|
679
|
+
let stderrBuffer = "";
|
|
680
|
+
|
|
681
|
+
child.stderr?.on("data", (data: Buffer) => {
|
|
682
|
+
stderrBuffer += data.toString();
|
|
683
|
+
const lines = stderrBuffer.split("\n");
|
|
684
|
+
stderrBuffer = lines.pop() || "";
|
|
685
|
+
for (const line of lines) {
|
|
686
|
+
if (line.trim() && onProgress) {
|
|
687
|
+
onProgress(line.trim());
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
child.stdout?.on("data", (data: Buffer) => {
|
|
693
|
+
stdoutBuffer += data.toString();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// 5 minute timeout (matching TLDR pattern)
|
|
697
|
+
const timeout = setTimeout(() => {
|
|
698
|
+
child.kill();
|
|
699
|
+
resolve({ files_indexed: 0, total_tokens: 0 });
|
|
700
|
+
}, 300000);
|
|
701
|
+
|
|
702
|
+
child.on("close", (code) => {
|
|
703
|
+
clearTimeout(timeout);
|
|
704
|
+
// Flush remaining stderr
|
|
705
|
+
if (stderrBuffer.trim() && onProgress) {
|
|
706
|
+
onProgress(stderrBuffer.trim());
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (code === 0 && stdoutBuffer.trim()) {
|
|
710
|
+
try {
|
|
711
|
+
const result = JSON.parse(stdoutBuffer.trim());
|
|
712
|
+
if (result.success) {
|
|
713
|
+
resolve({
|
|
714
|
+
files_indexed: result.files_indexed ?? 0,
|
|
715
|
+
total_tokens: result.total_tokens ?? 0,
|
|
716
|
+
});
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
720
|
+
// Parse error - fall through
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
resolve({ files_indexed: 0, total_tokens: 0 });
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
child.on("error", () => {
|
|
727
|
+
clearTimeout(timeout);
|
|
728
|
+
resolve({ files_indexed: 0, total_tokens: 0 });
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Spawn a child process to run reindexFromChanges in an isolated worker.
|
|
735
|
+
* Changes are passed via stdin as JSON.
|
|
736
|
+
*/
|
|
737
|
+
export async function reindexFromChangesInWorker(
|
|
738
|
+
projectRoot: string,
|
|
739
|
+
indexName: IndexName,
|
|
740
|
+
changes: FileChange[],
|
|
741
|
+
onProgress?: (message: string) => void,
|
|
742
|
+
): Promise<{ success: boolean; message: string; files: { path: string; action: string }[] }> {
|
|
743
|
+
const workerPath = join(import.meta.dirname, "knowledge-worker.ts");
|
|
744
|
+
|
|
745
|
+
return new Promise((resolve) => {
|
|
746
|
+
const child = spawn("npx", ["tsx", workerPath, "reindexFromChanges", indexName, projectRoot], {
|
|
747
|
+
cwd: join(projectRoot, ".allhands", "harness"),
|
|
748
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
let stdoutBuffer = "";
|
|
752
|
+
let stderrBuffer = "";
|
|
753
|
+
|
|
754
|
+
child.stderr?.on("data", (data: Buffer) => {
|
|
755
|
+
stderrBuffer += data.toString();
|
|
756
|
+
const lines = stderrBuffer.split("\n");
|
|
757
|
+
stderrBuffer = lines.pop() || "";
|
|
758
|
+
for (const line of lines) {
|
|
759
|
+
if (line.trim() && onProgress) {
|
|
760
|
+
onProgress(line.trim());
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
child.stdout?.on("data", (data: Buffer) => {
|
|
766
|
+
stdoutBuffer += data.toString();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Write changes to stdin
|
|
770
|
+
child.stdin?.write(JSON.stringify(changes));
|
|
771
|
+
child.stdin?.end();
|
|
772
|
+
|
|
773
|
+
// 5 minute timeout
|
|
774
|
+
const timeout = setTimeout(() => {
|
|
775
|
+
child.kill();
|
|
776
|
+
resolve({ success: false, message: "Worker timed out", files: [] });
|
|
777
|
+
}, 300000);
|
|
778
|
+
|
|
779
|
+
child.on("close", (code) => {
|
|
780
|
+
clearTimeout(timeout);
|
|
781
|
+
// Flush remaining stderr
|
|
782
|
+
if (stderrBuffer.trim() && onProgress) {
|
|
783
|
+
onProgress(stderrBuffer.trim());
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (code === 0 && stdoutBuffer.trim()) {
|
|
787
|
+
try {
|
|
788
|
+
const result = JSON.parse(stdoutBuffer.trim());
|
|
789
|
+
resolve({
|
|
790
|
+
success: result.success ?? false,
|
|
791
|
+
message: result.message ?? "Worker completed",
|
|
792
|
+
files: result.files ?? [],
|
|
793
|
+
});
|
|
794
|
+
return;
|
|
795
|
+
} catch {
|
|
796
|
+
// Parse error - fall through
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
resolve({ success: false, message: "Worker failed", files: [] });
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
child.on("error", () => {
|
|
803
|
+
clearTimeout(timeout);
|
|
804
|
+
resolve({ success: false, message: "Worker spawn failed", files: [] });
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export { INDEX_CONFIGS };
|
|
810
|
+
export type { DocumentMeta, FileChange, IndexMetadata, ReindexResult, SearchResult };
|