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

View File

@@ -0,0 +1,662 @@
import { EventEmitter } from 'node:events';
import { mkdirSync } from 'node:fs';
import { join } from 'node:path';
import {
execFile,
spawn as nodeSpawn,
type ChildProcess,
type SpawnOptionsWithoutStdio,
} from 'node:child_process';
import {
ensureBundledCourseAgents,
ensureBundledCourseSkills,
ensureBundledSuperpowersPlugin,
getManagedOpencodeConfigDir,
} from './superpowers';
import { logger } from '../utils/logger';
import {
prependPythonToPath,
type PythonRuntime,
} from '../utils/python-runtime';
import { promisify } from 'node:util';
export type OpencodeLifecycleState = 'stopped' | 'starting' | 'running' | 'error';
export interface OpencodeStatus {
state: OpencodeLifecycleState;
port: number;
url?: string;
pid?: number;
error?: string;
startedAt?: number;
}
type SpawnFn = (
command: string,
args: string[],
options: SpawnOptionsWithoutStdio,
) => ChildProcess;
type ConfigProvider = () => Promise<Record<string, unknown>> | Record<string, unknown>;
type RuntimeConfigProvider = () =>
| Promise<{ config: Record<string, unknown>; env?: Record<string, string> }>
| { config: Record<string, unknown>; env?: Record<string, string> };
type RuntimeConfig = { config: Record<string, unknown>; env: Record<string, string> };
type SpawnCommand = {
command: string;
args: string[];
env: Record<string, string>;
shell?: boolean;
};
type PortOwner = {
pid: number;
executablePath?: string;
commandLine?: string;
};
type FindPortOwner = (port: number) => Promise<PortOwner | null>;
type KillProcess = (pid: number) => boolean;
type RuntimeProviderDiagnostics = {
id: string;
name?: string;
npm?: string;
baseURL?: string;
modelIds: string[];
hasApiKey: boolean;
apiKeyEnv?: string;
headerNames: string[];
};
type RuntimeMcpServerDiagnostics = {
id: string;
type?: string;
enabled?: boolean;
command: string[];
envKeys: string[];
};
export function summarizeRuntimeConfigForDiagnostics(
config: Record<string, unknown>,
env: Record<string, string>,
): {
model: string | null;
smallModel: string | null;
providerIds: string[];
enabledProviderIds: string[];
envKeys: string[];
providers: RuntimeProviderDiagnostics[];
mcpServerIds: string[];
mcpServers: RuntimeMcpServerDiagnostics[];
} {
const providerRecord = config.provider && typeof config.provider === 'object' && !Array.isArray(config.provider)
? config.provider as Record<string, unknown>
: {};
const providerIds = Object.keys(providerRecord);
const enabledProviderIds = Array.isArray(config.enabled_providers)
? config.enabled_providers.filter((item): item is string => typeof item === 'string')
: providerIds;
const providers = providerIds.map((providerId) => {
const rawProvider = providerRecord[providerId];
const provider = rawProvider && typeof rawProvider === 'object' && !Array.isArray(rawProvider)
? rawProvider as Record<string, unknown>
: {};
const options = provider.options && typeof provider.options === 'object' && !Array.isArray(provider.options)
? provider.options as Record<string, unknown>
: {};
const models = provider.models && typeof provider.models === 'object' && !Array.isArray(provider.models)
? provider.models as Record<string, unknown>
: {};
const headers = options.headers && typeof options.headers === 'object' && !Array.isArray(options.headers)
? options.headers as Record<string, unknown>
: {};
const apiKey = typeof options.apiKey === 'string' ? options.apiKey : undefined;
const apiKeyEnv = apiKey?.match(/^\{env:([^}]+)\}$/)?.[1];
return {
id: providerId,
...(typeof provider.name === 'string' ? { name: provider.name } : {}),
...(typeof provider.npm === 'string' ? { npm: provider.npm } : {}),
...(typeof options.baseURL === 'string' ? { baseURL: options.baseURL } : {}),
modelIds: Object.keys(models),
hasApiKey: Boolean(apiKey),
...(apiKeyEnv ? { apiKeyEnv } : {}),
headerNames: Object.keys(headers),
};
});
const mcpRecord = config.mcp && typeof config.mcp === 'object' && !Array.isArray(config.mcp)
? config.mcp as Record<string, unknown>
: {};
const mcpServerIds = Object.keys(mcpRecord);
const mcpServers = mcpServerIds.map((serverId) => {
const rawServer = mcpRecord[serverId];
const server = rawServer && typeof rawServer === 'object' && !Array.isArray(rawServer)
? rawServer as Record<string, unknown>
: {};
const command = Array.isArray(server.command)
? server.command.filter((item): item is string => typeof item === 'string')
: [];
const serverEnv = server.env && typeof server.env === 'object' && !Array.isArray(server.env)
? server.env as Record<string, unknown>
: {};
return {
id: serverId,
...(typeof server.type === 'string' ? { type: server.type } : {}),
...(typeof server.enabled === 'boolean' ? { enabled: server.enabled } : {}),
command,
envKeys: Object.keys(serverEnv).sort(),
};
});
return {
model: typeof config.model === 'string' ? config.model : null,
smallModel: typeof config.small_model === 'string' ? config.small_model : null,
providerIds,
enabledProviderIds,
envKeys: Object.keys(env).sort(),
providers,
mcpServerIds,
mcpServers,
};
}
export interface OpencodeManagerOptions {
port: number;
binPath: string;
userDataDir?: string;
bundledSuperpowersDir?: string;
bundledCourseAgentsDir?: string;
bundledCourseSkillsDir?: string;
pythonRuntime?: PythonRuntime;
spawn?: SpawnFn;
configProvider?: ConfigProvider;
runtimeConfigProvider?: RuntimeConfigProvider;
startupTimeoutMs?: number;
findPortOwner?: FindPortOwner;
killProcess?: KillProcess;
}
const execFileAsync = promisify(execFile);
const ATTACHED_SERVER_STOP_TIMEOUT_MS = 2_000;
const ATTACHED_SERVER_STOP_POLL_MS = 100;
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function normalizeComparablePath(value: string | undefined): string | null {
const trimmed = value?.trim();
if (!trimmed) return null;
return trimmed.replace(/^"+|"+$/g, '').replace(/\\/g, '/').toLowerCase();
}
function isManagedOpencodePortOwner(owner: PortOwner, binPath: string): boolean {
const expectedPath = normalizeComparablePath(binPath);
if (!expectedPath) return false;
const executablePath = normalizeComparablePath(owner.executablePath);
if (executablePath && executablePath === expectedPath) return true;
const commandLine = normalizeComparablePath(owner.commandLine);
return Boolean(commandLine?.includes(expectedPath));
}
function normalizePortOwner(input: unknown): PortOwner | null {
if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
const record = input as Record<string, unknown>;
const rawPid = record.pid ?? record.ProcessId ?? record.processId;
const pid = typeof rawPid === 'number'
? rawPid
: typeof rawPid === 'string'
? Number(rawPid)
: NaN;
if (!Number.isInteger(pid) || pid <= 0) return null;
return {
pid,
...(typeof record.executablePath === 'string' ? { executablePath: record.executablePath } : {}),
...(typeof record.ExecutablePath === 'string' ? { executablePath: record.ExecutablePath } : {}),
...(typeof record.commandLine === 'string' ? { commandLine: record.commandLine } : {}),
...(typeof record.CommandLine === 'string' ? { commandLine: record.CommandLine } : {}),
};
}
async function findWindowsPortOwner(port: number): Promise<PortOwner | null> {
const script = [
`$connection = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1`,
'if (-not $connection) { exit 0 }',
'$process = Get-CimInstance Win32_Process -Filter "ProcessId=$($connection.OwningProcess)" -ErrorAction SilentlyContinue',
'if (-not $process) { [pscustomobject]@{ pid = [int]$connection.OwningProcess } | ConvertTo-Json -Compress; exit 0 }',
'[pscustomobject]@{ pid = [int]$process.ProcessId; executablePath = $process.ExecutablePath; commandLine = $process.CommandLine } | ConvertTo-Json -Compress',
].join('\n');
const { stdout } = await execFileAsync(
'powershell.exe',
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script],
{ encoding: 'utf8', windowsHide: true },
);
const trimmed = stdout.trim();
if (!trimmed) return null;
return normalizePortOwner(JSON.parse(trimmed) as unknown);
}
async function findUnixPortOwner(port: number): Promise<PortOwner | null> {
const { stdout } = await execFileAsync(
'lsof',
['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fp'],
{ encoding: 'utf8' },
);
const lines = stdout.split(/\r?\n/);
let pid: number | null = null;
for (const line of lines) {
if (line.startsWith('p')) {
const nextPid = Number(line.slice(1));
if (Number.isInteger(nextPid) && nextPid > 0) {
pid = nextPid;
break;
}
}
}
if (!pid) return null;
try {
const { stdout: commandOutput } = await execFileAsync(
'ps',
['-p', String(pid), '-o', 'command='],
{ encoding: 'utf8' },
);
const commandLine = commandOutput.trim();
return commandLine ? { pid, commandLine } : { pid };
} catch {
return { pid };
}
}
async function findListeningPortOwner(port: number): Promise<PortOwner | null> {
try {
return process.platform === 'win32'
? await findWindowsPortOwner(port)
: await findUnixPortOwner(port);
} catch {
return null;
}
}
function killProcessByPid(pid: number): boolean {
if (pid === process.pid) return false;
try {
return process.kill(pid);
} catch {
return false;
}
}
export class OpencodeManager extends EventEmitter {
private readonly spawnProcess: SpawnFn;
private readonly configProvider?: ConfigProvider;
private readonly runtimeConfigProvider?: RuntimeConfigProvider;
private readonly findPortOwner: FindPortOwner;
private readonly killProcess: KillProcess;
private readonly startupTimeoutMs: number;
private proc: ChildProcess | null = null;
private startPromise: Promise<OpencodeStatus> | null = null;
private status: OpencodeStatus;
constructor(private readonly options: OpencodeManagerOptions) {
super();
this.spawnProcess = options.spawn ?? nodeSpawn;
this.configProvider = options.configProvider;
this.runtimeConfigProvider = options.runtimeConfigProvider;
this.findPortOwner = options.findPortOwner ?? findListeningPortOwner;
this.killProcess = options.killProcess ?? killProcessByPid;
this.startupTimeoutMs = options.startupTimeoutMs ?? 10_000;
this.status = { state: 'stopped', port: options.port };
}
getStatus(): OpencodeStatus {
return { ...this.status };
}
getManagedConfigDir(): string | null {
const userDataDir = this.options.userDataDir?.trim();
return userDataDir ? getManagedOpencodeConfigDir(userDataDir) : null;
}
async start(): Promise<OpencodeStatus> {
if (this.status.state === 'running') return this.getStatus();
if (this.startPromise) return await this.startPromise;
this.startPromise = this.startRuntime();
try {
return await this.startPromise;
} finally {
this.startPromise = null;
}
}
async stop(): Promise<void> {
const proc = this.proc;
if (proc) {
if (!proc.kill()) {
throw new Error(`Failed to stop opencode process ${proc.pid ?? 'unknown'}`);
}
if (typeof proc.pid === 'number') {
const released = await this.waitForPortOwnerToRelease(proc.pid);
if (!released) {
throw new Error(`Timed out waiting for opencode port ${this.options.port} to be released`);
}
}
} else {
await this.stopAttachedServerIfManaged();
}
this.proc = null;
this.setStatus({ state: 'stopped', port: this.options.port });
}
async restart(): Promise<OpencodeStatus> {
await this.stop();
return await this.start();
}
async checkHealth(): Promise<{ ok: boolean; status: OpencodeStatus }> {
return {
ok: this.status.state === 'running',
status: this.getStatus(),
};
}
private async startRuntime(): Promise<OpencodeStatus> {
this.setStatus({ state: 'starting', port: this.options.port });
const providedRuntimeConfig = this.resolveRuntimeConfig();
const runtimeConfig = providedRuntimeConfig && typeof (providedRuntimeConfig as Promise<unknown>).then === 'function'
? await providedRuntimeConfig
: providedRuntimeConfig;
const runtimeArgs = ['serve', '--hostname=127.0.0.1', `--port=${this.options.port}`];
const runtimeEnv = this.resolveRuntimeEnv(runtimeConfig.env);
const spawnCommand = this.resolveSpawnCommand(runtimeArgs, runtimeEnv);
logger.info('[opencode-runtime] Starting runtime with provider config', {
port: this.options.port,
command: spawnCommand.command,
args: spawnCommand.args,
...summarizeRuntimeConfigForDiagnostics(runtimeConfig.config, runtimeConfig.env),
});
const childEnv = {
...process.env,
...spawnCommand.env,
OPENCODE_CONFIG_CONTENT: JSON.stringify(runtimeConfig.config),
};
// Meowa credentials belong to Electron Main and must never be inherited by
// the model runtime or exposed through the Agent's shell environment.
delete childEnv.MEOWART_API_KEY;
delete childEnv.MEOWART_DEV_KEY;
const proc = this.spawnProcess(spawnCommand.command, spawnCommand.args, {
env: childEnv,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
shell: spawnCommand.shell,
});
this.proc = proc;
return await new Promise<OpencodeStatus>((resolve, reject) => {
let settled = false;
let stderr = '';
const appendStderr = (message: string) => {
stderr = `${stderr}${message}`;
if (stderr.length > 8_000) {
stderr = stderr.slice(-8_000);
}
};
const formatStartupError = (message: string) => {
const detail = stderr.trim();
return detail ? `${message}\n${detail}` : message;
};
const finish = (callback: () => void) => {
if (settled) return;
settled = true;
clearTimeout(timer);
callback();
};
const timer = setTimeout(() => {
finish(() => {
const error = formatStartupError(`Timed out waiting for opencode server after ${this.startupTimeoutMs}ms`);
this.setStatus({ state: 'error', port: this.options.port, error });
proc.kill();
reject(new Error(error));
});
}, this.startupTimeoutMs);
proc.stdout?.on('data', (chunk: Buffer) => {
const text = chunk.toString();
const match = text.match(/opencode server listening\s+on\s+(https?:\/\/[^\s]+)/);
if (!match) return;
finish(() => {
this.setStatus({
state: 'running',
port: this.options.port,
url: match[1],
pid: proc.pid,
startedAt: Date.now(),
});
resolve(this.getStatus());
});
});
proc.stderr?.on('data', (chunk: Buffer) => {
const message = chunk.toString();
appendStderr(message);
this.emit('stderr', message);
});
proc.on('error', (error) => {
finish(() => {
this.proc = null;
this.setStatus({ state: 'error', port: this.options.port, error: error.message });
reject(error);
});
});
proc.on('exit', (code) => {
this.proc = null;
if (!settled) {
clearTimeout(timer);
void this.findExistingServer().then((existingServer) => {
if (existingServer) {
finish(() => {
logger.warn(
'[opencode-runtime] Attached to an existing server; newly generated provider config may not be active',
{
port: this.options.port,
url: existingServer.url,
},
);
this.setStatus(existingServer);
resolve(this.getStatus());
});
return;
}
finish(() => {
const error = formatStartupError(code == null ? 'opencode process exited' : `Exited with code ${code}`);
this.setStatus({ state: 'error', port: this.options.port, error });
reject(new Error(error));
});
});
return;
}
if (this.status.state !== 'stopped') {
this.setStatus({
state: 'stopped',
port: this.options.port,
error: code == null ? undefined : `Exited with code ${code}`,
});
}
});
});
}
private async findExistingServer(): Promise<OpencodeStatus | null> {
if (typeof globalThis.fetch !== 'function') return null;
const url = `http://127.0.0.1:${this.options.port}`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), Math.min(this.startupTimeoutMs, 1_000));
try {
const response = await globalThis.fetch(`${url}/global/health`, {
signal: controller.signal,
});
if (!response.ok) return null;
const body = await response.json().catch(() => null);
if (!body || typeof body !== 'object') return null;
if ((body as { healthy?: unknown }).healthy !== true) return null;
return {
state: 'running',
port: this.options.port,
url,
startedAt: Date.now(),
};
} catch {
return null;
} finally {
clearTimeout(timeout);
}
}
private async stopAttachedServerIfManaged(): Promise<void> {
const owner = await this.findPortOwner(this.options.port);
if (!owner || !isManagedOpencodePortOwner(owner, this.options.binPath)) {
return;
}
if (!this.killProcess(owner.pid)) {
throw new Error(`Failed to stop attached opencode process ${owner.pid}`);
}
logger.warn('[opencode-runtime] Stopped attached bundled opencode server on managed port', {
port: this.options.port,
pid: owner.pid,
});
const released = await this.waitForPortOwnerToRelease(owner.pid);
if (!released) {
throw new Error(`Timed out waiting for opencode port ${this.options.port} to be released`);
}
}
private async waitForPortOwnerToRelease(pid: number): Promise<boolean> {
const deadline = Date.now() + ATTACHED_SERVER_STOP_TIMEOUT_MS;
while (Date.now() < deadline) {
const owner = await this.findPortOwner(this.options.port);
if (!owner || owner.pid !== pid) return true;
await sleep(ATTACHED_SERVER_STOP_POLL_MS);
}
return false;
}
private resolveRuntimeEnv(runtimeEnv: Record<string, string>): Record<string, string> {
const userDataDir = this.options.userDataDir?.trim();
if (!userDataDir) {
return this.options.pythonRuntime
? prependPythonToPath(runtimeEnv, this.options.pythonRuntime)
: runtimeEnv;
}
const baseDir = join(userDataDir, 'opencode');
const configHome = join(baseDir, 'config');
const dataHome = join(baseDir, 'data');
const cacheHome = join(baseDir, 'cache');
const managedConfigDir = this.getManagedConfigDir() ?? getManagedOpencodeConfigDir(userDataDir);
mkdirSync(configHome, { recursive: true });
mkdirSync(dataHome, { recursive: true });
mkdirSync(cacheHome, { recursive: true });
mkdirSync(managedConfigDir, { recursive: true });
ensureBundledSuperpowersPlugin({
managedConfigDir,
sourceDir: this.options.bundledSuperpowersDir,
});
ensureBundledCourseSkills({
managedConfigDir,
sourceDir: this.options.bundledCourseSkillsDir,
});
ensureBundledCourseAgents({
managedConfigDir,
sourceDir: this.options.bundledCourseAgentsDir,
});
const environment = {
...runtimeEnv,
OPENCODE_CONFIG_DIR: managedConfigDir,
// Makelore exposes its managed and project-owned Skills explicitly. Avoid
// rescanning unrelated user-wide ~/.claude and ~/.agents Skill libraries
// during every project's cold bootstrap.
OPENCODE_DISABLE_EXTERNAL_SKILLS: '1',
NIANCODE_GAME_ASSETS_CLI: join(managedConfigDir, 'skills', 'game-assets', 'meowart_api.py'),
XDG_CONFIG_HOME: configHome,
XDG_DATA_HOME: dataHome,
XDG_CACHE_HOME: cacheHome,
};
return this.options.pythonRuntime
? prependPythonToPath(environment, this.options.pythonRuntime)
: environment;
}
private setStatus(next: OpencodeStatus): void {
this.status = next;
this.emit('status', this.getStatus());
}
private resolveSpawnCommand(runtimeArgs: string[], runtimeEnv: Record<string, string>): SpawnCommand {
const binPath = this.options.binPath;
if (process.platform !== 'win32') {
return {
command: binPath,
args: runtimeArgs,
env: runtimeEnv,
};
}
if (/\.(?:exe|cmd|bat)$/i.test(binPath)) {
return {
command: binPath,
args: runtimeArgs,
env: runtimeEnv,
shell: /\.(?:cmd|bat)$/i.test(binPath),
};
}
return {
command: process.execPath,
args: [binPath, ...runtimeArgs],
env: {
...runtimeEnv,
ELECTRON_RUN_AS_NODE: '1',
},
};
}
private resolveRuntimeConfig(): RuntimeConfig | Promise<RuntimeConfig> {
if (this.runtimeConfigProvider) {
const providedRuntimeConfig = this.runtimeConfigProvider();
if (providedRuntimeConfig && typeof (providedRuntimeConfig as Promise<unknown>).then === 'function') {
return providedRuntimeConfig.then((runtimeConfig) => ({
config: runtimeConfig.config,
env: runtimeConfig.env ?? {},
}));
}
return {
config: providedRuntimeConfig.config,
env: providedRuntimeConfig.env ?? {},
};
}
const providedConfig = this.configProvider ? this.configProvider() : {};
if (providedConfig && typeof (providedConfig as Promise<unknown>).then === 'function') {
return providedConfig.then((config) => ({ config, env: {} }));
}
return { config: providedConfig, env: {} };
}
}