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,40 @@
1
+ /**
2
+ * Seed Codex authentication into a Docker container.
3
+ * Copies the user's ~/.codex/ directory into the container's /root/.codex/
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 Codex 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 seedCodexAuth(containerId: string): boolean {
17
+ const codexDir = join(homedir(), ".codex");
18
+ if (!existsSync(codexDir)) {
19
+ console.warn("[codex-container-auth] ~/.codex/ 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/.codex`, {
26
+ timeout: 10_000,
27
+ });
28
+
29
+ // Copy host's .codex directory contents into the container
30
+ execSync(`docker cp "${codexDir}/." ${containerId}:/root/.codex/`, {
31
+ timeout: 30_000,
32
+ });
33
+
34
+ console.log(`[codex-container-auth] Seeded Codex auth into container ${containerId}`);
35
+ return true;
36
+ } catch (err) {
37
+ console.error(`[codex-container-auth] Failed to seed auth into container ${containerId}:`, err);
38
+ return false;
39
+ }
40
+ }
@@ -0,0 +1,26 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+
4
+ export const DEFAULT_CAMPFIRE_CODEX_HOME = join(
5
+ homedir(),
6
+ ".campfire",
7
+ "codex-home",
8
+ );
9
+
10
+ export function getLegacyCodexHome(): string {
11
+ return join(homedir(), ".codex");
12
+ }
13
+
14
+ export function resolveCampfireCodexHome(explicitCodexHome?: string): string {
15
+ // Intentionally do NOT fall back to process.env.CODEX_HOME here.
16
+ // That env var points to the user's global Codex home (~/.codex), which
17
+ // would break per-session isolation by nesting session dirs inside it.
18
+ return resolve(explicitCodexHome || DEFAULT_CAMPFIRE_CODEX_HOME);
19
+ }
20
+
21
+ export function resolveCampfireCodexSessionHome(
22
+ sessionId: string,
23
+ explicitCodexHome?: string,
24
+ ): string {
25
+ return join(resolveCampfireCodexHome(explicitCodexHome), sessionId);
26
+ }
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Collective Intelligence Layer β€” Orchestrator
3
+ *
4
+ * Wires all four CI layers together and hooks into WsBridge via two interception points:
5
+ * 1. processAgentMessage() β€” called for every agent β†’ browser message
6
+ * 2. processBrowserMessage() β€” called for every browser β†’ agent message
7
+ *
8
+ * Integration in ws-bridge.ts (3 lines each):
9
+ *
10
+ * // In handleCLIMessage / adapter message handler:
11
+ * if (this.collectiveIntelligence) {
12
+ * this.collectiveIntelligence.processAgentMessage(sessionId, browserMsg);
13
+ * }
14
+ *
15
+ * // In routeBrowserMessage before forwarding to agent:
16
+ * if (this.collectiveIntelligence) {
17
+ * const enriched = await this.collectiveIntelligence.processBrowserMessage(sessionId, msg);
18
+ * if (enriched === null) return; // rerouted or blocked
19
+ * msg = enriched;
20
+ * }
21
+ *
22
+ * Design: non-invasive observer pattern. All processing is async and non-blocking.
23
+ * If any CI layer throws, it logs and continues β€” the main chat flow is never affected.
24
+ */
25
+
26
+ import type { BrowserIncomingMessage, BrowserOutgoingMessage, BackendType } from "./session-types.js";
27
+ import { storeFragment, queryFragments, consolidateSession, getConsolidatedKnowledge } from "./semantic-memory.js";
28
+ import type { MemoryFragment, ConsolidatedKnowledge, GitContext } from "./semantic-memory.js";
29
+ import { deliberationEngine } from "./deliberation-engine.js";
30
+ import type { DeliberationProposal, DeliberationResolution } from "./deliberation-engine.js";
31
+ import { capabilityDiscovery } from "./capability-discovery.js";
32
+ import type { AgentCapabilities, RouteTaskResult } from "./capability-discovery.js";
33
+ import { sharedContextManager } from "./shared-context.js";
34
+ import type { ContextFragment, ConsensusState } from "./shared-context.js";
35
+
36
+ // ─── Broadcast callback ───────────────────────────────────────────────────────
37
+
38
+ /** Called by WsBridge to send CI-generated messages to all browsers in a session */
39
+ type BroadcastFn = (sessionId: string, msg: BrowserIncomingMessage) => void;
40
+
41
+ // ─── CollectiveIntelligenceLayer ──────────────────────────────────────────────
42
+
43
+ export class CollectiveIntelligenceLayer {
44
+ private broadcast: BroadcastFn | null = null;
45
+
46
+ constructor() {
47
+ // Wire up deliberation resolution β†’ broadcast
48
+ deliberationEngine.setOnResolution((resolution) => {
49
+ // Find which session this belongs to (resolution has proposalId, not sessionId)
50
+ // The proposal was registered with sessionId, but resolutions don't carry it.
51
+ // We broadcast to all sessions for now β€” ws-bridge filters by sessionId in practice.
52
+ // In a stricter implementation, store proposalId→sessionId mapping.
53
+ this.broadcast?.("__all__", {
54
+ type: "deliberation_resolved",
55
+ resolution,
56
+ } as BrowserIncomingMessage);
57
+ });
58
+
59
+ // Wire up shared context stream callbacks
60
+ sharedContextManager.setOnFragment((fragment, links) => {
61
+ this.broadcast?.(fragment.sessionId, {
62
+ type: "shared_thought",
63
+ fragment,
64
+ } as BrowserIncomingMessage);
65
+
66
+ for (const link of links) {
67
+ this.broadcast?.(fragment.sessionId, {
68
+ type: "semantic_link_added",
69
+ sourceId: fragment.fragmentId,
70
+ targetId: link.targetFragmentId,
71
+ relation: link.relation,
72
+ } as BrowserIncomingMessage);
73
+ }
74
+ });
75
+
76
+ sharedContextManager.setOnConsensus((state) => {
77
+ this.broadcast?.(state.sessionId, {
78
+ type: "consensus_update",
79
+ state,
80
+ } as BrowserIncomingMessage);
81
+ });
82
+ }
83
+
84
+ setBroadcast(fn: BroadcastFn): void {
85
+ this.broadcast = fn;
86
+ }
87
+
88
+ // ─── Agent β†’ Browser ────────────────────────────────────────────────────
89
+
90
+ /**
91
+ * Called for every message coming from an agent backend before it's sent to browsers.
92
+ * Non-blocking: runs async side effects (memory extraction, context stream update).
93
+ * Never delays the main message delivery.
94
+ */
95
+ processAgentMessage(
96
+ sessionId: string,
97
+ backendType: BackendType,
98
+ msg: BrowserIncomingMessage,
99
+ gitContext?: Partial<GitContext>,
100
+ ): void {
101
+ // Run all CI processing in background β€” never block the chat flow
102
+ this.processAgentMessageAsync(sessionId, backendType, msg, gitContext).catch((err) => {
103
+ console.warn("[ci] processAgentMessage error:", err);
104
+ });
105
+ }
106
+
107
+ private async processAgentMessageAsync(
108
+ sessionId: string,
109
+ backendType: BackendType,
110
+ msg: BrowserIncomingMessage,
111
+ gitContext?: Partial<GitContext>,
112
+ ): Promise<void> {
113
+ // Layer 1: Extract memory observations from assistant messages
114
+ if (msg.type === "assistant") {
115
+ await this.extractMemory(sessionId, backendType, msg.message, gitContext);
116
+ }
117
+
118
+ // Layer 2: Handle deliberation proposals emitted by agents
119
+ if (msg.type === "deliberation_proposal") {
120
+ const proposal = (msg as { type: "deliberation_proposal"; proposal: DeliberationProposal }).proposal;
121
+ deliberationEngine.register({
122
+ ...proposal,
123
+ sessionId,
124
+ backendType,
125
+ });
126
+ // Broadcast back so all browsers see it
127
+ this.broadcast?.(sessionId, msg);
128
+ }
129
+
130
+ // Layer 3: Track capability probe responses
131
+ if (msg.type === "assistant") {
132
+ // Check if assistant message is a probe response
133
+ const content = this.extractTextContent(msg.message);
134
+ if (content) {
135
+ // Try to resolve any pending probe (the agent may be responding to a probe)
136
+ // Probes embed their ID in the instruction, and agents include it in their response
137
+ const probeMatch = content.match(/Capability Probe ([a-f0-9-]{36})/i);
138
+ if (probeMatch) {
139
+ capabilityDiscovery.resolveProbe(probeMatch[1], content);
140
+ }
141
+ }
142
+ }
143
+
144
+ // Layer 4: Feed agent thoughts to shared context stream
145
+ if (msg.type === "stream_event") {
146
+ const event = (msg as { type: "stream_event"; event: Record<string, unknown> }).event as Record<string, unknown>;
147
+ // Capture Claude's thinking blocks
148
+ if (event?.type === "content_block_start") {
149
+ const block = event.content_block as Record<string, unknown> | undefined;
150
+ if (block?.type === "thinking" && typeof block.thinking === "string") {
151
+ const stream = sharedContextManager.getOrCreate(sessionId);
152
+ await stream.ingest({
153
+ agentId: sessionId,
154
+ backendType,
155
+ isHuman: false,
156
+ type: "thought",
157
+ content: block.thinking,
158
+ });
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ // ─── Browser β†’ Agent ────────────────────────────────────────────────────
165
+
166
+ /**
167
+ * Called for every browser β†’ agent message.
168
+ * Returns the (possibly enriched) message to forward to the agent,
169
+ * or null if the message was consumed (rerouted or blocked).
170
+ *
171
+ * Enrichment: prepends relevant memory context to user_message prompts.
172
+ */
173
+ async processBrowserMessage(
174
+ sessionId: string,
175
+ msg: BrowserOutgoingMessage,
176
+ ): Promise<BrowserOutgoingMessage | null> {
177
+ try {
178
+ // Layer 1: Enrich user prompts with semantic memory context
179
+ if (msg.type === "user_message") {
180
+ return await this.enrichWithMemory(sessionId, msg);
181
+ }
182
+
183
+ // Layer 2: Handle human deliberation responses
184
+ if (msg.type === "deliberation_respond") {
185
+ const m = msg as { type: "deliberation_respond"; proposalId: string; stance: string; reasoning: string; suggestedAlternative?: string; concerns?: string[] };
186
+ deliberationEngine.addResponse({
187
+ proposalId: m.proposalId,
188
+ responderId: "human",
189
+ responderType: "human",
190
+ timestamp: Date.now(),
191
+ stance: m.stance as "agree" | "disagree" | "suggest_alternative" | "abstain",
192
+ reasoning: m.reasoning,
193
+ suggestedAlternative: m.suggestedAlternative,
194
+ concerns: m.concerns,
195
+ });
196
+ return null; // consumed β€” no need to forward to agent
197
+ }
198
+
199
+ if (msg.type === "deliberation_resolve") {
200
+ const m = msg as { type: "deliberation_resolve"; proposalId: string };
201
+ deliberationEngine.resolveById(m.proposalId);
202
+ return null;
203
+ }
204
+
205
+ // Layer 1: Manual memory query from browser
206
+ if (msg.type === "memory_query") {
207
+ const m = msg as { type: "memory_query"; query: string; limit?: number };
208
+ const results = await queryFragments(m.query, { sessionId, limit: m.limit ?? 10 });
209
+ this.broadcast?.(sessionId, {
210
+ type: "memory_query_result",
211
+ query: m.query,
212
+ results,
213
+ } as BrowserIncomingMessage);
214
+ return null;
215
+ }
216
+
217
+ // Layer 1: Manual memory store from browser
218
+ if (msg.type === "memory_store") {
219
+ const m = msg as { type: "memory_store"; content: string; memoryType: string; tags: string[]; gitContext?: Record<string, unknown> };
220
+ const fragment = await storeFragment({
221
+ sessionId,
222
+ agentId: "human",
223
+ backendType: "claude",
224
+ type: (m.memoryType as "observation" | "hypothesis" | "decision" | "pattern") || "observation",
225
+ content: m.content,
226
+ tags: m.tags,
227
+ gitContext: (m.gitContext as unknown as GitContext) ?? { branch: "unknown", files: [], repoRoot: "" },
228
+ });
229
+ this.broadcast?.(sessionId, {
230
+ type: "memory_stored",
231
+ fragment,
232
+ } as BrowserIncomingMessage);
233
+ return null;
234
+ }
235
+
236
+ // Layer 3: Route task request
237
+ if (msg.type === "route_task") {
238
+ const m = msg as { type: "route_task"; taskDescription: string; availableSessions?: string[] };
239
+ const result = await capabilityDiscovery.route({
240
+ taskDescription: m.taskDescription,
241
+ availableSessions: m.availableSessions ?? [],
242
+ });
243
+ this.broadcast?.(sessionId, {
244
+ type: "route_result",
245
+ result,
246
+ } as BrowserIncomingMessage);
247
+ return null;
248
+ }
249
+
250
+ // Layer 4: Human injects thought into shared context stream
251
+ if (msg.type === "inject_thought") {
252
+ const m = msg as { type: "inject_thought"; content: string; thoughtType: string; parentId?: string };
253
+ const stream = sharedContextManager.getOrCreate(sessionId);
254
+ await stream.ingest({
255
+ agentId: "human",
256
+ isHuman: true,
257
+ type: m.thoughtType as "thought" | "observation" | "concern" | "question" | "answer" | "insight" | "plan",
258
+ content: m.content,
259
+ parentId: m.parentId,
260
+ });
261
+ return null;
262
+ }
263
+ } catch (err) {
264
+ console.warn("[ci] processBrowserMessage error:", err);
265
+ }
266
+
267
+ return msg; // pass through unchanged
268
+ }
269
+
270
+ // ─── Session lifecycle ────────────────────────────────────────────────────
271
+
272
+ /**
273
+ * Called when a session ends. Consolidates memory and promotes significant
274
+ * shared context fragments to semantic memory.
275
+ */
276
+ async onSessionEnd(sessionId: string, backendType: BackendType, repoRoot: string): Promise<void> {
277
+ try {
278
+ // Consolidate episodic β†’ semantic memory
279
+ await consolidateSession(sessionId, repoRoot);
280
+
281
+ // Promote significant shared context fragments to semantic memory
282
+ const stream = sharedContextManager.get(sessionId);
283
+ if (stream) {
284
+ const significant = stream.getSignificantFragments();
285
+ for (const f of significant) {
286
+ await storeFragment({
287
+ sessionId,
288
+ agentId: f.agentId,
289
+ backendType,
290
+ type: "observation",
291
+ content: f.content,
292
+ gitContext: { branch: "unknown", files: [], repoRoot },
293
+ tags: [f.type, "shared-context"],
294
+ confidence: f.consensusScore,
295
+ });
296
+ }
297
+ sharedContextManager.remove(sessionId);
298
+ }
299
+ } catch (err) {
300
+ console.warn("[ci] onSessionEnd error:", err);
301
+ }
302
+ }
303
+
304
+ // ─── Internal helpers ────────────────────────────────────────────────────
305
+
306
+ private async enrichWithMemory(
307
+ sessionId: string,
308
+ msg: BrowserOutgoingMessage & { type: "user_message" },
309
+ ): Promise<BrowserOutgoingMessage> {
310
+ const memories = await queryFragments(msg.content, { sessionId, limit: 5 });
311
+ if (memories.length === 0) return msg;
312
+
313
+ const context = this.formatMemoryContext(memories);
314
+ return { ...msg, content: `${context}\n\n${msg.content}` };
315
+ }
316
+
317
+ private formatMemoryContext(memories: MemoryFragment[]): string {
318
+ const lines = memories
319
+ .slice(0, 5)
320
+ .map((m) => `[Memory/${m.type}] ${m.content}${m.gitContext.files.length > 0 ? ` (${m.gitContext.files.slice(0, 2).join(", ")})` : ""}`)
321
+ .join("\n");
322
+ return `--- Relevant Context from Previous Sessions ---\n${lines}\n---`;
323
+ }
324
+
325
+ private async extractMemory(
326
+ sessionId: string,
327
+ backendType: BackendType,
328
+ message: unknown,
329
+ gitContext?: Partial<GitContext>,
330
+ ): Promise<void> {
331
+ // Extract meaningful observations from assistant messages.
332
+ // We look for tool results (Read/Write/Edit/Bash) which are rich in codebase knowledge.
333
+ const content = this.extractTextContent(message);
334
+ if (!content || content.length < 50) return; // too short to be meaningful
335
+
336
+ // Heuristic: only store if content looks like codebase knowledge
337
+ const keywords = ["function", "class", "interface", "module", "import", "export", "config", "pattern", "architecture", "convention"];
338
+ const hasKeyword = keywords.some((k) => content.toLowerCase().includes(k));
339
+ if (!hasKeyword) return;
340
+
341
+ await storeFragment({
342
+ sessionId,
343
+ agentId: sessionId,
344
+ backendType,
345
+ type: "observation",
346
+ content: content.slice(0, 500), // cap fragment length
347
+ gitContext: {
348
+ branch: gitContext?.branch ?? "unknown",
349
+ files: gitContext?.files ?? [],
350
+ repoRoot: gitContext?.repoRoot ?? "",
351
+ },
352
+ tags: this.extractTags(content),
353
+ confidence: 0.6,
354
+ });
355
+ }
356
+
357
+ private extractTextContent(message: unknown): string | null {
358
+ if (typeof message === "string") return message;
359
+ if (!message || typeof message !== "object") return null;
360
+ const msg = message as Record<string, unknown>;
361
+
362
+ // Claude-style: { content: [{ type: "text", text: "..." }] }
363
+ if (Array.isArray(msg.content)) {
364
+ const texts = (msg.content as Array<Record<string, unknown>>)
365
+ .filter((b) => b.type === "text")
366
+ .map((b) => b.text as string)
367
+ .join(" ");
368
+ return texts || null;
369
+ }
370
+
371
+ if (typeof msg.content === "string") return msg.content;
372
+ if (typeof msg.text === "string") return msg.text;
373
+ return null;
374
+ }
375
+
376
+ private extractTags(content: string): string[] {
377
+ const tags: string[] = [];
378
+ const lower = content.toLowerCase();
379
+ const tagKeywords: [string, string][] = [
380
+ ["auth", "auth"], ["jwt", "auth"], ["oauth", "auth"],
381
+ ["database", "database"], ["sql", "database"], ["postgres", "database"], ["redis", "cache"],
382
+ ["api", "api"], ["rest", "api"], ["graphql", "api"],
383
+ ["test", "testing"], ["spec", "testing"],
384
+ ["route", "routing"], ["router", "routing"],
385
+ ["config", "config"], ["env", "config"],
386
+ ["deploy", "deployment"], ["docker", "deployment"],
387
+ ["type", "typescript"], ["interface", "typescript"],
388
+ ];
389
+ const seen = new Set<string>();
390
+ for (const [keyword, tag] of tagKeywords) {
391
+ if (lower.includes(keyword) && !seen.has(tag)) {
392
+ tags.push(tag);
393
+ seen.add(tag);
394
+ }
395
+ }
396
+ return tags.slice(0, 5);
397
+ }
398
+ }
399
+
400
+ // Singleton instance β€” injected into WsBridge at startup
401
+ export const collectiveIntelligenceLayer = new CollectiveIntelligenceLayer();
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Commands Discovery β€” scans the filesystem for slash commands and skills.
3
+ *
4
+ * Discovers:
5
+ * - Slash commands: .md files in ~/.claude/commands/ and {cwd}/.claude/commands/
6
+ * - Skills: directories with SKILL.md in ~/.claude/skills/
7
+ *
8
+ * Returns structured data with names, descriptions, source labels,
9
+ * and file paths. Used to pre-populate session state before CLI connects.
10
+ */
11
+
12
+ import { readdirSync, readFileSync, existsSync } from "node:fs";
13
+ import { join, basename } from "node:path";
14
+ import { homedir } from "node:os";
15
+
16
+ // ─── Types ──────────────────────────────────────────────────────────────────
17
+
18
+ export interface DiscoveredCommand {
19
+ name: string;
20
+ description: string;
21
+ source: "user" | "project";
22
+ path: string;
23
+ }
24
+
25
+ export interface DiscoveredSkill {
26
+ slug: string;
27
+ name: string;
28
+ description: string;
29
+ path: string;
30
+ }
31
+
32
+ export interface DiscoveryResult {
33
+ commands: DiscoveredCommand[];
34
+ skills: DiscoveredSkill[];
35
+ slashCommandNames: string[];
36
+ skillSlugs: string[];
37
+ }
38
+
39
+ // ─── Discovery ──────────────────────────────────────────────────────────────
40
+
41
+ export function discoverCommandsAndSkills(cwd?: string): DiscoveryResult {
42
+ const commands: DiscoveredCommand[] = [];
43
+ const skills: DiscoveredSkill[] = [];
44
+ const seen = new Set<string>();
45
+
46
+ // User-level commands: ~/.claude/commands/*.md
47
+ const userCommandsDir = join(homedir(), ".claude", "commands");
48
+ for (const cmd of scanCommands(userCommandsDir, "user")) {
49
+ if (!seen.has(cmd.name)) {
50
+ commands.push(cmd);
51
+ seen.add(cmd.name);
52
+ }
53
+ }
54
+
55
+ // Project-level commands: {cwd}/.claude/commands/*.md
56
+ if (cwd) {
57
+ const projectCommandsDir = join(cwd, ".claude", "commands");
58
+ for (const cmd of scanCommands(projectCommandsDir, "project")) {
59
+ if (!seen.has(cmd.name)) {
60
+ commands.push(cmd);
61
+ seen.add(cmd.name);
62
+ }
63
+ }
64
+ }
65
+
66
+ // User-level skills: ~/.claude/skills/*/SKILL.md
67
+ const userSkillsDir = join(homedir(), ".claude", "skills");
68
+ skills.push(...scanSkills(userSkillsDir));
69
+
70
+ commands.sort((a, b) => a.name.localeCompare(b.name));
71
+ skills.sort((a, b) => a.slug.localeCompare(b.slug));
72
+
73
+ return {
74
+ commands,
75
+ skills,
76
+ slashCommandNames: commands.map((c) => c.name),
77
+ skillSlugs: skills.map((s) => s.slug),
78
+ };
79
+ }
80
+
81
+ /** Read a specific command file's content. */
82
+ export function readCommandContent(filePath: string): string | null {
83
+ try {
84
+ return readFileSync(filePath, "utf-8");
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ // ─── Scanners ───────────────────────────────────────────────────────────────
91
+
92
+ function scanCommands(dir: string, source: "user" | "project"): DiscoveredCommand[] {
93
+ if (!existsSync(dir)) return [];
94
+ try {
95
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
96
+ return files.map((f) => {
97
+ const filePath = join(dir, f);
98
+ const name = basename(f, ".md");
99
+ const description = extractFirstLine(filePath);
100
+ return { name, description, source, path: filePath };
101
+ });
102
+ } catch {
103
+ return [];
104
+ }
105
+ }
106
+
107
+ function scanSkills(dir: string): DiscoveredSkill[] {
108
+ if (!existsSync(dir)) return [];
109
+ try {
110
+ const entries = readdirSync(dir, { withFileTypes: true });
111
+ const results: DiscoveredSkill[] = [];
112
+ for (const entry of entries) {
113
+ if (!entry.isDirectory()) continue;
114
+ const skillMd = join(dir, entry.name, "SKILL.md");
115
+ if (!existsSync(skillMd)) continue;
116
+ const { name, description } = parseSkillFrontmatter(skillMd);
117
+ results.push({
118
+ slug: entry.name,
119
+ name: name || entry.name,
120
+ description,
121
+ path: skillMd,
122
+ });
123
+ }
124
+ return results;
125
+ } catch {
126
+ return [];
127
+ }
128
+ }
129
+
130
+ function extractFirstLine(filePath: string): string {
131
+ try {
132
+ const content = readFileSync(filePath, "utf-8");
133
+ // Skip frontmatter (---...---) if present
134
+ const stripped = content.replace(/^---[\s\S]*?---\n?/, "").trim();
135
+ const firstLine = stripped.split("\n")[0]?.trim() || "";
136
+ // Remove markdown heading prefix
137
+ return firstLine.replace(/^#+\s*/, "").slice(0, 120);
138
+ } catch {
139
+ return "";
140
+ }
141
+ }
142
+
143
+ function parseSkillFrontmatter(filePath: string): { name: string; description: string } {
144
+ try {
145
+ const content = readFileSync(filePath, "utf-8");
146
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
147
+ if (!match) return { name: "", description: "" };
148
+ const frontmatter = match[1];
149
+ // Use simple line-by-line parsing instead of backtracking regex
150
+ let name = "";
151
+ let description = "";
152
+ for (const line of frontmatter.split("\n")) {
153
+ if (line.startsWith("name:")) name = line.slice(5).trim();
154
+ else if (line.startsWith("description:")) description = line.slice(12).trim();
155
+ }
156
+ return { name, description };
157
+ } catch {
158
+ return { name: "", description: "" };
159
+ }
160
+ }
@@ -0,0 +1,5 @@
1
+ /** Default port for Campfire (user-facing, all modes). */
2
+ export const DEFAULT_PORT = 4567;
3
+
4
+ /** Internal backend port used only in dev mode (Vite proxies to this). Never user-facing. */
5
+ export const INTERNAL_DEV_BACKEND_PORT = 14567;