import path from 'node:path'; import { atomicWriteText } from '../../../coding-projects/atomic-json'; export const MAKELORE_PI_EXTENSION_VERSION = 5; export const MAKELORE_PI_EXTENSION_FILENAME = `makelore-runtime-v${MAKELORE_PI_EXTENSION_VERSION}.mjs`; const BUNDLE_SOURCE = String.raw` import { readFile } from 'node:fs/promises'; import path from 'node:path'; const MUTATION_TOOLS = new Set([ 'bash', 'edit', 'write', ]); const RUNTIME_FLAGS = { bridgeUrl: 'makelore-bridge-url', workerToken: 'makelore-worker-token', contextFile: 'makelore-context-file', workerRole: 'makelore-worker-role', projectPath: 'makelore-project-path', }; export function createMakeloreRuntime(runtimeDefaults = {}) { return async function makeloreRuntime(pi) { const fallbackFlagValues = new Map(); const registerRuntimeFlag = (name, defaultValue) => { fallbackFlagValues.set(name, defaultValue); if (typeof pi.registerFlag === 'function') { pi.registerFlag(name, { type: 'string', default: defaultValue }); } }; registerRuntimeFlag( RUNTIME_FLAGS.bridgeUrl, runtimeDefaults.bridgeUrl || process.env.MAKELORE_PI_BRIDGE_URL || '', ); registerRuntimeFlag( RUNTIME_FLAGS.workerToken, runtimeDefaults.workerToken || process.env.MAKELORE_PI_WORKER_TOKEN || '', ); registerRuntimeFlag( RUNTIME_FLAGS.contextFile, runtimeDefaults.contextFile || process.env.MAKELORE_PI_CONTEXT_FILE || '', ); registerRuntimeFlag( RUNTIME_FLAGS.workerRole, runtimeDefaults.workerRole || process.env.MAKELORE_PI_WORKER_ROLE || 'parent', ); registerRuntimeFlag( RUNTIME_FLAGS.projectPath, runtimeDefaults.projectPath || process.env.MAKELORE_PI_PROJECT_PATH || process.cwd(), ); const runtimeValue = (name) => { const value = typeof pi.getFlag === 'function' ? pi.getFlag(name) : fallbackFlagValues.get(name); return typeof value === 'string' ? value : ''; }; const workerRole = runtimeValue(RUNTIME_FLAGS.workerRole) || 'parent'; const leases = new Map(); const touchedPaths = new Map(); let dynamicLeaseTools = new Set(); async function readWorkerContext() { const contextFile = runtimeValue(RUNTIME_FLAGS.contextFile); if (!contextFile) throw new Error('Makelore runtime context file is unavailable'); const value = JSON.parse(await readFile(contextFile, 'utf8')); if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error('Makelore worker context is invalid'); } return value; } async function runtimeContext() { const value = await readWorkerContext(); if (!value.runId) throw new Error('Makelore run context is unavailable'); return value; } async function bridge(action, body, signal) { const context = await runtimeContext(); const bridgeUrl = runtimeValue(RUNTIME_FLAGS.bridgeUrl); const workerToken = runtimeValue(RUNTIME_FLAGS.workerToken); if (!bridgeUrl || !workerToken) throw new Error('Makelore runtime bridge is unavailable'); const response = await fetch(bridgeUrl, { method: 'POST', headers: { authorization: 'Bearer ' + workerToken, 'content-type': 'application/json', }, body: JSON.stringify({ ...context, action, ...body }), signal, }); const result = await response.json().catch(() => ({})); if (!response.ok) throw new Error(result.error || 'Makelore runtime bridge rejected the request'); return result; } async function bridgeStream(action, body, signal, onUpdate) { const context = await runtimeContext(); const bridgeUrl = runtimeValue(RUNTIME_FLAGS.bridgeUrl); const workerToken = runtimeValue(RUNTIME_FLAGS.workerToken); if (!bridgeUrl || !workerToken) throw new Error('Makelore runtime bridge is unavailable'); const response = await fetch(bridgeUrl, { method: 'POST', headers: { authorization: 'Bearer ' + workerToken, 'content-type': 'application/json', }, body: JSON.stringify({ ...context, action, ...body }), signal, }); if (!response.ok) { const result = await response.json().catch(() => ({})); throw new Error(result.error || 'Makelore runtime bridge rejected the request'); } if (!response.body) throw new Error('Makelore subagent stream returned no body'); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffered = ''; let details; while (true) { const { value, done } = await reader.read(); buffered += decoder.decode(value || new Uint8Array(), { stream: !done }); let newline = buffered.indexOf('\n'); while (newline >= 0) { const line = buffered.slice(0, newline); buffered = buffered.slice(newline + 1); if (line) { const item = JSON.parse(line); if (item.details?.schema === 'subagent.v1') { details = item.details; onUpdate?.({ content: [], details }); } } newline = buffered.indexOf('\n'); } if (done) break; } if (!details) throw new Error('Makelore subagent stream returned no details'); return details; } async function releaseLease(toolCallId) { const leaseId = leases.get(toolCallId); if (!leaseId) return; leases.delete(toolCallId); await bridge('lease.release', { leaseId, resourceId: toolCallId }).catch(() => undefined); } async function releaseAll() { await Promise.all([...leases.keys()].map(releaseLease)); touchedPaths.clear(); } function projectRelativePath(value) { if (typeof value !== 'string' || !value.trim()) return undefined; const projectPath = path.resolve(runtimeValue(RUNTIME_FLAGS.projectPath) || process.cwd()); const targetPath = path.resolve(projectPath, value); const relative = path.relative(projectPath, targetPath); if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return undefined; return relative.split(path.sep).join('/'); } async function invokeProduct(toolCallId, toolName, input, signal) { const response = await bridge('product.invoke', { resourceId: toolCallId, toolName, input, }, signal); return response.result; } function registerProductTool(name, label, description, parameters, projectWriteLease = false) { pi.registerTool({ name, label, description, parameters, ...(projectWriteLease ? { executionMode: 'sequential' } : {}), async execute(toolCallId, params, signal) { return await invokeProduct(toolCallId, name, params, signal); }, }); } function isRecord(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } function isToolDeclaration(value) { return isRecord(value) && typeof value.name === 'string' && typeof value.label === 'string' && typeof value.description === 'string' && isRecord(value.inputSchema); } async function registerDynamicProductTools() { if (workerRole !== 'parent') return; const context = await readWorkerContext(); const allowedToolNames = new Set( Array.isArray(context.allowedToolNames) ? context.allowedToolNames.filter((name) => typeof name === 'string') : [], ); const declarations = Array.isArray(context.tools) ? context.tools : []; dynamicLeaseTools = new Set( declarations .filter((declaration) => isToolDeclaration(declaration) && declaration.projectWriteLease === true && allowedToolNames.has(declaration.name)) .map((declaration) => declaration.name), ); for (const declaration of declarations) { if (!isToolDeclaration(declaration) || !allowedToolNames.has(declaration.name)) continue; registerProductTool( declaration.name, declaration.label, declaration.description, declaration.inputSchema, dynamicLeaseTools.has(declaration.name), ); } } if (workerRole === 'parent') pi.registerTool({ name: 'ask_user', label: 'Ask user', description: 'Ask the user for a selection, confirmation, short input, or editor text.', parameters: { type: 'object', additionalProperties: false, required: ['kind', 'title'], properties: { kind: { type: 'string', enum: ['select', 'confirm', 'input', 'editor'] }, title: { type: 'string' }, message: { type: 'string' }, options: { type: 'array', items: { type: 'string' } }, }, }, async execute(_toolCallId, params, signal, _onUpdate, ctx) { let value; if (params.kind === 'select') { value = await ctx.ui.select(params.title, Array.isArray(params.options) ? params.options : [], { signal }); } else if (params.kind === 'confirm') { value = await ctx.ui.confirm(params.title, params.message || '', { signal }); } else if (params.kind === 'editor') { value = await ctx.ui.editor(params.title, params.message || '', { signal }); } else { value = await ctx.ui.input(params.title, params.message || '', { signal }); } return { content: [{ type: 'text', text: value === undefined ? 'User cancelled' : String(value) }], details: { kind: params.kind, cancelled: value === undefined }, }; }, }); if (workerRole === 'parent') pi.registerTool({ name: 'subagent', label: 'Subagent', description: 'Dispatch one or more managed project Agents in single, parallel, or chain mode.', parameters: { type: 'object', additionalProperties: false, required: ['mode', 'tasks'], properties: { mode: { type: 'string', enum: ['single', 'parallel', 'chain'] }, tasks: { type: 'array', minItems: 1, maxItems: 8, items: { type: 'object', additionalProperties: false, required: ['agentId', 'task', 'toolProfile'], properties: { agentId: { type: 'string' }, task: { type: 'string' }, toolProfile: { type: 'string', enum: ['read-only', 'coding'] }, }, }, }, }, }, async execute(toolCallId, params, signal, onUpdate) { const details = await bridgeStream( 'subagent.dispatch', { resourceId: toolCallId, request: params }, signal, onUpdate, ); const complete = details.tasks.filter((task) => task.status === 'complete').length; const failed = details.tasks.length - complete; return { content: [{ type: 'text', text: failed === 0 ? complete + ' subagent task(s) completed' : complete + ' completed; ' + failed + ' did not complete', }], details, }; }, }); if (workerRole === 'parent') registerProductTool( 'agent_browser', 'Agent browser', 'Open, navigate, inspect, or close the Main-owned Makelore development browser.', { type: 'object', additionalProperties: true, required: ['action'], properties: { action: { type: 'string', enum: ['open', 'status', 'close', 'reset_profile', 'navigate', 'send_cdp', 'read_events', 'read_payload'] }, url: { type: 'string' }, injectProjectData: { type: 'boolean' }, navigation: { type: 'string', enum: ['url', 'back', 'forward', 'reload'] }, method: { type: 'string' }, params: { type: 'object' }, sessionRef: { type: 'string' }, timeoutMs: { type: 'number' }, after: { type: 'number' }, methods: { type: 'array', items: { type: 'string' } }, limit: { type: 'number' }, waitMs: { type: 'number' }, handle: { type: 'string' }, offset: { type: 'number' }, maxBytes: { type: 'number' }, }, }, ); if (workerRole === 'parent') registerProductTool( 'task_state', 'Task state', 'Publish versioned task steps and progress to the Makelore product timeline.', { type: 'object', additionalProperties: false, required: ['tasks'], properties: { tasks: { type: 'array', minItems: 1, maxItems: 100, items: { type: 'object', additionalProperties: false, required: ['id', 'title', 'status'], properties: { id: { type: 'string' }, title: { type: 'string' }, status: { type: 'string', enum: ['pending', 'running', 'complete', 'error'] }, }, }, }, }, }, ); if (workerRole === 'parent') registerProductTool( 'changed_file', 'Changed file', 'Report project-relative paths touched by managed tools and refresh Conversation changes.', { type: 'object', additionalProperties: false, properties: { path: { type: 'string' }, paths: { type: 'array', maxItems: 200, items: { type: 'string' } }, refresh: { type: 'boolean' }, }, }, ); if (workerRole === 'parent') registerProductTool( 'runtime_context', 'Runtime context', 'Read the safe selected-skill and command catalog for this managed worker.', { type: 'object', additionalProperties: false, properties: {} }, ); await registerDynamicProductTools(); pi.on('tool_call', async (event, ctx) => { if (!MUTATION_TOOLS.has(event.toolName) && !dynamicLeaseTools.has(event.toolName)) return; const input = event.input || event.arguments || event.args || {}; const touchedPath = (event.toolName === 'write' || event.toolName === 'edit') ? projectRelativePath(input.path) : undefined; if (touchedPath) touchedPaths.set(event.toolCallId, [touchedPath]); if (event.toolName === 'bash') { await bridge('changes.bash', { resourceId: event.toolCallId }, ctx.signal).catch(() => undefined); } ctx.ui.setStatus('makelore.write-lease', '等待项目写入'); try { const result = await bridge('lease.acquire', { resourceId: event.toolCallId }, ctx.signal); leases.set(event.toolCallId, result.leaseId); } finally { ctx.ui.setStatus('makelore.write-lease', undefined); } }); pi.on('tool_result', async (event) => { await releaseLease(event.toolCallId); const paths = touchedPaths.get(event.toolCallId); touchedPaths.delete(event.toolCallId); if (paths) { await bridge('changes.touched', { resourceId: event.toolCallId, paths, }).catch(() => undefined); } }); pi.on('agent_end', releaseAll); pi.on('session_shutdown', releaseAll); }; } export default createMakeloreRuntime(); `; export async function materializeMakelorePiExtension(extensionsDir: string): Promise { const extensionPath = path.join(extensionsDir, MAKELORE_PI_EXTENSION_FILENAME); await atomicWriteText(extensionPath, BUNDLE_SOURCE.trimStart()); return extensionPath; }