@wenathlan/devthink 1.1.15 → 2.0.2
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/CHANGELOG.md +1299 -0
- package/README.md +6 -0
- package/bridge.d.ts +698 -0
- package/bridge.d.ts.map +1 -0
- package/bridge.js +1877 -0
- package/bun.d.ts +8 -0
- package/bun.d.ts.map +1 -0
- package/bun.js +52329 -0
- package/caps/background.json +712 -0
- package/caps/cli.json +360 -0
- package/caps/library.json +3611 -0
- package/caps/mcp.json +126 -0
- package/caps/pagebridge.json +377 -0
- package/caps/popup.json +51 -0
- package/caps/sidepanel.json +522 -0
- package/checksums.txt +142 -0
- package/cli.d.ts +187 -0
- package/cli.d.ts.map +1 -0
- package/cli.js +18197 -0
- package/companion.d.ts +19 -0
- package/companion.d.ts.map +1 -0
- package/companion.js +389 -0
- package/crossbrowser.d.ts +277 -0
- package/crossbrowser.d.ts.map +1 -0
- package/crossbrowser.js +1191 -0
- package/dashdone.d.ts +273 -0
- package/dashdone.d.ts.map +1 -0
- package/dashdone.js +368 -0
- package/deno.d.ts +11 -0
- package/deno.d.ts.map +1 -0
- package/deno.js +52325 -0
- package/devthink.umd.js +52346 -0
- package/fixtures/bun.ts +7 -0
- package/fixtures/cjs.js +6 -0
- package/fixtures/deno.ts +6 -0
- package/fixtures/example-org-pagestate.json +22 -0
- package/fixtures/importers/automa-workflow.json +18 -0
- package/fixtures/importers/selenium-side.json +20 -0
- package/fixtures/importers/tabular-plan.csv +7 -0
- package/fixtures/importers/uivision-macro.json +10 -0
- package/fixtures/importers/v1-plan.json +13 -0
- package/fixtures/mcp-client.mjs +14 -0
- package/fixtures/pages/checkout.html +192 -0
- package/fixtures/pages/dashboard.html +110 -0
- package/fixtures/pages/feed.html +113 -0
- package/fixtures/pages/product-grid.html +150 -0
- package/fixtures/plans/form-inventory-plan.json +11 -0
- package/fixtures/plans/release-notes-plan.json +11 -0
- package/fixtures/recipes/agentscompete.json +11 -0
- package/fixtures/recipes/agentsforms.json +11 -0
- package/fixtures/recipes/agentsmonitor.json +11 -0
- package/fixtures/recipes/agentsreview.json +29 -0
- package/fixtures/recipes/agentswarm.json +11 -0
- package/fixtures/recipes/fillcalendar.json +12 -0
- package/fixtures/recipes/fillcheckout.json +13 -0
- package/fixtures/recipes/filldependentdropdowns.json +12 -0
- package/fixtures/recipes/fillfromcsv.json +13 -0
- package/fixtures/recipes/fillloginform.json +12 -0
- package/fixtures/recipes/fillmultistepwizard.json +13 -0
- package/fixtures/recipes/fillregistration.json +12 -0
- package/fixtures/recipes/fillsearchfilters.json +12 -0
- package/fixtures/recipes/filltypeahead.json +11 -0
- package/fixtures/recipes/monitoralerts.json +12 -0
- package/fixtures/recipes/monitoravailability.json +11 -0
- package/fixtures/recipes/monitornetworkquiet.json +12 -0
- package/fixtures/recipes/monitorpagechanges.json +11 -0
- package/fixtures/recipes/monitorpricechange.json +11 -0
- package/fixtures/recipes/monitorschedule.json +12 -0
- package/fixtures/recipes/scrapedynamicprices.json +13 -0
- package/fixtures/recipes/scrapeimagegallery.json +12 -0
- package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
- package/fixtures/recipes/scrapenestedlists.json +12 -0
- package/fixtures/recipes/scrapeproductgrid.json +12 -0
- package/fixtures/recipes/scrapereviews.json +12 -0
- package/fixtures/recipes/scrapesearchresults.json +12 -0
- package/fixtures/recipes/scrapetablepagination.json +12 -0
- package/fixtures/recipes/testaccessibility.json +34 -0
- package/fixtures/recipes/testconsoleerrors.json +12 -0
- package/fixtures/recipes/testdeeplinks.json +12 -0
- package/fixtures/recipes/testformvalidation.json +13 -0
- package/fixtures/recipes/testlayoutshift.json +12 -0
- package/fixtures/recipes/testlinkcheck.json +12 -0
- package/fixtures/recipes/testloadtiming.json +12 -0
- package/fixtures/recipes/testvisualdiff.json +12 -0
- package/fixtures/workflows/changelog-digest-workflow.json +13 -0
- package/fixtures/workflows/form-survey-workflow.json +13 -0
- package/gallery.json +706 -0
- package/hardening.d.ts +81 -0
- package/hardening.d.ts.map +1 -0
- package/hardening.js +188 -0
- package/headless.d.ts +114 -0
- package/headless.d.ts.map +1 -0
- package/headless.js +11522 -0
- package/http.d.ts +636 -0
- package/http.d.ts.map +1 -0
- package/http.js +2165 -0
- package/index.cjs +54682 -0
- package/index.d.ts +40 -0
- package/index.d.ts.map +1 -0
- package/index.js +52310 -0
- package/index.neutral.js +46343 -0
- package/manifest.json +199 -0
- package/mcp.d.ts +501 -0
- package/mcp.d.ts.map +1 -0
- package/mcp.js +20515 -0
- package/memory.d.ts +2397 -0
- package/memory.d.ts.map +1 -0
- package/memory.js +5782 -0
- package/nativehost.template.json +10 -0
- package/node.cjs +54686 -0
- package/node.d.ts +8 -0
- package/node.d.ts.map +1 -0
- package/pack.d.ts +211 -0
- package/pack.d.ts.map +1 -0
- package/pack.js +805 -0
- package/package.json +101 -51
- package/policy.d.ts +1854 -0
- package/policy.d.ts.map +1 -0
- package/policy.js +12580 -0
- package/progress.d.ts +390 -0
- package/progress.d.ts.map +1 -0
- package/progress.js +645 -0
- package/protocol.d.ts +2336 -0
- package/protocol.d.ts.map +1 -0
- package/protocol.js +8334 -0
- package/schemas/audit.schema.json +226 -0
- package/schemas/capability.schema.json +113 -0
- package/schemas/envelope.schema.json +162 -0
- package/schemas/memory.schema.json +51 -0
- package/schemas/observation.schema.json +113 -0
- package/schemas/plan.schema.json +208 -0
- package/schemas/progress.schema.json +126 -0
- package/schemas/proposal.schema.json +68 -0
- package/schemas/session.schema.json +82 -0
- package/schemas/tool.schema.json +94 -0
- package/server.d.ts +435 -0
- package/server.d.ts.map +1 -0
- package/server.js +28872 -0
- package/umd-example.html +18 -0
- package/umd.d.ts +6 -0
- package/umd.d.ts.map +1 -0
- package/build.ts +0 -41
- package/check-secrets.ts +0 -52
- package/compatibility.ts +0 -37
- package/config.ts +0 -246
- package/devthink.ts +0 -397
- package/identity.ts +0 -156
- package/ids.ts +0 -22
- package/memory.ts +0 -95
- package/modes.ts +0 -43
- package/plugin.ts +0 -68
- package/providers.ts +0 -179
- package/server.ts +0 -264
- package/session.ts +0 -155
- package/storage.ts +0 -82
- package/stream.ts +0 -130
- package/sync.ts +0 -27
- package/terminal-ui.tsx +0 -155
- package/ui.ts +0 -143
- package/workspace.ts +0 -20
package/memory.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import type { DevThinkPaths } from "./config.ts";
|
|
4
|
-
|
|
5
|
-
export type MemoryLayer = "session" | "project" | "global";
|
|
6
|
-
|
|
7
|
-
export type MemoryEntry = {
|
|
8
|
-
id: string;
|
|
9
|
-
layer: MemoryLayer;
|
|
10
|
-
key: string;
|
|
11
|
-
content: string;
|
|
12
|
-
createdAt: string;
|
|
13
|
-
updatedAt: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type MemoryQuery = {
|
|
17
|
-
key?: string;
|
|
18
|
-
text?: string;
|
|
19
|
-
sessionId?: string;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function memoryFile(paths: DevThinkPaths, layer: MemoryLayer, sessionId?: string): string {
|
|
23
|
-
if (layer === "session") return join(paths.sessions, `${sessionId || "active"}.memory.json`);
|
|
24
|
-
if (layer === "project") return join(process.cwd(), ".devthink", "project-memory.json");
|
|
25
|
-
return join(paths.memory, "global-memory.json");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function readEntries(path: string, layer: MemoryLayer): MemoryEntry[] {
|
|
29
|
-
try {
|
|
30
|
-
if (!existsSync(path)) return [];
|
|
31
|
-
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
32
|
-
return Array.isArray(parsed) ? (parsed as MemoryEntry[]).filter((entry) => entry.layer === layer) : [];
|
|
33
|
-
} catch {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function writeEntries(path: string, entries: MemoryEntry[]): void {
|
|
39
|
-
mkdirSync(join(path, ".."), { recursive: true });
|
|
40
|
-
writeFileSync(path, `${JSON.stringify(entries, null, 2)}\n`, "utf8");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function normalize(value: string): string {
|
|
44
|
-
return value.trim().toLowerCase();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function scoreEntry(entry: MemoryEntry, query: MemoryQuery): number {
|
|
48
|
-
const key = normalize(query.key || "");
|
|
49
|
-
const text = normalize(query.text || "");
|
|
50
|
-
let score = 0;
|
|
51
|
-
if (key && normalize(entry.key) === key) score += 10;
|
|
52
|
-
if (text && normalize(entry.content).includes(text)) score += 5;
|
|
53
|
-
if (!key && !text) score = 1;
|
|
54
|
-
return score;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function createMemoryStore(paths: DevThinkPaths) {
|
|
58
|
-
return {
|
|
59
|
-
remember(entry: Omit<MemoryEntry, "id" | "createdAt" | "updatedAt">): MemoryEntry {
|
|
60
|
-
const now = new Date().toISOString();
|
|
61
|
-
const path = memoryFile(paths, entry.layer, entry.layer === "session" ? entry.key : undefined);
|
|
62
|
-
const entries = readEntries(path, entry.layer);
|
|
63
|
-
const current = entries.find((item) => item.key === entry.key);
|
|
64
|
-
const next: MemoryEntry = {
|
|
65
|
-
id: current?.id || crypto.randomUUID(),
|
|
66
|
-
...entry,
|
|
67
|
-
createdAt: current?.createdAt || now,
|
|
68
|
-
updatedAt: now,
|
|
69
|
-
};
|
|
70
|
-
writeEntries(path, [...entries.filter((item) => item.id !== next.id), next]);
|
|
71
|
-
return next;
|
|
72
|
-
},
|
|
73
|
-
resolve(query: MemoryQuery): MemoryEntry | undefined {
|
|
74
|
-
const layers: MemoryLayer[] = ["session", "project", "global"];
|
|
75
|
-
for (const layer of layers) {
|
|
76
|
-
const entries = readEntries(memoryFile(paths, layer, query.sessionId), layer);
|
|
77
|
-
const match = entries.map((entry) => ({ entry, score: scoreEntry(entry, query) })).filter((item) => item.score > 0).sort((a, b) => b.score - a.score)[0];
|
|
78
|
-
if (match) return match.entry;
|
|
79
|
-
}
|
|
80
|
-
return undefined;
|
|
81
|
-
},
|
|
82
|
-
list(layer?: MemoryLayer, sessionId?: string): MemoryEntry[] {
|
|
83
|
-
const layers: MemoryLayer[] = layer ? [layer] : ["session", "project", "global"];
|
|
84
|
-
return layers.flatMap((item) => readEntries(memoryFile(paths, item, sessionId), item));
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export type MemoryStore = ReturnType<typeof createMemoryStore>;
|
|
90
|
-
|
|
91
|
-
export function memorySummary(store: MemoryStore): string {
|
|
92
|
-
const entries = store.list();
|
|
93
|
-
if (!entries.length) return "No local memory entries.";
|
|
94
|
-
return entries.map((entry) => `${entry.layer}:${entry.key} — ${entry.content}`).join("\n");
|
|
95
|
-
}
|
package/modes.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
export type ModeDefinition = {
|
|
2
|
-
id: string;
|
|
3
|
-
label: string;
|
|
4
|
-
purpose: string;
|
|
5
|
-
memory: boolean;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const MODES: ModeDefinition[] = [
|
|
9
|
-
["chat", "Chat", "Conversational interaction", true],
|
|
10
|
-
["code", "Code", "Code generation and editing", true],
|
|
11
|
-
["review", "Review", "Code review and analysis", true],
|
|
12
|
-
["debug", "Debug", "Debugging assistance", true],
|
|
13
|
-
["test", "Test", "Test generation and execution", true],
|
|
14
|
-
["doc", "Doc", "Documentation generation", true],
|
|
15
|
-
["refactor", "Refactor", "Refactoring suggestions", true],
|
|
16
|
-
["explain", "Explain", "Code explanation", true],
|
|
17
|
-
["search", "Search", "Codebase search", true],
|
|
18
|
-
["plan", "Plan", "Project planning", true],
|
|
19
|
-
["execute", "Execute", "Command execution assistance", true],
|
|
20
|
-
["analyze", "Analyze", "Data and code analysis", true],
|
|
21
|
-
["translate", "Translate", "Language and format translation", true],
|
|
22
|
-
["generate", "Generate", "Content generation", true],
|
|
23
|
-
["optimize", "Optimize", "Performance optimization", true],
|
|
24
|
-
["security", "Security", "Security analysis", true],
|
|
25
|
-
["deploy", "Deploy", "Deployment assistance", true],
|
|
26
|
-
["monitor", "Monitor", "Monitoring and health", true],
|
|
27
|
-
["config", "Config", "Configuration management", true],
|
|
28
|
-
["session", "Session", "Session and context management", true],
|
|
29
|
-
].map(([id, label, purpose, memory]) => ({ id, label, purpose, memory })) as ModeDefinition[];
|
|
30
|
-
|
|
31
|
-
export function listModes(): ModeDefinition[] {
|
|
32
|
-
return MODES.map((mode) => ({ ...mode }));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function resolveMode(id: string): ModeDefinition {
|
|
36
|
-
const mode = MODES.find((item) => item.id === id.toLowerCase());
|
|
37
|
-
if (!mode) throw new Error(`Unknown mode: ${id}. Use devthink modes.`);
|
|
38
|
-
return mode;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function modePrompt(mode: ModeDefinition): string {
|
|
42
|
-
return `You are DevThink in ${mode.label} mode. Focus on ${mode.purpose.toLowerCase()}. Prefer precise, actionable answers. Do not claim to have executed an action unless the user explicitly enabled and requested it.`;
|
|
43
|
-
}
|
package/plugin.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
|
|
5
|
-
export type PluginContext = {
|
|
6
|
-
cwd: string;
|
|
7
|
-
log: (message: string) => void;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export type PluginDefinition = {
|
|
11
|
-
name: string;
|
|
12
|
-
version?: string;
|
|
13
|
-
hooks?: Record<string, (payload: unknown, context: PluginContext) => unknown | Promise<unknown>>;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type PluginManifest = {
|
|
17
|
-
name: string;
|
|
18
|
-
entry: string;
|
|
19
|
-
enabled?: boolean;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function pluginRoot(): string {
|
|
23
|
-
return join(process.env.DEVTHINK_HOME || join(process.cwd(), ".devthink"), "plugins");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function readManifest(path: string): PluginManifest | undefined {
|
|
27
|
-
try {
|
|
28
|
-
const parsed = JSON.parse(readFileSync(path, "utf8")) as PluginManifest;
|
|
29
|
-
if (!parsed.name || !parsed.entry) return undefined;
|
|
30
|
-
return parsed;
|
|
31
|
-
} catch {
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function importPlugin(manifestPath: string, manifest: PluginManifest): Promise<PluginDefinition> {
|
|
37
|
-
const entry = resolve(join(manifestPath, "..", manifest.entry));
|
|
38
|
-
const loaded = (await import(pathToFileURL(entry).href)) as { default?: PluginDefinition } & PluginDefinition;
|
|
39
|
-
return loaded.default || loaded;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function listPluginManifests(root = pluginRoot()): PluginManifest[] {
|
|
43
|
-
if (!existsSync(root)) return [];
|
|
44
|
-
return readdirSync(root, { withFileTypes: true }).filter((item) => item.isDirectory()).map((item) => readManifest(join(root, item.name, "devthink.plugin.json"))).filter((item): item is PluginManifest => Boolean(item));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function loadPlugins(root = pluginRoot()): Promise<PluginDefinition[]> {
|
|
48
|
-
const manifests = listPluginManifests(root).filter((manifest) => manifest.enabled !== false);
|
|
49
|
-
const plugins: PluginDefinition[] = [];
|
|
50
|
-
for (const manifest of manifests) {
|
|
51
|
-
const path = join(root, manifest.name, "devthink.plugin.json");
|
|
52
|
-
try {
|
|
53
|
-
plugins.push(await importPlugin(path, manifest));
|
|
54
|
-
} catch {
|
|
55
|
-
// A broken optional plugin does not prevent the core CLI from starting.
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return plugins;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function runHook(plugins: PluginDefinition[], hook: string, payload: unknown, context: PluginContext): Promise<unknown[]> {
|
|
62
|
-
const results: unknown[] = [];
|
|
63
|
-
for (const plugin of plugins) {
|
|
64
|
-
const handler = plugin.hooks?.[hook];
|
|
65
|
-
if (handler) results.push(await handler(payload, context));
|
|
66
|
-
}
|
|
67
|
-
return results;
|
|
68
|
-
}
|
package/providers.ts
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { resolveCredential, type DevThinkConfig, type DevThinkPaths } from "./config.ts";
|
|
2
|
-
import { redactProviderError, retryDelay, type ProviderProtocol } from "./compatibility.ts";
|
|
3
|
-
import { parseEventStream, type ChatEvent } from "./stream.ts";
|
|
4
|
-
|
|
5
|
-
export type ChatRole = "system" | "user" | "assistant";
|
|
6
|
-
|
|
7
|
-
export type ChatMessage = {
|
|
8
|
-
role: ChatRole;
|
|
9
|
-
content: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type ChatRequest = {
|
|
13
|
-
provider: string;
|
|
14
|
-
model: string;
|
|
15
|
-
messages: ChatMessage[];
|
|
16
|
-
temperature?: number;
|
|
17
|
-
maxTokens?: number;
|
|
18
|
-
signal?: AbortSignal;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type ModelInfo = {
|
|
22
|
-
id: string;
|
|
23
|
-
provider: string;
|
|
24
|
-
contextWindow?: number;
|
|
25
|
-
supportsStreaming: boolean;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type ProviderDefinition = {
|
|
29
|
-
id: string;
|
|
30
|
-
baseUrl?: string;
|
|
31
|
-
protocol: ProviderProtocol;
|
|
32
|
-
env: string;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const PROVIDERS: ProviderDefinition[] = [
|
|
36
|
-
{ id: "openai", baseUrl: "https://api.openai.com/v1", protocol: "openai", env: "OPENAI_API_KEY" },
|
|
37
|
-
{ id: "zai", baseUrl: "https://api.z.ai/api/paas/v4", protocol: "openai", env: "ZAI_API_KEY" },
|
|
38
|
-
{ id: "openrouter", baseUrl: "https://openrouter.ai/api/v1", protocol: "openai", env: "OPENROUTER_API_KEY" },
|
|
39
|
-
{ id: "anthropic", baseUrl: "https://api.anthropic.com", protocol: "anthropic", env: "ANTHROPIC_API_KEY" },
|
|
40
|
-
{ id: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", protocol: "google", env: "GEMINI_API_KEY" },
|
|
41
|
-
{ id: "qwen", baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", protocol: "openai", env: "QWEN_API_KEY" },
|
|
42
|
-
{ id: "deepseek", baseUrl: "https://api.deepseek.com", protocol: "openai", env: "DEEPSEEK_API_KEY" },
|
|
43
|
-
{ id: "groq", baseUrl: "https://api.groq.com/openai/v1", protocol: "openai", env: "GROQ_API_KEY" },
|
|
44
|
-
{ id: "mistral", baseUrl: "https://api.mistral.ai/v1", protocol: "openai", env: "MISTRAL_API_KEY" },
|
|
45
|
-
{ id: "xai", baseUrl: "https://api.x.ai/v1", protocol: "openai", env: "XAI_API_KEY" },
|
|
46
|
-
{ id: "ollama", protocol: "openai", env: "OLLAMA_API_KEY" },
|
|
47
|
-
{ id: "mimo", baseUrl: "https://api.xiaomimimo.com/v1", protocol: "openai", env: "MIMO_API_KEY" },
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
function findProvider(provider: string): ProviderDefinition {
|
|
51
|
-
const definition = PROVIDERS.find((item) => item.id === provider.toLowerCase());
|
|
52
|
-
if (!definition) throw new Error(`Unknown provider: ${provider}. Use devthink providers.`);
|
|
53
|
-
return definition;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function joinUrl(baseUrl: string, path: string): string {
|
|
57
|
-
return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function cleanError(body: string, status: number): Error {
|
|
61
|
-
let detail = body.trim();
|
|
62
|
-
try {
|
|
63
|
-
const parsed = JSON.parse(body) as Record<string, unknown>;
|
|
64
|
-
const error = (parsed.error || parsed.message || parsed.detail) as unknown;
|
|
65
|
-
if (typeof error === "string") detail = error;
|
|
66
|
-
if (error && typeof error === "object") detail = JSON.stringify(error);
|
|
67
|
-
} catch {
|
|
68
|
-
detail = body.trim();
|
|
69
|
-
}
|
|
70
|
-
const safe = redactProviderError(detail);
|
|
71
|
-
return new Error(`${status} ${safe || "Provider request failed."}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Uses the provider's configured endpoint directly; custom gateways are represented by `providers.<id>.baseUrl`. */
|
|
75
|
-
async function fetchWithRetry(input: string, init: RequestInit, attempts = 3): Promise<Response> {
|
|
76
|
-
let lastError: unknown;
|
|
77
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
78
|
-
try {
|
|
79
|
-
const response = await fetch(input, init);
|
|
80
|
-
if (response.ok || response.status < 500 || attempt === attempts) return response;
|
|
81
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay(attempt - 1)));
|
|
82
|
-
} catch (error) {
|
|
83
|
-
lastError = error;
|
|
84
|
-
if (attempt === attempts) throw error;
|
|
85
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay(attempt - 1)));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
throw lastError instanceof Error ? lastError : new Error("Provider request failed.");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function buildOpenAiHeaders(credential: string): HeadersInit {
|
|
92
|
-
return { accept: "text/event-stream", "content-type": "application/json", authorization: `Bearer ${credential}` };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function buildAnthropicHeaders(credential: string): HeadersInit {
|
|
96
|
-
return { accept: "text/event-stream", "content-type": "application/json", "x-api-key": credential, "anthropic-version": "2023-06-01" };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function buildGoogleBody(request: ChatRequest): Record<string, unknown> {
|
|
100
|
-
const system = request.messages.filter((message) => message.role === "system").map((message) => ({ text: message.content }));
|
|
101
|
-
const contents = request.messages.filter((message) => message.role !== "system").map((message) => ({
|
|
102
|
-
role: message.role === "assistant" ? "model" : "user",
|
|
103
|
-
parts: [{ text: message.content }],
|
|
104
|
-
}));
|
|
105
|
-
return {
|
|
106
|
-
...(system.length ? { systemInstruction: { parts: system } } : {}),
|
|
107
|
-
contents,
|
|
108
|
-
generationConfig: {
|
|
109
|
-
...(request.temperature === undefined ? {} : { temperature: request.temperature }),
|
|
110
|
-
...(request.maxTokens === undefined ? {} : { maxOutputTokens: request.maxTokens }),
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function buildAnthropicBody(request: ChatRequest): Record<string, unknown> {
|
|
116
|
-
const system = request.messages.filter((message) => message.role === "system").map((message) => message.content).join("\n\n");
|
|
117
|
-
return {
|
|
118
|
-
model: request.model,
|
|
119
|
-
messages: request.messages.filter((message) => message.role !== "system"),
|
|
120
|
-
max_tokens: request.maxTokens ?? 4096,
|
|
121
|
-
stream: true,
|
|
122
|
-
...(system ? { system } : {}),
|
|
123
|
-
...(request.temperature === undefined ? {} : { temperature: request.temperature }),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function buildOpenAiBody(request: ChatRequest): Record<string, unknown> {
|
|
128
|
-
return {
|
|
129
|
-
model: request.model,
|
|
130
|
-
messages: request.messages,
|
|
131
|
-
stream: true,
|
|
132
|
-
...(request.temperature === undefined ? {} : { temperature: request.temperature }),
|
|
133
|
-
...(request.maxTokens === undefined ? {} : { max_tokens: request.maxTokens }),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function listProviders(): ProviderDefinition[] {
|
|
138
|
-
return PROVIDERS.map((provider) => ({ ...provider }));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function resolveProvider(provider: string, config: DevThinkConfig, paths?: DevThinkPaths): ProviderDefinition & { baseUrl: string; credential: string } {
|
|
142
|
-
const definition = findProvider(provider);
|
|
143
|
-
const isActive = provider === (config.activeProvider || config.provider);
|
|
144
|
-
const baseUrl = config.providers?.[provider]?.baseUrl || (isActive ? config.baseUrl : undefined) || definition.baseUrl;
|
|
145
|
-
if (!baseUrl) throw new Error(`Provider ${provider} requires baseUrl in config or --base-url.`);
|
|
146
|
-
const credential = resolveCredential(provider, config, paths);
|
|
147
|
-
if (!credential) throw new Error(`No credential found for ${provider}. Set ${definition.env} or configure it explicitly.`);
|
|
148
|
-
return { ...definition, baseUrl, credential };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function streamChat(request: ChatRequest, config: DevThinkConfig, paths?: DevThinkPaths): Promise<AsyncGenerator<ChatEvent>> {
|
|
152
|
-
const provider = resolveProvider(request.provider, config, paths);
|
|
153
|
-
const url = provider.protocol === "google"
|
|
154
|
-
? `${joinUrl(provider.baseUrl, `models/${encodeURIComponent(request.model)}:streamGenerateContent`)}?alt=sse`
|
|
155
|
-
: joinUrl(provider.baseUrl, provider.protocol === "anthropic" ? "/v1/messages" : "/chat/completions");
|
|
156
|
-
const headers = provider.protocol === "google"
|
|
157
|
-
? { accept: "text/event-stream", "content-type": "application/json", "x-goog-api-key": provider.credential }
|
|
158
|
-
: provider.protocol === "anthropic" ? buildAnthropicHeaders(provider.credential) : buildOpenAiHeaders(provider.credential);
|
|
159
|
-
const body = provider.protocol === "google" ? buildGoogleBody(request) : provider.protocol === "anthropic" ? buildAnthropicBody(request) : buildOpenAiBody(request);
|
|
160
|
-
const response = await fetchWithRetry(url, { method: "POST", headers, body: JSON.stringify(body), signal: request.signal });
|
|
161
|
-
if (!response.ok) throw cleanError(await response.text(), response.status);
|
|
162
|
-
return parseEventStream(response, { provider: provider.id, model: request.model, signal: request.signal });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export async function listModels(providerName: string, config: DevThinkConfig, paths?: DevThinkPaths): Promise<ModelInfo[]> {
|
|
166
|
-
const provider = resolveProvider(providerName, config, paths);
|
|
167
|
-
if (provider.protocol === "google") {
|
|
168
|
-
const response = await fetchWithRetry(joinUrl(provider.baseUrl, "models"), { headers: { "x-goog-api-key": provider.credential } });
|
|
169
|
-
if (!response.ok) throw cleanError(await response.text(), response.status);
|
|
170
|
-
const payload = (await response.json()) as { models?: Array<{ name?: string; inputTokenLimit?: number }> };
|
|
171
|
-
return (payload.models || []).map((model) => ({ id: (model.name || "").replace(/^models\//, ""), provider: provider.id, contextWindow: model.inputTokenLimit, supportsStreaming: true }));
|
|
172
|
-
}
|
|
173
|
-
const path = provider.protocol === "anthropic" ? "/v1/models" : "/models";
|
|
174
|
-
const headers = provider.protocol === "anthropic" ? buildAnthropicHeaders(provider.credential) : buildOpenAiHeaders(provider.credential);
|
|
175
|
-
const response = await fetchWithRetry(joinUrl(provider.baseUrl, path), { headers });
|
|
176
|
-
if (!response.ok) throw cleanError(await response.text(), response.status);
|
|
177
|
-
const payload = (await response.json()) as { data?: Array<{ id?: string; context_length?: number }> };
|
|
178
|
-
return (payload.data || []).filter((model) => model.id).map((model) => ({ id: model.id as string, provider: provider.id, contextWindow: model.context_length, supportsStreaming: true }));
|
|
179
|
-
}
|
package/server.ts
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
2
|
-
import { randomInt } from "node:crypto";
|
|
3
|
-
import { isAllowedOrigin } from "./compatibility.ts";
|
|
4
|
-
import { listModels, listProviders, streamChat, type ChatMessage } from "./providers.ts";
|
|
5
|
-
import { ensurePaths, resolvePaths, saveConfig, type DevThinkConfig, type DevThinkPaths } from "./config.ts";
|
|
6
|
-
import { consumePairing, getIdentity, pairingStatus, revokeBrowserSessions, setIdentityUserId, verifyBrowserSession } from "./identity.ts";
|
|
7
|
-
import { appendMessage, createSession, createTab, listSessions, loadSession, loadWorkspace, updateTab, type SessionSection } from "./session.ts";
|
|
8
|
-
import { readPreferences, savePreference } from "./storage.ts";
|
|
9
|
-
|
|
10
|
-
export type ServerOptions = {
|
|
11
|
-
port?: number;
|
|
12
|
-
config: DevThinkConfig;
|
|
13
|
-
paths?: DevThinkPaths;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type ServerHandle = {
|
|
17
|
-
address: string;
|
|
18
|
-
port: number;
|
|
19
|
-
stop: () => Promise<void>;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function writeJson(response: ServerResponse, status: number, payload: unknown, origin?: string): void {
|
|
23
|
-
if (origin) response.setHeader("access-control-allow-origin", origin);
|
|
24
|
-
response.setHeader("vary", "Origin");
|
|
25
|
-
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
26
|
-
response.end(JSON.stringify(payload));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function safeOrigin(request: IncomingMessage, config: DevThinkConfig): string | undefined {
|
|
30
|
-
const origin = request.headers.origin;
|
|
31
|
-
return isAllowedOrigin(origin, config.web?.allowedOrigins) ? origin : undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function bearerToken(request: IncomingMessage): string | undefined {
|
|
35
|
-
const value = request.headers.authorization;
|
|
36
|
-
return value?.startsWith("Bearer ") ? value.slice("Bearer ".length).trim() || undefined : undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function writeEvent(response: ServerResponse, type: string, payload: unknown): void {
|
|
40
|
-
response.write(`event: ${type}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function readBody(request: IncomingMessage): Promise<Record<string, unknown>> {
|
|
44
|
-
const chunks: Buffer[] = [];
|
|
45
|
-
for await (const chunk of request) chunks.push(Buffer.from(chunk));
|
|
46
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
47
|
-
const parsed: unknown = raw ? JSON.parse(raw) : {};
|
|
48
|
-
return parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function acceptsJson(request: IncomingMessage): boolean {
|
|
52
|
-
return request.headers["content-type"]?.toLowerCase().includes("application/json") === true;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const browserPreferenceKeys = new Set(["theme", "railMode", "interfaceZoom"]);
|
|
56
|
-
|
|
57
|
-
function messagesFromBody(value: unknown): ChatMessage[] {
|
|
58
|
-
if (!Array.isArray(value)) throw new Error("messages must be an array.");
|
|
59
|
-
return value.map((item) => {
|
|
60
|
-
const message = item as Record<string, unknown>;
|
|
61
|
-
if (!message.content || !["system", "user", "assistant"].includes(String(message.role))) throw new Error("Each message needs a supported role and content.");
|
|
62
|
-
return { role: message.role as ChatMessage["role"], content: String(message.content) };
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function sectionFrom(value: unknown): SessionSection {
|
|
67
|
-
return ["chat", "inspector", "settings", "memory", "providers", "projects", "routes", "usage"].includes(String(value)) ? value as SessionSection : "chat";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function routeParts(pathname: string): string[] {
|
|
71
|
-
return pathname.split("/").filter(Boolean).map(decodeURIComponent);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function streamChatRoute(request: IncomingMessage, response: ServerResponse, config: DevThinkConfig, paths: DevThinkPaths, origin?: string): Promise<void> {
|
|
75
|
-
const body = await readBody(request);
|
|
76
|
-
const provider = String(body.provider || config.provider || "");
|
|
77
|
-
const model = String(body.model || config.model || "");
|
|
78
|
-
if (!provider || !model) return writeJson(response, 400, { error: "provider and model are required." }, origin);
|
|
79
|
-
const incoming = messagesFromBody(body.messages);
|
|
80
|
-
const prompt = [...incoming].reverse().find((message) => message.role === "user");
|
|
81
|
-
if (!prompt) return writeJson(response, 400, { error: "messages must include a user message." }, origin);
|
|
82
|
-
const sessionId = typeof body.sessionId === "string" ? body.sessionId : undefined;
|
|
83
|
-
const workspaceId = typeof body.workspaceId === "string" ? body.workspaceId : undefined;
|
|
84
|
-
const tabId = typeof body.tabId === "string" ? body.tabId : undefined;
|
|
85
|
-
const sectionId = sectionFrom(body.sectionId);
|
|
86
|
-
let session = sessionId ? loadSession(paths, sessionId) : undefined;
|
|
87
|
-
if (sessionId && !session) return writeJson(response, 404, { error: "Session not found." }, origin);
|
|
88
|
-
if (!session) session = createSession(paths, { mode: typeof body.mode === "string" ? body.mode : "chat", model, provider, workspaceId, tabId, sectionId, title: typeof body.title === "string" ? body.title : undefined });
|
|
89
|
-
if (workspaceId && session.workspaceId !== workspaceId) return writeJson(response, 409, { error: "workspaceId does not match the session." }, origin);
|
|
90
|
-
if (tabId && !session.tabs.some((tab) => tab.id === tabId)) return writeJson(response, 404, { error: "Tab not found." }, origin);
|
|
91
|
-
session = appendMessage(paths, session, prompt, { tabId: tabId || session.activeTabId, sectionId });
|
|
92
|
-
const history = session.messages.map(({ role, content }) => ({ role, content }));
|
|
93
|
-
const events = await streamChat({ provider, model, messages: history, temperature: typeof body.temperature === "number" ? body.temperature : config.temperature, maxTokens: typeof body.maxTokens === "number" ? body.maxTokens : config.maxTokens }, config, paths);
|
|
94
|
-
if (origin) response.setHeader("access-control-allow-origin", origin);
|
|
95
|
-
response.setHeader("vary", "Origin");
|
|
96
|
-
response.writeHead(200, { "cache-control": "no-cache", connection: "keep-alive", "content-type": "text/event-stream; charset=utf-8" });
|
|
97
|
-
writeEvent(response, "identity", { workspaceId: session.workspaceId, sessionId: session.id, tabId: tabId || session.activeTabId, sectionId });
|
|
98
|
-
let text = "";
|
|
99
|
-
for await (const event of events) {
|
|
100
|
-
if (event.type === "text") text += event.text;
|
|
101
|
-
writeEvent(response, event.type, event);
|
|
102
|
-
}
|
|
103
|
-
const persisted = appendMessage(paths, session, { role: "assistant", content: text }, { tabId: tabId || session.activeTabId, sectionId });
|
|
104
|
-
writeEvent(response, "persisted", { workspaceId: persisted.workspaceId, sessionId: persisted.id, tabId: tabId || persisted.activeTabId, messageId: persisted.messages.at(-1)?.id });
|
|
105
|
-
response.end();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function route(request: IncomingMessage, response: ServerResponse, config: DevThinkConfig, paths: DevThinkPaths): Promise<void> {
|
|
109
|
-
const url = new URL(request.url || "/", "http://127.0.0.1");
|
|
110
|
-
const origin = safeOrigin(request, config);
|
|
111
|
-
if (request.method === "OPTIONS") {
|
|
112
|
-
if (origin) response.setHeader("access-control-allow-origin", origin);
|
|
113
|
-
response.setHeader("access-control-allow-methods", "GET, POST, PATCH, PUT, OPTIONS");
|
|
114
|
-
response.setHeader("access-control-allow-headers", "authorization, content-type");
|
|
115
|
-
response.setHeader("vary", "Origin");
|
|
116
|
-
response.writeHead(204);
|
|
117
|
-
response.end();
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (request.headers.origin && !origin) return writeJson(response, 403, { error: "Origin is not allowed by web.allowedOrigins." });
|
|
121
|
-
const mutationWithBody = ["POST", "PATCH", "PUT"].includes(request.method || "") && url.pathname !== "/pairings/revoke";
|
|
122
|
-
if (mutationWithBody && !acceptsJson(request)) return writeJson(response, 415, { error: "JSON content-type is required for mutation requests." }, origin);
|
|
123
|
-
if (request.method === "GET" && url.pathname === "/health") return writeJson(response, 200, { status: "ok", service: "devthink" }, origin);
|
|
124
|
-
if (request.method === "GET" && url.pathname === "/providers") return writeJson(response, 200, listProviders().map(({ id, protocol, env }) => ({ id, protocol, env })), origin);
|
|
125
|
-
if (request.method === "GET" && url.pathname === "/models") {
|
|
126
|
-
const provider = url.searchParams.get("provider") || config.provider;
|
|
127
|
-
if (!provider) return writeJson(response, 400, { error: "provider is required." }, origin);
|
|
128
|
-
return writeJson(response, 200, await listModels(provider, config, paths), origin);
|
|
129
|
-
}
|
|
130
|
-
const parts = routeParts(url.pathname);
|
|
131
|
-
if (request.method === "POST" && url.pathname === "/pairings/consume") {
|
|
132
|
-
const body = await readBody(request);
|
|
133
|
-
const pairingId = typeof body.pairingId === "string" ? body.pairingId : "";
|
|
134
|
-
const code = typeof body.code === "string" ? body.code.trim().toUpperCase() : "";
|
|
135
|
-
const paired = consumePairing(paths, pairingId, code);
|
|
136
|
-
return paired ? writeJson(response, 201, { token: paired.token, userId: paired.identity.userId, expiresAt: paired.expiresAt }, origin) : writeJson(response, 401, { error: "Pairing code is invalid, expired, or already used." }, origin);
|
|
137
|
-
}
|
|
138
|
-
const browserSession = origin ? verifyBrowserSession(paths, bearerToken(request)) : undefined;
|
|
139
|
-
if (origin && !browserSession) return writeJson(response, 401, { error: "A current browser pairing token is required." }, origin);
|
|
140
|
-
if (request.method === "GET" && url.pathname === "/identity") return writeJson(response, 200, { identity: getIdentity(paths), pairing: pairingStatus(paths) }, origin);
|
|
141
|
-
if (request.method === "PUT" && url.pathname === "/identity") {
|
|
142
|
-
const body = await readBody(request);
|
|
143
|
-
if (typeof body.userId !== "string") return writeJson(response, 400, { error: "userId is required." }, origin);
|
|
144
|
-
try { return writeJson(response, 200, { identity: setIdentityUserId(paths, body.userId), pairing: pairingStatus(paths) }, origin); }
|
|
145
|
-
catch (error) { return writeJson(response, 400, { error: error instanceof Error ? error.message : "Identity update was rejected." }, origin); }
|
|
146
|
-
}
|
|
147
|
-
if (request.method === "POST" && url.pathname === "/pairings/revoke") return writeJson(response, 200, { revoked: revokeBrowserSessions(paths) }, origin);
|
|
148
|
-
if (request.method === "GET" && url.pathname === "/sessions") return writeJson(response, 200, { sessions: listSessions(paths) }, origin);
|
|
149
|
-
if (request.method === "GET" && url.pathname === "/workspaces") {
|
|
150
|
-
const workspaces = new Map<string, { id: string; title: string; updatedAt: string; sessionCount: number }>();
|
|
151
|
-
for (const session of listSessions(paths)) {
|
|
152
|
-
const current = workspaces.get(session.workspaceId);
|
|
153
|
-
workspaces.set(session.workspaceId, { id: session.workspaceId, title: current?.title || session.title.replace(/^Untitled session$/, "Untitled workspace"), updatedAt: current?.updatedAt && current.updatedAt > session.updatedAt ? current.updatedAt : session.updatedAt, sessionCount: (current?.sessionCount || 0) + 1 });
|
|
154
|
-
}
|
|
155
|
-
return writeJson(response, 200, { workspaces: [...workspaces.values()].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)) }, origin);
|
|
156
|
-
}
|
|
157
|
-
if (request.method === "GET" && url.pathname === "/usage") {
|
|
158
|
-
const sessions = listSessions(paths);
|
|
159
|
-
const tabCount = sessions.reduce((total, session) => total + session.tabs.length, 0);
|
|
160
|
-
const messageCount = sessions.reduce((total, session) => total + session.messages.length, 0);
|
|
161
|
-
return writeJson(response, 200, { workspaces: new Set(sessions.map((session) => session.workspaceId)).size, sessions: sessions.length, tabs: tabCount, messages: messageCount, providers: listProviders().length, paired: pairingStatus(paths).activeSessions > 0 }, origin);
|
|
162
|
-
}
|
|
163
|
-
if (request.method === "GET" && url.pathname === "/preferences") {
|
|
164
|
-
return writeJson(response, 200, { preferences: Object.fromEntries(Object.entries(readPreferences(paths)).map(([key, preference]) => [key, preference.value])) }, origin);
|
|
165
|
-
}
|
|
166
|
-
if (request.method === "GET" && url.pathname === "/settings") {
|
|
167
|
-
const identity = getIdentity(paths);
|
|
168
|
-
const sessions = listSessions(paths);
|
|
169
|
-
return writeJson(response, 200, {
|
|
170
|
-
identity,
|
|
171
|
-
pairing: pairingStatus(paths),
|
|
172
|
-
preferences: Object.fromEntries(Object.entries(readPreferences(paths)).map(([key, preference]) => [key, preference.value])),
|
|
173
|
-
provider: { activeProvider: config.activeProvider || config.provider || undefined, activeModel: config.activeModel || config.model || undefined },
|
|
174
|
-
database: { ownerUserId: identity.userId, local: true, persistence: "cli-owned-sqlite", workspaces: new Set(sessions.map((session) => session.workspaceId)).size, sessions: sessions.length },
|
|
175
|
-
}, origin);
|
|
176
|
-
}
|
|
177
|
-
if (request.method === "PATCH" && url.pathname === "/preferences") {
|
|
178
|
-
const body = await readBody(request);
|
|
179
|
-
const key = typeof body.key === "string" ? body.key : "";
|
|
180
|
-
const value = typeof body.value === "string" ? body.value : "";
|
|
181
|
-
if (!browserPreferenceKeys.has(key) || !value || value.length > 32) return writeJson(response, 400, { error: "Preference is not supported." }, origin);
|
|
182
|
-
return writeJson(response, 200, { preference: savePreference(paths, key, value) }, origin);
|
|
183
|
-
}
|
|
184
|
-
if (request.method === "PATCH" && url.pathname === "/providers/active") {
|
|
185
|
-
const body = await readBody(request);
|
|
186
|
-
const provider = typeof body.provider === "string" ? body.provider : "";
|
|
187
|
-
const model = typeof body.model === "string" ? body.model : undefined;
|
|
188
|
-
if (!listProviders().some((item) => item.id === provider)) return writeJson(response, 400, { error: "provider is not registered." }, origin);
|
|
189
|
-
const next = { ...config, activeProvider: provider, ...(model ? { activeModel: model } : {}) };
|
|
190
|
-
saveConfig(next, paths);
|
|
191
|
-
Object.assign(config, next);
|
|
192
|
-
return writeJson(response, 200, { activeProvider: next.activeProvider, activeModel: next.activeModel }, origin);
|
|
193
|
-
}
|
|
194
|
-
if (request.method === "GET" && parts[0] === "workspaces" && parts[1] && parts.length === 2) {
|
|
195
|
-
const workspace = loadWorkspace(paths, parts[1]);
|
|
196
|
-
if (!workspace) return writeJson(response, 404, { error: "Workspace not found." }, origin);
|
|
197
|
-
return writeJson(response, 200, { workspace, sessions: listSessions(paths).filter((session) => session.workspaceId === workspace.id) }, origin);
|
|
198
|
-
}
|
|
199
|
-
if (request.method === "POST" && parts[0] === "sessions" && parts.length === 1) {
|
|
200
|
-
const body = await readBody(request);
|
|
201
|
-
const session = createSession(paths, { mode: typeof body.mode === "string" ? body.mode : "chat", model: typeof body.model === "string" ? body.model : config.activeModel, provider: typeof body.provider === "string" ? body.provider : config.activeProvider, workspaceId: typeof body.workspaceId === "string" ? body.workspaceId : undefined, tabId: typeof body.tabId === "string" ? body.tabId : undefined, sectionId: sectionFrom(body.sectionId), title: typeof body.title === "string" ? body.title : undefined });
|
|
202
|
-
return writeJson(response, 201, session, origin);
|
|
203
|
-
}
|
|
204
|
-
if (parts[0] === "sessions" && parts[1] && parts.length === 2 && request.method === "GET") {
|
|
205
|
-
const session = loadSession(paths, parts[1]);
|
|
206
|
-
return session ? writeJson(response, 200, session, origin) : writeJson(response, 404, { error: "Session not found." }, origin);
|
|
207
|
-
}
|
|
208
|
-
if (parts[0] === "sessions" && parts[1] && parts[2] === "tabs" && parts.length === 3 && request.method === "POST") {
|
|
209
|
-
const session = loadSession(paths, parts[1]);
|
|
210
|
-
if (!session) return writeJson(response, 404, { error: "Session not found." }, origin);
|
|
211
|
-
const body = await readBody(request);
|
|
212
|
-
return writeJson(response, 201, createTab(paths, session, { id: typeof body.id === "string" ? body.id : undefined, label: typeof body.label === "string" ? body.label : undefined, provider: typeof body.provider === "string" ? body.provider : undefined, sectionId: sectionFrom(body.sectionId) }), origin);
|
|
213
|
-
}
|
|
214
|
-
if (parts[0] === "sessions" && parts[1] && parts[2] === "tabs" && parts[3] && parts.length === 4 && request.method === "PATCH") {
|
|
215
|
-
const session = loadSession(paths, parts[1]);
|
|
216
|
-
if (!session) return writeJson(response, 404, { error: "Session not found." }, origin);
|
|
217
|
-
const body = await readBody(request);
|
|
218
|
-
return writeJson(response, 200, updateTab(paths, session, parts[3], { label: typeof body.label === "string" ? body.label : undefined, provider: typeof body.provider === "string" ? body.provider : undefined, sectionId: body.sectionId === undefined ? undefined : sectionFrom(body.sectionId) }), origin);
|
|
219
|
-
}
|
|
220
|
-
if (request.method === "POST" && url.pathname === "/chat") return streamChatRoute(request, response, config, paths, origin);
|
|
221
|
-
return writeJson(response, 404, { error: "Route not found." }, origin);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function randomPort(): number {
|
|
225
|
-
return randomInt(49_152, 65_535);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function bind(server: Server, port: number): Promise<number> {
|
|
229
|
-
return new Promise((resolve, reject) => {
|
|
230
|
-
const onError = (error: NodeJS.ErrnoException) => {
|
|
231
|
-
server.off("listening", onListening);
|
|
232
|
-
reject(error);
|
|
233
|
-
};
|
|
234
|
-
const onListening = () => {
|
|
235
|
-
server.off("error", onError);
|
|
236
|
-
const address = server.address();
|
|
237
|
-
resolve(typeof address === "object" && address ? address.port : port);
|
|
238
|
-
};
|
|
239
|
-
server.once("error", onError);
|
|
240
|
-
server.once("listening", onListening);
|
|
241
|
-
server.listen(port, "127.0.0.1");
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export async function startServer(options: ServerOptions): Promise<ServerHandle> {
|
|
246
|
-
const paths = ensurePaths(options.paths || resolvePaths());
|
|
247
|
-
const server = createServer((request, response) => {
|
|
248
|
-
route(request, response, options.config, paths).catch((error: unknown) => writeJson(response, 500, { error: error instanceof Error ? error.message : "Request failed." }, safeOrigin(request, options.config)));
|
|
249
|
-
});
|
|
250
|
-
server.setMaxListeners(0);
|
|
251
|
-
let port = options.port ?? 0;
|
|
252
|
-
let lastError: unknown;
|
|
253
|
-
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
254
|
-
try {
|
|
255
|
-
port = await bind(server, port);
|
|
256
|
-
const address = `http://127.0.0.1:${port}`;
|
|
257
|
-
return { address, port, stop: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())) };
|
|
258
|
-
} catch (error) {
|
|
259
|
-
lastError = error;
|
|
260
|
-
port = randomPort();
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
throw lastError instanceof Error ? lastError : new Error("Could not bind the local server.");
|
|
264
|
-
}
|