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.
Files changed (305) hide show
  1. package/.allhands/README.md +75 -0
  2. package/.allhands/agents/compounder.yaml +15 -0
  3. package/.allhands/agents/coordinator.yaml +17 -0
  4. package/.allhands/agents/documentor.yaml +15 -0
  5. package/.allhands/agents/e2e-test-planner.yaml +17 -0
  6. package/.allhands/agents/emergent.yaml +22 -0
  7. package/.allhands/agents/executor.yaml +14 -0
  8. package/.allhands/agents/ideation.yaml +11 -0
  9. package/.allhands/agents/initiative-steering.yaml +19 -0
  10. package/.allhands/agents/judge.yaml +13 -0
  11. package/.allhands/agents/planner.yaml +19 -0
  12. package/.allhands/agents/pr-reviewer.yaml +15 -0
  13. package/.allhands/docs.json +5 -0
  14. package/.allhands/docs.local.json +26 -0
  15. package/.allhands/flows/COMPOUNDING.md +203 -0
  16. package/.allhands/flows/COORDINATION.md +89 -0
  17. package/.allhands/flows/CORE.md +87 -0
  18. package/.allhands/flows/DOCUMENTATION.md +218 -0
  19. package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
  20. package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
  21. package/.allhands/flows/IDEATION_SCOPING.md +154 -0
  22. package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
  23. package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
  24. package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
  25. package/.allhands/flows/PR_REVIEWING.md +43 -0
  26. package/.allhands/flows/SPEC_PLANNING.md +216 -0
  27. package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
  28. package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
  29. package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
  30. package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
  31. package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
  32. package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
  33. package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
  34. package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
  35. package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
  36. package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
  37. package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
  38. package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
  39. package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
  40. package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
  41. package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
  42. package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
  43. package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
  44. package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
  45. package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
  46. package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
  47. package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
  48. package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
  49. package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
  50. package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
  51. package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
  52. package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
  53. package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
  54. package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
  55. package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
  56. package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
  57. package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
  58. package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
  59. package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
  60. package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
  61. package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
  62. package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
  63. package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
  64. package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
  65. package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
  66. package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
  67. package/.allhands/harness/ah +131 -0
  68. package/.allhands/harness/package-lock.json +5292 -0
  69. package/.allhands/harness/package.json +52 -0
  70. package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
  71. package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
  72. package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
  73. package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
  74. package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
  75. package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
  76. package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
  77. package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
  78. package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
  79. package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
  80. package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
  81. package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
  82. package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
  83. package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
  84. package/.allhands/harness/src/cli.ts +36 -0
  85. package/.allhands/harness/src/commands/complexity.ts +177 -0
  86. package/.allhands/harness/src/commands/context7.ts +202 -0
  87. package/.allhands/harness/src/commands/docs.ts +557 -0
  88. package/.allhands/harness/src/commands/hooks.ts +24 -0
  89. package/.allhands/harness/src/commands/index.ts +51 -0
  90. package/.allhands/harness/src/commands/knowledge.ts +382 -0
  91. package/.allhands/harness/src/commands/memories.ts +302 -0
  92. package/.allhands/harness/src/commands/notify.ts +61 -0
  93. package/.allhands/harness/src/commands/oracle.ts +158 -0
  94. package/.allhands/harness/src/commands/perplexity.ts +220 -0
  95. package/.allhands/harness/src/commands/planning.ts +245 -0
  96. package/.allhands/harness/src/commands/schema.ts +73 -0
  97. package/.allhands/harness/src/commands/skills.ts +128 -0
  98. package/.allhands/harness/src/commands/solutions.ts +353 -0
  99. package/.allhands/harness/src/commands/spawn.ts +158 -0
  100. package/.allhands/harness/src/commands/specs.ts +532 -0
  101. package/.allhands/harness/src/commands/tavily.ts +226 -0
  102. package/.allhands/harness/src/commands/tools.ts +579 -0
  103. package/.allhands/harness/src/commands/trace.ts +327 -0
  104. package/.allhands/harness/src/commands/tui.ts +960 -0
  105. package/.allhands/harness/src/commands/validate.ts +143 -0
  106. package/.allhands/harness/src/commands/validation-tools.ts +108 -0
  107. package/.allhands/harness/src/hooks/context.ts +1442 -0
  108. package/.allhands/harness/src/hooks/enforcement.ts +170 -0
  109. package/.allhands/harness/src/hooks/index.ts +54 -0
  110. package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
  111. package/.allhands/harness/src/hooks/notification.ts +104 -0
  112. package/.allhands/harness/src/hooks/observability.ts +551 -0
  113. package/.allhands/harness/src/hooks/session.ts +88 -0
  114. package/.allhands/harness/src/hooks/shared.ts +815 -0
  115. package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
  116. package/.allhands/harness/src/hooks/validation.ts +617 -0
  117. package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
  118. package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
  119. package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
  120. package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
  121. package/.allhands/harness/src/lib/base-command.ts +198 -0
  122. package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
  123. package/.allhands/harness/src/lib/compaction.ts +313 -0
  124. package/.allhands/harness/src/lib/ctags.ts +497 -0
  125. package/.allhands/harness/src/lib/docs-validation.ts +907 -0
  126. package/.allhands/harness/src/lib/event-loop.ts +662 -0
  127. package/.allhands/harness/src/lib/flows.ts +155 -0
  128. package/.allhands/harness/src/lib/git.ts +276 -0
  129. package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
  130. package/.allhands/harness/src/lib/knowledge.ts +810 -0
  131. package/.allhands/harness/src/lib/llm.ts +255 -0
  132. package/.allhands/harness/src/lib/mcp-client.ts +432 -0
  133. package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
  134. package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
  135. package/.allhands/harness/src/lib/notification.ts +115 -0
  136. package/.allhands/harness/src/lib/opencode/index.ts +70 -0
  137. package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
  138. package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
  139. package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
  140. package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
  141. package/.allhands/harness/src/lib/oracle.ts +926 -0
  142. package/.allhands/harness/src/lib/planning-utils.ts +150 -0
  143. package/.allhands/harness/src/lib/planning.ts +605 -0
  144. package/.allhands/harness/src/lib/pr-review.ts +225 -0
  145. package/.allhands/harness/src/lib/prompts.ts +522 -0
  146. package/.allhands/harness/src/lib/schema.ts +418 -0
  147. package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
  148. package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
  149. package/.allhands/harness/src/lib/session.ts +164 -0
  150. package/.allhands/harness/src/lib/specs.ts +348 -0
  151. package/.allhands/harness/src/lib/tldr.ts +829 -0
  152. package/.allhands/harness/src/lib/tmux.ts +1051 -0
  153. package/.allhands/harness/src/lib/trace-store.ts +714 -0
  154. package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
  155. package/.allhands/harness/src/mcp/_template.ts +47 -0
  156. package/.allhands/harness/src/mcp/filesystem.ts +33 -0
  157. package/.allhands/harness/src/mcp/index.ts +69 -0
  158. package/.allhands/harness/src/mcp/playwright.ts +34 -0
  159. package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
  160. package/.allhands/harness/src/schemas/docs.schema.json +44 -0
  161. package/.allhands/harness/src/schemas/settings.schema.json +214 -0
  162. package/.allhands/harness/src/tui/actions.ts +227 -0
  163. package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
  164. package/.allhands/harness/src/tui/index.ts +1574 -0
  165. package/.allhands/harness/src/tui/modal.ts +232 -0
  166. package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
  167. package/.allhands/harness/src/tui/status-pane.ts +434 -0
  168. package/.allhands/harness/tsconfig.json +22 -0
  169. package/.allhands/harness/vitest.config.ts +13 -0
  170. package/.allhands/pillars.md +33 -0
  171. package/.allhands/principles.md +88 -0
  172. package/.allhands/schemas/alignment.yaml +51 -0
  173. package/.allhands/schemas/documentation.yaml +10 -0
  174. package/.allhands/schemas/prompt.yaml +92 -0
  175. package/.allhands/schemas/skill.yaml +34 -0
  176. package/.allhands/schemas/solution.yaml +131 -0
  177. package/.allhands/schemas/spec.yaml +67 -0
  178. package/.allhands/schemas/validation-suite.yaml +49 -0
  179. package/.allhands/schemas/workflow.yaml +51 -0
  180. package/.allhands/settings.json +57 -0
  181. package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
  182. package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
  183. package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
  184. package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
  185. package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
  186. package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
  187. package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
  188. package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
  189. package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
  190. package/.allhands/validation/browser-automation.md +109 -0
  191. package/.allhands/validation/xcode-automation.md +195 -0
  192. package/.allhands/workflows/documentation.md +86 -0
  193. package/.allhands/workflows/investigation.md +81 -0
  194. package/.allhands/workflows/milestone.md +91 -0
  195. package/.allhands/workflows/optimization.md +85 -0
  196. package/.allhands/workflows/refactor.md +99 -0
  197. package/.allhands/workflows/triage.md +81 -0
  198. package/.claude/README.md +1 -0
  199. package/.claude/agents/explorer.md +10 -0
  200. package/.claude/agents/researcher.md +11 -0
  201. package/.claude/agents/task-runner.md +8 -0
  202. package/.claude/settings.json +231 -0
  203. package/.env.ai.example +7 -0
  204. package/.github/workflows/npm-publish.yml +69 -0
  205. package/.internal.json +45 -0
  206. package/.tldr/config.json +11 -0
  207. package/.tldrignore +90 -0
  208. package/CLAUDE.md +6 -0
  209. package/README.md +98 -0
  210. package/bin/sync-cli.js +7552 -0
  211. package/concerns.md +7 -0
  212. package/docs/README.md +41 -0
  213. package/docs/agents/README.md +24 -0
  214. package/docs/agents/agent-configuration-system.md +86 -0
  215. package/docs/agents/execution-agents.md +50 -0
  216. package/docs/agents/knowledge-agents.md +61 -0
  217. package/docs/agents/orchestration-agent.md +57 -0
  218. package/docs/agents/planning-agents.md +84 -0
  219. package/docs/agents/quality-review-agents.md +67 -0
  220. package/docs/agents/workflow-agent-orchestration.md +69 -0
  221. package/docs/flows/README.md +44 -0
  222. package/docs/flows/compounding.md +126 -0
  223. package/docs/flows/coordination.md +72 -0
  224. package/docs/flows/core-harness-integration.md +63 -0
  225. package/docs/flows/documentation-orchestration.md +98 -0
  226. package/docs/flows/e2e-test-plan-building.md +83 -0
  227. package/docs/flows/emergent-refinement.md +104 -0
  228. package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
  229. package/docs/flows/judge-reviewing.md +112 -0
  230. package/docs/flows/plan-deepening-and-research.md +107 -0
  231. package/docs/flows/plan-review-jury.md +114 -0
  232. package/docs/flows/pr-reviewing.md +54 -0
  233. package/docs/flows/prompt-task-execution.md +119 -0
  234. package/docs/flows/spec-planning.md +162 -0
  235. package/docs/flows/type-specific-scoping-flows.md +49 -0
  236. package/docs/flows/validation-and-skills-integration.md +145 -0
  237. package/docs/flows/wip/wip-flows.md +102 -0
  238. package/docs/harness/README.md +23 -0
  239. package/docs/harness/agent-profiles.md +84 -0
  240. package/docs/harness/cli/README.md +24 -0
  241. package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
  242. package/docs/harness/cli/docs-command.md +87 -0
  243. package/docs/harness/cli/knowledge-command.md +91 -0
  244. package/docs/harness/cli/minor-cli-commands.md +65 -0
  245. package/docs/harness/cli/oracle-command.md +113 -0
  246. package/docs/harness/cli/planning-command.md +95 -0
  247. package/docs/harness/cli/schema-and-validation-commands.md +154 -0
  248. package/docs/harness/cli/search-commands.md +97 -0
  249. package/docs/harness/cli/spawn-command.md +136 -0
  250. package/docs/harness/cli/specs-command.md +102 -0
  251. package/docs/harness/cli/tools-command.md +122 -0
  252. package/docs/harness/cli/trace-command.md +122 -0
  253. package/docs/harness/cli-daemon.md +92 -0
  254. package/docs/harness/event-loop.md +184 -0
  255. package/docs/harness/hooks/README.md +15 -0
  256. package/docs/harness/hooks/context-hooks.md +96 -0
  257. package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
  258. package/docs/harness/hooks/validation-hooks.md +97 -0
  259. package/docs/harness/test-harness.md +149 -0
  260. package/docs/harness/tui.md +176 -0
  261. package/docs/memories.md +20 -0
  262. package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
  263. package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
  264. package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
  265. package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
  266. package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
  267. package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
  268. package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
  269. package/docs/sync-cli/README.md +19 -0
  270. package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
  271. package/docs/sync-cli/commands/README.md +11 -0
  272. package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
  273. package/docs/sync-cli/commands/push-command.md +84 -0
  274. package/docs/sync-cli/commands/sync-command.md +71 -0
  275. package/docs/sync-cli/systems/README.md +14 -0
  276. package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
  277. package/docs/sync-cli/systems/interactive-ui.md +43 -0
  278. package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
  279. package/docs/sync-cli/systems/path-resolution.md +42 -0
  280. package/package.json +46 -0
  281. package/scripts/install-shim.sh +40 -0
  282. package/scripts/pre-pack.sh +25 -0
  283. package/specs/harness-maintenance-skill.spec.md +138 -0
  284. package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
  285. package/specs/sync-init-flag.spec.md +117 -0
  286. package/specs/unified-workflow-orchestration.spec.md +250 -0
  287. package/specs/validation-tooling-practice.spec.md +98 -0
  288. package/specs/workflow-domain-configuration.spec.md +265 -0
  289. package/src/commands/pull-manifest.ts +31 -0
  290. package/src/commands/push.ts +344 -0
  291. package/src/commands/sync.ts +289 -0
  292. package/src/lib/constants.ts +10 -0
  293. package/src/lib/dotfiles.ts +36 -0
  294. package/src/lib/fs-utils.ts +18 -0
  295. package/src/lib/gh.ts +40 -0
  296. package/src/lib/git.ts +63 -0
  297. package/src/lib/gitignore.ts +167 -0
  298. package/src/lib/manifest.ts +121 -0
  299. package/src/lib/marker-sync.ts +39 -0
  300. package/src/lib/paths.ts +38 -0
  301. package/src/lib/target-lines.ts +66 -0
  302. package/src/lib/ui.ts +78 -0
  303. package/src/sync-cli.ts +120 -0
  304. package/target-lines.json +23 -0
  305. package/tsconfig.json +20 -0
@@ -0,0 +1,255 @@
1
+ /**
2
+ * LLM - Multi-Provider Large Language Model Integration
3
+ *
4
+ * Generic LLM calling infrastructure supporting multiple providers.
5
+ * This is the foundation layer - provider configs and raw inference.
6
+ *
7
+ * Supported Providers:
8
+ * - Gemini (Google) - GEMINI_API_KEY (uses @google/genai SDK)
9
+ * - OpenAI (GPT) - OPENAI_API_KEY
10
+ */
11
+
12
+ import { existsSync, readFileSync } from 'fs';
13
+ import { GoogleGenAI } from '@google/genai';
14
+ import { loadProjectSettings } from '../hooks/shared.js';
15
+
16
+ // ============================================================================
17
+ // Types
18
+ // ============================================================================
19
+
20
+ export type ProviderName = 'gemini' | 'openai';
21
+
22
+ export interface ProviderConfig {
23
+ name: ProviderName;
24
+ apiKeyEnvVar: string;
25
+ defaultModel: string;
26
+ }
27
+
28
+ export interface LLMResult {
29
+ text: string;
30
+ model: string;
31
+ provider: ProviderName;
32
+ durationMs: number;
33
+ }
34
+
35
+ export interface AskOptions {
36
+ provider?: ProviderName;
37
+ model?: string;
38
+ files?: string[];
39
+ context?: string;
40
+ timeout?: number;
41
+ }
42
+
43
+ // ============================================================================
44
+ // Configuration
45
+ // ============================================================================
46
+
47
+ export const PROVIDERS: Record<ProviderName, ProviderConfig> = {
48
+ gemini: {
49
+ name: 'gemini',
50
+ apiKeyEnvVar: 'GEMINI_API_KEY',
51
+ defaultModel: 'gemini-3-pro-preview',
52
+ },
53
+ openai: {
54
+ name: 'openai',
55
+ apiKeyEnvVar: 'OPENAI_API_KEY',
56
+ defaultModel: 'gpt-5.2',
57
+ },
58
+ };
59
+
60
+ const DEFAULT_TIMEOUT = 120000;
61
+
62
+ // ============================================================================
63
+ // Public API
64
+ // ============================================================================
65
+
66
+ /**
67
+ * Get the default provider from settings or fallback
68
+ */
69
+ export function getDefaultProvider(): ProviderName {
70
+ const settings = loadProjectSettings();
71
+ const provider = settings?.oracle?.defaultProvider;
72
+ if (provider === 'openai' || provider === 'gemini') {
73
+ return provider;
74
+ }
75
+ return 'gemini';
76
+ }
77
+
78
+ /**
79
+ * Get the compaction provider from settings or fallback to gemini.
80
+ * Compaction uses gemini by default due to its large context window (1M+ tokens).
81
+ */
82
+ export function getCompactionProvider(): ProviderName {
83
+ const settings = loadProjectSettings();
84
+ const provider = settings?.oracle?.compactionProvider;
85
+ if (provider === 'openai' || provider === 'gemini') {
86
+ return provider;
87
+ }
88
+ return 'gemini'; // Default to gemini for large context handling
89
+ }
90
+
91
+ /**
92
+ * Generic LLM inference
93
+ *
94
+ * @param query - The prompt/question to send
95
+ * @param options - Provider, model, file context, etc.
96
+ * @returns LLM response with metadata
97
+ */
98
+ export async function ask(query: string, options: AskOptions = {}): Promise<LLMResult> {
99
+ const providerName = options.provider ?? getDefaultProvider();
100
+ const provider = PROVIDERS[providerName];
101
+
102
+ if (!provider) {
103
+ throw new Error(`Invalid provider: ${providerName}. Use: gemini, openai`);
104
+ }
105
+
106
+ const apiKey = process.env[provider.apiKeyEnvVar];
107
+ if (!apiKey) {
108
+ throw new Error(`${provider.apiKeyEnvVar} not set in environment`);
109
+ }
110
+
111
+ // Build prompt with context
112
+ const parts: string[] = [];
113
+
114
+ if (options.context) {
115
+ parts.push(options.context);
116
+ }
117
+
118
+ if (options.files && options.files.length > 0) {
119
+ const fileContents = readFiles(options.files);
120
+ if (Object.keys(fileContents).length > 0) {
121
+ const fileContext = Object.entries(fileContents)
122
+ .map(([path, content]) => `### ${path}\n\`\`\`\n${content}\n\`\`\``)
123
+ .join('\n\n');
124
+ parts.push(fileContext);
125
+ }
126
+ }
127
+
128
+ parts.push(query);
129
+ const prompt = parts.join('\n\n');
130
+
131
+ const model = options.model ?? provider.defaultModel;
132
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
133
+
134
+ const start = performance.now();
135
+ const result = await callProvider(providerName, apiKey, prompt, model, timeout);
136
+ const durationMs = Math.round(performance.now() - start);
137
+
138
+ return {
139
+ text: result.text,
140
+ model: result.model,
141
+ provider: providerName,
142
+ durationMs,
143
+ };
144
+ }
145
+
146
+ // ============================================================================
147
+ // Provider Implementations
148
+ // ============================================================================
149
+
150
+ interface ProviderResult {
151
+ text: string;
152
+ model: string;
153
+ }
154
+
155
+ async function callProvider(
156
+ provider: ProviderName,
157
+ apiKey: string,
158
+ prompt: string,
159
+ model: string,
160
+ timeout: number
161
+ ): Promise<ProviderResult> {
162
+ switch (provider) {
163
+ case 'gemini':
164
+ return callGemini(apiKey, prompt, model, timeout);
165
+ case 'openai':
166
+ return callOpenAI(apiKey, prompt, model, timeout);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Call Gemini API using the official @google/genai SDK.
172
+ * Uses API key authentication (Gemini Developer API, not Vertex AI).
173
+ */
174
+ async function callGemini(
175
+ apiKey: string,
176
+ prompt: string,
177
+ model: string,
178
+ timeout: number
179
+ ): Promise<ProviderResult> {
180
+ // Initialize with API key only - this uses Gemini Developer API, not Vertex AI
181
+ const ai = new GoogleGenAI({ apiKey });
182
+
183
+ // Create abort controller for timeout
184
+ const controller = new AbortController();
185
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
186
+
187
+ try {
188
+ const response = await ai.models.generateContent({
189
+ model,
190
+ contents: prompt,
191
+ });
192
+
193
+ const text = response.text ?? '';
194
+ return { text, model };
195
+ } finally {
196
+ clearTimeout(timeoutId);
197
+ }
198
+ }
199
+
200
+ async function callOpenAI(
201
+ apiKey: string,
202
+ prompt: string,
203
+ model: string,
204
+ timeout: number
205
+ ): Promise<ProviderResult> {
206
+ const controller = new AbortController();
207
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
208
+
209
+ try {
210
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
211
+ method: 'POST',
212
+ headers: {
213
+ Authorization: `Bearer ${apiKey}`,
214
+ 'Content-Type': 'application/json',
215
+ },
216
+ body: JSON.stringify({
217
+ model,
218
+ messages: [{ role: 'user', content: prompt }],
219
+ }),
220
+ signal: controller.signal,
221
+ });
222
+
223
+ if (!response.ok) {
224
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
225
+ }
226
+
227
+ const data = (await response.json()) as {
228
+ choices?: Array<{ message?: { content?: string } }>;
229
+ model?: string;
230
+ };
231
+ const text = data.choices?.[0]?.message?.content ?? '';
232
+
233
+ return { text, model: data.model ?? model };
234
+ } finally {
235
+ clearTimeout(timeoutId);
236
+ }
237
+ }
238
+
239
+ // ============================================================================
240
+ // Utilities
241
+ // ============================================================================
242
+
243
+ function readFiles(paths: string[]): Record<string, string> {
244
+ const result: Record<string, string> = {};
245
+ for (const path of paths) {
246
+ if (existsSync(path)) {
247
+ try {
248
+ result[path] = readFileSync(path, 'utf-8');
249
+ } catch {
250
+ // Skip unreadable files
251
+ }
252
+ }
253
+ }
254
+ return result;
255
+ }
@@ -0,0 +1,432 @@
1
+ /**
2
+ * MCP Client - Connects to session daemon for stateful servers.
3
+ *
4
+ * For stateful servers (playwright, xcodebuild, etc.), connects to a
5
+ * per-agent daemon that manages persistent MCP sessions.
6
+ *
7
+ * For stateless servers, uses direct one-shot connections.
8
+ *
9
+ * Daemon socket path: .allhands/harness/.cache/sessions/{AGENT_ID}.sock
10
+ *
11
+ * Session Lifecycle (stateful servers):
12
+ * - Sessions are auto-started on first tool call.
13
+ * - Sessions timeout after inactivity (configurable per-server).
14
+ * - When all sessions timeout, daemon exits.
15
+ * - Use --restart flag to recover from bad state.
16
+ */
17
+
18
+ import { connect, type Socket } from 'net';
19
+ import { spawn } from 'child_process';
20
+ import { existsSync, readFileSync, mkdirSync } from 'fs';
21
+ import { dirname, join } from 'path';
22
+ import { fileURLToPath } from 'url';
23
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
24
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
25
+ import type { McpServerConfig, McpToolSchema } from './mcp-runtime.js';
26
+ import { resolveEnvVars } from './mcp-runtime.js';
27
+
28
+ const __dirname = dirname(fileURLToPath(import.meta.url));
29
+ // Path: harness/src/lib/ -> harness/src/ -> harness/
30
+ const HARNESS_ROOT = join(__dirname, '..', '..');
31
+ const SESSIONS_DIR = join(HARNESS_ROOT, '.cache', 'sessions');
32
+ const DAEMON_SCRIPT = join(__dirname, 'mcp-daemon.ts');
33
+
34
+ /**
35
+ * Get the agent ID from environment.
36
+ */
37
+ export function getAgentId(): string {
38
+ return process.env.AGENT_ID ?? 'default';
39
+ }
40
+
41
+ /**
42
+ * Get the socket path for an agent.
43
+ */
44
+ function getSocketPath(agentId: string): string {
45
+ return join(SESSIONS_DIR, `${agentId}.sock`);
46
+ }
47
+
48
+ /**
49
+ * Get the PID file path for an agent.
50
+ */
51
+ function getPidPath(agentId: string): string {
52
+ return join(SESSIONS_DIR, `${agentId}.pid`);
53
+ }
54
+
55
+ /**
56
+ * Check if the daemon is running for an agent.
57
+ */
58
+ export function isDaemonRunning(agentId?: string): boolean {
59
+ const aid = agentId ?? getAgentId();
60
+ const pidPath = getPidPath(aid);
61
+ const socketPath = getSocketPath(aid);
62
+
63
+ if (!existsSync(pidPath) || !existsSync(socketPath)) {
64
+ return false;
65
+ }
66
+
67
+ try {
68
+ const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
69
+ // Check if process is running
70
+ process.kill(pid, 0);
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Sleep helper for async waiting.
79
+ */
80
+ function sleep(ms: number): Promise<void> {
81
+ return new Promise((resolve) => setTimeout(resolve, ms));
82
+ }
83
+
84
+ /**
85
+ * Start the daemon for an agent (if not already running).
86
+ */
87
+ export async function startDaemon(agentId?: string): Promise<void> {
88
+ const aid = agentId ?? getAgentId();
89
+
90
+ if (isDaemonRunning(aid)) {
91
+ return;
92
+ }
93
+
94
+ // Ensure sessions directory exists
95
+ if (!existsSync(SESSIONS_DIR)) {
96
+ mkdirSync(SESSIONS_DIR, { recursive: true });
97
+ }
98
+
99
+ // Spawn daemon as detached process
100
+ const child = spawn('npx', ['tsx', DAEMON_SCRIPT, aid], {
101
+ cwd: HARNESS_ROOT,
102
+ detached: true,
103
+ stdio: 'ignore',
104
+ env: { ...process.env, AGENT_ID: aid },
105
+ });
106
+
107
+ child.unref();
108
+
109
+ // Wait for socket to be created (with timeout)
110
+ const socketPath = getSocketPath(aid);
111
+ const startTime = Date.now();
112
+ const timeout = 10000; // 10 seconds
113
+
114
+ while (!existsSync(socketPath)) {
115
+ if (Date.now() - startTime > timeout) {
116
+ throw new Error(`Daemon failed to start for agent ${aid}`);
117
+ }
118
+ await sleep(100);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Send a command to the daemon and get response.
124
+ */
125
+ async function sendToDaemon<T>(agentId: string, command: unknown, timeoutMs = 30000): Promise<T> {
126
+ return new Promise((resolve, reject) => {
127
+ const socketPath = getSocketPath(agentId);
128
+
129
+ if (!existsSync(socketPath)) {
130
+ reject(new Error(`Daemon not running for agent ${agentId}`));
131
+ return;
132
+ }
133
+
134
+ const socket: Socket = connect(socketPath);
135
+ let buffer = '';
136
+
137
+ socket.on('connect', () => {
138
+ socket.write(JSON.stringify(command) + '\n');
139
+ });
140
+
141
+ socket.on('data', (data) => {
142
+ buffer += data.toString();
143
+ const newlineIdx = buffer.indexOf('\n');
144
+ if (newlineIdx !== -1) {
145
+ const response = buffer.slice(0, newlineIdx);
146
+ socket.end();
147
+ try {
148
+ resolve(JSON.parse(response) as T);
149
+ } catch (e) {
150
+ reject(new Error(`Invalid response from daemon: ${response}`));
151
+ }
152
+ }
153
+ });
154
+
155
+ socket.on('error', (err) => {
156
+ reject(err);
157
+ });
158
+
159
+ socket.on('timeout', () => {
160
+ socket.destroy();
161
+ reject(new Error('Daemon connection timeout'));
162
+ });
163
+
164
+ socket.setTimeout(timeoutMs);
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Create and connect a new MCP client for a one-shot call.
170
+ */
171
+ async function createOneShot(config: McpServerConfig): Promise<{ client: Client; transport: StdioClientTransport }> {
172
+ if (!config.command) {
173
+ throw new Error(`Server ${config.name} requires 'command' for stdio transport`);
174
+ }
175
+
176
+ const env = resolveEnvVars(config.env);
177
+
178
+ const transport = new StdioClientTransport({
179
+ command: config.command,
180
+ args: config.args,
181
+ env: { ...process.env, ...env } as Record<string, string>,
182
+ stderr: 'pipe',
183
+ });
184
+
185
+ const client = new Client(
186
+ { name: 'allhands-cli', version: '0.1.0' },
187
+ { capabilities: {} }
188
+ );
189
+
190
+ await client.connect(transport);
191
+
192
+ return { client, transport };
193
+ }
194
+
195
+ // ============================================================================
196
+ // Public API
197
+ // ============================================================================
198
+
199
+ /**
200
+ * Restart a server session (for recovery from bad state).
201
+ */
202
+ export async function restartServer(
203
+ config: McpServerConfig,
204
+ agentId?: string
205
+ ): Promise<{ success: boolean; pid?: number; error?: string }> {
206
+ const aid = agentId ?? getAgentId();
207
+
208
+ // Ensure daemon is running
209
+ await startDaemon(aid);
210
+
211
+ return sendToDaemon(aid, {
212
+ cmd: 'restart',
213
+ server: config.name,
214
+ config,
215
+ });
216
+ }
217
+
218
+ /**
219
+ * List all active sessions.
220
+ */
221
+ export async function listSessions(agentId?: string): Promise<Array<{
222
+ serverName: string;
223
+ startedAt: Date;
224
+ lastUsedAt: Date;
225
+ timeoutMs: number;
226
+ pid?: number;
227
+ }>> {
228
+ const aid = agentId ?? getAgentId();
229
+
230
+ if (!isDaemonRunning(aid)) {
231
+ return [];
232
+ }
233
+
234
+ const result = await sendToDaemon<{
235
+ success: boolean;
236
+ sessions: Array<{ server: string; startedAt: string; lastUsedAt: string; timeoutMs: number; pid?: number }>;
237
+ }>(aid, { cmd: 'list' });
238
+
239
+ return result.sessions.map((s) => ({
240
+ serverName: s.server,
241
+ startedAt: new Date(s.startedAt),
242
+ lastUsedAt: new Date(s.lastUsedAt),
243
+ timeoutMs: s.timeoutMs,
244
+ pid: s.pid,
245
+ }));
246
+ }
247
+
248
+ /**
249
+ * Discover tools from an MCP server.
250
+ *
251
+ * For stateful servers, uses daemon (auto-starts session).
252
+ * For stateless servers, uses one-shot connection.
253
+ */
254
+ export async function discoverTools(
255
+ config: McpServerConfig,
256
+ agentId?: string
257
+ ): Promise<McpToolSchema[]> {
258
+ const aid = agentId ?? getAgentId();
259
+
260
+ if (config.stateful) {
261
+ // Ensure daemon is running, then discover via daemon
262
+ await startDaemon(aid);
263
+
264
+ const result = await sendToDaemon<{
265
+ success: boolean;
266
+ tools?: McpToolSchema[];
267
+ error?: string;
268
+ }>(aid, {
269
+ cmd: 'discover',
270
+ server: config.name,
271
+ config,
272
+ });
273
+
274
+ if (!result.success) {
275
+ throw new Error(result.error ?? 'Failed to discover tools');
276
+ }
277
+
278
+ return result.tools ?? [];
279
+ }
280
+
281
+ // Stateless: one-shot discovery
282
+ const { client, transport } = await createOneShot(config);
283
+
284
+ try {
285
+ const result = await client.listTools();
286
+ const tools = result.tools.map((t) => ({
287
+ name: t.name,
288
+ description: t.description,
289
+ inputSchema: t.inputSchema as McpToolSchema['inputSchema'],
290
+ }));
291
+
292
+ return config.hiddenTools?.length
293
+ ? tools.filter((t) => !config.hiddenTools!.includes(t.name))
294
+ : tools;
295
+ } finally {
296
+ await transport.close();
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Call a tool on an MCP server.
302
+ *
303
+ * For stateful servers, uses daemon (auto-starts session).
304
+ * For stateless servers, uses one-shot connection.
305
+ */
306
+ export async function callTool(
307
+ config: McpServerConfig,
308
+ toolName: string,
309
+ params: Record<string, unknown>,
310
+ agentId?: string
311
+ ): Promise<unknown> {
312
+ const aid = agentId ?? getAgentId();
313
+
314
+ if (config.stateful) {
315
+ // Ensure daemon is running, then call via daemon
316
+ await startDaemon(aid);
317
+
318
+ const callTimeout = config.stateful_session_timeout ?? 60000;
319
+ const result = await sendToDaemon<{
320
+ success: boolean;
321
+ result?: unknown;
322
+ error?: string;
323
+ }>(aid, {
324
+ cmd: 'call',
325
+ server: config.name,
326
+ tool: toolName,
327
+ params,
328
+ config,
329
+ }, callTimeout);
330
+
331
+ if (!result.success) {
332
+ throw new Error(result.error ?? 'Tool call failed');
333
+ }
334
+
335
+ return result.result;
336
+ }
337
+
338
+ // Stateless: one-shot connection
339
+ const { client, transport } = await createOneShot(config);
340
+
341
+ try {
342
+ const result = await client.callTool({ name: toolName, arguments: params });
343
+
344
+ if ('content' in result && Array.isArray(result.content)) {
345
+ const textContent = result.content.find((c) => c.type === 'text');
346
+ if (textContent && 'text' in textContent) {
347
+ try {
348
+ return JSON.parse(textContent.text);
349
+ } catch {
350
+ return textContent.text;
351
+ }
352
+ }
353
+ return result.content;
354
+ }
355
+
356
+ return result;
357
+ } finally {
358
+ await transport.close();
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Shutdown the daemon for an agent.
364
+ * Called by tmux cleanup or manual shutdown.
365
+ */
366
+ export async function shutdownDaemon(agentId?: string): Promise<void> {
367
+ const aid = agentId ?? getAgentId();
368
+
369
+ if (!isDaemonRunning(aid)) {
370
+ return;
371
+ }
372
+
373
+ try {
374
+ await sendToDaemon(aid, { cmd: 'shutdown' });
375
+ } catch {
376
+ // Daemon may have already exited
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Get daemon info.
382
+ */
383
+ export async function getDaemonInfo(agentId?: string): Promise<{
384
+ running: boolean;
385
+ pid?: number;
386
+ socketPath?: string;
387
+ sessionCount?: number;
388
+ sessions?: string[];
389
+ }> {
390
+ const aid = agentId ?? getAgentId();
391
+ const pidPath = getPidPath(aid);
392
+ const socketPath = getSocketPath(aid);
393
+
394
+ if (!isDaemonRunning(aid)) {
395
+ return { running: false };
396
+ }
397
+
398
+ try {
399
+ const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
400
+
401
+ // Get live info from daemon
402
+ const info = await sendToDaemon<{
403
+ success: boolean;
404
+ pid: number;
405
+ sessionCount: number;
406
+ sessions: string[];
407
+ }>(aid, { cmd: 'info' });
408
+
409
+ return {
410
+ running: true,
411
+ pid,
412
+ socketPath,
413
+ sessionCount: info.sessionCount,
414
+ sessions: info.sessions,
415
+ };
416
+ } catch {
417
+ return { running: false };
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Send ping to daemon to keep sessions alive.
423
+ */
424
+ export async function pingDaemon(agentId?: string): Promise<{ success: boolean; sessionsRefreshed: number }> {
425
+ const aid = agentId ?? getAgentId();
426
+
427
+ if (!isDaemonRunning(aid)) {
428
+ return { success: false, sessionsRefreshed: 0 };
429
+ }
430
+
431
+ return sendToDaemon(aid, { cmd: 'ping' });
432
+ }