Initial Coworker app scaffold
This commit is contained in:
10
packages/core/package.json
Normal file
10
packages/core/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@coworker/core",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json"
|
||||
}
|
||||
}
|
||||
60
packages/core/src/agent.ts
Normal file
60
packages/core/src/agent.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { AgentRunRequest, AgentRunResult, ChatMessage } from "./types";
|
||||
import { recallBrain } from "./brain";
|
||||
import { createId, nowIso } from "./ids";
|
||||
|
||||
export async function runAgent(
|
||||
request: AgentRunRequest,
|
||||
completion: (messages: ChatMessage[]) => Promise<string>
|
||||
): Promise<AgentRunResult> {
|
||||
const recalled = recallBrain(request.brain, request.prompt, 8);
|
||||
const systemContent = [
|
||||
"You are Coworker, a local-first coding assistant.",
|
||||
"Show proposed file changes as unified diffs and wait for user approval before writes.",
|
||||
request.workspace ? `Current workspace: ${request.workspace.name}` : "No workspace is open.",
|
||||
recalled.length > 0
|
||||
? `Recalled Brain items:\n${recalled.map((r) => `- ${r.item.title}: ${r.item.body}`).join("\n")}`
|
||||
: "No Brain items were recalled."
|
||||
].join("\n\n");
|
||||
|
||||
const messages: ChatMessage[] = [
|
||||
{ id: createId("msg"), role: "system", content: systemContent, createdAt: nowIso() },
|
||||
...request.messages,
|
||||
{ id: createId("msg"), role: "user", content: request.prompt, createdAt: nowIso() }
|
||||
];
|
||||
|
||||
const content = await completion(messages);
|
||||
return {
|
||||
assistantMessage: {
|
||||
id: createId("msg"),
|
||||
role: "assistant",
|
||||
content,
|
||||
createdAt: nowIso()
|
||||
},
|
||||
recalled,
|
||||
proposedDiffs: extractDiffs(content),
|
||||
toolLog: [
|
||||
{
|
||||
id: createId("log"),
|
||||
level: "info",
|
||||
message: `Recalled ${recalled.length} Brain item(s).`,
|
||||
createdAt: nowIso()
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
export function extractDiffs(content: string) {
|
||||
const blocks = [...content.matchAll(/```(?:diff|patch)\n([\s\S]*?)```/g)];
|
||||
return blocks.map((block, index) => ({
|
||||
id: createId("diff"),
|
||||
filePath: inferDiffFile(block[1]) ?? `proposed-change-${index + 1}.patch`,
|
||||
title: `Proposed change ${index + 1}`,
|
||||
patch: block[1].trim(),
|
||||
applied: false
|
||||
}));
|
||||
}
|
||||
|
||||
function inferDiffFile(patch: string): string | undefined {
|
||||
const match = patch.match(/^\+\+\+\s+b\/(.+)$/m);
|
||||
return match?.[1]?.trim();
|
||||
}
|
||||
26
packages/core/src/brain.test.ts
Normal file
26
packages/core/src/brain.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createBrainItem, recallBrain } from "./brain";
|
||||
|
||||
describe("Brain recall", () => {
|
||||
it("returns matching pinned and semantic keyword items", () => {
|
||||
const items = [
|
||||
createBrainItem({
|
||||
kind: "memory",
|
||||
scope: "global",
|
||||
title: "Model preference",
|
||||
body: "User prefers local Ollama models for coding.",
|
||||
pinned: true
|
||||
}),
|
||||
createBrainItem({
|
||||
kind: "skill",
|
||||
scope: "project",
|
||||
title: "Diff workflow",
|
||||
body: "Always present patches before writing files."
|
||||
})
|
||||
];
|
||||
|
||||
const recalled = recallBrain(items, "Use ollama and patches");
|
||||
expect(recalled.map((r) => r.item.title)).toContain("Model preference");
|
||||
expect(recalled.map((r) => r.item.title)).toContain("Diff workflow");
|
||||
});
|
||||
});
|
||||
76
packages/core/src/brain.ts
Normal file
76
packages/core/src/brain.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { BrainItem, BrainItemKind, BrainRecall, BrainScope } from "./types";
|
||||
import { createId, nowIso } from "./ids";
|
||||
|
||||
export interface BrainInput {
|
||||
kind: BrainItemKind;
|
||||
scope: BrainScope;
|
||||
title: string;
|
||||
body: string;
|
||||
tags?: string[];
|
||||
projectId?: string;
|
||||
sessionId?: string;
|
||||
pinned?: boolean;
|
||||
}
|
||||
|
||||
export function createBrainItem(input: BrainInput): BrainItem {
|
||||
const createdAt = nowIso();
|
||||
return {
|
||||
id: createId(input.kind),
|
||||
kind: input.kind,
|
||||
scope: input.scope,
|
||||
projectId: input.projectId,
|
||||
sessionId: input.sessionId,
|
||||
title: input.title.trim(),
|
||||
body: input.body.trim(),
|
||||
tags: normalizeTags(input.tags ?? []),
|
||||
pinned: Boolean(input.pinned),
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
}
|
||||
|
||||
export function updateBrainItem(item: BrainItem, patch: Partial<BrainInput>): BrainItem {
|
||||
return {
|
||||
...item,
|
||||
title: patch.title === undefined ? item.title : patch.title.trim(),
|
||||
body: patch.body === undefined ? item.body : patch.body.trim(),
|
||||
tags: patch.tags === undefined ? item.tags : normalizeTags(patch.tags),
|
||||
pinned: patch.pinned === undefined ? item.pinned : Boolean(patch.pinned),
|
||||
updatedAt: nowIso()
|
||||
};
|
||||
}
|
||||
|
||||
export function recallBrain(items: BrainItem[], query: string, limit = 8): BrainRecall[] {
|
||||
const terms = tokenize(query);
|
||||
if (terms.length === 0) {
|
||||
return items.filter((item) => item.pinned).slice(0, limit).map((item) => ({
|
||||
item,
|
||||
score: 1,
|
||||
matchedTerms: ["pinned"]
|
||||
}));
|
||||
}
|
||||
|
||||
return items
|
||||
.map((item) => {
|
||||
const haystack = tokenize(`${item.title} ${item.body} ${item.tags.join(" ")}`);
|
||||
const matchedTerms = terms.filter((term) => haystack.includes(term));
|
||||
const pinBoost = item.pinned ? 0.5 : 0;
|
||||
const skillBoost = item.kind === "skill" ? 0.2 : 0;
|
||||
return {
|
||||
item,
|
||||
score: matchedTerms.length + pinBoost + skillBoost,
|
||||
matchedTerms
|
||||
};
|
||||
})
|
||||
.filter((recall) => recall.score > 0)
|
||||
.sort((a, b) => b.score - a.score || b.item.updatedAt.localeCompare(a.item.updatedAt))
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
export function normalizeTags(tags: string[]): string[] {
|
||||
return Array.from(new Set(tags.map((tag) => tag.trim().toLowerCase()).filter(Boolean))).sort();
|
||||
}
|
||||
|
||||
export function tokenize(text: string): string[] {
|
||||
return Array.from(new Set(text.toLowerCase().match(/[a-z0-9_äöüß-]{2,}/gi) ?? []));
|
||||
}
|
||||
9
packages/core/src/ids.ts
Normal file
9
packages/core/src/ids.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function createId(prefix: string): string {
|
||||
const random = Math.random().toString(36).slice(2, 10);
|
||||
const time = Date.now().toString(36);
|
||||
return `${prefix}_${time}_${random}`;
|
||||
}
|
||||
|
||||
export function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
6
packages/core/src/index.ts
Normal file
6
packages/core/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./agent";
|
||||
export * from "./brain";
|
||||
export * from "./ids";
|
||||
export * from "./pairing";
|
||||
export * from "./providers";
|
||||
export * from "./types";
|
||||
30
packages/core/src/pairing.ts
Normal file
30
packages/core/src/pairing.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { PairedDevice, PairingChallenge } from "./types";
|
||||
import { createId, nowIso } from "./ids";
|
||||
|
||||
export function createPairingChallenge(ttlMs = 5 * 60 * 1000): PairingChallenge {
|
||||
const code = String(Math.floor(100000 + Math.random() * 900000));
|
||||
return {
|
||||
id: createId("pair"),
|
||||
code,
|
||||
expiresAt: new Date(Date.now() + ttlMs).toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export async function createPairedDevice(
|
||||
name: string,
|
||||
token: string,
|
||||
digest: (value: string) => Promise<string>
|
||||
): Promise<PairedDevice> {
|
||||
return {
|
||||
id: createId("device"),
|
||||
name: name.trim() || "Paired device",
|
||||
tokenHash: await digest(token),
|
||||
createdAt: nowIso()
|
||||
};
|
||||
}
|
||||
|
||||
export function isPairingChallengeValid(challenge: PairingChallenge | undefined, code: string): boolean {
|
||||
if (!challenge) return false;
|
||||
if (challenge.code !== code.trim()) return false;
|
||||
return Date.parse(challenge.expiresAt) > Date.now();
|
||||
}
|
||||
65
packages/core/src/providers.ts
Normal file
65
packages/core/src/providers.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ModelProvider, ProviderProbeResult } from "./types";
|
||||
import { createId, nowIso } from "./ids";
|
||||
|
||||
export function createProvider(
|
||||
kind: ModelProvider["kind"],
|
||||
name: string,
|
||||
baseUrl: string
|
||||
): ModelProvider {
|
||||
const createdAt = nowIso();
|
||||
return {
|
||||
id: createId("provider"),
|
||||
name: name.trim(),
|
||||
kind,
|
||||
baseUrl: normalizeBaseUrl(baseUrl),
|
||||
enabled: true,
|
||||
supportsTools: kind !== "ollama",
|
||||
contextWindow: 8192,
|
||||
defaults: {},
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeBaseUrl(baseUrl: string): string {
|
||||
return baseUrl.trim().replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export async function probeOpenAiCompatibleProvider(
|
||||
provider: ModelProvider,
|
||||
fetchImpl: typeof fetch = fetch
|
||||
): Promise<ProviderProbeResult> {
|
||||
const url = `${normalizeBaseUrl(provider.baseUrl)}/models`;
|
||||
try {
|
||||
const response = await fetchImpl(url, {
|
||||
headers: provider.apiKey ? { Authorization: `Bearer ${provider.apiKey}` } : undefined
|
||||
});
|
||||
if (!response.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
providerId: provider.id,
|
||||
models: [],
|
||||
message: `Provider returned HTTP ${response.status}`
|
||||
};
|
||||
}
|
||||
const body = await response.json() as { data?: Array<{ id?: string }>; models?: string[] };
|
||||
const models = Array.isArray(body.data)
|
||||
? body.data.map((model) => model.id).filter(Boolean) as string[]
|
||||
: Array.isArray(body.models)
|
||||
? body.models
|
||||
: [];
|
||||
return {
|
||||
ok: models.length > 0,
|
||||
providerId: provider.id,
|
||||
models,
|
||||
message: models.length > 0 ? "Provider is reachable." : "Provider responded without model ids."
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
providerId: provider.id,
|
||||
models: [],
|
||||
message: error instanceof Error ? error.message : "Provider probe failed."
|
||||
};
|
||||
}
|
||||
}
|
||||
129
packages/core/src/types.ts
Normal file
129
packages/core/src/types.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
export type ModelProviderKind = "ollama" | "lmstudio" | "openai-compatible";
|
||||
|
||||
export type ModelPurpose = "chat" | "coding" | "reasoning" | "embedding";
|
||||
|
||||
export interface ModelProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: ModelProviderKind;
|
||||
baseUrl: string;
|
||||
apiKey?: string;
|
||||
enabled: boolean;
|
||||
supportsTools: boolean;
|
||||
contextWindow: number;
|
||||
defaults: Partial<Record<ModelPurpose, string>>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProviderProbeResult {
|
||||
ok: boolean;
|
||||
providerId: string;
|
||||
models: string[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type BrainScope = "global" | "project" | "session";
|
||||
export type BrainItemKind = "memory" | "skill";
|
||||
|
||||
export interface BrainItem {
|
||||
id: string;
|
||||
kind: BrainItemKind;
|
||||
scope: BrainScope;
|
||||
projectId?: string;
|
||||
sessionId?: string;
|
||||
title: string;
|
||||
body: string;
|
||||
tags: string[];
|
||||
pinned: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BrainRecall {
|
||||
item: BrainItem;
|
||||
score: number;
|
||||
matchedTerms: string[];
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: "system" | "user" | "assistant" | "tool";
|
||||
content: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceFile {
|
||||
path: string;
|
||||
type: "file" | "directory";
|
||||
size?: number;
|
||||
modifiedAt?: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceState {
|
||||
id: string;
|
||||
name: string;
|
||||
rootPath: string;
|
||||
openedAt: string;
|
||||
}
|
||||
|
||||
export interface AgentRunRequest {
|
||||
prompt: string;
|
||||
workspace?: WorkspaceState;
|
||||
provider?: ModelProvider;
|
||||
model?: string;
|
||||
messages: ChatMessage[];
|
||||
brain: BrainItem[];
|
||||
}
|
||||
|
||||
export interface AgentRunResult {
|
||||
assistantMessage: ChatMessage;
|
||||
recalled: BrainRecall[];
|
||||
proposedDiffs: ProposedDiff[];
|
||||
toolLog: ToolLogEntry[];
|
||||
}
|
||||
|
||||
export interface ProposedDiff {
|
||||
id: string;
|
||||
filePath: string;
|
||||
title: string;
|
||||
patch: string;
|
||||
applied: boolean;
|
||||
}
|
||||
|
||||
export interface ToolLogEntry {
|
||||
id: string;
|
||||
level: "info" | "warning" | "error";
|
||||
message: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface WebAccessSettings {
|
||||
enabled: boolean;
|
||||
bindHost: "127.0.0.1" | "0.0.0.0";
|
||||
port: number;
|
||||
requirePairing: boolean;
|
||||
}
|
||||
|
||||
export interface PairedDevice {
|
||||
id: string;
|
||||
name: string;
|
||||
tokenHash: string;
|
||||
createdAt: string;
|
||||
lastSeenAt?: string;
|
||||
revokedAt?: string;
|
||||
}
|
||||
|
||||
export interface PairingChallenge {
|
||||
id: string;
|
||||
code: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
providers: ModelProvider[];
|
||||
brain: BrainItem[];
|
||||
workspaces: WorkspaceState[];
|
||||
webAccess: WebAccessSettings;
|
||||
pairedDevices: PairedDevice[];
|
||||
}
|
||||
13
packages/core/tsconfig.json
Normal file
13
packages/core/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user