the-campfire 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 (180) hide show
  1. package/bin/cli.ts +182 -0
  2. package/dist/apple-touch-icon.png +0 -0
  3. package/dist/assets/MermaidDiagram-By1J3Whj.js +2 -0
  4. package/dist/assets/_basePickBy-Dq6gnD-p.js +1 -0
  5. package/dist/assets/_baseUniq-SpgN6cGc.js +1 -0
  6. package/dist/assets/arc-D9SEA8dX.js +1 -0
  7. package/dist/assets/architectureDiagram-VXUJARFQ-CoTcwLeR.js +36 -0
  8. package/dist/assets/blockDiagram-VD42YOAC-Dvc63voV.js +122 -0
  9. package/dist/assets/c4Diagram-YG6GDRKO-tuDcG4_4.js +10 -0
  10. package/dist/assets/channel-CxhcHn7S.js +1 -0
  11. package/dist/assets/chunk-4BX2VUAB-x1hChYu9.js +1 -0
  12. package/dist/assets/chunk-55IACEB6-BX6HWnWP.js +1 -0
  13. package/dist/assets/chunk-B4BG7PRW-B4IlF03G.js +165 -0
  14. package/dist/assets/chunk-DI55MBZ5-TpYb5rLg.js +220 -0
  15. package/dist/assets/chunk-FMBD7UC4-DZX4LUPx.js +15 -0
  16. package/dist/assets/chunk-QN33PNHL-CuQs0NDY.js +1 -0
  17. package/dist/assets/chunk-QZHKN3VN-CygWwVx8.js +1 -0
  18. package/dist/assets/chunk-TZMSLE5B-CtI-IdKV.js +1 -0
  19. package/dist/assets/classDiagram-2ON5EDUG-BM0LptJF.js +1 -0
  20. package/dist/assets/classDiagram-v2-WZHVMYZB-BM0LptJF.js +1 -0
  21. package/dist/assets/clone-C6kt1rE7.js +1 -0
  22. package/dist/assets/cose-bilkent-S5V4N54A-PdySs991.js +1 -0
  23. package/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
  24. package/dist/assets/dagre-6UL2VRFP-J0SxEiEu.js +4 -0
  25. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  26. package/dist/assets/diagram-PSM6KHXK-CW0HLXaH.js +24 -0
  27. package/dist/assets/diagram-QEK2KX5R-DADHF-k4.js +43 -0
  28. package/dist/assets/diagram-S2PKOQOG-9S0-PC3S.js +24 -0
  29. package/dist/assets/erDiagram-Q2GNP2WA-BEHAiY_q.js +60 -0
  30. package/dist/assets/flowDiagram-NV44I4VS-Cu6n9Xnd.js +162 -0
  31. package/dist/assets/ganttDiagram-JELNMOA3-Cr_BHryu.js +267 -0
  32. package/dist/assets/gitGraphDiagram-V2S2FVAM-D5pT35v5.js +65 -0
  33. package/dist/assets/graph-DKRbrxcs.js +1 -0
  34. package/dist/assets/index-Byjg9zgI.css +32 -0
  35. package/dist/assets/index-Cwj3m0hT.js +284 -0
  36. package/dist/assets/infoDiagram-HS3SLOUP-BaJlrgKV.js +2 -0
  37. package/dist/assets/init-Gi6I4Gst.js +1 -0
  38. package/dist/assets/journeyDiagram-XKPGCS4Q-CXC4-DJv.js +139 -0
  39. package/dist/assets/kanban-definition-3W4ZIXB7-BtHBu9cx.js +89 -0
  40. package/dist/assets/katex-BlHpptmG.js +261 -0
  41. package/dist/assets/layout-DnxSwLDJ.js +1 -0
  42. package/dist/assets/linear-CWbbIT8l.js +1 -0
  43. package/dist/assets/mermaid.core-DXn1cFAK.js +256 -0
  44. package/dist/assets/mindmap-definition-VGOIOE7T-BR8br1hs.js +68 -0
  45. package/dist/assets/ordinal-BENe2yWM.js +1 -0
  46. package/dist/assets/pieDiagram-ADFJNKIX-B7RmXV7I.js +30 -0
  47. package/dist/assets/quadrantDiagram-AYHSOK5B-DK0FhyV_.js +7 -0
  48. package/dist/assets/requirementDiagram-UZGBJVZJ-3zAGxSpL.js +64 -0
  49. package/dist/assets/sankeyDiagram-TZEHDZUN-DZppKtjc.js +10 -0
  50. package/dist/assets/sequenceDiagram-WL72ISMW-D1x3r3wV.js +145 -0
  51. package/dist/assets/stateDiagram-FKZM4ZOC-2ByD08Vq.js +1 -0
  52. package/dist/assets/stateDiagram-v2-4FDKWEC3-Vm1cYjgV.js +1 -0
  53. package/dist/assets/timeline-definition-IT6M3QCI-l5S35biR.js +61 -0
  54. package/dist/assets/treemap-GDKQZRPO-DIPadPiF.js +162 -0
  55. package/dist/assets/xychartDiagram-PRI3JC2R-9GUvwS2b.js +7 -0
  56. package/dist/favicon.svg +3 -0
  57. package/dist/fonts/Geist-Variable.woff2 +0 -0
  58. package/dist/fonts/GeistMono-Variable.woff2 +0 -0
  59. package/dist/icon-192.png +0 -0
  60. package/dist/icon-512.png +0 -0
  61. package/dist/index.html +33 -0
  62. package/dist/logo-codex.svg +14 -0
  63. package/dist/logo.svg +3 -0
  64. package/dist/manifest.json +26 -0
  65. package/dist/og-image.png +0 -0
  66. package/dist/sw.js +130 -0
  67. package/package.json +92 -0
  68. package/server/ADAPTERS.md +121 -0
  69. package/server/adapter-registry-types.ts +28 -0
  70. package/server/adapter-registry.ts +293 -0
  71. package/server/adapter-types.ts +51 -0
  72. package/server/agent-executor.ts +249 -0
  73. package/server/agent-mcp-bridge.ts +148 -0
  74. package/server/agent-mcp-stdio.ts +171 -0
  75. package/server/agent-mcp-tools.ts +76 -0
  76. package/server/agent-store.ts +183 -0
  77. package/server/agent-types.ts +79 -0
  78. package/server/aider-adapter.ts +525 -0
  79. package/server/auth.ts +153 -0
  80. package/server/auto-namer.ts +132 -0
  81. package/server/capability-discovery.ts +431 -0
  82. package/server/claude-container-auth.ts +40 -0
  83. package/server/claude-stdio-adapter.ts +452 -0
  84. package/server/clawhub-export.ts +218 -0
  85. package/server/cli-launcher.ts +1613 -0
  86. package/server/codex-adapter.ts +1987 -0
  87. package/server/codex-container-auth.ts +40 -0
  88. package/server/codex-home.ts +26 -0
  89. package/server/collective-intelligence.ts +401 -0
  90. package/server/commands-discovery.ts +160 -0
  91. package/server/constants.ts +5 -0
  92. package/server/container-manager.ts +328 -0
  93. package/server/cron-scheduler.ts +243 -0
  94. package/server/cron-store.ts +149 -0
  95. package/server/cron-types.ts +63 -0
  96. package/server/deliberation-engine.ts +323 -0
  97. package/server/embedding.ts +89 -0
  98. package/server/env-manager.ts +146 -0
  99. package/server/environment-detector.ts +106 -0
  100. package/server/environment-rules.ts +106 -0
  101. package/server/gallery-store.ts +216 -0
  102. package/server/gallery-types.ts +52 -0
  103. package/server/gallery-votes.ts +101 -0
  104. package/server/git-utils.ts +384 -0
  105. package/server/github-pr.ts +379 -0
  106. package/server/goose-adapter.ts +929 -0
  107. package/server/image-pull-manager.ts +144 -0
  108. package/server/index.ts +363 -0
  109. package/server/linear-project-manager.ts +78 -0
  110. package/server/moltbook-client.ts +160 -0
  111. package/server/openclaw-adapter.ts +916 -0
  112. package/server/openclaw-channel/channel.ts +188 -0
  113. package/server/openclaw-channel/index.ts +68 -0
  114. package/server/openclaw-channel/package.json +19 -0
  115. package/server/openclaw-channel/types.ts +93 -0
  116. package/server/opencode-adapter.ts +951 -0
  117. package/server/openhands-adapter.ts +775 -0
  118. package/server/orchestrator-executor.ts +184 -0
  119. package/server/orchestrator-store.ts +106 -0
  120. package/server/orchestrator-types.ts +86 -0
  121. package/server/path-resolver.ts +168 -0
  122. package/server/pr-poller.ts +162 -0
  123. package/server/proactive-keepalive.ts +121 -0
  124. package/server/prompt-manager.ts +103 -0
  125. package/server/protocol-monitor.ts +197 -0
  126. package/server/race-controller.ts +295 -0
  127. package/server/race-store.ts +82 -0
  128. package/server/recorder.ts +359 -0
  129. package/server/recording-hub/compat-validator.ts +122 -0
  130. package/server/recording-hub/diagnostics.ts +289 -0
  131. package/server/recording-hub/hub-routes.ts +146 -0
  132. package/server/recording-hub/hub-store.ts +224 -0
  133. package/server/replay.ts +78 -0
  134. package/server/routes/adapter-routes.ts +31 -0
  135. package/server/routes/agent-mcp-routes.ts +35 -0
  136. package/server/routes/agent-routes.ts +185 -0
  137. package/server/routes/auth-routes.ts +101 -0
  138. package/server/routes/ci-routes.ts +165 -0
  139. package/server/routes/commands-routes.ts +94 -0
  140. package/server/routes/cron-routes.ts +100 -0
  141. package/server/routes/env-routes.ts +50 -0
  142. package/server/routes/folder-routes.ts +56 -0
  143. package/server/routes/fs-routes.ts +403 -0
  144. package/server/routes/gallery-routes.ts +410 -0
  145. package/server/routes/git-routes.ts +108 -0
  146. package/server/routes/index.ts +62 -0
  147. package/server/routes/linear-routes.ts +254 -0
  148. package/server/routes/monitor-routes.ts +11 -0
  149. package/server/routes/orchestrator-routes.ts +210 -0
  150. package/server/routes/prompt-routes.ts +47 -0
  151. package/server/routes/race-routes.ts +103 -0
  152. package/server/routes/recording-routes.ts +101 -0
  153. package/server/routes/route-deps.ts +32 -0
  154. package/server/routes/session-routes.ts +560 -0
  155. package/server/routes/settings-routes.ts +108 -0
  156. package/server/routes/skills-routes.ts +52 -0
  157. package/server/routes/system-routes.ts +243 -0
  158. package/server/routes/webhook-routes.ts +135 -0
  159. package/server/routes.ts +44 -0
  160. package/server/security-middleware.ts +100 -0
  161. package/server/semantic-memory.ts +389 -0
  162. package/server/service.ts +733 -0
  163. package/server/session-folders.ts +107 -0
  164. package/server/session-git-info.ts +89 -0
  165. package/server/session-linear-issues.ts +94 -0
  166. package/server/session-names.ts +67 -0
  167. package/server/session-store.ts +169 -0
  168. package/server/session-types.ts +415 -0
  169. package/server/settings-manager.ts +120 -0
  170. package/server/shared-context.ts +401 -0
  171. package/server/skills-manager.ts +250 -0
  172. package/server/sub-session-manager.ts +263 -0
  173. package/server/terminal-manager.ts +185 -0
  174. package/server/update-checker.ts +114 -0
  175. package/server/usage-limits.ts +192 -0
  176. package/server/webhook-manager.ts +228 -0
  177. package/server/webhook-store.ts +168 -0
  178. package/server/webhook-types.ts +66 -0
  179. package/server/worktree-tracker.ts +84 -0
  180. package/server/ws-bridge.ts +2322 -0
@@ -0,0 +1,132 @@
1
+ import { DEFAULT_OPENROUTER_MODEL, getSettings } from "./settings-manager.js";
2
+
3
+ const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
4
+
5
+ function sanitizeTitle(raw: string): string | null {
6
+ const title = raw.replace(/^"|"$/g, "").replace(/^'|'$/g, "").trim();
7
+ if (!title || title.length >= 100) return null;
8
+ return title;
9
+ }
10
+
11
+ function titleCaseWord(word: string): string {
12
+ if (/^[A-Z0-9]{2,}$/.test(word)) return word;
13
+ const lower = word.toLowerCase();
14
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
15
+ }
16
+
17
+ function fallbackTitleFromRequest(request: string): string | null {
18
+ const arithmetic = request.match(/\b\d+(?:\s*[+\-*/]\s*\d+)+\b/);
19
+ if (arithmetic) {
20
+ const expression = arithmetic[0];
21
+ if (expression.includes("+")) return "Addition Calculation";
22
+ if (expression.includes("-")) return "Subtraction Calculation";
23
+ if (expression.includes("*")) return "Multiplication Calculation";
24
+ if (expression.includes("/")) return "Division Calculation";
25
+ return "Math Calculation";
26
+ }
27
+
28
+ const words = request
29
+ .replace(/['"`]/g, "")
30
+ .split(/[^A-Za-z0-9]+/)
31
+ .map((word) => word.trim())
32
+ .filter((word) => word.length > 2)
33
+ .filter((word) => !/^(hey|what|who|why|how|when|where|please|could|would|should|about|into|with|from|that|this|there|their|models?)$/i.test(word))
34
+ .slice(0, 5)
35
+ .map(titleCaseWord);
36
+
37
+ if (words.length === 0) return null;
38
+ return words.join(" ");
39
+ }
40
+
41
+ function extractTextContent(content: unknown): string {
42
+ if (typeof content === "string") return content;
43
+ if (Array.isArray(content)) {
44
+ return content
45
+ .map((item) => {
46
+ if (typeof item === "string") return item;
47
+ if (item && typeof item === "object") {
48
+ const maybe = item as { text?: unknown };
49
+ return typeof maybe.text === "string" ? maybe.text : "";
50
+ }
51
+ return "";
52
+ })
53
+ .join("\n")
54
+ .trim();
55
+ }
56
+ return "";
57
+ }
58
+
59
+ /**
60
+ * Generates a short session title using OpenRouter.
61
+ * Returns null if OpenRouter isn't configured or if generation fails.
62
+ */
63
+ export async function generateSessionTitle(
64
+ firstUserMessage: string,
65
+ _model: string,
66
+ options?: {
67
+ timeoutMs?: number;
68
+ },
69
+ ): Promise<string | null> {
70
+ const timeout = options?.timeoutMs || 25_000;
71
+ const settings = getSettings();
72
+ const apiKey = settings.openrouterApiKey.trim();
73
+
74
+ if (!apiKey) {
75
+ return null;
76
+ }
77
+
78
+ const model = settings.openrouterModel?.trim() || DEFAULT_OPENROUTER_MODEL;
79
+ const truncated = firstUserMessage.slice(0, 500);
80
+ const userPrompt = `Generate a concise 3-5 word session title for this user request. Output only the title.\n\nRequest: ${truncated}`;
81
+
82
+ const controller = new AbortController();
83
+ const timer = setTimeout(() => controller.abort(), timeout);
84
+
85
+ try {
86
+ const res = await fetch(OPENROUTER_URL, {
87
+ method: "POST",
88
+ headers: {
89
+ "Content-Type": "application/json",
90
+ Authorization: `Bearer ${apiKey}`,
91
+ },
92
+ body: JSON.stringify({
93
+ model,
94
+ messages: [
95
+ {
96
+ role: "user",
97
+ content: userPrompt,
98
+ },
99
+ ],
100
+ max_tokens: 96,
101
+ temperature: 0.2,
102
+ }),
103
+ signal: controller.signal,
104
+ });
105
+
106
+ if (!res.ok) {
107
+ console.warn(`[auto-namer] OpenRouter request failed: ${res.status} ${res.statusText}`);
108
+ return null;
109
+ }
110
+
111
+ const data = await res.json() as {
112
+ choices?: Array<{
113
+ message?: {
114
+ content?: unknown;
115
+ };
116
+ }>;
117
+ };
118
+
119
+ const raw = extractTextContent(data.choices?.[0]?.message?.content);
120
+ const title = sanitizeTitle(raw);
121
+ if (title) return title;
122
+ if (raw.trim()) return null;
123
+
124
+ console.warn("[auto-namer] OpenRouter returned no usable title; using fallback title");
125
+ return fallbackTitleFromRequest(firstUserMessage);
126
+ } catch (err) {
127
+ console.warn("[auto-namer] Failed to generate session title via OpenRouter:", err);
128
+ return null;
129
+ } finally {
130
+ clearTimeout(timer);
131
+ }
132
+ }
@@ -0,0 +1,431 @@
1
+ /**
2
+ * Layer 3: Capability Discovery & Intelligent Routing
3
+ *
4
+ * Dynamically routes tasks to the best-suited agent backend using:
5
+ * 1. Self-reported capabilities (agent declares its strengths/tools on init)
6
+ * 2. Historical performance (task execution success rates from ~/.campfire/capability-learning.jsonl)
7
+ * 3. Real-time probing (send agent a "rate your confidence" message, parse JSON response)
8
+ *
9
+ * Scoring formula:
10
+ * score = selfReportedFit × 0.3
11
+ * + historicalSuccessRate × 0.4
12
+ * + contextAvailability × 0.2
13
+ * + costEfficiency × 0.1
14
+ *
15
+ * If the top candidate scores < 0.5, sends a capability_probe to the top 3 sessions
16
+ * and updates scores with real-time confidence before returning the final routing result.
17
+ */
18
+
19
+ import { mkdirSync, appendFileSync, readFileSync, existsSync, writeFileSync } from "node:fs";
20
+ import { join, dirname } from "node:path";
21
+ import { homedir } from "node:os";
22
+ import { randomUUID } from "node:crypto";
23
+ import type { BackendType } from "./session-types.js";
24
+
25
+ // ─── Types ────────────────────────────────────────────────────────────────────
26
+
27
+ export interface AgentCapabilities {
28
+ sessionId: string;
29
+ backendType: BackendType;
30
+ reportedAt: number;
31
+ strengths: string[];
32
+ weaknesses: string[];
33
+ availableTools: string[];
34
+ contextWindowTokens: number;
35
+ contextUsedPercent: number; // 0–100
36
+ costPerInputToken?: number;
37
+ costPerOutputToken?: number;
38
+ }
39
+
40
+ export interface TaskExecution {
41
+ id: string;
42
+ sessionId: string;
43
+ backendType: BackendType;
44
+ taskDescription: string;
45
+ taskType: string;
46
+ startedAt: number;
47
+ completedAt?: number;
48
+ outcome: "success" | "failure" | "partial";
49
+ humanFeedback?: "positive" | "negative" | "neutral";
50
+ costUsd?: number;
51
+ turnsUsed?: number;
52
+ }
53
+
54
+ export interface RouteTaskRequest {
55
+ taskDescription: string;
56
+ availableSessions: string[];
57
+ constraints?: {
58
+ maxCostUsd?: number;
59
+ maxTurns?: number;
60
+ requiredTools?: string[];
61
+ };
62
+ }
63
+
64
+ export interface RouteTaskResult {
65
+ sessionId: string;
66
+ backendType: BackendType;
67
+ confidence: number; // 0.0–1.0
68
+ reasoning: string;
69
+ alternatives: Array<{ sessionId: string; confidence: number; backendType: BackendType }>;
70
+ }
71
+
72
+ export interface CapabilityProbe {
73
+ probeId: string;
74
+ sessionId: string;
75
+ taskDescription: string;
76
+ /** Text to inject as user_message into the agent session */
77
+ instruction: string;
78
+ }
79
+
80
+ // ─── Storage helpers ──────────────────────────────────────────────────────────
81
+
82
+ const LEARNING_LOG = join(homedir(), ".campfire", "capability-learning.jsonl");
83
+ const CAPABILITIES_DIR = join(homedir(), ".campfire", "capabilities");
84
+
85
+ function ensureDirs(): void {
86
+ mkdirSync(dirname(LEARNING_LOG), { recursive: true });
87
+ mkdirSync(CAPABILITIES_DIR, { recursive: true });
88
+ }
89
+
90
+ function capabilityPath(sessionId: string): string {
91
+ return join(CAPABILITIES_DIR, `${sessionId}.json`);
92
+ }
93
+
94
+ // ─── Capability Discovery ─────────────────────────────────────────────────────
95
+
96
+ export class CapabilityDiscovery {
97
+ // In-memory capability cache (persisted to disk)
98
+ private capabilities = new Map<string, AgentCapabilities>();
99
+ // Pending capability probes: probeId → { sessionId, resolve }
100
+ private pendingProbes = new Map<string, { sessionId: string; resolve: (result: { confidence: number; reasoning: string }) => void }>();
101
+
102
+ constructor() {
103
+ ensureDirs();
104
+ this.loadCapabilities();
105
+ }
106
+
107
+ // ─── Capability registration ─────────────────────────────────────────────
108
+
109
+ /**
110
+ * Register or update capabilities for an agent session.
111
+ * Called when an agent initializes (from session_init or adapter metadata).
112
+ */
113
+ registerCapabilities(caps: AgentCapabilities): void {
114
+ this.capabilities.set(caps.sessionId, caps);
115
+ writeFileSync(capabilityPath(caps.sessionId), JSON.stringify(caps, null, 2), "utf-8");
116
+ }
117
+
118
+ getCapabilities(sessionId: string): AgentCapabilities | null {
119
+ return this.capabilities.get(sessionId) ?? null;
120
+ }
121
+
122
+ getAllCapabilities(): AgentCapabilities[] {
123
+ return Array.from(this.capabilities.values());
124
+ }
125
+
126
+ // ─── Task execution tracking ─────────────────────────────────────────────
127
+
128
+ /**
129
+ * Record the start of a task execution. Returns an execution ID.
130
+ */
131
+ startExecution(sessionId: string, backendType: BackendType, taskDescription: string): string {
132
+ const exec: TaskExecution = {
133
+ id: randomUUID(),
134
+ sessionId,
135
+ backendType,
136
+ taskDescription,
137
+ taskType: classifyTask(taskDescription),
138
+ startedAt: Date.now(),
139
+ outcome: "partial", // updated on completion
140
+ };
141
+ this.appendExecution(exec);
142
+ return exec.id;
143
+ }
144
+
145
+ /**
146
+ * Record task completion outcome. Appends updated record to the log.
147
+ */
148
+ completeExecution(
149
+ executionId: string,
150
+ sessionId: string,
151
+ backendType: BackendType,
152
+ taskDescription: string,
153
+ outcome: TaskExecution["outcome"],
154
+ opts?: { costUsd?: number; turnsUsed?: number },
155
+ ): void {
156
+ const exec: TaskExecution = {
157
+ id: executionId,
158
+ sessionId,
159
+ backendType,
160
+ taskDescription,
161
+ taskType: classifyTask(taskDescription),
162
+ startedAt: Date.now() - 1000, // approximate
163
+ completedAt: Date.now(),
164
+ outcome,
165
+ ...opts,
166
+ };
167
+ this.appendExecution(exec);
168
+ }
169
+
170
+ recordFeedback(executionId: string, sessionId: string, backendType: BackendType, taskDescription: string, feedback: "positive" | "negative" | "neutral"): void {
171
+ const exec: TaskExecution = {
172
+ id: executionId,
173
+ sessionId,
174
+ backendType,
175
+ taskDescription,
176
+ taskType: classifyTask(taskDescription),
177
+ startedAt: Date.now(),
178
+ outcome: "partial",
179
+ humanFeedback: feedback,
180
+ };
181
+ this.appendExecution(exec);
182
+ }
183
+
184
+ // ─── Intelligent routing ─────────────────────────────────────────────────
185
+
186
+ /**
187
+ * Route a task to the best-available agent session.
188
+ *
189
+ * Returns a RouteTaskResult with confidence and reasoning.
190
+ * If confidence < 0.5, callers should send capability probes first.
191
+ */
192
+ async route(request: RouteTaskRequest): Promise<RouteTaskResult> {
193
+ const { taskDescription, availableSessions, constraints } = request;
194
+
195
+ if (availableSessions.length === 0) {
196
+ return {
197
+ sessionId: "",
198
+ backendType: "claude",
199
+ confidence: 0,
200
+ reasoning: "No available sessions to route to.",
201
+ alternatives: [],
202
+ };
203
+ }
204
+
205
+ const history = this.loadHistory();
206
+ const taskType = classifyTask(taskDescription);
207
+
208
+ const scored = availableSessions
209
+ .map((sessionId) => {
210
+ const caps = this.capabilities.get(sessionId);
211
+ const score = this.scoreSession(sessionId, caps, taskDescription, taskType, history, constraints);
212
+ return { sessionId, caps, score };
213
+ })
214
+ .filter((s) => s.score > 0)
215
+ .sort((a, b) => b.score - a.score);
216
+
217
+ if (scored.length === 0) {
218
+ // No sessions with positive score — return first available
219
+ const fallback = availableSessions[0];
220
+ const caps = this.capabilities.get(fallback);
221
+ return {
222
+ sessionId: fallback,
223
+ backendType: caps?.backendType ?? "claude",
224
+ confidence: 0.1,
225
+ reasoning: "No capability data available — routing to first available session.",
226
+ alternatives: [],
227
+ };
228
+ }
229
+
230
+ const best = scored[0];
231
+ const alternatives = scored
232
+ .slice(1, 4)
233
+ .map((s) => ({ sessionId: s.sessionId, confidence: s.score, backendType: s.caps?.backendType ?? "claude" }));
234
+
235
+ return {
236
+ sessionId: best.sessionId,
237
+ backendType: best.caps?.backendType ?? "claude",
238
+ confidence: best.score,
239
+ reasoning: this.explainScore(best.sessionId, best.caps, taskType, best.score),
240
+ alternatives,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Create a capability probe to send to a session.
246
+ * The probe is injected as a user_message asking the agent to self-assess.
247
+ *
248
+ * Call resolveProbe() when the agent's response comes back.
249
+ */
250
+ createProbe(sessionId: string, taskDescription: string): CapabilityProbe {
251
+ const probeId = randomUUID();
252
+ return {
253
+ probeId,
254
+ sessionId,
255
+ taskDescription,
256
+ instruction: `[Capability Probe ${probeId}] Please rate your confidence for the following task on a scale of 0.0 to 1.0. Reply with only a JSON object in this exact format: { "confidence": 0.8, "reasoning": "I have strong experience with this task type" }\n\nTask: ${taskDescription}`,
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Register a pending probe — allows resolveProbe() to update routing scores.
262
+ */
263
+ registerProbe(probe: CapabilityProbe): Promise<{ confidence: number; reasoning: string }> {
264
+ return new Promise((resolve) => {
265
+ this.pendingProbes.set(probe.probeId, { sessionId: probe.sessionId, resolve });
266
+ // Timeout after 30s — default to 0.5 (neutral)
267
+ setTimeout(() => {
268
+ if (this.pendingProbes.has(probe.probeId)) {
269
+ this.pendingProbes.delete(probe.probeId);
270
+ resolve({ confidence: 0.5, reasoning: "Probe timed out" });
271
+ }
272
+ }, 30_000);
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Called when an agent responds to a capability probe.
278
+ * Parses the JSON response and resolves the probe promise.
279
+ */
280
+ resolveProbe(probeId: string, agentResponse: string): boolean {
281
+ const pending = this.pendingProbes.get(probeId);
282
+ if (!pending) return false;
283
+
284
+ try {
285
+ const jsonMatch = agentResponse.match(/\{[^}]+\}/);
286
+ if (jsonMatch) {
287
+ const parsed = JSON.parse(jsonMatch[0]) as { confidence?: number; reasoning?: string };
288
+ const confidence = typeof parsed.confidence === "number"
289
+ ? Math.max(0, Math.min(1, parsed.confidence))
290
+ : 0.5;
291
+ const reasoning = parsed.reasoning ?? "No reasoning provided";
292
+ pending.resolve({ confidence, reasoning });
293
+ this.pendingProbes.delete(probeId);
294
+ return true;
295
+ }
296
+ } catch {
297
+ // malformed — resolve with neutral
298
+ }
299
+
300
+ pending.resolve({ confidence: 0.5, reasoning: "Could not parse probe response" });
301
+ this.pendingProbes.delete(probeId);
302
+ return false;
303
+ }
304
+
305
+ getExecutionHistory(opts?: { backendType?: BackendType; taskType?: string }): TaskExecution[] {
306
+ return this.loadHistory().filter((e) => {
307
+ if (opts?.backendType && e.backendType !== opts.backendType) return false;
308
+ if (opts?.taskType && e.taskType !== opts.taskType) return false;
309
+ return true;
310
+ });
311
+ }
312
+
313
+ // ─── Internal scoring ────────────────────────────────────────────────────
314
+
315
+ private scoreSession(
316
+ sessionId: string,
317
+ caps: AgentCapabilities | undefined,
318
+ taskDescription: string,
319
+ taskType: string,
320
+ history: TaskExecution[],
321
+ constraints?: RouteTaskRequest["constraints"],
322
+ ): number {
323
+ // Filter out sessions that can't meet hard constraints
324
+ if (constraints?.requiredTools && caps) {
325
+ const available = new Set(caps.availableTools);
326
+ if (!constraints.requiredTools.every((t) => available.has(t))) return 0;
327
+ }
328
+
329
+ // self-reported fit (0.3 weight)
330
+ const selfFit = caps ? this.computeSelfFit(caps, taskDescription, taskType) : 0.5;
331
+
332
+ // historical success rate (0.4 weight)
333
+ const sessionHistory = history.filter((e) => e.sessionId === sessionId && e.taskType === taskType);
334
+ const successRate = sessionHistory.length > 0
335
+ ? sessionHistory.filter((e) => e.outcome === "success").length / sessionHistory.length
336
+ : 0.5; // neutral prior when no history
337
+
338
+ // context availability (0.2 weight) — penalize sessions near context limit
339
+ const contextAvail = caps ? Math.max(0, 1 - (caps.contextUsedPercent / 100)) : 0.5;
340
+
341
+ // cost efficiency (0.1 weight) — lower cost = higher score
342
+ const costScore = caps?.costPerInputToken != null
343
+ ? Math.max(0, 1 - (caps.costPerInputToken * 1e6)) // normalize token cost
344
+ : 0.5;
345
+
346
+ return (selfFit * 0.3) + (successRate * 0.4) + (contextAvail * 0.2) + (costScore * 0.1);
347
+ }
348
+
349
+ private computeSelfFit(caps: AgentCapabilities, taskDescription: string, taskType: string): number {
350
+ const desc = taskDescription.toLowerCase();
351
+ const strengthMatches = caps.strengths.filter((s) =>
352
+ desc.includes(s.toLowerCase()) || taskType.includes(s.toLowerCase())
353
+ ).length;
354
+
355
+ const weaknessMatches = caps.weaknesses.filter((w) =>
356
+ desc.includes(w.toLowerCase()) || taskType.includes(w.toLowerCase())
357
+ ).length;
358
+
359
+ return Math.max(0, Math.min(1, 0.5 + (strengthMatches * 0.2) - (weaknessMatches * 0.3)));
360
+ }
361
+
362
+ private explainScore(sessionId: string, caps: AgentCapabilities | undefined, taskType: string, score: number): string {
363
+ const parts: string[] = [];
364
+ parts.push(`Routing to session ${sessionId} (${caps?.backendType ?? "unknown"}) with confidence ${(score * 100).toFixed(0)}%.`);
365
+ if (caps?.strengths?.length) {
366
+ parts.push(`Strengths: ${caps.strengths.slice(0, 3).join(", ")}.`);
367
+ }
368
+ if (taskType !== "general") {
369
+ parts.push(`Task classified as: ${taskType}.`);
370
+ }
371
+ return parts.join(" ");
372
+ }
373
+
374
+ // ─── Persistence ─────────────────────────────────────────────────────────
375
+
376
+ private appendExecution(exec: TaskExecution): void {
377
+ ensureDirs();
378
+ appendFileSync(LEARNING_LOG, JSON.stringify(exec) + "\n", "utf-8");
379
+ }
380
+
381
+ private loadHistory(): TaskExecution[] {
382
+ if (!existsSync(LEARNING_LOG)) return [];
383
+ try {
384
+ return readFileSync(LEARNING_LOG, "utf-8")
385
+ .split("\n")
386
+ .filter(Boolean)
387
+ .map((line) => JSON.parse(line) as TaskExecution);
388
+ } catch {
389
+ return [];
390
+ }
391
+ }
392
+
393
+ private loadCapabilities(): void {
394
+ if (!existsSync(CAPABILITIES_DIR)) return;
395
+ try {
396
+ const { readdirSync } = require("node:fs") as typeof import("node:fs");
397
+ const files = readdirSync(CAPABILITIES_DIR).filter((f: string) => f.endsWith(".json"));
398
+ for (const file of files) {
399
+ try {
400
+ const caps = JSON.parse(readFileSync(join(CAPABILITIES_DIR, file), "utf-8")) as AgentCapabilities;
401
+ this.capabilities.set(caps.sessionId, caps);
402
+ } catch {
403
+ // skip corrupt file
404
+ }
405
+ }
406
+ } catch {
407
+ // directory not readable yet
408
+ }
409
+ }
410
+ }
411
+
412
+ // ─── Task classification ──────────────────────────────────────────────────────
413
+
414
+ /**
415
+ * Classify a task description into a task type keyword.
416
+ * Used for matching historical performance records.
417
+ */
418
+ function classifyTask(description: string): string {
419
+ const d = description.toLowerCase();
420
+ if (d.includes("refactor") || d.includes("restructure") || d.includes("reorganize")) return "refactoring";
421
+ if (d.includes("debug") || d.includes("fix") || d.includes("bug") || d.includes("error")) return "debugging";
422
+ if (d.includes("test") || d.includes("spec") || d.includes("coverage")) return "testing";
423
+ if (d.includes("document") || d.includes("readme") || d.includes("comment")) return "documentation";
424
+ if (d.includes("implement") || d.includes("add") || d.includes("create") || d.includes("build")) return "implementation";
425
+ if (d.includes("review") || d.includes("audit") || d.includes("analyze")) return "analysis";
426
+ if (d.includes("migration") || d.includes("upgrade") || d.includes("migrate")) return "migration";
427
+ return "general";
428
+ }
429
+
430
+ // Singleton instance
431
+ export const capabilityDiscovery = new CapabilityDiscovery();
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Seed Claude Code authentication into a Docker container.
3
+ * Copies the user's ~/.claude/ directory into the container's /root/.claude/
4
+ */
5
+
6
+ import { execSync } from "node:child_process";
7
+ import { join } from "node:path";
8
+ import { homedir } from "node:os";
9
+ import { existsSync } from "node:fs";
10
+
11
+ /**
12
+ * Copy Claude Code auth config from host into a running container.
13
+ * @param containerId Docker container ID or name
14
+ * @returns true if auth was seeded successfully
15
+ */
16
+ export function seedClaudeAuth(containerId: string): boolean {
17
+ const claudeDir = join(homedir(), ".claude");
18
+ if (!existsSync(claudeDir)) {
19
+ console.warn("[claude-container-auth] ~/.claude/ not found, skipping auth seeding");
20
+ return false;
21
+ }
22
+
23
+ try {
24
+ // Ensure target directory exists inside container
25
+ execSync(`docker exec ${containerId} mkdir -p /root/.claude`, {
26
+ timeout: 10_000,
27
+ });
28
+
29
+ // Copy host's .claude directory contents into the container
30
+ execSync(`docker cp "${claudeDir}/." ${containerId}:/root/.claude/`, {
31
+ timeout: 30_000,
32
+ });
33
+
34
+ console.log(`[claude-container-auth] Seeded Claude auth into container ${containerId}`);
35
+ return true;
36
+ } catch (err) {
37
+ console.error(`[claude-container-auth] Failed to seed auth into container ${containerId}:`, err);
38
+ return false;
39
+ }
40
+ }