Makelore 2.0 initial clean snapshot

This commit is contained in:
inman
2026-07-29 17:22:35 +08:00
commit b8ca3f8eea
694 changed files with 139782 additions and 0 deletions

341
electron/opencode/client.ts Normal file
View File

@@ -0,0 +1,341 @@
import { realpathSync } from 'node:fs';
import { logger } from '../utils/logger';
export interface DecorateRequestInput {
baseUrl: string;
path: string;
directory: string;
}
export interface DecoratedRequest {
url: string;
headers: Record<string, string>;
}
export interface OpencodeClientOptions {
baseUrl: string;
directory: string;
fetchImpl?: typeof fetch;
}
export interface OpencodeModelReference {
providerID: string;
modelID: string;
}
export interface OpencodeFilePartInput {
type: 'file';
mime: string;
filename?: string;
url: string;
}
export interface OpencodeCommandInfo {
name: string;
description?: string;
agent?: string;
model?: string;
source?: 'command' | 'mcp' | 'skill';
subtask?: boolean;
hints: string[];
template?: unknown;
}
export interface ForkOpencodeSessionInput {
messageID?: string;
}
export interface SummarizeOpencodeSessionInput {
providerID: string;
modelID: string;
}
export interface OpencodeCommandTextPartInput {
type: 'text';
text: string;
synthetic: true;
}
export type OpencodeCommandPartInput =
| OpencodeCommandTextPartInput
| OpencodeFilePartInput;
export interface ExecuteOpencodeSessionCommandInput {
command: string;
arguments: string;
agent?: string;
model?: string;
variant?: string;
parts?: OpencodeCommandPartInput[];
}
export interface SendOpencodeSessionMessageInput {
text: string;
system?: string;
agent?: string;
files?: OpencodeFilePartInput[];
model?: OpencodeModelReference;
}
export interface FindOpencodeFilesOptions {
limit?: number;
}
export interface UpdateOpencodeSessionInput {
title?: string;
}
export interface OpencodeSkillInfo {
name: string;
description?: string;
location: string;
content: string;
}
export interface RevertOpencodeSessionMessageInput {
messageID: string;
partID?: string;
}
export type OpencodePermissionReply = 'once' | 'always' | 'reject';
type OpencodeTextPartInput = { type: 'text'; text: string };
type OpencodeMessagePartInput = OpencodeTextPartInput | OpencodeFilePartInput;
type OpencodeTextPartsPayload = {
parts: OpencodeMessagePartInput[];
model?: OpencodeModelReference;
system?: string;
agent?: string;
};
function buildTextPartsPayload(input: SendOpencodeSessionMessageInput) {
const payload: OpencodeTextPartsPayload = {
parts: [
{
type: 'text',
text: input.text,
},
...(input.files ?? []),
],
};
if (input.model) {
payload.model = input.model;
}
if (input.system) {
payload.system = input.system;
}
if (input.agent) {
payload.agent = input.agent;
}
return payload;
}
function summarizePromptPayload(sessionID: string, payload: OpencodeTextPartsPayload) {
const fileParts = payload.parts.filter((part): part is OpencodeFilePartInput => part.type === 'file');
const textParts = payload.parts.filter((part): part is OpencodeTextPartInput => part.type === 'text');
return {
sessionID,
model: payload.model ? `${payload.model.providerID}/${payload.model.modelID}` : null,
textLength: textParts.reduce((sum, part) => sum + part.text.length, 0),
partCount: payload.parts.length,
filePartCount: fileParts.length,
fileMimes: fileParts.map((part) => part.mime),
};
}
type ResolveRealPath = (directory: string) => string;
export function canonicalizeOpencodeDirectory(
directory: string,
resolveRealPath: ResolveRealPath = realpathSync.native,
): string {
try {
return resolveRealPath(directory);
} catch {
return directory;
}
}
export function decorateOpencodeRequest(
input: DecorateRequestInput,
canonicalizeDirectory: (directory: string) => string = canonicalizeOpencodeDirectory,
): DecoratedRequest {
const url = new URL(input.path, input.baseUrl);
const directory = canonicalizeDirectory(input.directory);
const encodedDirectory = encodeURIComponent(directory);
if (!url.searchParams.has('directory')) {
url.searchParams.set('directory', directory);
}
return {
url: url.toString(),
headers: {
'Content-Type': 'application/json',
'x-opencode-directory': encodedDirectory,
},
};
}
async function parseJsonResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
throw new Error(`Runtime request failed: ${response.status} ${response.statusText}`);
}
if (response.status === 204) return undefined as T;
return await response.json() as T;
}
export function createOpencodeClient(options: OpencodeClientOptions) {
const fetchImpl = options.fetchImpl ?? fetch;
const request = async <T>(path: string, init?: RequestInit): Promise<T> => {
const decorated = decorateOpencodeRequest({
baseUrl: options.baseUrl,
path,
directory: options.directory,
});
const requestUrl = new URL(decorated.url);
logger.info('[opencode-client] Forwarding request to runtime', {
method: init?.method ?? 'GET',
pathname: requestUrl.pathname,
});
return await parseJsonResponse<T>(await fetchImpl(decorated.url, {
...init,
headers: {
...decorated.headers,
...(init?.headers ?? {}),
},
}));
};
const findFilesPath = (query: string, options?: FindOpencodeFilesOptions) => {
const params = new URLSearchParams({ query });
if (typeof options?.limit === 'number') {
params.set('limit', String(options.limit));
}
return `/find/file?${params.toString()}`;
};
const fileContentPath = (filePath: string) => {
const params = new URLSearchParams({ path: filePath });
return `/file/content?${params.toString()}`;
};
const searchTextPath = (pattern: string) => {
const params = new URLSearchParams({ pattern });
return `/find?${params.toString()}`;
};
const opencodeCommandMethods = {
listCommands: (): Promise<OpencodeCommandInfo[]> =>
request<OpencodeCommandInfo[]>('/command'),
forkSession: (
sessionID: string,
payload: ForkOpencodeSessionInput = {},
): Promise<unknown> =>
request<unknown>(`/session/${encodeURIComponent(sessionID)}/fork`, {
method: 'POST',
body: JSON.stringify(payload),
}),
shareSession: (sessionID: string): Promise<unknown> =>
request<unknown>(`/session/${encodeURIComponent(sessionID)}/share`, {
method: 'POST',
}),
unshareSession: (sessionID: string): Promise<unknown> =>
request<unknown>(`/session/${encodeURIComponent(sessionID)}/share`, {
method: 'DELETE',
}),
summarizeSession: (
sessionID: string,
payload: SummarizeOpencodeSessionInput,
): Promise<boolean> =>
request<boolean>(`/session/${encodeURIComponent(sessionID)}/summarize`, {
method: 'POST',
body: JSON.stringify(payload),
}),
executeSessionCommand: (
sessionID: string,
payload: ExecuteOpencodeSessionCommandInput,
): Promise<unknown> =>
request<unknown>(`/session/${encodeURIComponent(sessionID)}/command`, {
method: 'POST',
body: JSON.stringify(payload),
}),
};
return {
...opencodeCommandMethods,
listSkills: () => request<OpencodeSkillInfo[]>('/skill'),
listSessions: () => request<unknown[]>('/session'),
createSession: (payload: Record<string, unknown>) => request<unknown>('/session', {
method: 'POST',
body: JSON.stringify(payload),
}),
updateSession: (sessionID: string, payload: UpdateOpencodeSessionInput) =>
request<unknown>(`/session/${encodeURIComponent(sessionID)}`, {
method: 'PATCH',
body: JSON.stringify(payload),
}),
deleteSession: (sessionID: string) =>
request<void>(`/session/${encodeURIComponent(sessionID)}`, {
method: 'DELETE',
}),
abortSession: (sessionID: string) =>
request<void>(`/session/${encodeURIComponent(sessionID)}/abort`, {
method: 'POST',
}),
getSessionDiff: (sessionID: string) =>
request<unknown[]>(`/session/${encodeURIComponent(sessionID)}/diff`),
revertSessionMessage: (sessionID: string, payload: RevertOpencodeSessionMessageInput) =>
request<unknown>(`/session/${encodeURIComponent(sessionID)}/revert`, {
method: 'POST',
body: JSON.stringify(payload),
}),
unrevertSession: (sessionID: string) =>
request<unknown>(`/session/${encodeURIComponent(sessionID)}/unrevert`, {
method: 'POST',
}),
getSessionTodos: (sessionID: string) =>
request<unknown[]>(`/session/${encodeURIComponent(sessionID)}/todo`),
getSessionStatuses: () => request<Record<string, unknown>>('/session/status'),
listQuestions: () => request<unknown[]>('/question'),
replyQuestion: (requestID: string, answers: string[][]) =>
request<void>(`/question/${encodeURIComponent(requestID)}/reply`, {
method: 'POST',
body: JSON.stringify({ answers }),
}),
rejectQuestion: (requestID: string) =>
request<void>(`/question/${encodeURIComponent(requestID)}/reject`, {
method: 'POST',
}),
getConfig: () => request<Record<string, unknown>>('/config'),
listPermissions: () => request<unknown[]>('/permission'),
replyPermission: (requestID: string, reply: OpencodePermissionReply, message?: string) =>
request<void>(`/permission/${encodeURIComponent(requestID)}/reply`, {
method: 'POST',
body: JSON.stringify({
reply,
...(message ? { message } : {}),
}),
}),
getSessionMessages: (sessionID: string) =>
request<unknown[]>(`/session/${encodeURIComponent(sessionID)}/message`),
sendSessionMessage: (sessionID: string, payload: SendOpencodeSessionMessageInput) =>
request<unknown>(`/session/${encodeURIComponent(sessionID)}/message`, {
method: 'POST',
body: JSON.stringify(buildTextPartsPayload(payload)),
}),
promptSessionAsync: (sessionID: string, payload: SendOpencodeSessionMessageInput) => {
const requestPayload = buildTextPartsPayload(payload);
logger.info('[opencode-client] Sending prompt_async payload', summarizePromptPayload(sessionID, requestPayload));
return request<void>(`/session/${encodeURIComponent(sessionID)}/prompt_async`, {
method: 'POST',
body: JSON.stringify(requestPayload),
});
},
getFileStatuses: () => request<unknown[]>('/file/status'),
findFiles: (query: string, options?: FindOpencodeFilesOptions) =>
request<unknown[]>(findFilesPath(query, options)),
searchText: (pattern: string) => request<unknown[]>(searchTextPath(pattern)),
getFileContent: (filePath: string) => request<unknown>(fileContentPath(filePath)),
};
}