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,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aider Adapter
|
|
3
|
+
*
|
|
4
|
+
* Translates between Aider's unstructured CLI output (stdout/stdin)
|
|
5
|
+
* and Campfire's BrowserIncomingMessage/BrowserOutgoingMessage types.
|
|
6
|
+
*
|
|
7
|
+
* Aider has no structured protocol — it's a terminal tool. We spawn it with
|
|
8
|
+
* `--no-pretty --yes --no-auto-commits` and parse stdout for:
|
|
9
|
+
* - SEARCH/REPLACE edit blocks → tool_use/tool_result
|
|
10
|
+
* - "Applied edit to {file}" → success tool_result
|
|
11
|
+
* - "Tokens: ..." → usage/result message
|
|
12
|
+
* - Other non-empty lines → streaming assistant text
|
|
13
|
+
* - Aider prompt ">" → turn complete
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import type { Subprocess } from "bun";
|
|
18
|
+
import type {
|
|
19
|
+
BrowserIncomingMessage,
|
|
20
|
+
BrowserOutgoingMessage,
|
|
21
|
+
SessionState,
|
|
22
|
+
CLIResultMessage,
|
|
23
|
+
} from "./session-types.js";
|
|
24
|
+
import type { RecorderManager } from "./recorder.js";
|
|
25
|
+
import type { AgentAdapter, AdapterSessionMeta } from "./adapter-types.js";
|
|
26
|
+
|
|
27
|
+
// ─── Adapter Options ─────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export interface AiderAdapterOptions {
|
|
30
|
+
model?: string;
|
|
31
|
+
cwd?: string;
|
|
32
|
+
permissionMode?: string;
|
|
33
|
+
recorder?: RecorderManager;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Edit Block Parser State ─────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
interface EditBlock {
|
|
39
|
+
file: string;
|
|
40
|
+
search: string;
|
|
41
|
+
replace: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ParseState = "idle" | "search" | "divider" | "replace";
|
|
45
|
+
|
|
46
|
+
// ─── Aider Adapter ──────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export class AiderAdapter implements AgentAdapter {
|
|
49
|
+
private proc: Subprocess;
|
|
50
|
+
private sessionId: string;
|
|
51
|
+
private options: AiderAdapterOptions;
|
|
52
|
+
|
|
53
|
+
private browserMessageCb: ((msg: BrowserIncomingMessage) => void) | null = null;
|
|
54
|
+
private sessionMetaCb: ((meta: AdapterSessionMeta) => void) | null = null;
|
|
55
|
+
private disconnectCb: (() => void) | null = null;
|
|
56
|
+
private initErrorCb: ((error: string) => void) | null = null;
|
|
57
|
+
|
|
58
|
+
// State
|
|
59
|
+
private _connected = false;
|
|
60
|
+
private streamingText = "";
|
|
61
|
+
private streamingActive = false;
|
|
62
|
+
|
|
63
|
+
// Edit block parsing
|
|
64
|
+
private parseState: ParseState = "idle";
|
|
65
|
+
private currentFile = "";
|
|
66
|
+
private searchLines: string[] = [];
|
|
67
|
+
private replaceLines: string[] = [];
|
|
68
|
+
private editCounter = 0;
|
|
69
|
+
|
|
70
|
+
// Prompt detection
|
|
71
|
+
private lastLineWasPrompt = false;
|
|
72
|
+
|
|
73
|
+
// Buffer for incomplete lines from stdout
|
|
74
|
+
private stdoutBuffer = "";
|
|
75
|
+
|
|
76
|
+
constructor(proc: Subprocess, sessionId: string, options: AiderAdapterOptions = {}) {
|
|
77
|
+
this.proc = proc;
|
|
78
|
+
this.sessionId = sessionId;
|
|
79
|
+
this.options = options;
|
|
80
|
+
|
|
81
|
+
const stdout = proc.stdout;
|
|
82
|
+
if (!stdout || typeof stdout === "number") {
|
|
83
|
+
throw new Error("Aider process must have stdout pipe");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Read stdout
|
|
87
|
+
this.readStdout(stdout as ReadableStream<Uint8Array>);
|
|
88
|
+
|
|
89
|
+
// Wire stderr for debugging
|
|
90
|
+
const stderr = proc.stderr;
|
|
91
|
+
if (stderr && typeof stderr !== "number") {
|
|
92
|
+
this.readStderr(stderr as ReadableStream<Uint8Array>);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Monitor process exit
|
|
96
|
+
proc.exited.then(() => {
|
|
97
|
+
this._connected = false;
|
|
98
|
+
this.flushStreaming();
|
|
99
|
+
this.disconnectCb?.();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Emit session_init immediately
|
|
103
|
+
this._connected = true;
|
|
104
|
+
this.emitInit();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
sendBrowserMessage(msg: BrowserOutgoingMessage): boolean {
|
|
110
|
+
switch (msg.type) {
|
|
111
|
+
case "user_message":
|
|
112
|
+
this.sendUserMessage(msg.content);
|
|
113
|
+
return true;
|
|
114
|
+
case "interrupt":
|
|
115
|
+
this.proc.kill("SIGINT");
|
|
116
|
+
return true;
|
|
117
|
+
default:
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onBrowserMessage(cb: (msg: BrowserIncomingMessage) => void): void {
|
|
123
|
+
this.browserMessageCb = cb;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
onSessionMeta(cb: (meta: AdapterSessionMeta) => void): void {
|
|
127
|
+
this.sessionMetaCb = cb;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
onDisconnect(cb: () => void): void {
|
|
131
|
+
this.disconnectCb = cb;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onInitError(cb: (error: string) => void): void {
|
|
135
|
+
this.initErrorCb = cb;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
isConnected(): boolean {
|
|
139
|
+
return this._connected;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async disconnect(): Promise<void> {
|
|
143
|
+
this._connected = false;
|
|
144
|
+
try {
|
|
145
|
+
this.proc.kill("SIGTERM");
|
|
146
|
+
await Promise.race([
|
|
147
|
+
this.proc.exited,
|
|
148
|
+
new Promise((r) => setTimeout(r, 5000)),
|
|
149
|
+
]);
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getBackendSessionId(): string | null {
|
|
154
|
+
return null; // Aider has no session concept
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Initialization ─────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
private emitInit(): void {
|
|
160
|
+
this.sessionMetaCb?.({
|
|
161
|
+
model: this.options.model,
|
|
162
|
+
cwd: this.options.cwd,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const state: SessionState = {
|
|
166
|
+
session_id: this.sessionId,
|
|
167
|
+
backend_type: "aider",
|
|
168
|
+
model: this.options.model || "default",
|
|
169
|
+
cwd: this.options.cwd || "",
|
|
170
|
+
tools: [],
|
|
171
|
+
permissionMode: "default",
|
|
172
|
+
claude_code_version: "",
|
|
173
|
+
mcp_servers: [],
|
|
174
|
+
agents: [],
|
|
175
|
+
slash_commands: [],
|
|
176
|
+
skills: [],
|
|
177
|
+
total_cost_usd: 0,
|
|
178
|
+
num_turns: 0,
|
|
179
|
+
context_used_percent: 0,
|
|
180
|
+
is_compacting: false,
|
|
181
|
+
git_branch: "",
|
|
182
|
+
is_worktree: false,
|
|
183
|
+
repo_root: "",
|
|
184
|
+
git_ahead: 0,
|
|
185
|
+
git_behind: 0,
|
|
186
|
+
total_lines_added: 0,
|
|
187
|
+
total_lines_removed: 0,
|
|
188
|
+
total_duration_api_ms: 0,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
this.emit({ type: "session_init", session: state });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Outgoing ───────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
private sendUserMessage(content: string): void {
|
|
197
|
+
const stdin = this.proc.stdin;
|
|
198
|
+
if (!stdin) return;
|
|
199
|
+
|
|
200
|
+
// Record outgoing message
|
|
201
|
+
if (this.options.recorder) {
|
|
202
|
+
this.options.recorder.record(
|
|
203
|
+
this.sessionId,
|
|
204
|
+
"out",
|
|
205
|
+
content,
|
|
206
|
+
"cli",
|
|
207
|
+
"aider",
|
|
208
|
+
this.options.cwd || "",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Write message to stdin followed by newline
|
|
213
|
+
const writer = (stdin as unknown as WritableStream<Uint8Array>).getWriter();
|
|
214
|
+
writer.write(new TextEncoder().encode(content + "\n")).then(() => writer.releaseLock());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Stdout parsing ─────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
private async readStdout(stdout: ReadableStream<Uint8Array>): Promise<void> {
|
|
220
|
+
const reader = stdout.getReader();
|
|
221
|
+
const decoder = new TextDecoder();
|
|
222
|
+
try {
|
|
223
|
+
while (true) {
|
|
224
|
+
const { done, value } = await reader.read();
|
|
225
|
+
if (done) break;
|
|
226
|
+
const text = decoder.decode(value, { stream: true });
|
|
227
|
+
|
|
228
|
+
// Record raw incoming
|
|
229
|
+
if (this.options.recorder) {
|
|
230
|
+
this.options.recorder.record(
|
|
231
|
+
this.sessionId,
|
|
232
|
+
"in",
|
|
233
|
+
text.replace(/\n$/, ""),
|
|
234
|
+
"cli",
|
|
235
|
+
"aider",
|
|
236
|
+
this.options.cwd || "",
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.stdoutBuffer += text;
|
|
241
|
+
this.processStdoutBuffer();
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error("[aider-adapter] stdout reader error:", err);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private processStdoutBuffer(): void {
|
|
249
|
+
const lines = this.stdoutBuffer.split("\n");
|
|
250
|
+
this.stdoutBuffer = lines.pop() || ""; // Keep incomplete last line
|
|
251
|
+
|
|
252
|
+
for (const line of lines) {
|
|
253
|
+
this.processLine(line);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Process a single line of aider output. */
|
|
258
|
+
processLine(line: string): void {
|
|
259
|
+
const trimmed = line.trim();
|
|
260
|
+
|
|
261
|
+
// Detect aider prompt (turn complete)
|
|
262
|
+
if (trimmed === ">" || trimmed.endsWith("> ") || trimmed === "aider>") {
|
|
263
|
+
this.lastLineWasPrompt = true;
|
|
264
|
+
this.flushStreaming();
|
|
265
|
+
this.emitResult("success", false);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Edit block detection: <<<<<<< SEARCH
|
|
270
|
+
if (trimmed.startsWith("<<<<<<< SEARCH")) {
|
|
271
|
+
this.flushStreaming();
|
|
272
|
+
this.parseState = "search";
|
|
273
|
+
this.searchLines = [];
|
|
274
|
+
this.replaceLines = [];
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Edit block: separator
|
|
279
|
+
if (this.parseState === "search" && trimmed === "=======") {
|
|
280
|
+
this.parseState = "replace";
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Edit block: end
|
|
285
|
+
if (this.parseState === "replace" && trimmed.startsWith(">>>>>>> REPLACE")) {
|
|
286
|
+
this.parseState = "idle";
|
|
287
|
+
const editBlock: EditBlock = {
|
|
288
|
+
file: this.currentFile,
|
|
289
|
+
search: this.searchLines.join("\n"),
|
|
290
|
+
replace: this.replaceLines.join("\n"),
|
|
291
|
+
};
|
|
292
|
+
this.emitEditBlock(editBlock);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Accumulate lines within edit blocks
|
|
297
|
+
if (this.parseState === "search") {
|
|
298
|
+
this.searchLines.push(line);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (this.parseState === "replace") {
|
|
302
|
+
this.replaceLines.push(line);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// File reference: "> filename" (appears before edit blocks)
|
|
307
|
+
if (trimmed.startsWith("> ") && !trimmed.includes(" ") === false) {
|
|
308
|
+
const possibleFile = trimmed.slice(2);
|
|
309
|
+
// Only treat as file reference if it looks like a path
|
|
310
|
+
if (possibleFile && !possibleFile.includes(" ") && (possibleFile.includes(".") || possibleFile.includes("/"))) {
|
|
311
|
+
this.currentFile = possibleFile;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// "Applied edit to {file}" — success indication
|
|
317
|
+
if (trimmed.startsWith("Applied edit to ")) {
|
|
318
|
+
const file = trimmed.slice("Applied edit to ".length);
|
|
319
|
+
this.emitAppliedEdit(file);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Token usage lines (e.g. "Tokens: 1.2k sent, 0.5k received.")
|
|
324
|
+
if (trimmed.startsWith("Tokens:") || trimmed.startsWith("Cost:")) {
|
|
325
|
+
// Don't stream these to chat — they're metadata
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Empty lines — skip during idle
|
|
330
|
+
if (!trimmed) return;
|
|
331
|
+
|
|
332
|
+
// Default: accumulate as streaming text
|
|
333
|
+
this.lastLineWasPrompt = false;
|
|
334
|
+
this.appendStreaming(trimmed);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── Streaming ──────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
private appendStreaming(text: string): void {
|
|
340
|
+
if (!this.streamingActive) {
|
|
341
|
+
this.streamingActive = true;
|
|
342
|
+
this.streamingText = "";
|
|
343
|
+
|
|
344
|
+
// Emit message_start
|
|
345
|
+
this.emit({
|
|
346
|
+
type: "stream_event",
|
|
347
|
+
event: {
|
|
348
|
+
type: "message_start",
|
|
349
|
+
message: {
|
|
350
|
+
id: `aider-msg-${randomUUID()}`,
|
|
351
|
+
type: "message",
|
|
352
|
+
role: "assistant",
|
|
353
|
+
model: this.options.model || "",
|
|
354
|
+
content: [],
|
|
355
|
+
stop_reason: null,
|
|
356
|
+
usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
parent_tool_use_id: null,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
this.emit({
|
|
363
|
+
type: "stream_event",
|
|
364
|
+
event: {
|
|
365
|
+
type: "content_block_start",
|
|
366
|
+
index: 0,
|
|
367
|
+
content_block: { type: "text", text: "" },
|
|
368
|
+
},
|
|
369
|
+
parent_tool_use_id: null,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const chunk = (this.streamingText ? "\n" : "") + text;
|
|
374
|
+
this.streamingText += chunk;
|
|
375
|
+
|
|
376
|
+
this.emit({
|
|
377
|
+
type: "stream_event",
|
|
378
|
+
event: {
|
|
379
|
+
type: "content_block_delta",
|
|
380
|
+
index: 0,
|
|
381
|
+
delta: { type: "text_delta", text: chunk },
|
|
382
|
+
},
|
|
383
|
+
parent_tool_use_id: null,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private flushStreaming(): void {
|
|
388
|
+
if (!this.streamingActive) return;
|
|
389
|
+
|
|
390
|
+
this.emit({
|
|
391
|
+
type: "stream_event",
|
|
392
|
+
event: { type: "content_block_stop", index: 0 },
|
|
393
|
+
parent_tool_use_id: null,
|
|
394
|
+
});
|
|
395
|
+
this.emit({
|
|
396
|
+
type: "stream_event",
|
|
397
|
+
event: {
|
|
398
|
+
type: "message_delta",
|
|
399
|
+
delta: { stop_reason: "end_turn" },
|
|
400
|
+
usage: { output_tokens: 0 },
|
|
401
|
+
},
|
|
402
|
+
parent_tool_use_id: null,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (this.streamingText) {
|
|
406
|
+
this.emit({
|
|
407
|
+
type: "assistant",
|
|
408
|
+
message: {
|
|
409
|
+
id: `aider-msg-${randomUUID()}`,
|
|
410
|
+
type: "message",
|
|
411
|
+
role: "assistant",
|
|
412
|
+
model: this.options.model || "",
|
|
413
|
+
content: [{ type: "text", text: this.streamingText }],
|
|
414
|
+
stop_reason: "end_turn",
|
|
415
|
+
usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
|
|
416
|
+
},
|
|
417
|
+
parent_tool_use_id: null,
|
|
418
|
+
timestamp: Date.now(),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.streamingText = "";
|
|
423
|
+
this.streamingActive = false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── Edit block emission ────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
private emitEditBlock(edit: EditBlock): void {
|
|
429
|
+
const toolUseId = `aider-edit-${++this.editCounter}`;
|
|
430
|
+
|
|
431
|
+
// Emit tool_use (Edit tool)
|
|
432
|
+
this.emit({
|
|
433
|
+
type: "assistant",
|
|
434
|
+
message: {
|
|
435
|
+
id: `aider-tu-${toolUseId}`,
|
|
436
|
+
type: "message",
|
|
437
|
+
role: "assistant",
|
|
438
|
+
model: this.options.model || "",
|
|
439
|
+
content: [
|
|
440
|
+
{
|
|
441
|
+
type: "tool_use",
|
|
442
|
+
id: toolUseId,
|
|
443
|
+
name: "Edit",
|
|
444
|
+
input: {
|
|
445
|
+
file_path: edit.file,
|
|
446
|
+
old_string: edit.search,
|
|
447
|
+
new_string: edit.replace,
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
stop_reason: null,
|
|
452
|
+
usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
|
|
453
|
+
},
|
|
454
|
+
parent_tool_use_id: null,
|
|
455
|
+
timestamp: Date.now(),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private emitAppliedEdit(file: string): void {
|
|
460
|
+
// Find most recent edit tool use ID for this file
|
|
461
|
+
const toolUseId = `aider-edit-${this.editCounter}`;
|
|
462
|
+
this.emit({
|
|
463
|
+
type: "assistant",
|
|
464
|
+
message: {
|
|
465
|
+
id: `aider-tr-${toolUseId}`,
|
|
466
|
+
type: "message",
|
|
467
|
+
role: "assistant",
|
|
468
|
+
model: this.options.model || "",
|
|
469
|
+
content: [
|
|
470
|
+
{
|
|
471
|
+
type: "tool_result",
|
|
472
|
+
tool_use_id: toolUseId,
|
|
473
|
+
content: `Applied edit to ${file}`,
|
|
474
|
+
is_error: false,
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
stop_reason: null,
|
|
478
|
+
usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
|
|
479
|
+
},
|
|
480
|
+
parent_tool_use_id: null,
|
|
481
|
+
timestamp: Date.now(),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
486
|
+
|
|
487
|
+
private emit(msg: BrowserIncomingMessage): void {
|
|
488
|
+
this.browserMessageCb?.(msg);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private emitResult(subtype: CLIResultMessage["subtype"], isError: boolean): void {
|
|
492
|
+
const result: CLIResultMessage = {
|
|
493
|
+
type: "result",
|
|
494
|
+
subtype,
|
|
495
|
+
is_error: isError,
|
|
496
|
+
duration_ms: 0,
|
|
497
|
+
duration_api_ms: 0,
|
|
498
|
+
num_turns: 1,
|
|
499
|
+
total_cost_usd: 0,
|
|
500
|
+
stop_reason: subtype === "success" ? "end_turn" : "error",
|
|
501
|
+
usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
|
|
502
|
+
uuid: randomUUID(),
|
|
503
|
+
session_id: this.sessionId,
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
this.emit({ type: "result", data: result });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private async readStderr(stderr: ReadableStream<Uint8Array>): Promise<void> {
|
|
510
|
+
const reader = stderr.getReader();
|
|
511
|
+
const decoder = new TextDecoder();
|
|
512
|
+
try {
|
|
513
|
+
while (true) {
|
|
514
|
+
const { done, value } = await reader.read();
|
|
515
|
+
if (done) break;
|
|
516
|
+
const text = decoder.decode(value, { stream: true });
|
|
517
|
+
for (const line of text.split("\n")) {
|
|
518
|
+
if (line.trim()) {
|
|
519
|
+
console.log(`[aider-adapter:stderr] ${line}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} catch {}
|
|
524
|
+
}
|
|
525
|
+
}
|
package/server/auth.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { randomBytes, createHash, scryptSync, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
|
|
6
|
+
const AUTH_FILE = join(homedir(), ".campfire", "auth.json");
|
|
7
|
+
|
|
8
|
+
interface AuthConfig {
|
|
9
|
+
/** Salted scrypt hash (`scrypt:<saltHex>:<hashHex>`), or a legacy unsalted SHA-256 hex digest. */
|
|
10
|
+
passwordHash: string;
|
|
11
|
+
/** Active session tokens (browser login sessions) */
|
|
12
|
+
sessions: { token: string; createdAt: number; userAgent?: string }[];
|
|
13
|
+
/** Whether auth is enabled */
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
18
|
+
const SCRYPT_PREFIX = "scrypt";
|
|
19
|
+
const SCRYPT_KEYLEN = 32;
|
|
20
|
+
|
|
21
|
+
function sha256(input: string): string {
|
|
22
|
+
return createHash("sha256").update(input).digest("hex");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hashPassword(password: string): string {
|
|
26
|
+
const salt = randomBytes(16);
|
|
27
|
+
const hash = scryptSync(password, salt, SCRYPT_KEYLEN);
|
|
28
|
+
return `${SCRYPT_PREFIX}:${salt.toString("hex")}:${hash.toString("hex")}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function constantTimeEqualHex(aHex: string, bHex: string): boolean {
|
|
32
|
+
const a = Buffer.from(aHex, "hex");
|
|
33
|
+
const b = Buffer.from(bHex, "hex");
|
|
34
|
+
if (a.length === 0 || a.length !== b.length) return false;
|
|
35
|
+
return timingSafeEqual(a, b);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Verify a password against a stored hash. Supports the current salted
|
|
39
|
+
* scrypt format and the legacy unsalted SHA-256 format (pre-migration). */
|
|
40
|
+
function verifyPassword(password: string, stored: string): { valid: boolean; legacy: boolean } {
|
|
41
|
+
if (stored.startsWith(`${SCRYPT_PREFIX}:`)) {
|
|
42
|
+
const [, saltHex, hashHex] = stored.split(":");
|
|
43
|
+
if (!saltHex || !hashHex) return { valid: false, legacy: false };
|
|
44
|
+
const candidate = scryptSync(password, Buffer.from(saltHex, "hex"), SCRYPT_KEYLEN);
|
|
45
|
+
return { valid: constantTimeEqualHex(candidate.toString("hex"), hashHex), legacy: false };
|
|
46
|
+
}
|
|
47
|
+
return { valid: constantTimeEqualHex(sha256(password), stored), legacy: true };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function load(): AuthConfig {
|
|
51
|
+
try {
|
|
52
|
+
if (existsSync(AUTH_FILE)) {
|
|
53
|
+
return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
|
|
54
|
+
}
|
|
55
|
+
} catch { /* ignore */ }
|
|
56
|
+
return { passwordHash: "", sessions: [], enabled: false };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function save(config: AuthConfig): void {
|
|
60
|
+
mkdirSync(dirname(AUTH_FILE), { recursive: true });
|
|
61
|
+
writeFileSync(AUTH_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Check if auth is enabled (either via env var or saved config) */
|
|
65
|
+
export function isAuthEnabled(): boolean {
|
|
66
|
+
// CAMPFIRE_PASSWORD env var takes priority
|
|
67
|
+
if (process.env.CAMPFIRE_PASSWORD) return true;
|
|
68
|
+
return load().enabled;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Set a password (for UI-based setup) */
|
|
72
|
+
export function setPassword(password: string): void {
|
|
73
|
+
const config = load();
|
|
74
|
+
config.passwordHash = hashPassword(password);
|
|
75
|
+
config.enabled = true;
|
|
76
|
+
save(config);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Disable auth entirely */
|
|
80
|
+
export function disableAuth(): void {
|
|
81
|
+
const config = load();
|
|
82
|
+
config.enabled = false;
|
|
83
|
+
config.passwordHash = "";
|
|
84
|
+
config.sessions = [];
|
|
85
|
+
save(config);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Check the supplied password against the env var or saved config.
|
|
89
|
+
* Returns whether it matched and whether a legacy hash should be upgraded. */
|
|
90
|
+
function checkPassword(password: string, config: AuthConfig): { valid: boolean; upgradeLegacy: boolean } {
|
|
91
|
+
const envPassword = process.env.CAMPFIRE_PASSWORD;
|
|
92
|
+
if (envPassword) {
|
|
93
|
+
// Hash both sides so the comparison is constant-time for any input length.
|
|
94
|
+
return { valid: constantTimeEqualHex(sha256(password), sha256(envPassword)), upgradeLegacy: false };
|
|
95
|
+
}
|
|
96
|
+
if (!config.enabled || !config.passwordHash) return { valid: false, upgradeLegacy: false };
|
|
97
|
+
const { valid, legacy } = verifyPassword(password, config.passwordHash);
|
|
98
|
+
return { valid, upgradeLegacy: valid && legacy };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Verify a password and return a session token if valid */
|
|
102
|
+
export function login(password: string, userAgent?: string): string | null {
|
|
103
|
+
const config = load();
|
|
104
|
+
const { valid, upgradeLegacy } = checkPassword(password, config);
|
|
105
|
+
if (!valid) return null;
|
|
106
|
+
|
|
107
|
+
// Transparently migrate legacy unsalted SHA-256 hashes to salted scrypt.
|
|
108
|
+
if (upgradeLegacy) {
|
|
109
|
+
config.passwordHash = hashPassword(password);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const token = randomBytes(32).toString("hex");
|
|
113
|
+
// Prune expired sessions
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
config.sessions = config.sessions.filter((s) => now - s.createdAt < SESSION_MAX_AGE_MS);
|
|
116
|
+
config.sessions.push({ token: sha256(token), createdAt: now, userAgent });
|
|
117
|
+
save(config);
|
|
118
|
+
return token;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Verify a session token */
|
|
122
|
+
export function verifyToken(token: string): boolean {
|
|
123
|
+
if (!isAuthEnabled()) return true; // no auth = always valid
|
|
124
|
+
const config = load();
|
|
125
|
+
const hashed = sha256(token);
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
return config.sessions.some(
|
|
128
|
+
(s) => s.token === hashed && now - s.createdAt < SESSION_MAX_AGE_MS,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Logout — invalidate a session token */
|
|
133
|
+
export function logout(token: string): void {
|
|
134
|
+
const config = load();
|
|
135
|
+
const hashed = sha256(token);
|
|
136
|
+
config.sessions = config.sessions.filter((s) => s.token !== hashed);
|
|
137
|
+
save(config);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Get auth status (for the frontend) */
|
|
141
|
+
export function getAuthStatus(): { enabled: boolean; hasPassword: boolean; activeSessions: number } {
|
|
142
|
+
const envPassword = !!process.env.CAMPFIRE_PASSWORD;
|
|
143
|
+
const config = load();
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const activeSessions = config.sessions.filter(
|
|
146
|
+
(s) => now - s.createdAt < SESSION_MAX_AGE_MS,
|
|
147
|
+
).length;
|
|
148
|
+
return {
|
|
149
|
+
enabled: envPassword || config.enabled,
|
|
150
|
+
hasPassword: envPassword || !!config.passwordHash,
|
|
151
|
+
activeSessions,
|
|
152
|
+
};
|
|
153
|
+
}
|