Initial Coworker app scaffold
Some checks failed
Codex Template Compliance / compliance (push) Successful in 7s
Release Dry Run / release-dry-run (push) Failing after 44s
Build / build (push) Failing after 45s

This commit is contained in:
2026-06-19 01:07:16 +02:00
commit 2ec18964b7
46 changed files with 2582 additions and 0 deletions

View File

@@ -0,0 +1,416 @@
import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
import { createHash, randomBytes } from "node:crypto";
import { createReadStream } from "node:fs";
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import { createServer, IncomingMessage, Server, ServerResponse } from "node:http";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
AppState,
BrainInput,
ModelProvider,
WebAccessSettings,
createBrainItem,
createPairedDevice,
createPairingChallenge,
createProvider,
isPairingChallengeValid,
nowIso,
probeOpenAiCompatibleProvider,
runAgent,
updateBrainItem
} from "@coworker/core";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rendererDir = path.resolve(__dirname, "../dist");
const stateFile = () => path.join(app.getPath("userData"), "state.json");
let mainWindow: BrowserWindow | undefined;
let webServer: Server | undefined;
let activePairing = createPairingChallenge(0);
const defaultState: AppState = {
providers: [
createProvider("ollama", "Ollama", "http://localhost:11434/v1"),
createProvider("lmstudio", "LM Studio", "http://localhost:1234/v1")
],
brain: [],
workspaces: [],
webAccess: {
enabled: false,
bindHost: "127.0.0.1",
port: 4789,
requirePairing: true
},
pairedDevices: []
};
async function readState(): Promise<AppState> {
try {
return { ...defaultState, ...JSON.parse(await readFile(stateFile(), "utf8")) };
} catch {
return defaultState;
}
}
async function writeState(state: AppState): Promise<AppState> {
await mkdir(path.dirname(stateFile()), { recursive: true });
await writeFile(stateFile(), JSON.stringify(state, null, 2), "utf8");
return state;
}
async function mutateState(mutator: (state: AppState) => AppState | Promise<AppState>): Promise<AppState> {
const current = await readState();
const next = await mutator(current);
return writeState(next);
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1320,
height: 860,
minWidth: 980,
minHeight: 680,
title: "Coworker",
backgroundColor: "#111317",
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false
}
});
const devUrl = process.env.VITE_DEV_SERVER_URL;
if (devUrl) {
void mainWindow.loadURL(devUrl);
} else {
void mainWindow.loadFile(path.join(rendererDir, "index.html"));
}
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
void shell.openExternal(url);
return { action: "deny" };
});
}
function registerIpc() {
ipcMain.handle("state:get", () => readState());
ipcMain.handle("providers:save", async (_event, provider: ModelProvider) => mutateState((state) => {
const next = { ...provider, baseUrl: provider.baseUrl.replace(/\/+$/, ""), updatedAt: nowIso() };
return {
...state,
providers: state.providers.some((item) => item.id === next.id)
? state.providers.map((item) => item.id === next.id ? next : item)
: [...state.providers, next]
};
}));
ipcMain.handle("providers:remove", async (_event, id: string) => mutateState((state) => ({
...state,
providers: state.providers.filter((provider) => provider.id !== id)
})));
ipcMain.handle("providers:probe", async (_event, id: string) => {
const state = await readState();
const provider = state.providers.find((item) => item.id === id);
if (!provider) throw new Error("Provider not found.");
return probeOpenAiCompatibleProvider(provider);
});
ipcMain.handle("brain:add", async (_event, input: BrainInput) => mutateState((state) => ({
...state,
brain: [createBrainItem(input), ...state.brain]
})));
ipcMain.handle("brain:update", async (_event, id: string, patch: Partial<BrainInput>) => mutateState((state) => ({
...state,
brain: state.brain.map((item) => item.id === id ? updateBrainItem(item, patch) : item)
})));
ipcMain.handle("brain:remove", async (_event, id: string) => mutateState((state) => ({
...state,
brain: state.brain.filter((item) => item.id !== id)
})));
ipcMain.handle("workspace:open", async () => {
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
if (result.canceled || !result.filePaths[0]) return undefined;
const rootPath = result.filePaths[0];
const workspace = {
id: createHash("sha256").update(rootPath).digest("hex").slice(0, 16),
name: path.basename(rootPath),
rootPath,
openedAt: nowIso()
};
await mutateState((state) => ({
...state,
workspaces: [workspace, ...state.workspaces.filter((item) => item.id !== workspace.id)]
}));
return workspace;
});
ipcMain.handle("workspace:list", async (_event, id: string) => {
const workspace = (await readState()).workspaces.find((item) => item.id === id);
if (!workspace) throw new Error("Workspace not found.");
const entries = await readdir(workspace.rootPath, { withFileTypes: true });
return Promise.all(entries.slice(0, 200).map(async (entry) => {
const absolute = path.join(workspace.rootPath, entry.name);
const info = await stat(absolute);
return {
path: entry.name,
type: entry.isDirectory() ? "directory" : "file",
size: entry.isFile() ? info.size : undefined,
modifiedAt: info.mtime.toISOString()
};
}));
});
ipcMain.handle("workspace:read", async (_event, id: string, relativePath: string) => {
const workspace = (await readState()).workspaces.find((item) => item.id === id);
if (!workspace) throw new Error("Workspace not found.");
const absolute = confinedPath(workspace.rootPath, relativePath);
const info = await stat(absolute);
if (!info.isFile() || info.size > 512 * 1024) throw new Error("File is not readable in preview.");
return readFile(absolute, "utf8");
});
ipcMain.handle("agent:run", async (_event, request) => {
const state = await readState();
const provider = request.providerId
? state.providers.find((item) => item.id === request.providerId)
: state.providers.find((item) => item.enabled);
const model = request.model ?? provider?.defaults.coding ?? provider?.defaults.chat;
return runAgent(
{
prompt: String(request.prompt ?? ""),
workspace: request.workspace,
provider,
model,
messages: request.messages ?? [],
brain: state.brain
},
(messages) => completeWithProvider(provider, model, messages)
);
});
ipcMain.handle("web:settings", async (_event, settings: WebAccessSettings) => {
const state = await mutateState((current) => ({ ...current, webAccess: settings }));
await restartWebServer(state.webAccess);
return state;
});
ipcMain.handle("web:pairing:create", () => {
activePairing = createPairingChallenge();
return activePairing;
});
}
async function completeWithProvider(provider: ModelProvider | undefined, model: string | undefined, messages: { role: string; content: string }[]) {
if (!provider || !model) {
return "No model is configured yet. Add Ollama, LM Studio, or another OpenAI-compatible provider in Settings.";
}
const response = await fetch(`${provider.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(provider.apiKey ? { Authorization: `Bearer ${provider.apiKey}` } : {})
},
body: JSON.stringify({
model,
messages: messages.map((message) => ({ role: message.role, content: message.content })),
temperature: 0.2
})
});
if (!response.ok) return `Model request failed with HTTP ${response.status}.`;
const body = await response.json() as { choices?: Array<{ message?: { content?: string } }> };
return body.choices?.[0]?.message?.content ?? "The model returned an empty response.";
}
function confinedPath(root: string, relativePath: string) {
const absolute = path.resolve(root, relativePath);
const normalizedRoot = path.resolve(root);
if (absolute !== normalizedRoot && !absolute.startsWith(`${normalizedRoot}${path.sep}`)) {
throw new Error("Path is outside the workspace.");
}
return absolute;
}
async function restartWebServer(settings: WebAccessSettings) {
if (webServer) {
await new Promise<void>((resolve) => webServer?.close(() => resolve()));
webServer = undefined;
}
if (!settings.enabled) return;
webServer = createServer(handleWebRequest);
await new Promise<void>((resolve, reject) => {
webServer?.listen(settings.port, settings.bindHost, resolve);
webServer?.on("error", reject);
});
}
async function handleWebRequest(req: IncomingMessage, res: ServerResponse) {
const url = new URL(req.url ?? "/", "http://coworker.local");
if (url.pathname === "/api/pair" && req.method === "POST") return handlePair(req, res);
if (url.pathname === "/api/state") return sendJson(res, await publicState(req));
if (url.pathname.startsWith("/api/") && !(await isAuthorized(req))) return sendJson(res, { error: "Pairing required." }, 401);
if (url.pathname === "/api/providers" && req.method === "POST") return handleWebProviderSave(req, res);
if (url.pathname.match(/^\/api\/providers\/[^/]+\/probe$/) && req.method === "POST") return handleWebProviderProbe(url, res);
if (url.pathname === "/api/brain" && req.method === "POST") return handleWebBrainAdd(req, res);
if (url.pathname.match(/^\/api\/brain\/[^/]+$/) && req.method === "DELETE") return handleWebBrainRemove(url, res);
if (url.pathname.match(/^\/api\/workspaces\/[^/]+\/files$/)) return handleWebWorkspaceList(url, res);
if (url.pathname.match(/^\/api\/workspaces\/[^/]+\/files\/.+$/)) return handleWebWorkspaceRead(url, res);
if (url.pathname === "/api/agent/run" && req.method === "POST") return handleWebAgent(req, res);
return serveStatic(url.pathname, res);
}
async function handlePair(req: IncomingMessage, res: ServerResponse) {
const body = await readJsonBody<{ code?: string; name?: string }>(req);
if (!isPairingChallengeValid(activePairing, body.code ?? "")) return sendJson(res, { error: "Invalid pairing code." }, 403);
const token = randomBytes(24).toString("hex");
const device = await createPairedDevice(body.name ?? "Mobile device", token, digest);
await mutateState((state) => ({ ...state, pairedDevices: [device, ...state.pairedDevices] }));
res.setHeader("Set-Cookie", `coworker_token=${token}; HttpOnly; SameSite=Lax; Path=/`);
sendJson(res, { token });
}
async function handleWebProviderSave(req: IncomingMessage, res: ServerResponse) {
const provider = await readJsonBody<ModelProvider>(req);
const state = await mutateState((current) => ({
...current,
providers: current.providers.some((item) => item.id === provider.id)
? current.providers.map((item) => item.id === provider.id ? { ...provider, updatedAt: nowIso() } : item)
: [...current.providers, { ...createProvider(provider.kind, provider.name, provider.baseUrl), ...provider, updatedAt: nowIso() }]
}));
sendJson(res, sanitizeState(state));
}
async function handleWebProviderProbe(url: URL, res: ServerResponse) {
const id = decodeURIComponent(url.pathname.split("/")[3]);
const provider = (await readState()).providers.find((item) => item.id === id);
if (!provider) return sendJson(res, { error: "Provider not found." }, 404);
sendJson(res, await probeOpenAiCompatibleProvider(provider));
}
async function handleWebBrainAdd(req: IncomingMessage, res: ServerResponse) {
const input = await readJsonBody<BrainInput>(req);
const state = await mutateState((current) => ({ ...current, brain: [createBrainItem(input), ...current.brain] }));
sendJson(res, sanitizeState(state));
}
async function handleWebBrainRemove(url: URL, res: ServerResponse) {
const id = decodeURIComponent(url.pathname.split("/")[3]);
const state = await mutateState((current) => ({ ...current, brain: current.brain.filter((item) => item.id !== id) }));
sendJson(res, sanitizeState(state));
}
async function handleWebWorkspaceList(url: URL, res: ServerResponse) {
const id = decodeURIComponent(url.pathname.split("/")[3]);
const workspace = (await readState()).workspaces.find((item) => item.id === id);
if (!workspace) return sendJson(res, { error: "Workspace not found." }, 404);
const entries = await readdir(workspace.rootPath, { withFileTypes: true });
const files = await Promise.all(entries.slice(0, 200).map(async (entry) => {
const absolute = path.join(workspace.rootPath, entry.name);
const info = await stat(absolute);
return {
path: entry.name,
type: entry.isDirectory() ? "directory" : "file",
size: entry.isFile() ? info.size : undefined,
modifiedAt: info.mtime.toISOString()
};
}));
sendJson(res, files);
}
async function handleWebWorkspaceRead(url: URL, res: ServerResponse) {
const parts = url.pathname.split("/");
const id = decodeURIComponent(parts[3]);
const relativePath = decodeURIComponent(parts.slice(5).join("/"));
const workspace = (await readState()).workspaces.find((item) => item.id === id);
if (!workspace) return sendJson(res, { error: "Workspace not found." }, 404);
const absolute = confinedPath(workspace.rootPath, relativePath);
const info = await stat(absolute);
if (!info.isFile() || info.size > 512 * 1024) return sendJson(res, { error: "File is not readable in preview." }, 400);
sendJson(res, { content: await readFile(absolute, "utf8") });
}
async function handleWebAgent(req: IncomingMessage, res: ServerResponse) {
const body = await readJsonBody<{ prompt?: string; messages?: unknown[]; providerId?: string; model?: string }>(req);
const state = await readState();
const provider = body.providerId ? state.providers.find((item) => item.id === body.providerId) : state.providers[0];
const result = await runAgent(
{ prompt: body.prompt ?? "", messages: [], brain: state.brain, provider, model: body.model },
(messages) => completeWithProvider(provider, body.model ?? provider?.defaults.chat, messages)
);
sendJson(res, result);
}
async function publicState(req: IncomingMessage) {
const state = await readState();
const paired = await isAuthorized(req);
return {
paired,
webAccess: state.webAccess,
providers: paired ? state.providers.map(({ apiKey, ...provider }) => provider) : [],
brain: paired ? state.brain : [],
workspaces: paired ? state.workspaces : []
};
}
async function isAuthorized(req: IncomingMessage) {
const token = (req.headers.cookie ?? "").split(";").map((item) => item.trim()).find((item) => item.startsWith("coworker_token="))?.split("=")[1];
if (!token) return false;
const hash = await digest(token);
const state = await readState();
return state.pairedDevices.some((device) => !device.revokedAt && device.tokenHash === hash);
}
async function digest(value: string) {
return createHash("sha256").update(value).digest("hex");
}
function sanitizeState(state: AppState): AppState {
return {
...state,
providers: state.providers.map(({ apiKey, ...provider }) => provider),
pairedDevices: state.pairedDevices.map((device) => ({ ...device, tokenHash: "" }))
};
}
async function readJsonBody<T>(req: IncomingMessage): Promise<T> {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(Buffer.from(chunk));
return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}") as T;
}
function sendJson(res: ServerResponse, data: unknown, status = 200) {
res.writeHead(status, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
}
function serveStatic(urlPath: string, res: ServerResponse) {
try {
const safePath = urlPath === "/" ? "index.html" : urlPath.replace(/^\/+/, "");
const absolute = confinedPath(rendererDir, safePath);
const finalPath = absolute.endsWith(path.sep) ? path.join(absolute, "index.html") : absolute;
createReadStream(finalPath)
.on("error", () => createReadStream(path.join(rendererDir, "index.html")).pipe(res))
.pipe(res);
} catch {
res.writeHead(403);
res.end("Forbidden");
}
}
app.whenReady().then(async () => {
registerIpc();
createWindow();
await restartWebServer((await readState()).webAccess);
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});