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,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 4: Shared Context Stream
|
|
3
|
+
*
|
|
4
|
+
* Real-time collective reasoning stream where agents and humans "think aloud"
|
|
5
|
+
* into a shared space. Other agents and humans can observe, react, and build
|
|
6
|
+
* on each other's thinking.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - ContextFragments (thought/observation/plan/question/answer/insight/concern)
|
|
10
|
+
* - Semantic linking: detects agrees_with / disagrees_with / builds_on / contradicts / questions
|
|
11
|
+
* - Consensus detection: tracks which fragments are widely agreed upon vs controversial
|
|
12
|
+
* - Thread support via parentId chains
|
|
13
|
+
* - On session end, significant fragments promoted to SemanticMemory (not done here — done in CI orchestrator)
|
|
14
|
+
*
|
|
15
|
+
* Storage: In-memory per session (ephemeral).
|
|
16
|
+
* Significant fragments are promoted to LanceDB SemanticMemory on session end.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import type { BackendType } from "./session-types.js";
|
|
21
|
+
|
|
22
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export type ContextFragmentType =
|
|
25
|
+
| "thought" // internal reasoning step
|
|
26
|
+
| "observation" // noticed something
|
|
27
|
+
| "plan" // intended next steps
|
|
28
|
+
| "question" // posed to others
|
|
29
|
+
| "answer" // response to a question
|
|
30
|
+
| "insight" // non-obvious connection
|
|
31
|
+
| "concern"; // risk or issue identified
|
|
32
|
+
|
|
33
|
+
export type SemanticRelation =
|
|
34
|
+
| "agrees_with"
|
|
35
|
+
| "disagrees_with"
|
|
36
|
+
| "builds_on"
|
|
37
|
+
| "contradicts"
|
|
38
|
+
| "questions";
|
|
39
|
+
|
|
40
|
+
export interface SemanticLink {
|
|
41
|
+
targetFragmentId: string;
|
|
42
|
+
relation: SemanticRelation;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ContextFragment {
|
|
46
|
+
fragmentId: string;
|
|
47
|
+
sessionId: string;
|
|
48
|
+
agentId: string;
|
|
49
|
+
backendType?: BackendType;
|
|
50
|
+
isHuman: boolean;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
type: ContextFragmentType;
|
|
53
|
+
content: string;
|
|
54
|
+
parentId?: string;
|
|
55
|
+
semanticLinks: SemanticLink[];
|
|
56
|
+
consensusScore: number; // 0–1: how widely agreed upon
|
|
57
|
+
isControversial: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DisagreementCluster {
|
|
61
|
+
fragmentIds: string[];
|
|
62
|
+
topic: string;
|
|
63
|
+
summary: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ConsensusState {
|
|
67
|
+
sessionId: string;
|
|
68
|
+
updatedAt: number;
|
|
69
|
+
consensusPoints: string[]; // fragmentIds with high consensusScore (> 0.7)
|
|
70
|
+
disagreements: DisagreementCluster[];
|
|
71
|
+
openQuestions: string[]; // unanswered question fragmentIds
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type FragmentCallback = (fragment: ContextFragment, links: SemanticLink[]) => void;
|
|
75
|
+
export type ConsensusCallback = (state: ConsensusState) => void;
|
|
76
|
+
|
|
77
|
+
// ─── SharedContextStream ──────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Per-session shared context stream.
|
|
81
|
+
* Instantiated by CollectiveIntelligenceLayer, one per session.
|
|
82
|
+
*/
|
|
83
|
+
export class SharedContextStream {
|
|
84
|
+
private fragments = new Map<string, ContextFragment>(); // fragmentId → fragment
|
|
85
|
+
private onFragment: FragmentCallback | null = null;
|
|
86
|
+
private onConsensus: ConsensusCallback | null = null;
|
|
87
|
+
|
|
88
|
+
constructor(public readonly sessionId: string) {}
|
|
89
|
+
|
|
90
|
+
setOnFragment(cb: FragmentCallback): void { this.onFragment = cb; }
|
|
91
|
+
setOnConsensus(cb: ConsensusCallback): void { this.onConsensus = cb; }
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Ingest a new fragment into the stream.
|
|
95
|
+
* Runs semantic linking and consensus detection asynchronously.
|
|
96
|
+
*/
|
|
97
|
+
async ingest(opts: {
|
|
98
|
+
agentId: string;
|
|
99
|
+
backendType?: BackendType;
|
|
100
|
+
isHuman: boolean;
|
|
101
|
+
type: ContextFragmentType;
|
|
102
|
+
content: string;
|
|
103
|
+
parentId?: string;
|
|
104
|
+
}): Promise<ContextFragment> {
|
|
105
|
+
const fragment: ContextFragment = {
|
|
106
|
+
fragmentId: randomUUID(),
|
|
107
|
+
sessionId: this.sessionId,
|
|
108
|
+
agentId: opts.agentId,
|
|
109
|
+
backendType: opts.backendType,
|
|
110
|
+
isHuman: opts.isHuman,
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
type: opts.type,
|
|
113
|
+
content: opts.content,
|
|
114
|
+
parentId: opts.parentId,
|
|
115
|
+
semanticLinks: [],
|
|
116
|
+
consensusScore: 0.5, // neutral prior
|
|
117
|
+
isControversial: false,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
this.fragments.set(fragment.fragmentId, fragment);
|
|
121
|
+
|
|
122
|
+
// Semantic linking (lightweight heuristic, non-blocking)
|
|
123
|
+
const links = this.detectSemanticLinks(fragment);
|
|
124
|
+
if (links.length > 0) {
|
|
125
|
+
fragment.semanticLinks = links;
|
|
126
|
+
// Update target fragments with back-references
|
|
127
|
+
this.applyLinks(fragment.fragmentId, links);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Consensus detection
|
|
131
|
+
this.updateConsensus(fragment);
|
|
132
|
+
|
|
133
|
+
// Emit callback (broadcast to browsers)
|
|
134
|
+
this.onFragment?.(fragment, links);
|
|
135
|
+
|
|
136
|
+
// Check if consensus state changed materially
|
|
137
|
+
const state = this.getConsensusState();
|
|
138
|
+
this.onConsensus?.(state);
|
|
139
|
+
|
|
140
|
+
return fragment;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getFragment(fragmentId: string): ContextFragment | null {
|
|
144
|
+
return this.fragments.get(fragmentId) ?? null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getAllFragments(): ContextFragment[] {
|
|
148
|
+
return Array.from(this.fragments.values()).sort((a, b) => a.timestamp - b.timestamp);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Return a thread: the root fragment and all replies in chain order.
|
|
153
|
+
*/
|
|
154
|
+
getThread(fragmentId: string): ContextFragment[] {
|
|
155
|
+
// Find root
|
|
156
|
+
let current = this.fragments.get(fragmentId);
|
|
157
|
+
if (!current) return [];
|
|
158
|
+
|
|
159
|
+
// Walk up to root
|
|
160
|
+
const root = this.findRoot(fragmentId);
|
|
161
|
+
// Collect all descendants
|
|
162
|
+
return this.collectThread(root);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getConsensusState(): ConsensusState {
|
|
166
|
+
const allFragments = Array.from(this.fragments.values());
|
|
167
|
+
|
|
168
|
+
const consensusPoints = allFragments
|
|
169
|
+
.filter((f) => f.consensusScore > 0.7)
|
|
170
|
+
.map((f) => f.fragmentId);
|
|
171
|
+
|
|
172
|
+
const controversialPairs = allFragments
|
|
173
|
+
.filter((f) => f.isControversial)
|
|
174
|
+
.slice(0, 10); // cap for performance
|
|
175
|
+
|
|
176
|
+
const disagreements: DisagreementCluster[] = [];
|
|
177
|
+
if (controversialPairs.length > 0) {
|
|
178
|
+
// Group controversial fragments as a single cluster per topic
|
|
179
|
+
const topics = new Map<string, string[]>();
|
|
180
|
+
for (const f of controversialPairs) {
|
|
181
|
+
const topic = this.extractTopic(f.content);
|
|
182
|
+
if (!topics.has(topic)) topics.set(topic, []);
|
|
183
|
+
topics.get(topic)!.push(f.fragmentId);
|
|
184
|
+
}
|
|
185
|
+
for (const [topic, ids] of topics) {
|
|
186
|
+
disagreements.push({
|
|
187
|
+
fragmentIds: ids,
|
|
188
|
+
topic,
|
|
189
|
+
summary: `Disagreement detected around: ${topic}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const openQuestions = allFragments
|
|
195
|
+
.filter((f) => f.type === "question" && !this.hasAnswer(f.fragmentId))
|
|
196
|
+
.map((f) => f.fragmentId);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
sessionId: this.sessionId,
|
|
200
|
+
updatedAt: Date.now(),
|
|
201
|
+
consensusPoints,
|
|
202
|
+
disagreements,
|
|
203
|
+
openQuestions,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Return fragments that are significant enough to promote to SemanticMemory.
|
|
209
|
+
* Criteria: consensusScore > 0.6 OR type is "insight" or "decision".
|
|
210
|
+
*/
|
|
211
|
+
getSignificantFragments(): ContextFragment[] {
|
|
212
|
+
return Array.from(this.fragments.values()).filter(
|
|
213
|
+
(f) => f.consensusScore > 0.6 || f.type === "insight",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Internal: Semantic Linking ───────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
private detectSemanticLinks(fragment: ContextFragment): SemanticLink[] {
|
|
220
|
+
const links: SemanticLink[] = [];
|
|
221
|
+
const content = fragment.content.toLowerCase();
|
|
222
|
+
|
|
223
|
+
// Heuristic patterns for detecting semantic relations
|
|
224
|
+
// In production, replace with a fast embedding similarity check or LLM call
|
|
225
|
+
const recent = Array.from(this.fragments.values())
|
|
226
|
+
.filter((f) => f.fragmentId !== fragment.fragmentId)
|
|
227
|
+
.slice(-20); // compare against recent N fragments only (for performance)
|
|
228
|
+
|
|
229
|
+
for (const other of recent) {
|
|
230
|
+
const otherContent = other.content.toLowerCase();
|
|
231
|
+
const relation = this.inferRelation(content, otherContent, fragment, other);
|
|
232
|
+
if (relation) {
|
|
233
|
+
links.push({ targetFragmentId: other.fragmentId, relation });
|
|
234
|
+
if (links.length >= 5) break; // cap links per fragment
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Thread reply: if parentId is set, always "builds_on" parent
|
|
239
|
+
if (fragment.parentId && this.fragments.has(fragment.parentId)) {
|
|
240
|
+
const alreadyLinked = links.some((l) => l.targetFragmentId === fragment.parentId);
|
|
241
|
+
if (!alreadyLinked) {
|
|
242
|
+
links.unshift({ targetFragmentId: fragment.parentId, relation: "builds_on" });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return links;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private inferRelation(
|
|
250
|
+
content: string,
|
|
251
|
+
otherContent: string,
|
|
252
|
+
fragment: ContextFragment,
|
|
253
|
+
other: ContextFragment,
|
|
254
|
+
): SemanticRelation | null {
|
|
255
|
+
// Agreement signals
|
|
256
|
+
if (content.includes("agree") || content.includes("correct") || content.includes("exactly")) {
|
|
257
|
+
if (this.topicOverlap(content, otherContent) > 0.3) return "agrees_with";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Disagreement signals
|
|
261
|
+
if (content.includes("disagree") || content.includes("wrong") || content.includes("no,") || content.includes("actually,")) {
|
|
262
|
+
if (this.topicOverlap(content, otherContent) > 0.3) return "disagrees_with";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Contradiction signals
|
|
266
|
+
if (content.includes("contradict") || content.includes("opposite") || content.includes("not true")) {
|
|
267
|
+
return "contradicts";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Question about previous fragment
|
|
271
|
+
if (fragment.type === "question" && other.type !== "question") {
|
|
272
|
+
if (this.topicOverlap(content, otherContent) > 0.4) return "questions";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Building on previous
|
|
276
|
+
if (fragment.type === "answer" && other.type === "question") return "builds_on";
|
|
277
|
+
if (fragment.type === "insight" && this.topicOverlap(content, otherContent) > 0.5) return "builds_on";
|
|
278
|
+
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private topicOverlap(a: string, b: string): number {
|
|
283
|
+
const aWords = new Set(a.split(/\s+/).filter((w) => w.length > 4));
|
|
284
|
+
const bWords = new Set(b.split(/\s+/).filter((w) => w.length > 4));
|
|
285
|
+
let overlap = 0;
|
|
286
|
+
for (const word of aWords) {
|
|
287
|
+
if (bWords.has(word)) overlap++;
|
|
288
|
+
}
|
|
289
|
+
const total = Math.max(aWords.size, bWords.size);
|
|
290
|
+
return total > 0 ? overlap / total : 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private applyLinks(sourceId: string, links: SemanticLink[]): void {
|
|
294
|
+
// Update consensusScore on linked fragments based on relation type
|
|
295
|
+
for (const link of links) {
|
|
296
|
+
const target = this.fragments.get(link.targetFragmentId);
|
|
297
|
+
if (!target) continue;
|
|
298
|
+
|
|
299
|
+
if (link.relation === "agrees_with" || link.relation === "builds_on") {
|
|
300
|
+
target.consensusScore = Math.min(1, target.consensusScore + 0.1);
|
|
301
|
+
} else if (link.relation === "disagrees_with" || link.relation === "contradicts") {
|
|
302
|
+
target.consensusScore = Math.max(0, target.consensusScore - 0.15);
|
|
303
|
+
target.isControversial = target.consensusScore < 0.4;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ─── Internal: Consensus Detection ───────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
private updateConsensus(fragment: ContextFragment): void {
|
|
311
|
+
// Agreements boost consensus score; disagreements lower it
|
|
312
|
+
const agreementLinks = fragment.semanticLinks.filter(
|
|
313
|
+
(l) => l.relation === "agrees_with" || l.relation === "builds_on"
|
|
314
|
+
).length;
|
|
315
|
+
const disagreementLinks = fragment.semanticLinks.filter(
|
|
316
|
+
(l) => l.relation === "disagrees_with" || l.relation === "contradicts"
|
|
317
|
+
).length;
|
|
318
|
+
|
|
319
|
+
fragment.consensusScore = Math.max(0, Math.min(1,
|
|
320
|
+
fragment.consensusScore + agreementLinks * 0.1 - disagreementLinks * 0.15
|
|
321
|
+
));
|
|
322
|
+
fragment.isControversial = fragment.consensusScore < 0.4 && disagreementLinks > 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private findRoot(fragmentId: string): string {
|
|
326
|
+
let current = this.fragments.get(fragmentId);
|
|
327
|
+
const visited = new Set<string>();
|
|
328
|
+
while (current?.parentId && !visited.has(current.parentId)) {
|
|
329
|
+
visited.add(current.fragmentId);
|
|
330
|
+
current = this.fragments.get(current.parentId);
|
|
331
|
+
}
|
|
332
|
+
return current?.fragmentId ?? fragmentId;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private collectThread(rootId: string): ContextFragment[] {
|
|
336
|
+
const result: ContextFragment[] = [];
|
|
337
|
+
const queue = [rootId];
|
|
338
|
+
while (queue.length > 0) {
|
|
339
|
+
const id = queue.shift()!;
|
|
340
|
+
const fragment = this.fragments.get(id);
|
|
341
|
+
if (!fragment) continue;
|
|
342
|
+
result.push(fragment);
|
|
343
|
+
// Find children
|
|
344
|
+
for (const f of this.fragments.values()) {
|
|
345
|
+
if (f.parentId === id) queue.push(f.fragmentId);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private hasAnswer(questionId: string): boolean {
|
|
352
|
+
for (const f of this.fragments.values()) {
|
|
353
|
+
if (f.type === "answer" && f.parentId === questionId) return true;
|
|
354
|
+
if (f.semanticLinks.some((l) => l.targetFragmentId === questionId && l.relation === "builds_on")) return true;
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private extractTopic(content: string): string {
|
|
360
|
+
// Extract topic heuristically: longest noun phrase in first 100 chars
|
|
361
|
+
const words = content.slice(0, 100).split(/\s+/);
|
|
362
|
+
const meaningful = words.filter((w) => w.length > 5).slice(0, 3);
|
|
363
|
+
return meaningful.join(" ") || "general";
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ─── Manager ──────────────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Manages SharedContextStream instances per session.
|
|
371
|
+
* One SharedContextManager per server (singleton).
|
|
372
|
+
*/
|
|
373
|
+
export class SharedContextManager {
|
|
374
|
+
private streams = new Map<string, SharedContextStream>();
|
|
375
|
+
private onFragment: FragmentCallback | null = null;
|
|
376
|
+
private onConsensus: ConsensusCallback | null = null;
|
|
377
|
+
|
|
378
|
+
setOnFragment(cb: FragmentCallback): void { this.onFragment = cb; }
|
|
379
|
+
setOnConsensus(cb: ConsensusCallback): void { this.onConsensus = cb; }
|
|
380
|
+
|
|
381
|
+
getOrCreate(sessionId: string): SharedContextStream {
|
|
382
|
+
if (!this.streams.has(sessionId)) {
|
|
383
|
+
const stream = new SharedContextStream(sessionId);
|
|
384
|
+
stream.setOnFragment((f, links) => this.onFragment?.(f, links));
|
|
385
|
+
stream.setOnConsensus((state) => this.onConsensus?.(state));
|
|
386
|
+
this.streams.set(sessionId, stream);
|
|
387
|
+
}
|
|
388
|
+
return this.streams.get(sessionId)!;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
get(sessionId: string): SharedContextStream | null {
|
|
392
|
+
return this.streams.get(sessionId) ?? null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
remove(sessionId: string): void {
|
|
396
|
+
this.streams.delete(sessionId);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Singleton instance
|
|
401
|
+
export const sharedContextManager = new SharedContextManager();
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Manager — reads Claude Code plugins/skills from ~/.claude/plugins/
|
|
3
|
+
* and provides CRUD operations for the Skills Management UI.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
|
|
9
|
+
const PLUGINS_DIR = join(homedir(), ".claude", "plugins");
|
|
10
|
+
const INSTALLED_FILE = join(PLUGINS_DIR, "installed_plugins.json");
|
|
11
|
+
const BLOCKLIST_FILE = join(PLUGINS_DIR, "blocklist.json");
|
|
12
|
+
const CACHE_DIR = join(PLUGINS_DIR, "cache");
|
|
13
|
+
|
|
14
|
+
export interface PluginInfo {
|
|
15
|
+
/** Plugin identifier, e.g., "frontend-design@claude-plugins-official" */
|
|
16
|
+
id: string;
|
|
17
|
+
/** Plugin name */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Plugin description */
|
|
20
|
+
description: string;
|
|
21
|
+
/** Marketplace source */
|
|
22
|
+
marketplace: string;
|
|
23
|
+
/** Author name */
|
|
24
|
+
author?: string;
|
|
25
|
+
/** Install path on disk */
|
|
26
|
+
installPath: string;
|
|
27
|
+
/** Version string */
|
|
28
|
+
version: string;
|
|
29
|
+
/** When installed */
|
|
30
|
+
installedAt: string;
|
|
31
|
+
/** Whether the plugin is blocked */
|
|
32
|
+
blocked: boolean;
|
|
33
|
+
/** Block reason (if blocked) */
|
|
34
|
+
blockReason?: string;
|
|
35
|
+
/** Skills provided by this plugin */
|
|
36
|
+
skills: SkillInfo[];
|
|
37
|
+
/** Commands provided by this plugin */
|
|
38
|
+
commands: CommandInfo[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SkillInfo {
|
|
42
|
+
name: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
path: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CommandInfo {
|
|
48
|
+
name: string;
|
|
49
|
+
path: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface InstalledPluginsFile {
|
|
53
|
+
version: number;
|
|
54
|
+
plugins: Record<string, Array<{
|
|
55
|
+
scope: string;
|
|
56
|
+
installPath: string;
|
|
57
|
+
version: string;
|
|
58
|
+
installedAt: string;
|
|
59
|
+
lastUpdated: string;
|
|
60
|
+
gitCommitSha?: string;
|
|
61
|
+
}>>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface BlocklistFile {
|
|
65
|
+
fetchedAt: string;
|
|
66
|
+
plugins: Array<{
|
|
67
|
+
plugin: string;
|
|
68
|
+
added_at: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
text: string;
|
|
71
|
+
}>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readJSON<T>(path: string): T | null {
|
|
75
|
+
try {
|
|
76
|
+
if (!existsSync(path)) return null;
|
|
77
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readPluginJson(installPath: string): { name?: string; description?: string; author?: { name?: string } } {
|
|
84
|
+
const pluginJsonPath = join(installPath, ".claude-plugin", "plugin.json");
|
|
85
|
+
return readJSON(pluginJsonPath) ?? {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function discoverSkills(installPath: string): SkillInfo[] {
|
|
89
|
+
const skillsDir = join(installPath, "skills");
|
|
90
|
+
if (!existsSync(skillsDir)) return [];
|
|
91
|
+
|
|
92
|
+
const skills: SkillInfo[] = [];
|
|
93
|
+
try {
|
|
94
|
+
for (const entry of readdirSync(skillsDir)) {
|
|
95
|
+
const skillDir = join(skillsDir, entry);
|
|
96
|
+
if (!statSync(skillDir).isDirectory()) continue;
|
|
97
|
+
const skillMd = join(skillDir, "SKILL.md");
|
|
98
|
+
if (!existsSync(skillMd)) continue;
|
|
99
|
+
|
|
100
|
+
// Try to extract description from SKILL.md frontmatter
|
|
101
|
+
let description: string | undefined;
|
|
102
|
+
try {
|
|
103
|
+
const content = readFileSync(skillMd, "utf-8");
|
|
104
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
105
|
+
if (fmMatch) {
|
|
106
|
+
const descMatch = fmMatch[1].match(/description:\s*(.+)/);
|
|
107
|
+
if (descMatch) description = descMatch[1].trim();
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
|
|
111
|
+
skills.push({ name: entry, description, path: skillDir });
|
|
112
|
+
}
|
|
113
|
+
} catch {}
|
|
114
|
+
return skills;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function discoverCommands(installPath: string): CommandInfo[] {
|
|
118
|
+
const commandsDir = join(installPath, "commands");
|
|
119
|
+
if (!existsSync(commandsDir)) return [];
|
|
120
|
+
|
|
121
|
+
const commands: CommandInfo[] = [];
|
|
122
|
+
try {
|
|
123
|
+
for (const entry of readdirSync(commandsDir)) {
|
|
124
|
+
if (!entry.endsWith(".md")) continue;
|
|
125
|
+
commands.push({
|
|
126
|
+
name: entry.replace(/\.md$/, ""),
|
|
127
|
+
path: join(commandsDir, entry),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
} catch {}
|
|
131
|
+
return commands;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** List all installed plugins with their skills and commands. */
|
|
135
|
+
export function listPlugins(): PluginInfo[] {
|
|
136
|
+
const installed = readJSON<InstalledPluginsFile>(INSTALLED_FILE);
|
|
137
|
+
if (!installed?.plugins) return [];
|
|
138
|
+
|
|
139
|
+
const blocklist = readJSON<BlocklistFile>(BLOCKLIST_FILE);
|
|
140
|
+
const blockedSet = new Map<string, string>();
|
|
141
|
+
if (blocklist?.plugins) {
|
|
142
|
+
for (const b of blocklist.plugins) {
|
|
143
|
+
blockedSet.set(b.plugin, b.reason);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const results: PluginInfo[] = [];
|
|
148
|
+
for (const [id, installs] of Object.entries(installed.plugins)) {
|
|
149
|
+
if (!installs || installs.length === 0) continue;
|
|
150
|
+
const latest = installs[0]; // First install entry
|
|
151
|
+
const [name, marketplace] = id.split("@");
|
|
152
|
+
|
|
153
|
+
const pluginJson = readPluginJson(latest.installPath);
|
|
154
|
+
const skills = discoverSkills(latest.installPath);
|
|
155
|
+
const commands = discoverCommands(latest.installPath);
|
|
156
|
+
|
|
157
|
+
results.push({
|
|
158
|
+
id,
|
|
159
|
+
name: pluginJson.name || name,
|
|
160
|
+
description: pluginJson.description || "",
|
|
161
|
+
marketplace: marketplace || "unknown",
|
|
162
|
+
author: pluginJson.author?.name,
|
|
163
|
+
installPath: latest.installPath,
|
|
164
|
+
version: latest.version,
|
|
165
|
+
installedAt: latest.installedAt,
|
|
166
|
+
blocked: blockedSet.has(id),
|
|
167
|
+
blockReason: blockedSet.get(id),
|
|
168
|
+
skills,
|
|
169
|
+
commands,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Get a single plugin by ID. */
|
|
176
|
+
export function getPlugin(id: string): PluginInfo | null {
|
|
177
|
+
const all = listPlugins();
|
|
178
|
+
return all.find((p) => p.id === id) ?? null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Read the content of a skill's SKILL.md file. */
|
|
182
|
+
export function readSkillContent(pluginId: string, skillName: string): string | null {
|
|
183
|
+
const plugin = getPlugin(pluginId);
|
|
184
|
+
if (!plugin) return null;
|
|
185
|
+
const skill = plugin.skills.find((s) => s.name === skillName);
|
|
186
|
+
if (!skill) return null;
|
|
187
|
+
|
|
188
|
+
const skillMd = join(skill.path, "SKILL.md");
|
|
189
|
+
try {
|
|
190
|
+
return readFileSync(skillMd, "utf-8");
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Read the content of a command file. */
|
|
197
|
+
export function readCommandContent(pluginId: string, commandName: string): string | null {
|
|
198
|
+
const plugin = getPlugin(pluginId);
|
|
199
|
+
if (!plugin) return null;
|
|
200
|
+
const cmd = plugin.commands.find((c) => c.name === commandName);
|
|
201
|
+
if (!cmd) return null;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
return readFileSync(cmd.path, "utf-8");
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Enable/disable a plugin by modifying ~/.campfire/skills-config.json.
|
|
212
|
+
* (This is a Campfire-level toggle, not modifying Claude's own config.)
|
|
213
|
+
*/
|
|
214
|
+
const SKILLS_CONFIG_FILE = join(homedir(), ".campfire", "skills-config.json");
|
|
215
|
+
|
|
216
|
+
interface SkillsConfig {
|
|
217
|
+
disabled: string[]; // List of plugin IDs that are disabled in Campfire
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readSkillsConfig(): SkillsConfig {
|
|
221
|
+
return readJSON<SkillsConfig>(SKILLS_CONFIG_FILE) ?? { disabled: [] };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function writeSkillsConfig(config: SkillsConfig): void {
|
|
225
|
+
const dir = join(homedir(), ".campfire");
|
|
226
|
+
if (!existsSync(dir)) {
|
|
227
|
+
const { mkdirSync } = require("node:fs");
|
|
228
|
+
mkdirSync(dir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
writeFileSync(SKILLS_CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function isPluginDisabled(pluginId: string): boolean {
|
|
234
|
+
const config = readSkillsConfig();
|
|
235
|
+
return config.disabled.includes(pluginId);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function setPluginDisabled(pluginId: string, disabled: boolean): void {
|
|
239
|
+
const config = readSkillsConfig();
|
|
240
|
+
if (disabled && !config.disabled.includes(pluginId)) {
|
|
241
|
+
config.disabled.push(pluginId);
|
|
242
|
+
} else if (!disabled) {
|
|
243
|
+
config.disabled = config.disabled.filter((id) => id !== pluginId);
|
|
244
|
+
}
|
|
245
|
+
writeSkillsConfig(config);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function getDisabledPlugins(): string[] {
|
|
249
|
+
return readSkillsConfig().disabled;
|
|
250
|
+
}
|