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.
- package/bin/cli.ts +182 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/MermaidDiagram-By1J3Whj.js +2 -0
- package/dist/assets/_basePickBy-Dq6gnD-p.js +1 -0
- package/dist/assets/_baseUniq-SpgN6cGc.js +1 -0
- package/dist/assets/arc-D9SEA8dX.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-CoTcwLeR.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-Dvc63voV.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-tuDcG4_4.js +10 -0
- package/dist/assets/channel-CxhcHn7S.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-x1hChYu9.js +1 -0
- package/dist/assets/chunk-55IACEB6-BX6HWnWP.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-B4IlF03G.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-TpYb5rLg.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-DZX4LUPx.js +15 -0
- package/dist/assets/chunk-QN33PNHL-CuQs0NDY.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-CygWwVx8.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-CtI-IdKV.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-BM0LptJF.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-BM0LptJF.js +1 -0
- package/dist/assets/clone-C6kt1rE7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-PdySs991.js +1 -0
- package/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/dist/assets/dagre-6UL2VRFP-J0SxEiEu.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-CW0HLXaH.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-DADHF-k4.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-9S0-PC3S.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-BEHAiY_q.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-Cu6n9Xnd.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-Cr_BHryu.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-D5pT35v5.js +65 -0
- package/dist/assets/graph-DKRbrxcs.js +1 -0
- package/dist/assets/index-Byjg9zgI.css +32 -0
- package/dist/assets/index-Cwj3m0hT.js +284 -0
- package/dist/assets/infoDiagram-HS3SLOUP-BaJlrgKV.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-CXC4-DJv.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-BtHBu9cx.js +89 -0
- package/dist/assets/katex-BlHpptmG.js +261 -0
- package/dist/assets/layout-DnxSwLDJ.js +1 -0
- package/dist/assets/linear-CWbbIT8l.js +1 -0
- package/dist/assets/mermaid.core-DXn1cFAK.js +256 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-BR8br1hs.js +68 -0
- package/dist/assets/ordinal-BENe2yWM.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-B7RmXV7I.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-DK0FhyV_.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-3zAGxSpL.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-DZppKtjc.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-D1x3r3wV.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-2ByD08Vq.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-Vm1cYjgV.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-l5S35biR.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-DIPadPiF.js +162 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-9GUvwS2b.js +7 -0
- package/dist/favicon.svg +3 -0
- package/dist/fonts/Geist-Variable.woff2 +0 -0
- package/dist/fonts/GeistMono-Variable.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +33 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo.svg +3 -0
- package/dist/manifest.json +26 -0
- package/dist/og-image.png +0 -0
- package/dist/sw.js +130 -0
- package/package.json +92 -0
- package/server/ADAPTERS.md +121 -0
- package/server/adapter-registry-types.ts +28 -0
- package/server/adapter-registry.ts +293 -0
- package/server/adapter-types.ts +51 -0
- package/server/agent-executor.ts +249 -0
- package/server/agent-mcp-bridge.ts +148 -0
- package/server/agent-mcp-stdio.ts +171 -0
- package/server/agent-mcp-tools.ts +76 -0
- package/server/agent-store.ts +183 -0
- package/server/agent-types.ts +79 -0
- package/server/aider-adapter.ts +525 -0
- package/server/auth.ts +153 -0
- package/server/auto-namer.ts +132 -0
- package/server/capability-discovery.ts +431 -0
- package/server/claude-container-auth.ts +40 -0
- package/server/claude-stdio-adapter.ts +452 -0
- package/server/clawhub-export.ts +218 -0
- package/server/cli-launcher.ts +1613 -0
- package/server/codex-adapter.ts +1987 -0
- package/server/codex-container-auth.ts +40 -0
- package/server/codex-home.ts +26 -0
- package/server/collective-intelligence.ts +401 -0
- package/server/commands-discovery.ts +160 -0
- package/server/constants.ts +5 -0
- package/server/container-manager.ts +328 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +149 -0
- package/server/cron-types.ts +63 -0
- package/server/deliberation-engine.ts +323 -0
- package/server/embedding.ts +89 -0
- package/server/env-manager.ts +146 -0
- package/server/environment-detector.ts +106 -0
- package/server/environment-rules.ts +106 -0
- package/server/gallery-store.ts +216 -0
- package/server/gallery-types.ts +52 -0
- package/server/gallery-votes.ts +101 -0
- package/server/git-utils.ts +384 -0
- package/server/github-pr.ts +379 -0
- package/server/goose-adapter.ts +929 -0
- package/server/image-pull-manager.ts +144 -0
- package/server/index.ts +363 -0
- package/server/linear-project-manager.ts +78 -0
- package/server/moltbook-client.ts +160 -0
- package/server/openclaw-adapter.ts +916 -0
- package/server/openclaw-channel/channel.ts +188 -0
- package/server/openclaw-channel/index.ts +68 -0
- package/server/openclaw-channel/package.json +19 -0
- package/server/openclaw-channel/types.ts +93 -0
- package/server/opencode-adapter.ts +951 -0
- package/server/openhands-adapter.ts +775 -0
- package/server/orchestrator-executor.ts +184 -0
- package/server/orchestrator-store.ts +106 -0
- package/server/orchestrator-types.ts +86 -0
- package/server/path-resolver.ts +168 -0
- package/server/pr-poller.ts +162 -0
- package/server/proactive-keepalive.ts +121 -0
- package/server/prompt-manager.ts +103 -0
- package/server/protocol-monitor.ts +197 -0
- package/server/race-controller.ts +295 -0
- package/server/race-store.ts +82 -0
- package/server/recorder.ts +359 -0
- package/server/recording-hub/compat-validator.ts +122 -0
- package/server/recording-hub/diagnostics.ts +289 -0
- package/server/recording-hub/hub-routes.ts +146 -0
- package/server/recording-hub/hub-store.ts +224 -0
- package/server/replay.ts +78 -0
- package/server/routes/adapter-routes.ts +31 -0
- package/server/routes/agent-mcp-routes.ts +35 -0
- package/server/routes/agent-routes.ts +185 -0
- package/server/routes/auth-routes.ts +101 -0
- package/server/routes/ci-routes.ts +165 -0
- package/server/routes/commands-routes.ts +94 -0
- package/server/routes/cron-routes.ts +100 -0
- package/server/routes/env-routes.ts +50 -0
- package/server/routes/folder-routes.ts +56 -0
- package/server/routes/fs-routes.ts +403 -0
- package/server/routes/gallery-routes.ts +410 -0
- package/server/routes/git-routes.ts +108 -0
- package/server/routes/index.ts +62 -0
- package/server/routes/linear-routes.ts +254 -0
- package/server/routes/monitor-routes.ts +11 -0
- package/server/routes/orchestrator-routes.ts +210 -0
- package/server/routes/prompt-routes.ts +47 -0
- package/server/routes/race-routes.ts +103 -0
- package/server/routes/recording-routes.ts +101 -0
- package/server/routes/route-deps.ts +32 -0
- package/server/routes/session-routes.ts +560 -0
- package/server/routes/settings-routes.ts +108 -0
- package/server/routes/skills-routes.ts +52 -0
- package/server/routes/system-routes.ts +243 -0
- package/server/routes/webhook-routes.ts +135 -0
- package/server/routes.ts +44 -0
- package/server/security-middleware.ts +100 -0
- package/server/semantic-memory.ts +389 -0
- package/server/service.ts +733 -0
- package/server/session-folders.ts +107 -0
- package/server/session-git-info.ts +89 -0
- package/server/session-linear-issues.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-store.ts +169 -0
- package/server/session-types.ts +415 -0
- package/server/settings-manager.ts +120 -0
- package/server/shared-context.ts +401 -0
- package/server/skills-manager.ts +250 -0
- package/server/sub-session-manager.ts +263 -0
- package/server/terminal-manager.ts +185 -0
- package/server/update-checker.ts +114 -0
- package/server/usage-limits.ts +192 -0
- package/server/webhook-manager.ts +228 -0
- package/server/webhook-store.ts +168 -0
- package/server/webhook-types.ts +66 -0
- package/server/worktree-tracker.ts +84 -0
- 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
|
+
}
|