diff --git a/resources/pi-agent-server.mjs b/resources/pi-agent-server.mjs index ffbb142..361f2ff 100644 --- a/resources/pi-agent-server.mjs +++ b/resources/pi-agent-server.mjs @@ -177,11 +177,12 @@ async function createThreadRuntime(channel, rawOptions) { const appendSystemPrompts = optionValues(args, '--append-system-prompt'); const sessionId = requiredString(optionValue(args, '--session-id'), 'Thread session id'); const forkSessionId = optionValue(args, '--fork'); - const extensionPaths = optionValues(args, '--extension').map((value) => path.resolve(value)); + const [makeloreExtensionPath, ...additionalExtensionPaths] = optionValues(args, '--extension') + .map((value) => path.resolve(value)); const skillPaths = optionValues(args, '--skill').map((value) => path.resolve(value)); - if (extensionPaths.length !== 1) throw new Error('Thread requires exactly one Makelore extension'); + if (!makeloreExtensionPath) throw new Error('Thread requires a Makelore extension'); if (appendSystemPrompts.length !== 1) throw new Error('Thread requires exactly one Makelore language prompt'); - const extensionModule = await import(pathToFileURL(extensionPaths[0]).href); + const extensionModule = await import(pathToFileURL(makeloreExtensionPath).href); if (typeof extensionModule.createMakeloreRuntime !== 'function') { throw new Error('Makelore runtime extension factory is unavailable'); } @@ -215,6 +216,7 @@ async function createThreadRuntime(channel, rawOptions) { cwd: runtimeCwd, })), }], + additionalExtensionPaths, additionalSkillPaths: skillPaths, noExtensions: true, noSkills: true, diff --git a/tests/unit/pi-agent-server-process-real.test.ts b/tests/unit/pi-agent-server-process-real.test.ts index 17a95f6..9ec4001 100644 --- a/tests/unit/pi-agent-server-process-real.test.ts +++ b/tests/unit/pi-agent-server-process-real.test.ts @@ -196,6 +196,22 @@ describe('Pi Agent Server real process', () => { writeFile(languagePromptPath, MAKELORE_DEFAULT_LANGUAGE_PROMPT), ]); const extensionPath = await materializeMakelorePiExtension(extensionDir); + const firstDeviceExtensionPath = path.join(extensionDir, 'first-device-extension.mjs'); + const secondDeviceExtensionPath = path.join(extensionDir, 'second-device-extension.mjs'); + await Promise.all([ + writeFile(firstDeviceExtensionPath, [ + 'export default function (pi) {', + " pi.registerCommand('first-device-ping', { description: 'First device ping', handler: async () => {} });", + '}', + '', + ].join('\n')), + writeFile(secondDeviceExtensionPath, [ + 'export default function (pi) {', + " pi.registerCommand('second-device-ping', { description: 'Second device ping', handler: async () => {} });", + '}', + '', + ].join('\n')), + ]); const runtimeRoot = path.resolve('node_modules/@earendil-works/pi-coding-agent'); const server = new PiAgentServerProcess({ executablePath: process.execPath, @@ -221,6 +237,8 @@ describe('Pi Agent Server real process', () => { '--system-prompt', promptPath, '--append-system-prompt', languagePromptPath, '--extension', extensionPath, + '--extension', firstDeviceExtensionPath, + '--extension', secondDeviceExtensionPath, '--session-id', `session-${conversationId}`, ], env: { @@ -261,6 +279,15 @@ describe('Pi Agent Server real process', () => { .resolves.toMatchObject({ data: { models: expect.arrayContaining([expect.objectContaining({ id: 'test-model' })]) }, }); + await expect(left.request<{ commands: Array<{ name: string; source: string }> }>({ type: 'get_commands' })) + .resolves.toMatchObject({ + data: { + commands: expect.arrayContaining([ + expect.objectContaining({ name: 'first-device-ping', source: 'extension' }), + expect.objectContaining({ name: 'second-device-ping', source: 'extension' }), + ]), + }, + }); const settled = (worker: typeof left) => new Promise((resolve) => { const unsubscribe = worker.subscribe((event) => {