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,829 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLDR Daemon Client
|
|
3
|
+
*
|
|
4
|
+
* Core library for communicating with the llm-tldr daemon.
|
|
5
|
+
* Provides token-efficient code analysis via AST/call-graph/DFG queries.
|
|
6
|
+
*
|
|
7
|
+
* All functions gracefully degrade if TLDR is not installed or daemon is unavailable.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync, spawnSync, spawn } from 'child_process';
|
|
11
|
+
import { existsSync, readFileSync } from 'fs';
|
|
12
|
+
import { createHash } from 'crypto';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { tmpdir } from 'os';
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// Types
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface DaemonResponse {
|
|
21
|
+
status: 'ok' | 'error' | 'indexing';
|
|
22
|
+
result?: unknown;
|
|
23
|
+
indexing?: boolean;
|
|
24
|
+
message?: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
callers?: unknown[]; // For impact analysis responses
|
|
27
|
+
// Notify command response fields (dirty file tracking)
|
|
28
|
+
dirty_count?: number;
|
|
29
|
+
threshold?: number;
|
|
30
|
+
reindex_triggered?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SearchResult {
|
|
34
|
+
file: string;
|
|
35
|
+
name: string;
|
|
36
|
+
type: string;
|
|
37
|
+
line: number;
|
|
38
|
+
signature?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ExtractSymbol {
|
|
42
|
+
name: string;
|
|
43
|
+
line_number: number;
|
|
44
|
+
signature?: string;
|
|
45
|
+
docstring?: string;
|
|
46
|
+
is_async?: boolean;
|
|
47
|
+
params?: string[];
|
|
48
|
+
return_type?: string;
|
|
49
|
+
decorators?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ExtractResult {
|
|
53
|
+
file_path: string;
|
|
54
|
+
language: string;
|
|
55
|
+
docstring?: string;
|
|
56
|
+
imports: Array<{ module: string; names: string[]; is_from: boolean }>;
|
|
57
|
+
classes: ExtractSymbol[];
|
|
58
|
+
functions: ExtractSymbol[];
|
|
59
|
+
call_graph?: {
|
|
60
|
+
calls: Record<string, string[]>;
|
|
61
|
+
called_by: Record<string, string[]>;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ContextResult {
|
|
66
|
+
entry: string;
|
|
67
|
+
callees: string[];
|
|
68
|
+
callers: string[];
|
|
69
|
+
depth: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface CFGResult {
|
|
73
|
+
file: string;
|
|
74
|
+
function: string;
|
|
75
|
+
nodes: Array<{
|
|
76
|
+
id: string;
|
|
77
|
+
type: string;
|
|
78
|
+
label: string;
|
|
79
|
+
line?: number;
|
|
80
|
+
}>;
|
|
81
|
+
edges: Array<{
|
|
82
|
+
from: string;
|
|
83
|
+
to: string;
|
|
84
|
+
label?: string;
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface DFGResult {
|
|
89
|
+
file: string;
|
|
90
|
+
function: string;
|
|
91
|
+
variables: Array<{
|
|
92
|
+
name: string;
|
|
93
|
+
definitions: number[];
|
|
94
|
+
uses: number[];
|
|
95
|
+
}>;
|
|
96
|
+
flows: Array<{
|
|
97
|
+
from: string;
|
|
98
|
+
to: string;
|
|
99
|
+
line: number;
|
|
100
|
+
}>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ArchResult {
|
|
104
|
+
layers: Array<{
|
|
105
|
+
name: string;
|
|
106
|
+
files: string[];
|
|
107
|
+
description?: string;
|
|
108
|
+
}>;
|
|
109
|
+
dependencies: Array<{
|
|
110
|
+
from: string;
|
|
111
|
+
to: string;
|
|
112
|
+
}>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface DiagnosticsResult {
|
|
116
|
+
file: string;
|
|
117
|
+
errors: Array<{
|
|
118
|
+
line: number;
|
|
119
|
+
column: number;
|
|
120
|
+
message: string;
|
|
121
|
+
severity: 'error' | 'warning' | 'info';
|
|
122
|
+
source: string;
|
|
123
|
+
}>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
127
|
+
// Detection & Availability
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if llm-tldr is installed.
|
|
132
|
+
*/
|
|
133
|
+
export function isTldrInstalled(): boolean {
|
|
134
|
+
try {
|
|
135
|
+
execSync('which tldr', { stdio: 'pipe' });
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get the socket path for the TLDR daemon.
|
|
144
|
+
* Format: {tmpdir}/tldr-{md5(projectDir).slice(0,8)}.sock
|
|
145
|
+
* Uses system tmpdir to match TLDR daemon behavior on macOS.
|
|
146
|
+
*/
|
|
147
|
+
export function getTldrSocketPath(projectDir: string): string {
|
|
148
|
+
const hash = createHash('md5').update(projectDir).digest('hex').slice(0, 8);
|
|
149
|
+
return join(tmpdir(), `tldr-${hash}.sock`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if the TLDR daemon is running for a project.
|
|
154
|
+
* Checks both socket-based daemon and status file.
|
|
155
|
+
*/
|
|
156
|
+
export function isTldrDaemonRunning(projectDir: string): boolean {
|
|
157
|
+
// Check socket-based daemon first
|
|
158
|
+
const socketPath = getTldrSocketPath(projectDir);
|
|
159
|
+
if (existsSync(socketPath)) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Fall back to status file check (used by newer TLDR versions)
|
|
164
|
+
const statusPath = join(projectDir, '.tldr', 'status');
|
|
165
|
+
if (existsSync(statusPath)) {
|
|
166
|
+
try {
|
|
167
|
+
const status = readFileSync(statusPath, 'utf-8').trim();
|
|
168
|
+
return status === 'ready';
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Start the TLDR daemon if not already running.
|
|
179
|
+
* Returns true if daemon is running (started or already was).
|
|
180
|
+
*/
|
|
181
|
+
export async function ensureTldrDaemon(projectDir: string): Promise<boolean> {
|
|
182
|
+
if (!isTldrInstalled()) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (isTldrDaemonRunning(projectDir)) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
execSync(`tldr daemon start --project "${projectDir}"`, {
|
|
192
|
+
stdio: 'ignore',
|
|
193
|
+
timeout: 5000,
|
|
194
|
+
});
|
|
195
|
+
// Give daemon a moment to start accepting connections
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
197
|
+
return isTldrDaemonRunning(projectDir);
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if semantic index exists for a project.
|
|
205
|
+
*/
|
|
206
|
+
export function hasSemanticIndex(projectDir: string): boolean {
|
|
207
|
+
const indexPath = join(projectDir, '.tldr', 'cache', 'semantic', 'index.faiss');
|
|
208
|
+
return existsSync(indexPath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Build semantic index for a project.
|
|
213
|
+
* This can take a while for large projects - runs synchronously with progress output.
|
|
214
|
+
* Returns true if index was built successfully.
|
|
215
|
+
*/
|
|
216
|
+
export function buildSemanticIndex(
|
|
217
|
+
projectDir: string,
|
|
218
|
+
lang: string = 'all'
|
|
219
|
+
): boolean {
|
|
220
|
+
if (!isTldrInstalled()) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
execSync(`tldr semantic index "${projectDir}" --lang ${lang}`, {
|
|
226
|
+
stdio: 'inherit', // Show progress to user
|
|
227
|
+
timeout: 300000, // 5 min timeout for large projects
|
|
228
|
+
});
|
|
229
|
+
// Track the branch that was indexed
|
|
230
|
+
trackIndexedBranch(projectDir);
|
|
231
|
+
return true;
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface SemanticIndexResult {
|
|
238
|
+
success: boolean;
|
|
239
|
+
filesIndexed: number;
|
|
240
|
+
languages: string[];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build semantic index asynchronously with progress reporting.
|
|
245
|
+
* Returns result object with success status, file count, and languages.
|
|
246
|
+
*
|
|
247
|
+
* @param projectDir - Project directory to index
|
|
248
|
+
* @param onProgress - Callback for progress messages (each line of output)
|
|
249
|
+
* @param lang - Language to index (default 'all')
|
|
250
|
+
*/
|
|
251
|
+
export async function buildSemanticIndexAsync(
|
|
252
|
+
projectDir: string,
|
|
253
|
+
onProgress?: (message: string) => void,
|
|
254
|
+
lang: string = 'all'
|
|
255
|
+
): Promise<SemanticIndexResult> {
|
|
256
|
+
if (!isTldrInstalled()) {
|
|
257
|
+
return { success: false, filesIndexed: 0, languages: [] };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return new Promise((resolve) => {
|
|
261
|
+
const child = spawn('tldr', ['semantic', 'index', projectDir, '--lang', lang], {
|
|
262
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Buffer for partial lines
|
|
266
|
+
let stdoutBuffer = '';
|
|
267
|
+
let stderrBuffer = '';
|
|
268
|
+
|
|
269
|
+
// Track file counts and languages from output
|
|
270
|
+
let totalFilesIndexed = 0;
|
|
271
|
+
const detectedLanguages: string[] = [];
|
|
272
|
+
|
|
273
|
+
const processLine = (line: string) => {
|
|
274
|
+
if (!line.trim()) return;
|
|
275
|
+
|
|
276
|
+
// Parse "Detected languages: javascript, typescript"
|
|
277
|
+
const langMatch = line.match(/Detected languages?:\s*(.+)/i);
|
|
278
|
+
if (langMatch) {
|
|
279
|
+
const langs = langMatch[1].split(',').map(l => l.trim());
|
|
280
|
+
detectedLanguages.push(...langs);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Parse "Indexed N files" or "Extracted N code units" patterns
|
|
284
|
+
const indexedMatch = line.match(/(?:Indexed|Extracted|Processed)\s+(\d+)\s+(?:files?|code units?)/i);
|
|
285
|
+
if (indexedMatch) {
|
|
286
|
+
totalFilesIndexed += parseInt(indexedMatch[1], 10);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Also check for patterns like "✓ 123 files indexed"
|
|
290
|
+
const filesMatch = line.match(/(\d+)\s+files?\s+indexed/i);
|
|
291
|
+
if (filesMatch) {
|
|
292
|
+
totalFilesIndexed = parseInt(filesMatch[1], 10); // Use this as the total
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (onProgress) {
|
|
296
|
+
onProgress(line.trim());
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
301
|
+
stdoutBuffer += data.toString();
|
|
302
|
+
const lines = stdoutBuffer.split('\n');
|
|
303
|
+
// Keep incomplete last line in buffer
|
|
304
|
+
stdoutBuffer = lines.pop() || '';
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
processLine(line);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
311
|
+
stderrBuffer += data.toString();
|
|
312
|
+
const lines = stderrBuffer.split('\n');
|
|
313
|
+
stderrBuffer = lines.pop() || '';
|
|
314
|
+
for (const line of lines) {
|
|
315
|
+
processLine(line);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
child.on('close', (code) => {
|
|
320
|
+
// Flush any remaining content in buffers
|
|
321
|
+
if (stdoutBuffer.trim()) {
|
|
322
|
+
processLine(stdoutBuffer);
|
|
323
|
+
}
|
|
324
|
+
if (stderrBuffer.trim()) {
|
|
325
|
+
processLine(stderrBuffer);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (code === 0) {
|
|
329
|
+
trackIndexedBranch(projectDir);
|
|
330
|
+
resolve({
|
|
331
|
+
success: true,
|
|
332
|
+
filesIndexed: totalFilesIndexed,
|
|
333
|
+
languages: detectedLanguages,
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
resolve({
|
|
337
|
+
success: false,
|
|
338
|
+
filesIndexed: totalFilesIndexed,
|
|
339
|
+
languages: detectedLanguages,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
child.on('error', () => {
|
|
345
|
+
resolve({ success: false, filesIndexed: 0, languages: [] });
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Set timeout (5 minutes)
|
|
349
|
+
setTimeout(() => {
|
|
350
|
+
child.kill();
|
|
351
|
+
resolve({ success: false, filesIndexed: totalFilesIndexed, languages: detectedLanguages });
|
|
352
|
+
}, 300000);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get the path to the branch tracking file.
|
|
358
|
+
*/
|
|
359
|
+
function getBranchTrackingPath(projectDir: string): string {
|
|
360
|
+
return join(projectDir, '.tldr', 'cache', 'semantic', 'indexed_branch');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Track which branch the semantic index was built for.
|
|
365
|
+
*/
|
|
366
|
+
export function trackIndexedBranch(projectDir: string): void {
|
|
367
|
+
try {
|
|
368
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
369
|
+
cwd: projectDir,
|
|
370
|
+
encoding: 'utf-8',
|
|
371
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
372
|
+
}).trim();
|
|
373
|
+
const trackingPath = getBranchTrackingPath(projectDir);
|
|
374
|
+
const { writeFileSync, mkdirSync } = require('fs');
|
|
375
|
+
const { dirname } = require('path');
|
|
376
|
+
mkdirSync(dirname(trackingPath), { recursive: true });
|
|
377
|
+
writeFileSync(trackingPath, branch);
|
|
378
|
+
} catch {
|
|
379
|
+
// Ignore errors - best effort tracking
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get the branch the semantic index was last built for.
|
|
385
|
+
*/
|
|
386
|
+
export function getIndexedBranch(projectDir: string): string | null {
|
|
387
|
+
try {
|
|
388
|
+
const trackingPath = getBranchTrackingPath(projectDir);
|
|
389
|
+
const { readFileSync } = require('fs');
|
|
390
|
+
return readFileSync(trackingPath, 'utf-8').trim();
|
|
391
|
+
} catch {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Check if semantic index needs rebuild due to branch switch.
|
|
398
|
+
*/
|
|
399
|
+
export function needsSemanticRebuild(projectDir: string): boolean {
|
|
400
|
+
if (!hasSemanticIndex(projectDir)) {
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
406
|
+
cwd: projectDir,
|
|
407
|
+
encoding: 'utf-8',
|
|
408
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
409
|
+
}).trim();
|
|
410
|
+
const indexedBranch = getIndexedBranch(projectDir);
|
|
411
|
+
return indexedBranch !== null && indexedBranch !== currentBranch;
|
|
412
|
+
} catch {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
418
|
+
// Daemon Communication
|
|
419
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Query the TLDR daemon synchronously using netcat.
|
|
423
|
+
* Returns null if daemon is unavailable or times out.
|
|
424
|
+
*
|
|
425
|
+
* @param cmd - Command object to send to daemon
|
|
426
|
+
* @param projectDir - Project directory for socket path
|
|
427
|
+
* @param timeoutMs - Timeout in milliseconds (default 5000)
|
|
428
|
+
*/
|
|
429
|
+
export function queryDaemonSync(
|
|
430
|
+
cmd: object,
|
|
431
|
+
projectDir: string,
|
|
432
|
+
timeoutMs: number = 5000
|
|
433
|
+
): DaemonResponse | null {
|
|
434
|
+
if (!isTldrInstalled()) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const socketPath = getTldrSocketPath(projectDir);
|
|
439
|
+
if (!existsSync(socketPath)) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const cmdJson = JSON.stringify(cmd);
|
|
445
|
+
const timeoutSec = Math.ceil(timeoutMs / 1000);
|
|
446
|
+
|
|
447
|
+
// Use netcat for synchronous socket query
|
|
448
|
+
const result = spawnSync('nc', ['-U', '-w', String(timeoutSec), socketPath], {
|
|
449
|
+
input: cmdJson,
|
|
450
|
+
encoding: 'utf-8',
|
|
451
|
+
timeout: timeoutMs + 1000, // Buffer for nc startup
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (result.status !== 0 || !result.stdout) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const response = JSON.parse(result.stdout.trim()) as DaemonResponse;
|
|
459
|
+
|
|
460
|
+
// Check if daemon is indexing
|
|
461
|
+
if (response.indexing) {
|
|
462
|
+
return { status: 'indexing', indexing: true, message: 'Index in progress' };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return response;
|
|
466
|
+
} catch {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Query the TLDR daemon asynchronously.
|
|
473
|
+
* Uses the same netcat approach but wrapped in a promise.
|
|
474
|
+
*/
|
|
475
|
+
export async function queryDaemon(
|
|
476
|
+
cmd: object,
|
|
477
|
+
projectDir: string,
|
|
478
|
+
timeoutMs: number = 5000
|
|
479
|
+
): Promise<DaemonResponse | null> {
|
|
480
|
+
// For now, use sync version wrapped in promise
|
|
481
|
+
// Could be optimized with proper async socket handling
|
|
482
|
+
return new Promise((resolve) => {
|
|
483
|
+
const result = queryDaemonSync(cmd, projectDir, timeoutMs);
|
|
484
|
+
resolve(result);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
489
|
+
// Query Helpers
|
|
490
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Fallback search using ripgrep when daemon is unavailable or indexing.
|
|
494
|
+
*/
|
|
495
|
+
function ripgrepFallback(pattern: string, projectDir: string): SearchResult[] {
|
|
496
|
+
try {
|
|
497
|
+
const escaped = pattern.replace(/"/g, '\\"').replace(/\$/g, '\\$');
|
|
498
|
+
const result = spawnSync(
|
|
499
|
+
'rg',
|
|
500
|
+
['--json', '-m', '20', '--no-heading', escaped, projectDir],
|
|
501
|
+
{ encoding: 'utf-8', timeout: 5000 }
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
if (result.status !== 0 || !result.stdout) {
|
|
505
|
+
return [];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const results: SearchResult[] = [];
|
|
509
|
+
for (const line of result.stdout.split('\n')) {
|
|
510
|
+
if (!line.trim()) continue;
|
|
511
|
+
try {
|
|
512
|
+
const json = JSON.parse(line);
|
|
513
|
+
if (json.type === 'match') {
|
|
514
|
+
results.push({
|
|
515
|
+
file: json.data.path.text,
|
|
516
|
+
name: pattern,
|
|
517
|
+
type: 'match',
|
|
518
|
+
line: json.data.line_number,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
// Skip non-JSON lines
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return results;
|
|
526
|
+
} catch {
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Search for symbols matching a pattern.
|
|
533
|
+
* Falls back to ripgrep when daemon is indexing or unavailable.
|
|
534
|
+
*/
|
|
535
|
+
export function searchDaemon(pattern: string, projectDir: string): SearchResult[] {
|
|
536
|
+
const response = queryDaemonSync({ cmd: 'search', pattern }, projectDir);
|
|
537
|
+
|
|
538
|
+
// If daemon is indexing or unavailable, fall back to ripgrep
|
|
539
|
+
if (!response || response.indexing || response.status === 'indexing') {
|
|
540
|
+
return ripgrepFallback(pattern, projectDir);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Daemon returns 'results' (plural) not 'result'
|
|
544
|
+
const results = (response as unknown as { results?: unknown[] }).results;
|
|
545
|
+
if (response.status !== 'ok' || !results) {
|
|
546
|
+
return ripgrepFallback(pattern, projectDir);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return results as SearchResult[];
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Extract symbols from a file.
|
|
554
|
+
*/
|
|
555
|
+
export function extractDaemon(filePath: string, projectDir: string): ExtractResult | null {
|
|
556
|
+
const response = queryDaemonSync({ cmd: 'extract', file: filePath }, projectDir);
|
|
557
|
+
if (!response || response.status !== 'ok' || !response.result) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
return response.result as ExtractResult;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Get call graph context for an entry point.
|
|
565
|
+
*/
|
|
566
|
+
export function contextDaemon(entry: string, projectDir: string): ContextResult | null {
|
|
567
|
+
const response = queryDaemonSync({ cmd: 'context', entry }, projectDir);
|
|
568
|
+
if (!response || response.status !== 'ok' || !response.result) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
return response.result as ContextResult;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Get control flow graph for a function.
|
|
576
|
+
*/
|
|
577
|
+
export function cfgDaemon(file: string, fn: string, projectDir: string): CFGResult | null {
|
|
578
|
+
const response = queryDaemonSync({ cmd: 'cfg', file, function: fn }, projectDir);
|
|
579
|
+
if (!response || response.status !== 'ok' || !response.result) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
return response.result as CFGResult;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Get data flow graph for a function.
|
|
587
|
+
*/
|
|
588
|
+
export function dfgDaemon(file: string, fn: string, projectDir: string): DFGResult | null {
|
|
589
|
+
const response = queryDaemonSync({ cmd: 'dfg', file, function: fn }, projectDir);
|
|
590
|
+
if (!response || response.status !== 'ok' || !response.result) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
return response.result as DFGResult;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Get architecture layers for the project.
|
|
598
|
+
*/
|
|
599
|
+
export function archDaemon(projectDir: string): ArchResult | null {
|
|
600
|
+
const response = queryDaemonSync({ cmd: 'arch' }, projectDir);
|
|
601
|
+
if (!response || response.status !== 'ok' || !response.result) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
return response.result as ArchResult;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Get diagnostics for a file (pyright + ruff).
|
|
609
|
+
*/
|
|
610
|
+
export function diagnosticsDaemon(file: string, projectDir: string): DiagnosticsResult | null {
|
|
611
|
+
const response = queryDaemonSync({ cmd: 'diagnostics', file }, projectDir);
|
|
612
|
+
if (!response || response.status !== 'ok' || !response.result) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
return response.result as DiagnosticsResult;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Caller location for impact analysis */
|
|
619
|
+
export interface ImpactCaller {
|
|
620
|
+
file: string;
|
|
621
|
+
function: string;
|
|
622
|
+
line: number;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Impact analysis result */
|
|
626
|
+
export interface ImpactResult {
|
|
627
|
+
target: string;
|
|
628
|
+
callers: ImpactCaller[];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Get impact analysis for a function (reverse call graph).
|
|
633
|
+
* Returns all functions that call the target function.
|
|
634
|
+
*/
|
|
635
|
+
export function impactDaemon(funcName: string, projectDir: string): ImpactResult | null {
|
|
636
|
+
const response = queryDaemonSync({ cmd: 'impact', func: funcName }, projectDir);
|
|
637
|
+
|
|
638
|
+
// If daemon is indexing, return null (caller should handle gracefully)
|
|
639
|
+
if (!response || response.indexing || response.status === 'indexing') {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (response.status !== 'ok') {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Handle both response formats: { callers: [...] } or { result: { callers: [...] } }
|
|
648
|
+
const callers = (response.callers ?? (response.result as { callers?: ImpactCaller[] })?.callers ?? []) as ImpactCaller[];
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
target: funcName,
|
|
652
|
+
callers,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
657
|
+
// TUI Integration Points
|
|
658
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Warm the TLDR index for a project.
|
|
662
|
+
* Starts the daemon and builds semantic index if missing.
|
|
663
|
+
* Returns true if warming was initiated, false if unavailable.
|
|
664
|
+
*/
|
|
665
|
+
export async function warmIndex(projectDir: string): Promise<boolean> {
|
|
666
|
+
if (!isTldrInstalled()) {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
// Start daemon in background
|
|
672
|
+
execSync(`tldr daemon start --project "${projectDir}" &`, {
|
|
673
|
+
stdio: 'ignore',
|
|
674
|
+
timeout: 2000,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// Build semantic index if missing (run in background)
|
|
678
|
+
if (!hasSemanticIndex(projectDir)) {
|
|
679
|
+
execSync(`tldr semantic index "${projectDir}" --lang all &`, {
|
|
680
|
+
stdio: 'ignore',
|
|
681
|
+
timeout: 2000,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return true;
|
|
686
|
+
} catch {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export interface WarmResult {
|
|
692
|
+
success: boolean;
|
|
693
|
+
files: number;
|
|
694
|
+
edges: number;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Run tldr warm to build call graph cache.
|
|
699
|
+
* Uses config default language from .tldr/config.json.
|
|
700
|
+
* Returns result with file and edge counts.
|
|
701
|
+
*/
|
|
702
|
+
export async function warmCallGraph(
|
|
703
|
+
projectDir: string,
|
|
704
|
+
onProgress?: (message: string) => void
|
|
705
|
+
): Promise<WarmResult> {
|
|
706
|
+
if (!isTldrInstalled()) {
|
|
707
|
+
return { success: false, files: 0, edges: 0 };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
const { spawn } = await import('child_process');
|
|
712
|
+
|
|
713
|
+
return new Promise((resolve) => {
|
|
714
|
+
// Run tldr warm (uses config default language)
|
|
715
|
+
const proc = spawn('tldr', ['warm', projectDir], {
|
|
716
|
+
cwd: projectDir,
|
|
717
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
let stdout = '';
|
|
721
|
+
let stderr = '';
|
|
722
|
+
|
|
723
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
724
|
+
const line = data.toString().trim();
|
|
725
|
+
stdout += line + '\n';
|
|
726
|
+
if (onProgress && line) {
|
|
727
|
+
onProgress(line);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
732
|
+
stderr += data.toString();
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
proc.on('close', (code) => {
|
|
736
|
+
if (code === 0) {
|
|
737
|
+
// Parse output for stats (e.g., "Total: Indexed 123 files, found 456 edges")
|
|
738
|
+
const match = stdout.match(/Indexed (\d+) files.*?(\d+) edges/);
|
|
739
|
+
const files = match ? parseInt(match[1], 10) : 0;
|
|
740
|
+
const edges = match ? parseInt(match[2], 10) : 0;
|
|
741
|
+
resolve({ success: true, files, edges });
|
|
742
|
+
} else {
|
|
743
|
+
resolve({ success: false, files: 0, edges: 0 });
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
proc.on('error', () => {
|
|
748
|
+
resolve({ success: false, files: 0, edges: 0 });
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// Timeout after 5 minutes
|
|
752
|
+
setTimeout(() => {
|
|
753
|
+
proc.kill();
|
|
754
|
+
resolve({ success: false, files: 0, edges: 0 });
|
|
755
|
+
}, 300000);
|
|
756
|
+
});
|
|
757
|
+
} catch {
|
|
758
|
+
return { success: false, files: 0, edges: 0 };
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Notify daemon of a file change for incremental updates.
|
|
764
|
+
*/
|
|
765
|
+
export async function notifyFileChanged(
|
|
766
|
+
projectDir: string,
|
|
767
|
+
filePath: string
|
|
768
|
+
): Promise<DaemonResponse | null> {
|
|
769
|
+
return queryDaemon({ cmd: 'notify', file: filePath, event: 'change' }, projectDir);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Initialize TLDR on spec start.
|
|
775
|
+
*/
|
|
776
|
+
export async function onSpecInit(projectDir: string): Promise<void> {
|
|
777
|
+
if (!isTldrInstalled()) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Ensure daemon is running
|
|
782
|
+
if (!isTldrDaemonRunning(projectDir)) {
|
|
783
|
+
await warmIndex(projectDir);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Handle merge completion - re-index affected files.
|
|
789
|
+
*/
|
|
790
|
+
export async function onMergeComplete(targetDir: string): Promise<void> {
|
|
791
|
+
if (!isTldrInstalled() || !isTldrDaemonRunning(targetDir)) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Trigger full re-index after merge
|
|
796
|
+
await queryDaemon({ cmd: 'reindex' }, targetDir);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
800
|
+
// Hook Activity Tracking
|
|
801
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Track hook activity for observability.
|
|
805
|
+
* Synchronous to avoid blocking hook execution.
|
|
806
|
+
*/
|
|
807
|
+
export function trackHookActivitySync(
|
|
808
|
+
hookName: string,
|
|
809
|
+
projectDir: string,
|
|
810
|
+
success: boolean,
|
|
811
|
+
metrics?: object
|
|
812
|
+
): void {
|
|
813
|
+
// Best-effort tracking - don't block on failures
|
|
814
|
+
try {
|
|
815
|
+
queryDaemonSync(
|
|
816
|
+
{
|
|
817
|
+
cmd: 'track',
|
|
818
|
+
hook: hookName,
|
|
819
|
+
success,
|
|
820
|
+
metrics,
|
|
821
|
+
timestamp: new Date().toISOString(),
|
|
822
|
+
},
|
|
823
|
+
projectDir,
|
|
824
|
+
1000 // Short timeout for tracking
|
|
825
|
+
);
|
|
826
|
+
} catch {
|
|
827
|
+
// Ignore tracking failures
|
|
828
|
+
}
|
|
829
|
+
}
|