ashlrcode 1.0.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/LICENSE +21 -0
- package/README.md +295 -0
- package/package.json +46 -0
- package/src/__tests__/branded-types.test.ts +47 -0
- package/src/__tests__/context.test.ts +163 -0
- package/src/__tests__/cost-tracker.test.ts +274 -0
- package/src/__tests__/cron.test.ts +197 -0
- package/src/__tests__/dream.test.ts +204 -0
- package/src/__tests__/error-handler.test.ts +192 -0
- package/src/__tests__/features.test.ts +69 -0
- package/src/__tests__/file-history.test.ts +177 -0
- package/src/__tests__/hooks.test.ts +145 -0
- package/src/__tests__/keybindings.test.ts +159 -0
- package/src/__tests__/model-patches.test.ts +82 -0
- package/src/__tests__/permissions-rules.test.ts +121 -0
- package/src/__tests__/permissions.test.ts +108 -0
- package/src/__tests__/project-config.test.ts +63 -0
- package/src/__tests__/retry.test.ts +321 -0
- package/src/__tests__/router.test.ts +158 -0
- package/src/__tests__/session-compact.test.ts +191 -0
- package/src/__tests__/session.test.ts +145 -0
- package/src/__tests__/skill-registry.test.ts +130 -0
- package/src/__tests__/speculation.test.ts +196 -0
- package/src/__tests__/tasks-v2.test.ts +267 -0
- package/src/__tests__/telemetry.test.ts +149 -0
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-registry.test.ts +166 -0
- package/src/__tests__/undercover.test.ts +93 -0
- package/src/__tests__/workflow.test.ts +195 -0
- package/src/agent/async-context.ts +64 -0
- package/src/agent/context.ts +245 -0
- package/src/agent/cron.ts +189 -0
- package/src/agent/dream.ts +165 -0
- package/src/agent/error-handler.ts +108 -0
- package/src/agent/ipc.ts +256 -0
- package/src/agent/kairos.ts +207 -0
- package/src/agent/loop.ts +314 -0
- package/src/agent/model-patches.ts +68 -0
- package/src/agent/speculation.ts +219 -0
- package/src/agent/sub-agent.ts +125 -0
- package/src/agent/system-prompt.ts +231 -0
- package/src/agent/team.ts +220 -0
- package/src/agent/tool-executor.ts +162 -0
- package/src/agent/workflow.ts +189 -0
- package/src/agent/worktree-manager.ts +86 -0
- package/src/autopilot/queue.ts +186 -0
- package/src/autopilot/scanner.ts +245 -0
- package/src/autopilot/types.ts +58 -0
- package/src/bridge/bridge-client.ts +57 -0
- package/src/bridge/bridge-server.ts +81 -0
- package/src/cli.ts +1120 -0
- package/src/config/features.ts +51 -0
- package/src/config/git.ts +137 -0
- package/src/config/hooks.ts +201 -0
- package/src/config/permissions.ts +251 -0
- package/src/config/project-config.ts +63 -0
- package/src/config/remote-settings.ts +163 -0
- package/src/config/settings-sync.ts +170 -0
- package/src/config/settings.ts +113 -0
- package/src/config/undercover.ts +76 -0
- package/src/config/upgrade-notice.ts +65 -0
- package/src/mcp/client.ts +197 -0
- package/src/mcp/manager.ts +125 -0
- package/src/mcp/oauth.ts +252 -0
- package/src/mcp/types.ts +61 -0
- package/src/persistence/memory.ts +129 -0
- package/src/persistence/session.ts +289 -0
- package/src/planning/plan-mode.ts +128 -0
- package/src/planning/plan-tools.ts +138 -0
- package/src/providers/anthropic.ts +177 -0
- package/src/providers/cost-tracker.ts +184 -0
- package/src/providers/retry.ts +264 -0
- package/src/providers/router.ts +159 -0
- package/src/providers/types.ts +79 -0
- package/src/providers/xai.ts +217 -0
- package/src/repl.tsx +1384 -0
- package/src/setup.ts +119 -0
- package/src/skills/loader.ts +78 -0
- package/src/skills/registry.ts +78 -0
- package/src/skills/types.ts +11 -0
- package/src/state/file-history.ts +264 -0
- package/src/telemetry/event-log.ts +116 -0
- package/src/tools/agent.ts +133 -0
- package/src/tools/ask-user.ts +229 -0
- package/src/tools/bash.ts +146 -0
- package/src/tools/config.ts +147 -0
- package/src/tools/diff.ts +137 -0
- package/src/tools/file-edit.ts +123 -0
- package/src/tools/file-read.ts +82 -0
- package/src/tools/file-write.ts +82 -0
- package/src/tools/glob.ts +76 -0
- package/src/tools/grep.ts +187 -0
- package/src/tools/ls.ts +77 -0
- package/src/tools/lsp.ts +375 -0
- package/src/tools/mcp-resources.ts +83 -0
- package/src/tools/mcp-tool.ts +47 -0
- package/src/tools/memory.ts +148 -0
- package/src/tools/notebook-edit.ts +133 -0
- package/src/tools/peers.ts +113 -0
- package/src/tools/powershell.ts +83 -0
- package/src/tools/registry.ts +114 -0
- package/src/tools/send-message.ts +75 -0
- package/src/tools/sleep.ts +50 -0
- package/src/tools/snip.ts +143 -0
- package/src/tools/tasks.ts +349 -0
- package/src/tools/team.ts +309 -0
- package/src/tools/todo-write.ts +93 -0
- package/src/tools/tool-search.ts +83 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/web-browser.ts +263 -0
- package/src/tools/web-fetch.ts +118 -0
- package/src/tools/web-search.ts +107 -0
- package/src/tools/workflow.ts +188 -0
- package/src/tools/worktree.ts +143 -0
- package/src/types/branded.ts +22 -0
- package/src/ui/App.tsx +184 -0
- package/src/ui/BuddyPanel.tsx +52 -0
- package/src/ui/PermissionPrompt.tsx +29 -0
- package/src/ui/banner.ts +217 -0
- package/src/ui/buddy-ai.ts +108 -0
- package/src/ui/buddy.ts +466 -0
- package/src/ui/context-bar.ts +60 -0
- package/src/ui/effort.ts +65 -0
- package/src/ui/keybindings.ts +143 -0
- package/src/ui/markdown.ts +271 -0
- package/src/ui/message-renderer.ts +73 -0
- package/src/ui/mode.ts +80 -0
- package/src/ui/notifications.ts +57 -0
- package/src/ui/speech-bubble.ts +95 -0
- package/src/ui/spinner.ts +116 -0
- package/src/ui/theme.ts +98 -0
- package/src/version.ts +5 -0
- package/src/voice/voice-mode.ts +169 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codebase scanner — discovers work items autonomously.
|
|
3
|
+
*
|
|
4
|
+
* Scans for: TODOs, missing tests, lint errors, type errors,
|
|
5
|
+
* security issues, dead code, complexity, missing docs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import type { WorkItem, WorkItemType, WorkItemPriority } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
interface ScanContext {
|
|
12
|
+
cwd: string;
|
|
13
|
+
runCommand: (cmd: string) => Promise<string>;
|
|
14
|
+
searchFiles: (pattern: string, path?: string) => Promise<string>;
|
|
15
|
+
grepContent: (pattern: string, glob?: string) => Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run a full scan and return discovered work items.
|
|
20
|
+
*/
|
|
21
|
+
export async function scanCodebase(ctx: ScanContext, types: WorkItemType[]): Promise<WorkItem[]> {
|
|
22
|
+
const items: WorkItem[] = [];
|
|
23
|
+
const now = new Date().toISOString();
|
|
24
|
+
|
|
25
|
+
// Run scans in parallel for speed
|
|
26
|
+
const scanners: Array<Promise<WorkItem[]>> = [];
|
|
27
|
+
|
|
28
|
+
if (types.includes("todo")) scanners.push(scanTodos(ctx, now));
|
|
29
|
+
if (types.includes("missing_test")) scanners.push(scanMissingTests(ctx, now));
|
|
30
|
+
if (types.includes("type_error")) scanners.push(scanTypeErrors(ctx, now));
|
|
31
|
+
if (types.includes("lint_error")) scanners.push(scanLintErrors(ctx, now));
|
|
32
|
+
if (types.includes("complexity")) scanners.push(scanComplexity(ctx, now));
|
|
33
|
+
if (types.includes("security")) scanners.push(scanSecurity(ctx, now));
|
|
34
|
+
if (types.includes("dead_code")) scanners.push(scanDeadCode(ctx, now));
|
|
35
|
+
|
|
36
|
+
const results = await Promise.allSettled(scanners);
|
|
37
|
+
for (const result of results) {
|
|
38
|
+
if (result.status === "fulfilled") {
|
|
39
|
+
items.push(...result.value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return items;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Individual Scanners ──
|
|
47
|
+
|
|
48
|
+
async function scanTodos(ctx: ScanContext, now: string): Promise<WorkItem[]> {
|
|
49
|
+
const items: WorkItem[] = [];
|
|
50
|
+
try {
|
|
51
|
+
const result = await ctx.grepContent("(TODO|FIXME|HACK|XXX):", "*.{ts,tsx,js,jsx,py,go,rs}");
|
|
52
|
+
const lines = result.split("\n").filter(l => l.trim());
|
|
53
|
+
|
|
54
|
+
for (const line of lines.slice(0, 50)) { // Cap at 50
|
|
55
|
+
const match = line.match(/^(.+?):(\d+):(.+)$/);
|
|
56
|
+
if (!match) continue;
|
|
57
|
+
const [, file, lineNum, content] = match;
|
|
58
|
+
const isFixme = content!.includes("FIXME") || content!.includes("HACK");
|
|
59
|
+
|
|
60
|
+
items.push({
|
|
61
|
+
id: randomUUID().slice(0, 8),
|
|
62
|
+
type: "todo",
|
|
63
|
+
priority: isFixme ? "high" : "medium",
|
|
64
|
+
title: `${isFixme ? "FIXME" : "TODO"} in ${file}:${lineNum}`,
|
|
65
|
+
description: content!.trim().slice(0, 200),
|
|
66
|
+
file: file!,
|
|
67
|
+
line: parseInt(lineNum!, 10),
|
|
68
|
+
status: "discovered",
|
|
69
|
+
discoveredAt: now,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
return items;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function scanMissingTests(ctx: ScanContext, now: string): Promise<WorkItem[]> {
|
|
77
|
+
const items: WorkItem[] = [];
|
|
78
|
+
try {
|
|
79
|
+
const srcFiles = await ctx.searchFiles("src/**/*.ts");
|
|
80
|
+
const testFiles = await ctx.searchFiles("src/**/*.test.ts");
|
|
81
|
+
|
|
82
|
+
const srcList = srcFiles.split("\n").filter(f => f.trim() && !f.includes(".test.") && !f.includes("__tests__"));
|
|
83
|
+
const testList = new Set(testFiles.split("\n").map(f => f.trim()));
|
|
84
|
+
|
|
85
|
+
for (const src of srcList.slice(0, 20)) {
|
|
86
|
+
const expectedTest = src.replace(".ts", ".test.ts");
|
|
87
|
+
if (!testList.has(expectedTest) && !src.includes("/types") && !src.includes("/index")) {
|
|
88
|
+
items.push({
|
|
89
|
+
id: randomUUID().slice(0, 8),
|
|
90
|
+
type: "missing_test",
|
|
91
|
+
priority: "medium",
|
|
92
|
+
title: `No tests for ${src.split("/").pop()}`,
|
|
93
|
+
description: `${src} has no corresponding test file`,
|
|
94
|
+
file: src,
|
|
95
|
+
status: "discovered",
|
|
96
|
+
discoveredAt: now,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
return items;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function scanTypeErrors(ctx: ScanContext, now: string): Promise<WorkItem[]> {
|
|
105
|
+
const items: WorkItem[] = [];
|
|
106
|
+
try {
|
|
107
|
+
const result = await ctx.runCommand("bunx tsc --noEmit 2>&1 || true");
|
|
108
|
+
const errors = result.split("\n").filter(l => l.includes("error TS"));
|
|
109
|
+
|
|
110
|
+
for (const error of errors.slice(0, 20)) {
|
|
111
|
+
const match = error.match(/^(.+?)\((\d+),\d+\):\s*error\s+(TS\d+):\s*(.+)$/);
|
|
112
|
+
if (!match) continue;
|
|
113
|
+
const [, file, lineNum, code, msg] = match;
|
|
114
|
+
|
|
115
|
+
items.push({
|
|
116
|
+
id: randomUUID().slice(0, 8),
|
|
117
|
+
type: "type_error",
|
|
118
|
+
priority: "high",
|
|
119
|
+
title: `${code} in ${file}:${lineNum}`,
|
|
120
|
+
description: msg!.trim(),
|
|
121
|
+
file: file!,
|
|
122
|
+
line: parseInt(lineNum!, 10),
|
|
123
|
+
status: "discovered",
|
|
124
|
+
discoveredAt: now,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
} catch {}
|
|
128
|
+
return items;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function scanLintErrors(ctx: ScanContext, now: string): Promise<WorkItem[]> {
|
|
132
|
+
const items: WorkItem[] = [];
|
|
133
|
+
try {
|
|
134
|
+
const result = await ctx.runCommand("npx eslint src/ --format json 2>/dev/null || true");
|
|
135
|
+
// Parse JSON output if available
|
|
136
|
+
try {
|
|
137
|
+
const data = JSON.parse(result);
|
|
138
|
+
for (const file of data) {
|
|
139
|
+
for (const msg of (file.messages ?? []).slice(0, 10)) {
|
|
140
|
+
items.push({
|
|
141
|
+
id: randomUUID().slice(0, 8),
|
|
142
|
+
type: "lint_error",
|
|
143
|
+
priority: msg.severity === 2 ? "high" : "low",
|
|
144
|
+
title: `${msg.ruleId} in ${file.filePath.split("/").pop()}:${msg.line}`,
|
|
145
|
+
description: msg.message,
|
|
146
|
+
file: file.filePath,
|
|
147
|
+
line: msg.line,
|
|
148
|
+
status: "discovered",
|
|
149
|
+
discoveredAt: now,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// ESLint not available or no JSON output
|
|
155
|
+
}
|
|
156
|
+
} catch {}
|
|
157
|
+
return items;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function scanComplexity(ctx: ScanContext, now: string): Promise<WorkItem[]> {
|
|
161
|
+
const items: WorkItem[] = [];
|
|
162
|
+
try {
|
|
163
|
+
// Find functions > 50 lines by searching for function/method patterns
|
|
164
|
+
const result = await ctx.grepContent("^(export )?(async )?(function |const .+ = )", "*.{ts,tsx}");
|
|
165
|
+
const lines = result.split("\n").filter(l => l.trim());
|
|
166
|
+
|
|
167
|
+
// Group by file to detect long functions
|
|
168
|
+
// (Simplified: just flag files with many function definitions as potentially complex)
|
|
169
|
+
const fileCounts = new Map<string, number>();
|
|
170
|
+
for (const line of lines) {
|
|
171
|
+
const file = line.split(":")[0];
|
|
172
|
+
if (file) fileCounts.set(file, (fileCounts.get(file) ?? 0) + 1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const [file, count] of fileCounts) {
|
|
176
|
+
if (count > 15) { // Files with many functions might need splitting
|
|
177
|
+
items.push({
|
|
178
|
+
id: randomUUID().slice(0, 8),
|
|
179
|
+
type: "complexity",
|
|
180
|
+
priority: "low",
|
|
181
|
+
title: `${file.split("/").pop()} has ${count} functions`,
|
|
182
|
+
description: `Consider splitting ${file} into smaller modules`,
|
|
183
|
+
file,
|
|
184
|
+
status: "discovered",
|
|
185
|
+
discoveredAt: now,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
return items;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function scanSecurity(ctx: ScanContext, now: string): Promise<WorkItem[]> {
|
|
194
|
+
const items: WorkItem[] = [];
|
|
195
|
+
try {
|
|
196
|
+
const result = await ctx.runCommand("bun pm ls 2>/dev/null | grep -i 'vulnerab' || npm audit --json 2>/dev/null || true");
|
|
197
|
+
if (result.includes("vulnerab") || result.includes("critical") || result.includes("high")) {
|
|
198
|
+
items.push({
|
|
199
|
+
id: randomUUID().slice(0, 8),
|
|
200
|
+
type: "security",
|
|
201
|
+
priority: "critical",
|
|
202
|
+
title: "Dependency vulnerabilities detected",
|
|
203
|
+
description: result.slice(0, 300),
|
|
204
|
+
file: "package.json",
|
|
205
|
+
status: "discovered",
|
|
206
|
+
discoveredAt: now,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
return items;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function scanDeadCode(ctx: ScanContext, now: string): Promise<WorkItem[]> {
|
|
214
|
+
const items: WorkItem[] = [];
|
|
215
|
+
try {
|
|
216
|
+
// Find exports that might not be imported anywhere
|
|
217
|
+
const exports = await ctx.grepContent("^export (function|const|class|interface|type) (\\w+)", "*.ts");
|
|
218
|
+
const exportLines = exports.split("\n").filter(l => l.trim()).slice(0, 100);
|
|
219
|
+
|
|
220
|
+
for (const line of exportLines.slice(0, 10)) {
|
|
221
|
+
const match = line.match(/export (?:function|const|class|interface|type) (\w+)/);
|
|
222
|
+
if (!match) continue;
|
|
223
|
+
const name = match[1]!;
|
|
224
|
+
|
|
225
|
+
// Check if it's imported anywhere
|
|
226
|
+
const imports = await ctx.grepContent(name, "*.{ts,tsx}");
|
|
227
|
+
const importCount = imports.split("\n").filter(l => l.trim()).length;
|
|
228
|
+
|
|
229
|
+
if (importCount <= 1) { // Only the definition itself
|
|
230
|
+
const file = line.split(":")[0]!;
|
|
231
|
+
items.push({
|
|
232
|
+
id: randomUUID().slice(0, 8),
|
|
233
|
+
type: "dead_code",
|
|
234
|
+
priority: "low",
|
|
235
|
+
title: `Unused export: ${name}`,
|
|
236
|
+
description: `${name} in ${file} may not be used anywhere`,
|
|
237
|
+
file,
|
|
238
|
+
status: "discovered",
|
|
239
|
+
discoveredAt: now,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch {}
|
|
244
|
+
return items;
|
|
245
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autopilot types — autonomous work discovery and execution.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type WorkItemType =
|
|
6
|
+
| "todo" // TODO/FIXME/HACK comments
|
|
7
|
+
| "missing_test" // files without test coverage
|
|
8
|
+
| "lint_error" // linter issues
|
|
9
|
+
| "type_error" // TypeScript errors
|
|
10
|
+
| "security" // dependency vulnerabilities
|
|
11
|
+
| "dead_code" // unused exports/imports
|
|
12
|
+
| "complexity" // functions > 50 lines
|
|
13
|
+
| "missing_docs" // public APIs without docs
|
|
14
|
+
| "stale_dep" // outdated dependencies
|
|
15
|
+
| "error_handling" // uncaught errors, missing try/catch
|
|
16
|
+
|
|
17
|
+
export type WorkItemPriority = "critical" | "high" | "medium" | "low";
|
|
18
|
+
|
|
19
|
+
export type WorkItemStatus =
|
|
20
|
+
| "discovered" // found by scanner
|
|
21
|
+
| "approved" // user approved for execution
|
|
22
|
+
| "in_progress" // being worked on
|
|
23
|
+
| "completed" // done
|
|
24
|
+
| "rejected" // user rejected
|
|
25
|
+
| "failed" // execution failed
|
|
26
|
+
|
|
27
|
+
export interface WorkItem {
|
|
28
|
+
id: string;
|
|
29
|
+
type: WorkItemType;
|
|
30
|
+
priority: WorkItemPriority;
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
file: string;
|
|
34
|
+
line?: number;
|
|
35
|
+
status: WorkItemStatus;
|
|
36
|
+
discoveredAt: string;
|
|
37
|
+
completedAt?: string;
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type TrustLevel = "propose" | "auto";
|
|
42
|
+
|
|
43
|
+
export interface AutopilotConfig {
|
|
44
|
+
trustLevel: TrustLevel;
|
|
45
|
+
scanInterval: number; // ms between scans (default: 60000)
|
|
46
|
+
maxConcurrent: number; // max items to work on at once
|
|
47
|
+
scanTypes: WorkItemType[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const DEFAULT_CONFIG: AutopilotConfig = {
|
|
51
|
+
trustLevel: "propose",
|
|
52
|
+
scanInterval: 60_000,
|
|
53
|
+
maxConcurrent: 1,
|
|
54
|
+
scanTypes: [
|
|
55
|
+
"todo", "missing_test", "lint_error", "type_error",
|
|
56
|
+
"security", "dead_code", "complexity",
|
|
57
|
+
],
|
|
58
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge Client — connect to a remote AshlrCode instance.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface BridgeClientConfig {
|
|
6
|
+
url: string;
|
|
7
|
+
authToken: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class BridgeClient {
|
|
11
|
+
private url: string;
|
|
12
|
+
private token: string;
|
|
13
|
+
|
|
14
|
+
constructor(config: BridgeClientConfig) {
|
|
15
|
+
this.url = config.url.replace(/\/$/, "");
|
|
16
|
+
this.token = config.authToken;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private async request(path: string, options?: RequestInit): Promise<any> {
|
|
20
|
+
const response = await fetch(`${this.url}${path}`, {
|
|
21
|
+
...options,
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${this.token}`,
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
...options?.headers,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) throw new Error(`Bridge error: ${response.status}`);
|
|
29
|
+
return response.json();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getStatus(): Promise<{
|
|
33
|
+
mode: string;
|
|
34
|
+
contextPercent: number;
|
|
35
|
+
isProcessing: boolean;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
}> {
|
|
38
|
+
return this.request("/api/status");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async submit(prompt: string): Promise<{ result: string }> {
|
|
42
|
+
return this.request("/api/submit", {
|
|
43
|
+
method: "POST",
|
|
44
|
+
body: JSON.stringify({ prompt }),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getHistory(): Promise<{
|
|
49
|
+
messages: Array<{ role: string; content: string }>;
|
|
50
|
+
}> {
|
|
51
|
+
return this.request("/api/history");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async health(): Promise<{ status: string; uptime: number }> {
|
|
55
|
+
return this.request("/api/health");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge Server — HTTP API for IDE and remote client integration.
|
|
3
|
+
* Runs alongside the REPL on a configurable port.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface BridgeConfig {
|
|
7
|
+
port: number;
|
|
8
|
+
authToken: string;
|
|
9
|
+
onSubmit: (prompt: string) => Promise<string>;
|
|
10
|
+
getStatus: () => { mode: string; contextPercent: number; isProcessing: boolean; sessionId: string };
|
|
11
|
+
getHistory: () => Array<{ role: string; content: string }>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let _server: ReturnType<typeof Bun.serve> | null = null;
|
|
15
|
+
let _config: BridgeConfig | null = null;
|
|
16
|
+
|
|
17
|
+
export function startBridgeServer(config: BridgeConfig): void {
|
|
18
|
+
_config = config;
|
|
19
|
+
|
|
20
|
+
_server = Bun.serve({
|
|
21
|
+
port: config.port,
|
|
22
|
+
fetch: async (req) => {
|
|
23
|
+
// Auth check
|
|
24
|
+
const auth = req.headers.get("Authorization");
|
|
25
|
+
if (auth !== `Bearer ${config.authToken}`) {
|
|
26
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
27
|
+
status: 401,
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const url = new URL(req.url);
|
|
33
|
+
|
|
34
|
+
// Routes
|
|
35
|
+
switch (url.pathname) {
|
|
36
|
+
case "/api/status": {
|
|
37
|
+
const status = config.getStatus();
|
|
38
|
+
return Response.json(status);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
case "/api/submit": {
|
|
42
|
+
if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
|
|
43
|
+
const body = (await req.json()) as { prompt: string };
|
|
44
|
+
if (!body.prompt) return Response.json({ error: "prompt required" }, { status: 400 });
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const result = await config.onSubmit(body.prompt);
|
|
48
|
+
return Response.json({ result });
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return Response.json({ error: String(err) }, { status: 500 });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
case "/api/history": {
|
|
55
|
+
const history = config.getHistory();
|
|
56
|
+
return Response.json({ messages: history.slice(-50) });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case "/api/health": {
|
|
60
|
+
return Response.json({ status: "ok", uptime: process.uptime() });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
default:
|
|
64
|
+
return new Response("Not found", { status: 404 });
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log(` Bridge server listening on http://localhost:${config.port}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function stopBridgeServer(): void {
|
|
73
|
+
if (_server) {
|
|
74
|
+
_server.stop();
|
|
75
|
+
_server = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getBridgePort(): number | null {
|
|
80
|
+
return _config?.port ?? null;
|
|
81
|
+
}
|