diff --git a/apps/buddy/package.json b/apps/buddy/package.json index 7fe9bb25..8242b87d 100644 --- a/apps/buddy/package.json +++ b/apps/buddy/package.json @@ -49,8 +49,8 @@ }, "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.77", - "@earendil-works/pi-ai": "^0.87.1", - "@earendil-works/pi-coding-agent": "^0.87.1", + "@earendil-works/pi-ai": "^0.99.1", + "@earendil-works/pi-coding-agent": "^0.99.1", "@js-temporal/polyfill": "^0.5.1", "@modelcontextprotocol/client": "2.0.0", "@mozilla/readability": "^0.6.0", diff --git a/apps/buddy/service/src/agent/context/__tests__/buildBuddyRequestContext.spec.ts b/apps/buddy/service/src/agent/context/__tests__/buildBuddyRequestContext.spec.ts index 6e9a9843..25ef3388 100644 --- a/apps/buddy/service/src/agent/context/__tests__/buildBuddyRequestContext.spec.ts +++ b/apps/buddy/service/src/agent/context/__tests__/buildBuddyRequestContext.spec.ts @@ -5,7 +5,7 @@ import { buildBuddyRequestContext } from '../buildBuddyRequestContext' describe('buildBuddyRequestContext', () => { it('includes only active first-party guidelines once and does not promote MCP metadata into instructions', () => { - const tools = ['lexora_visible', 'lexora_hidden', 'mcp__service__action'].map(name => ({ name, description: name, parameters: Type.Object({}), sourceInfo: { source: 'extension', path: '', origin: 'top-level' as const, scope: 'temporary' as const }, promptGuidelines: [`GUIDELINE_${name}`, `GUIDELINE_${name}`] })) + const tools = ['lexora_visible', 'lexora_hidden', 'mcp__service__action'].map(name => ({ name, exposure: 'direct' as const, description: name, parameters: Type.Object({}), sourceInfo: { source: 'extension', path: '', origin: 'top-level' as const, scope: 'temporary' as const }, promptGuidelines: [`GUIDELINE_${name}`, `GUIDELINE_${name}`] })) const context = buildBuddyRequestContext({ systemPrompt: 'Buddy base prompt', messages: [], tools: [tools[0]!, tools[2]!] }, tools) expect(getCurrentSystemPrompt(context.messages).match(/GUIDELINE_lexora_visible/g)).toHaveLength(1) expect(getCurrentSystemPrompt(context.messages)).not.toContain('GUIDELINE_lexora_hidden') @@ -14,7 +14,7 @@ describe('buildBuddyRequestContext', () => { }) it('preserves transcript history and follows the current tool declarations after removal', () => { - const tools = ['lexora_old', 'lexora_new'].map(name => ({ name, description: name, parameters: Type.Object({}), sourceInfo: { source: 'extension', path: '', origin: 'top-level' as const, scope: 'temporary' as const }, promptGuidelines: [`GUIDELINE_${name}`] })) + const tools = ['lexora_old', 'lexora_new'].map(name => ({ name, exposure: 'direct' as const, description: name, parameters: Type.Object({}), sourceInfo: { source: 'extension', path: '', origin: 'top-level' as const, scope: 'temporary' as const }, promptGuidelines: [`GUIDELINE_${name}`] })) const input = normalizeContext({ messages: [ { role: 'system', content: 'Buddy base prompt', toolsAdded: [tools[0]!], timestamp: 0 }, { role: 'user', content: 'Continue', timestamp: 1 }, diff --git a/apps/buddy/service/src/agent/events/__tests__/toolPresentation.spec.ts b/apps/buddy/service/src/agent/events/__tests__/toolPresentation.spec.ts index 6147fbdb..9d39681a 100644 --- a/apps/buddy/service/src/agent/events/__tests__/toolPresentation.spec.ts +++ b/apps/buddy/service/src/agent/events/__tests__/toolPresentation.spec.ts @@ -39,19 +39,18 @@ describe('createBuddyToolPresentation', () => { }, }, }) - const error = await tool.execute('tool-1', { command: 'fixture' }).then( - () => { throw new Error('Expected the command to fail') }, - error => error as Error, - ) + const result = await tool.execute('tool-1', { command: 'fixture' }) + expect(result.isError).toBe(true) + expect(result.structuredContent).toMatchObject({ exit_code: 2 }) expect(createBuddyToolPresentation({ arguments: { command: 'fixture' }, isError: true, - result: { content: [{ type: 'text', text: error.message }] }, + result, toolName: 'bash', })).toMatchObject({ card: 'terminal', exitCode: 2, signal: null }) }) - it('projects an expired internal action binding as recoverable without exposing internals', () => { + it('projects an expired action binding as recoverable', () => { const failure = { error: { code: 'SYSTEM_ACTION_EXPIRED', @@ -87,7 +86,5 @@ describe('createBuddyToolPresentation', () => { target: null, verified: null, }) - expect(JSON.stringify(presentation)).not.toContain('startTicks') - expect(JSON.stringify(presentation)).not.toContain('executable') }) }) diff --git a/apps/buddy/service/src/agent/events/toolPresentation.ts b/apps/buddy/service/src/agent/events/toolPresentation.ts index c70d6769..ec6d8fc5 100644 --- a/apps/buddy/service/src/agent/events/toolPresentation.ts +++ b/apps/buddy/service/src/agent/events/toolPresentation.ts @@ -145,6 +145,9 @@ function readDiff(value: unknown): string | null { function readExitCode(value: unknown, isError: boolean | undefined): number | null { if (value === undefined) return null + const structured = readRecord(readRecord(value)?.structuredContent)?.exit_code + if (typeof structured === 'number' && Number.isSafeInteger(structured)) + return structured if (isError === false) return 0 if (isError === undefined) diff --git a/apps/buddy/service/src/agent/extensions/__tests__/readFileContract.spec.ts b/apps/buddy/service/src/agent/extensions/__tests__/readFileContract.spec.ts index 77b28048..1c6c737e 100644 --- a/apps/buddy/service/src/agent/extensions/__tests__/readFileContract.spec.ts +++ b/apps/buddy/service/src/agent/extensions/__tests__/readFileContract.spec.ts @@ -1,5 +1,5 @@ import type { ToolResultMessage } from '@earendil-works/pi-ai' -import type { ExtensionContext, ReadToolInput, ReadToolOptions } from '@earendil-works/pi-coding-agent' +import type { ExtensionToolContext, ReadToolInput, ReadToolOptions } from '@earendil-works/pi-coding-agent' import { Buffer } from 'node:buffer' import { mkdtemp, readFile, rm, truncate, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' @@ -12,7 +12,7 @@ import { createBuddyReadTool } from '../readFileExtension' function createReadTool(cwd: string, options?: Pick) { const tool = createBuddyReadTool(cwd, options) return { - execute: (id: string, parameters: ReadToolInput, signal?: AbortSignal) => tool.execute(id, parameters, signal, undefined, { cwd } as ExtensionContext), + execute: (id: string, parameters: ReadToolInput, signal?: AbortSignal) => tool.execute(id, parameters, signal, undefined, { cwd } as ExtensionToolContext), } } diff --git a/apps/buddy/service/src/agent/extensions/discovery/__tests__/SessionToolCapabilities.spec.ts b/apps/buddy/service/src/agent/extensions/discovery/__tests__/SessionToolCapabilities.spec.ts index da39d775..03ba6eb2 100644 --- a/apps/buddy/service/src/agent/extensions/discovery/__tests__/SessionToolCapabilities.spec.ts +++ b/apps/buddy/service/src/agent/extensions/discovery/__tests__/SessionToolCapabilities.spec.ts @@ -5,7 +5,7 @@ import { Type } from 'typebox' import { describe, expect, it } from 'vitest' import { SessionToolCapabilities } from '../SessionToolCapabilities' -const tools: ToolInfo[] = ['read', 'create_image'].map(name => ({ name, description: name, parameters: Type.Object({}), sourceInfo: { source: 'extension', path: '', origin: 'top-level', scope: 'temporary' } })) +const tools: ToolInfo[] = ['read', 'create_image'].map(name => ({ name, exposure: 'direct' as const, description: name, parameters: Type.Object({}), sourceInfo: { source: 'extension', path: '', origin: 'top-level', scope: 'temporary' } })) const model = { provider: 'fixture', id: 'images' } as Model function setup() { const state = new SessionToolCapabilities([{ source: 'connector', id: 'fixture', revision: 'catalog-1' }]) diff --git a/apps/buddy/service/src/agent/extensions/discovery/__tests__/ToolDisclosure.spec.ts b/apps/buddy/service/src/agent/extensions/discovery/__tests__/ToolDisclosure.spec.ts index a9da321d..8fa672d0 100644 --- a/apps/buddy/service/src/agent/extensions/discovery/__tests__/ToolDisclosure.spec.ts +++ b/apps/buddy/service/src/agent/extensions/discovery/__tests__/ToolDisclosure.spec.ts @@ -7,7 +7,7 @@ import { ToolDisclosure } from '../ToolDisclosure' import { TOOL_SEARCH_NAME } from '../toolDiscoveryContract' const names = ['read', TOOL_SEARCH_NAME, 'lexora_browser_open', 'lexora_browser_act', 'lexora_buddy_automation', 'lexora_image_generate', 'mcp__calendar__events'] -const tools: ToolInfo[] = names.map(name => ({ name, description: name, parameters: Type.Object({ query: Type.String() }), sourceInfo: { source: 'extension', path: '', origin: 'top-level', scope: 'temporary' } })) +const tools: ToolInfo[] = names.map(name => ({ name, exposure: 'direct' as const, description: name, parameters: Type.Object({ query: Type.String() }), sourceInfo: { source: 'extension', path: '', origin: 'top-level', scope: 'temporary' } })) const model = { id: 'image-model' } as Model function create() { return new ToolDisclosure(tools, names, [ @@ -64,12 +64,16 @@ describe('toolDisclosure', () => { expect(disclosure.active({ model })).not.toContain(name) }) - it('does not expose undeclared extensions just because Pi initially activates them', () => { - const disclosure = new ToolDisclosure(tools, names, []) - expect(disclosure.active({ model })).toEqual([TOOL_SEARCH_NAME, 'read']) - expect(disclosure.search({ toolNames: ['lexora_browser_act'] }, { model }).tools).toEqual([]) - const deferred = new ToolDisclosure(tools, names, [], () => 'on_demand') - expect(deferred.active({ model })).toEqual([TOOL_SEARCH_NAME]) + it('keeps undeclared extensions unavailable regardless of Pi initial activation', () => { + for (const initial of [names, ['read', TOOL_SEARCH_NAME]]) { + const disclosure = new ToolDisclosure(tools, initial, []) + expect(disclosure.active({ model })).toEqual([TOOL_SEARCH_NAME, 'read']) + expect(disclosure.search({ toolNames: ['lexora_browser_act'] }, { model })).toMatchObject({ tools: [], notFound: ['lexora_browser_act'] }) + expect(disclosure.search({ query: 'lexora_browser_act' }, { model }).tools.map(tool => tool.name)).not.toContain('lexora_browser_act') + expect(disclosure.active({ model })).toEqual([TOOL_SEARCH_NAME, 'read']) + const deferred = new ToolDisclosure(tools, initial, [], () => 'on_demand') + expect(deferred.active({ model })).toEqual([TOOL_SEARCH_NAME]) + } }) it('keeps catalog ordering independent of registration and search order and rejects ambiguous identities', () => { @@ -83,13 +87,6 @@ describe('toolDisclosure', () => { expect(() => new ToolDisclosure(tools, names, [policy, policy])).toThrow('TOOL_CATALOG_DUPLICATE') }) - it('does not activate registered tools outside the platform baseline or a disclosure policy', () => { - const disclosure = new ToolDisclosure(tools, ['read', TOOL_SEARCH_NAME], []) - expect(disclosure.search({ toolNames: ['lexora_browser_act'] }, { model }).notFound).toEqual(['lexora_browser_act'].sort()) - expect(disclosure.search({ query: 'lexora_browser_act' }, { model }).tools.map(tool => tool.name)).not.toContain('lexora_browser_act') - expect(disclosure.active({ model })).toEqual(['read', TOOL_SEARCH_NAME].sort()) - }) - it('keeps large and external schemas out of new sessions and discovers Chinese capabilities', () => { const disclosure = create() expect(disclosure.active({ model })).toEqual(['read', TOOL_SEARCH_NAME].sort()) diff --git a/apps/buddy/service/src/agent/sessions/__tests__/createBuddySession.spec.ts b/apps/buddy/service/src/agent/sessions/__tests__/createBuddySession.spec.ts index 646509cb..61cc7e05 100644 --- a/apps/buddy/service/src/agent/sessions/__tests__/createBuddySession.spec.ts +++ b/apps/buddy/service/src/agent/sessions/__tests__/createBuddySession.spec.ts @@ -1,3 +1,4 @@ +import type { ExtensionToolContext } from '@earendil-works/pi-coding-agent' import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -63,7 +64,7 @@ describe('createBuddySession', () => { { toolNames: ['lexora_image_generate'] }, undefined, undefined, - result.session.extensionRunner.createContext(), + result.session.extensionRunner.createContext() as ExtensionToolContext, ) expect(result.session.getActiveToolNames()).toContain('lexora_image_generate') await result.session.setModel(model) diff --git a/apps/buddy/service/src/plugins/__tests__/ExtensionAgentRuntime.spec.ts b/apps/buddy/service/src/plugins/__tests__/ExtensionAgentRuntime.spec.ts index 647017c2..4ed438d8 100644 --- a/apps/buddy/service/src/plugins/__tests__/ExtensionAgentRuntime.spec.ts +++ b/apps/buddy/service/src/plugins/__tests__/ExtensionAgentRuntime.spec.ts @@ -82,7 +82,7 @@ describe('extension agent capability and invocation lifetimes', () => { const definitions: ToolDefinition[] = [] for (const capability of capabilities) await capability.extension.factory({ registerTool: (tool: ToolDefinition) => definitions.push(tool), on() {} } as never) - const tools = definitions.map(tool => ({ ...tool, sourceInfo: { source: 'extension' as const, path: '', origin: 'top-level' as const, scope: 'temporary' as const } })) + const tools = definitions.map(tool => ({ ...tool, exposure: tool.exposure ?? 'direct', sourceInfo: { source: 'extension' as const, path: '', origin: 'top-level' as const, scope: 'temporary' as const } })) const names = definitions.map(tool => tool.name) const policies = capabilities.flatMap(capability => capability.disclosure!) const disclosure = new ToolDisclosure(tools, names, policies) diff --git a/apps/buddy/service/src/providers/ProviderService.ts b/apps/buddy/service/src/providers/ProviderService.ts index dd1a51bb..9112187a 100644 --- a/apps/buddy/service/src/providers/ProviderService.ts +++ b/apps/buddy/service/src/providers/ProviderService.ts @@ -1,5 +1,6 @@ import type { AuthType, + LoginOptions, Provider, } from '@earendil-works/pi-ai' import type { ModelRuntime } from '@earendil-works/pi-coding-agent' @@ -63,6 +64,7 @@ export interface ProviderModelRuntime extends ProviderModelCatalogRuntime { type ModelSnapshot = Pick & Partial> export interface ProviderServiceOptions { + loginOptions?: LoginOptions requestHeaders?: ProviderRequestHeaders createBuiltinSource?: (providerId: string) => Provider | undefined authInteractions: AuthInteractionService @@ -78,6 +80,7 @@ export interface ProviderServiceOptions { } export class ProviderService { + readonly #loginOptions: LoginOptions | undefined readonly #requestHeaders: ProviderRequestHeaders readonly #builtins: BuiltinProviderConfigRepository readonly #builtinTemplates: ReadonlyMap @@ -111,6 +114,7 @@ export class ProviderService { get snapshot() { return this.#state.snapshot } constructor(options: ProviderServiceOptions) { + this.#loginOptions = options.loginOptions this.#requestHeaders = options.requestHeaders ?? new ProviderRequestHeaders(options.providers.states) this.#builtins = options.providers.builtins this.#builtinTemplates = new Map(options.modelRuntime.getProviders().map(provider => [provider.id, provider])) @@ -194,7 +198,7 @@ export class ProviderService { } listBuiltinPresets() { - return [...this.#builtinTemplates.values()].map(provider => ({ + return [...this.#builtinTemplates.values()].filter(provider => provider.getModels().length > 0 || !provider.getAllModels?.().length).map(provider => ({ id: provider.id, displayName: provider.name, baseUrl: provider.baseUrl ?? null, @@ -521,7 +525,7 @@ export class ProviderService { const handle = this.#authInteractions.beginLogin(providerId) let outcome: 'completed' | 'failed' | 'cancelled' = 'failed' try { - await this.#modelRuntime.login(providerId, type, handle.interaction) + await this.#modelRuntime.login(providerId, type, handle.interaction, this.#loginOptions) const instance = this.#builtins.findById(providerId) if (instance) { if (provider.refreshModels) @@ -664,7 +668,7 @@ export class ProviderService { id: instance.id, name: instance.displayName ?? template.name, source, - getCatalogModels: () => template.getModels(), + getCatalogModels: () => template.getAllModels?.() ?? template.getModels(), }) } diff --git a/apps/buddy/service/src/providers/__tests__/BuiltinModelDiscovery.spec.ts b/apps/buddy/service/src/providers/__tests__/BuiltinModelDiscovery.spec.ts index 3ed8bd7c..b37d98e6 100644 --- a/apps/buddy/service/src/providers/__tests__/BuiltinModelDiscovery.spec.ts +++ b/apps/buddy/service/src/providers/__tests__/BuiltinModelDiscovery.spec.ts @@ -83,7 +83,9 @@ describe('built-in model discovery through the production runtime', () => { let contextWindow = remote.contextWindow let status = 200 vi.stubGlobal('fetch', async (input: URL | string, init: RequestInit) => { - expect(String(input)).toBe(`https://pi.dev/api/models/providers/${providerId}`) + const url = new URL(input) + expect(`${url.origin}${url.pathname}`).toBe(`https://pi.dev/api/models/providers/${providerId}`) + expect(url.searchParams.get('types')).toBe('chat,image,classifier') expect(new Headers(init.headers).has('authorization')).toBe(false) if (status !== 200) return new Response(null, { status }) diff --git a/apps/buddy/service/src/providers/__tests__/BuiltinProviderInstances.spec.ts b/apps/buddy/service/src/providers/__tests__/BuiltinProviderInstances.spec.ts index 92ef2983..f4714ad4 100644 --- a/apps/buddy/service/src/providers/__tests__/BuiltinProviderInstances.spec.ts +++ b/apps/buddy/service/src/providers/__tests__/BuiltinProviderInstances.spec.ts @@ -1,14 +1,19 @@ -import type { Api, AssistantMessage, Context, Model, Provider } from '@earendil-works/pi-ai' +import type { AnyModel, Api, AssistantMessage, Context, Model, Provider } from '@earendil-works/pi-ai' import type { DatabaseSync } from 'node:sqlite' -import { createModels, InMemoryCredentialStore, InMemoryModelsStore, lazyStream } from '@earendil-works/pi-ai' +import { createModels, InMemoryCredentialStore, InMemoryModelsStore, isModelType, lazyStream } from '@earendil-works/pi-ai' import { ModelRuntime } from '@earendil-works/pi-coding-agent' import { afterEach, describe, expect, it } from 'vitest' import { openBuddyDatabase } from '../../storage/database' import { createProviderRepository } from '../../storage/providerRepository' +import { createProviderStateRepository } from '../../storage/providerStateRepository' +import { createWorkspaceRepository } from '../../storage/workspaceRepository' import { AuthInteractionService } from '../AuthInteractionService' import { createBuiltinProviderInstance } from '../createBuiltinProviderInstance' +import { createProviderLoginOptions } from '../createProviderLoginOptions' +import { createProviderModelRuntime } from '../createProviderModelRuntime' import { createProviderCredentialStatus } from '../ProviderCredentialStatus' import { ProviderModelSnapshotService } from '../ProviderModelSnapshotService' +import { ProviderRequestHeaders } from '../ProviderRequestHeaders' import { providerAuthChallengeSchema } from '../providerSchemas' import { ProviderService } from '../ProviderService' import { resolveInteractiveModelSelection } from '../resolveInteractiveModelSelection' @@ -17,6 +22,91 @@ const databases: DatabaseSync[] = [] afterEach(() => databases.splice(0).forEach(database => database.close())) describe('built-in provider instances', () => { + it('preserves mixed catalogs and authenticates each operation with its own instance and headers', async () => { + const database = openBuddyDatabase({ databasePath: ':memory:' }) + databases.push(database) + const credentials = new InMemoryCredentialStore() + const runtime = await ModelRuntime.create({ credentials, modelsPath: null, modelsStore: new InMemoryModelsStore(), refreshOnCreate: false }) + const states = createProviderStateRepository(database) + const headers = new ProviderRequestHeaders(states) + const registration = createProviderModelRuntime(runtime, headers) + const { provider } = createSource() + const chat = provider.getModels()[0]! + const base = { id: chat.id, name: chat.name, provider: provider.id, baseUrl: chat.baseUrl, input: chat.input, cost: chat.cost } + const catalog: AnyModel[] = [chat, { ...base, type: 'image', api: 'fixture-images', output: ['image'] }, { ...base, type: 'classifier', api: 'fixture-classifier', contextWindow: 8000 }] + provider.getAllModels = () => catalog + provider.filterAllModels = models => models.filter(model => model.provider === provider.id) + const requests: unknown[] = [] + provider.generateImages = async (model, _context, options) => { + requests.push({ model, key: options?.apiKey, headers: options?.headers }) + return { api: model.api, provider: model.provider, model: model.id, output: [], stopReason: 'stop', timestamp: 1 } + } + provider.classify = async (model, _context, options) => { + requests.push({ model, key: options?.apiKey, headers: options?.headers }) + return { api: model.api, provider: model.provider, model: model.id, answers: {}, stopReason: 'stop', timestamp: 1 } + } + for (const id of ['account-a', 'account-b']) { + states.upsert({ providerId: id, enabled: true, createdAt: 'now', updatedAt: 'now' }) + registration.registerNativeProvider(createBuiltinProviderInstance({ id, name: id, source: provider, getCatalogModels: () => catalog })) + await credentials.modify(id, async () => ({ type: 'api_key', key: `fixture-${id}` })) + headers.save(id, [{ name: 'X-Account', value: id }, { name: 'X-Token', value: `\${apiKey}` }]) + expect(runtime.getModels(id)).toMatchObject([{ id: chat.id, provider: id }]) + expect(await runtime.getAllAvailable(id)).toHaveLength(3) + const image = runtime.getModelOfType('image', id, chat.id)! + const classifier = runtime.getModelOfType('classifier', id, chat.id)! + expect(await runtime.generateImages(image, { input: [{ type: 'text', text: 'fixture' }] })).toMatchObject({ provider: id, stopReason: 'stop' }) + expect(await runtime.classify(classifier, { state: {}, questions: {} })).toMatchObject({ provider: id, stopReason: 'stop' }) + expect(requests.slice(-2)).toEqual(Array.from({ length: 2 }, () => expect.objectContaining({ model: expect.objectContaining({ provider: provider.id }), key: `fixture-${id}`, headers: expect.objectContaining({ 'x-account': id, 'x-token': `fixture-${id}` }) }))) + } + expect(catalog.every(model => model.provider === provider.id)).toBe(true) + }) + + it('reuses a lazily persisted installation ID across provider logins and runtime recreation', async () => { + const database = openBuddyDatabase({ databasePath: ':memory:' }) + databases.push(database) + const settings = createWorkspaceRepository(database) + const ids: string[] = [] + const credentials = new InMemoryCredentialStore() + const setup = async () => { + const runtime = await ModelRuntime.create({ credentials, modelsPath: null, modelsStore: new InMemoryModelsStore(), refreshOnCreate: false }) + const source = () => { + const { provider } = createSource() + provider.auth.oauth!.login = async (_interaction, options) => { + const id = options?.getDeviceId?.() + expect(id).toMatch(/^[0-9a-f-]{36}$/) + ids.push(id!) + return { type: 'oauth', access: 'fixture-access', refresh: 'fixture-refresh', expires: Date.now() + 3_600_000 } + } + return provider + } + runtime.registerNativeProvider(source()) + const service = new ProviderService({ + authInteractions: new AuthInteractionService(), + createBuiltinSource: source, + credentialStatus: createProviderCredentialStatus(credentials), + loginOptions: createProviderLoginOptions(createWorkspaceRepository(database)), + modelDiscovery: { supports: () => false, discover: async () => [] }, + modelRuntime: runtime, + providers: createProviderRepository(database), + }) + await service.initializeProviders() + return service + } + const first = await setup() + const account = await first.addProvider('fixture-builtin') + expect(settings.listKeys()).toEqual([]) + await first.login(account.id, 'oauth') + await first.dispose() + const restored = await setup() + await restored.login(account.id, 'oauth') + const other = await restored.addProvider('fixture-builtin') + await restored.login(other.id, 'oauth') + await restored.dispose() + expect(ids).toHaveLength(3) + expect(new Set(ids).size).toBe(1) + expect(settings.get('buddy.providers.device-id')).toBe(ids[0]) + }) + it('keeps account model availability consistent across login, sync and runtime recreation', async () => { const database = openBuddyDatabase({ databasePath: ':memory:' }) databases.push(database) @@ -78,7 +168,7 @@ describe('built-in provider instances', () => { await expectAccountModels(unauthenticated) }) - it('keeps credential-specific dynamic catalogs separate when refreshing and restoring caches', async () => { + it('keeps mixed account catalogs separate across refresh and offline restoration', async () => { const credentials = new InMemoryCredentialStore() const modelsStore = new InMemoryModelsStore() const setup = () => { @@ -86,11 +176,15 @@ describe('built-in provider instances', () => { for (const id of ['account-a', 'account-b']) { const { provider } = createSource() const baseline = provider.getModels()[0]! - let catalog: readonly Model[] = [] - provider.getModels = () => catalog + let catalog: readonly AnyModel[] = [] + provider.getModels = () => catalog.filter(model => isModelType(model, 'chat')) + provider.getAllModels = () => catalog provider.refreshModels = async (context) => { - const next = context.allowNetwork && context.credential?.type === 'api_key' - ? [{ ...baseline, id: context.credential.key! }] + const next: readonly AnyModel[] = context.allowNetwork && context.credential?.type === 'api_key' + ? [ + { ...baseline, id: context.credential.key! }, + { id: context.credential.key!, name: 'Image model', provider: provider.id, type: 'image', api: 'fixture-images', baseUrl: baseline.baseUrl, input: ['text'], output: ['image'], cost: baseline.cost }, + ] : context.stored?.models.filter(model => model.provider === provider.id) ?? [] await context.publish({ persist: { models: next }, @@ -109,12 +203,18 @@ describe('built-in provider instances', () => { await models.refresh({ allowNetwork: true }) for (const id of ['account-a', 'account-b']) { expect(models.getModels(id)).toMatchObject([{ provider: id, id: `${id}-model` }]) - expect(await modelsStore.read(id)).toMatchObject({ models: [{ provider: id, id: `${id}-model` }] }) + expect(models.getModelOfType('image', id, `${id}-model`)).toMatchObject({ provider: id, id: `${id}-model`, type: 'image' }) + expect(await modelsStore.read(id)).toMatchObject({ models: [ + { provider: id, id: `${id}-model` }, + { provider: id, id: `${id}-model`, type: 'image' }, + ] }) } const restored = setup() await restored.refresh({ allowNetwork: false }) - expect(restored.getModels('account-a')).toMatchObject([{ provider: 'account-a', id: 'account-a-model' }]) - expect(restored.getModels('account-b')).toMatchObject([{ provider: 'account-b', id: 'account-b-model' }]) + for (const id of ['account-a', 'account-b']) { + expect(restored.getModels(id)).toMatchObject([{ provider: id, id: `${id}-model` }]) + expect(restored.getModelOfType('image', id, `${id}-model`)).toMatchObject({ provider: id, id: `${id}-model`, type: 'image' }) + } }) it('resolves service tiers using the built-in source instead of the instance identifier', async () => { @@ -267,13 +367,17 @@ describe('built-in provider instances', () => { const model = runtime.getModels('account-a')[0]! const ownHistory = answer(model) const legacyHistory = { ...ownHistory, provider: 'fixture-builtin' } - const stream = runtime.streamSimple(model, { messages: [legacyHistory, ownHistory] }) + const observedProviders: string[] = [] + const stream = runtime.streamSimple(model, { messages: [legacyHistory, ownHistory] }, { + onProviderStreamEvent: (_data, observed) => { observedProviders.push(observed.provider) }, + }) const events = [] for await (const event of stream) events.push(event) expect(events.map(event => event.type)).toEqual(['start', 'done']) expect(events[0]).toMatchObject({ partial: { provider: 'account-a' } }) expect(await stream.result()).toMatchObject({ provider: 'account-a' }) + expect(observedProviders).toEqual(['account-a']) expect(await credentials.read('account-a')).toMatchObject({ access: 'refreshed-account-a' }) expect(await credentials.read('account-b')).toMatchObject({ access: 'account-b' }) expect(sources.requests[0]?.context.messages).toMatchObject([ @@ -302,6 +406,7 @@ function createSource() { cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, } const stream: Provider['streamSimple'] = (model, context, options) => lazyStream(model, async () => { + await options?.onProviderStreamEvent?.({ type: 'fixture' }, model) requests.push({ provider: model.provider, key: options?.apiKey, context }) const message = answer(model) return (async function* () { diff --git a/apps/buddy/service/src/providers/__tests__/OpenAiStreamCompletion.spec.ts b/apps/buddy/service/src/providers/__tests__/OpenAiStreamCompletion.spec.ts index a1d6cb8c..8d9c0ab6 100644 --- a/apps/buddy/service/src/providers/__tests__/OpenAiStreamCompletion.spec.ts +++ b/apps/buddy/service/src/providers/__tests__/OpenAiStreamCompletion.spec.ts @@ -47,7 +47,7 @@ function stream(body: ReadableStream | string, options: StreamOption } describe('openAI stream completion compatibility', () => { - it.each(['\n', '\r\n'])('preserves incremental Unicode text and usage with a byte-split %j terminator', async (newline) => { + it.each(['\n', '\r\n', '\r'])('preserves incremental Unicode text and usage with a byte-split %j terminator', async (newline) => { let controller!: ReadableStreamDefaultController const body = new ReadableStream({ start: value => controller = value }) const response = stream(body) @@ -63,7 +63,7 @@ describe('openAI stream completion compatibility', () => { controller.close() } } - expect(event.type).not.toBe('error') + expect(event.type, event.type === 'error' ? event.error.errorMessage : undefined).not.toBe('error') } const result = await response.result() expect(deltas).toEqual(['你好', ',世界']) @@ -116,14 +116,6 @@ describe('openAI stream completion compatibility', () => { expect(result.diagnostics).toBeUndefined() }) - it('keeps completion evidence isolated between concurrent requests', async () => { - const results = await Promise.all([ - stream(`${textDelta('complete')}data: [DONE]\n\n`).result(), - stream(textDelta('partial')).result(), - ]) - expect(results.map(result => result.stopReason)).toEqual(['stop', 'error']) - }) - it('leaves the existing provider-specific compatibility policy intact', async () => { const result = await provider.stream({ ...model, compat: { supportsFinishReason: false } }, context, { apiKey: 'fixture-key', @@ -144,16 +136,20 @@ describe('openAI stream completion compatibility', () => { expect(JSON.stringify(diagnostics)).not.toMatch(/private|fixture-key|fixture.example/) }) - it('correlates concurrent requests without cross-talk and observes inferred versus standard completions', async () => { - await Promise.all([ + it('isolates concurrent completion evidence and diagnostics for inferred, standard and incomplete streams', async () => { + const messages = await Promise.all([ diagnosticContext.run({ operationId: 'operation-a', runId: 'run-a' }, () => stream(`${textDelta('private-answer')}data: [DONE]\n\n`).result()), diagnosticContext.run({ operationId: 'operation-b', runId: 'run-b' }, () => stream(`${textDelta('private-answer')}${frame({ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] })}data: [DONE]\n\n`).result()), + diagnosticContext.run({ operationId: 'operation-c', runId: 'run-c' }, () => stream(textDelta('private-answer')).result()), ]) + expect(messages.map(message => message.stopReason)).toEqual(['stop', 'stop', 'error']) const results = diagnostics.filter(record => record.event === 'provider.request.completed') expect(results).toHaveLength(2) expect(results.find(record => record.runId === 'run-a')).toMatchObject({ operationId: 'operation-a', providerRequest: { completion: 'inferred', doneMarker: 'observed', contentEvents: 1, toolCalls: 0 } }) expect(results.find(record => record.runId === 'run-b')).toMatchObject({ operationId: 'operation-b', providerRequest: { completion: 'sdk' } }) - expect(new Set(results.map(record => record.requestId)).size).toBe(2) + const failed = diagnostics.filter(record => record.event === 'provider.request.failed') + expect(failed).toMatchObject([{ operationId: 'operation-c', runId: 'run-c', providerRequest: { completion: 'incomplete', doneMarker: 'not_observed' } }]) + expect(new Set([...results, ...failed].map(record => record.requestId)).size).toBe(3) expect(JSON.stringify(diagnostics)).not.toContain('private-answer') }) diff --git a/apps/buddy/service/src/providers/createBuiltinProviderInstance.ts b/apps/buddy/service/src/providers/createBuiltinProviderInstance.ts index cb63874e..ee670851 100644 --- a/apps/buddy/service/src/providers/createBuiltinProviderInstance.ts +++ b/apps/buddy/service/src/providers/createBuiltinProviderInstance.ts @@ -1,18 +1,23 @@ -import type { Api, AssistantMessageEvent, Model, Provider, TranscriptContext } from '@earendil-works/pi-ai' -import { lazyStream } from '@earendil-works/pi-ai' +import type { AnyModel, Api, AssistantMessageEvent, Model, Provider, StreamOptions, TranscriptContext } from '@earendil-works/pi-ai' +import { isModelType, lazyStream } from '@earendil-works/pi-ai' interface BuiltinProviderInstanceOptions { id: string name: string source: Provider - getCatalogModels: () => readonly Model[] + getCatalogModels: () => readonly AnyModel[] } export function createBuiltinProviderInstance(options: BuiltinProviderInstanceOptions): Provider { const { id, name, source } = options - let models = source.getModels() - const toSourceModel = (model: Model): Model => ({ ...model, provider: source.id }) - const toInstanceModel = (model: Model): Model => ({ ...model, provider: id }) + const readSourceModels = () => source.getAllModels?.() ?? source.getModels() + let models = readSourceModels() + const toSourceModel = (model: T): T => ({ ...model, provider: source.id }) + const toInstanceModel = (model: T): T => ({ ...model, provider: id }) + const getAllModels = () => (source.refreshModels ? models : options.getCatalogModels()).map(toInstanceModel) + const toSourceOptions = (options?: T): T | undefined => options?.onProviderStreamEvent + ? { ...options, onProviderStreamEvent: (data, model) => options.onProviderStreamEvent!(data, toInstanceModel(model)) } + : options const toSourceContext = (context: TranscriptContext): TranscriptContext => ({ ...context, messages: context.messages.map((message) => { @@ -35,10 +40,14 @@ export function createBuiltinProviderInstance(options: BuiltinProviderInstanceOp baseUrl: source.baseUrl, headers: source.headers, auth: source.auth, - getModels: () => (source.refreshModels ? models : options.getCatalogModels()).map(toInstanceModel), + getModels: () => getAllModels().filter(model => isModelType(model, 'chat')), + getAllModels, filterModels: source.filterModels ? (models, credential) => source.filterModels!(models.map(toSourceModel), credential).map(toInstanceModel) : undefined, + filterAllModels: source.filterAllModels + ? (models, credential) => source.filterAllModels!(models.map(toSourceModel), credential).map(toInstanceModel) + : undefined, refreshModels: source.refreshModels ? context => source.refreshModels!({ ...context, @@ -47,7 +56,7 @@ export function createBuiltinProviderInstance(options: BuiltinProviderInstanceOp ...publication, update: () => { publication.update?.() - models = source.getModels() + models = readSourceModels() }, persist: publication.persist && { ...publication.persist, @@ -56,14 +65,20 @@ export function createBuiltinProviderInstance(options: BuiltinProviderInstanceOp }), }) : undefined, - stream: (model, context, streamOptions) => forward(model, () => source.stream(toSourceModel(model), toSourceContext(context), streamOptions)), - streamSimple: (model, context, streamOptions) => forward(model, () => source.streamSimple(toSourceModel(model), toSourceContext(context), streamOptions)), + stream: (model, context, streamOptions) => forward(model, () => source.stream(toSourceModel(model), toSourceContext(context), toSourceOptions(streamOptions))), + streamSimple: (model, context, streamOptions) => forward(model, () => source.streamSimple(toSourceModel(model), toSourceContext(context), toSourceOptions(streamOptions))), fetchDeferred: source.fetchDeferred ? (model, handle, fetchOptions) => forward(model, () => source.fetchDeferred!(toSourceModel(model), handle, fetchOptions)) : undefined, cancelDeferred: source.cancelDeferred ? (model, handle, cancelOptions) => source.cancelDeferred!(toSourceModel(model), handle, cancelOptions) : undefined, + generateImages: source.generateImages + ? async (model, context, imageOptions) => ({ ...await source.generateImages!(toSourceModel(model), context, imageOptions), provider: id }) + : undefined, + classify: source.classify + ? async (model, context, classifierOptions) => ({ ...await source.classify!(toSourceModel(model), context, classifierOptions), provider: id }) + : undefined, } } diff --git a/apps/buddy/service/src/providers/createProviderLoginOptions.ts b/apps/buddy/service/src/providers/createProviderLoginOptions.ts new file mode 100644 index 00000000..24d499d0 --- /dev/null +++ b/apps/buddy/service/src/providers/createProviderLoginOptions.ts @@ -0,0 +1,19 @@ +import type { LoginOptions } from '@earendil-works/pi-ai' +import type { WorkspaceRepository } from '../storage/workspaceRepository' +import { randomUUID } from 'node:crypto' +import { z } from 'zod' + +const DEVICE_ID_KEY = 'buddy.providers.device-id' + +export function createProviderLoginOptions(settings: WorkspaceRepository): LoginOptions { + return { + getDeviceId() { + const stored = settings.get(DEVICE_ID_KEY) + if (stored !== null) + return z.uuid().parse(stored) + const id = randomUUID() + settings.set(DEVICE_ID_KEY, id, new Date().toISOString()) + return id + }, + } +} diff --git a/apps/buddy/service/src/providers/createProviderService.ts b/apps/buddy/service/src/providers/createProviderService.ts index b34cf56d..6742ecbf 100644 --- a/apps/buddy/service/src/providers/createProviderService.ts +++ b/apps/buddy/service/src/providers/createProviderService.ts @@ -9,7 +9,9 @@ import { registerBunOAuthFlows } from '@earendil-works/pi-ai/bun-oauth' import { ModelRuntime } from '@earendil-works/pi-coding-agent' import { providerNotifications } from '../../../shared/providers/providerApi' import { createProviderRepository } from '../storage/providerRepository' +import { createWorkspaceRepository } from '../storage/workspaceRepository' import { AuthInteractionService } from './AuthInteractionService' +import { createProviderLoginOptions } from './createProviderLoginOptions' import { createProviderModelRuntime } from './createProviderModelRuntime' import { HostCredentialStore } from './HostCredentialStore' import { createProviderCredentialStatus } from './ProviderCredentialStatus' @@ -57,6 +59,7 @@ export async function createProviderService( authInteractions.onDidChange(change => options.record?.({ event: `provider.auth.${change.kind.replaceAll('-', '_')}`, level: 'info', operationId: change.loginId, count: change.kind === 'challenge-opened' ? 1 : 0 })) const service = new ProviderService({ authInteractions, + loginOptions: createProviderLoginOptions(createWorkspaceRepository(options.database)), credentials, credentialStatus: createProviderCredentialStatus(credentials), getActiveRuns: options.getActiveRuns, diff --git a/apps/buddy/service/src/providers/withProviderRequestHeaders.ts b/apps/buddy/service/src/providers/withProviderRequestHeaders.ts index c1e77ed6..50ff3e72 100644 --- a/apps/buddy/service/src/providers/withProviderRequestHeaders.ts +++ b/apps/buddy/service/src/providers/withProviderRequestHeaders.ts @@ -3,11 +3,11 @@ import type { ProviderRequestHeaders } from './ProviderRequestHeaders' import { lazyStream } from '@earendil-works/pi-ai' export function withProviderRequestHeaders(provider: Provider, headers: ProviderRequestHeaders): Provider { - async function prepare(options?: T): Promise { + async function prepare>(options?: T): Promise { return { ...options, headers: await headers.resolve(provider.id, options?.headers, options?.apiKey), - } as T & ProviderRequestOptions + } as T } return { ...provider, @@ -19,5 +19,11 @@ export function withProviderRequestHeaders(provider: Provider, headers: Provider cancelDeferred: provider.cancelDeferred ? async (model, handle, options) => provider.cancelDeferred!(model, handle, await prepare(options)) : undefined, + generateImages: provider.generateImages + ? async (model, context, options) => provider.generateImages!(model, context, await prepare(options)) + : undefined, + classify: provider.classify + ? async (model, context, options) => provider.classify!(model, context, await prepare(options)) + : undefined, } } diff --git a/apps/buddy/service/src/providers/withProviderStream.ts b/apps/buddy/service/src/providers/withProviderStream.ts index fcb22031..a586ce17 100644 --- a/apps/buddy/service/src/providers/withProviderStream.ts +++ b/apps/buddy/service/src/providers/withProviderStream.ts @@ -50,11 +50,15 @@ function complete(model: Model, options: T | undef evidence.doneMarker = 'not_observed' const decoder = new TextDecoder() let tail = '\n\n' + let afterCarriageReturn = false const body = response.body.pipeThrough(new TransformStream({ transform(chunk, controller) { if (!receivedDone) { - const text = tail + decoder.decode(chunk, { stream: true }) - receivedDone = /\r?\n\r?\ndata: ?\[DONE\]\r?\n\r?\n/.test(text) + const decoded = decoder.decode(chunk, { stream: true }) + const text = tail + (afterCarriageReturn && decoded.startsWith('\n') ? decoded.slice(1) : decoded).replace(/\r\n?/g, '\n') + if (decoded.length) + afterCarriageReturn = decoded.endsWith('\r') + receivedDone = /\n\ndata: ?\[DONE\]\n\n/.test(text) if (receivedDone) evidence.doneMarker = 'observed' tail = text.slice(-32) diff --git a/apps/buddy/service/src/sandbox/__tests__/shellNetworkFeedback.spec.ts b/apps/buddy/service/src/sandbox/__tests__/shellNetworkFeedback.spec.ts index ceb79623..9221261c 100644 --- a/apps/buddy/service/src/sandbox/__tests__/shellNetworkFeedback.spec.ts +++ b/apps/buddy/service/src/sandbox/__tests__/shellNetworkFeedback.spec.ts @@ -77,18 +77,17 @@ describe.each(['bash', 'powershell'] as const)('%s network authorization feedbac [new Error('synthetic-private-error-detail'), 'NETWORK_APPROVAL_FAILED'], ])('preserves authorization outcome %s without replacing the command exit status', async (decision, code) => { const check = await fixture({ dialect, decision }) - const error = await check.invoke().catch(error => error as Error) - expect(error).toBeInstanceOf(Error) - if (!(error instanceof Error)) - throw new Error('Expected a command failure') - expect(error.message).toContain('original-command-output') - expect(error.message).toContain(JSON.stringify({ ...target, code })) - expect(error.message).not.toContain('synthetic-private-error-detail') + const result = await check.invoke() + expect(result.isError).toBe(true) + const output = result.content.find(item => item.type === 'text')!.text + expect(output).toContain('original-command-output') + expect(output).toContain(JSON.stringify({ ...target, code })) + expect(output).not.toContain('synthetic-private-error-detail') if (code !== 'APPROVAL_DENIED') - expect(error.message).not.toContain('APPROVAL_DENIED') - expect(error.message).toMatch(/Command exited with code 22$/) + expect(output).not.toContain('APPROVAL_DENIED') + expect(output).toMatch(/Command exited with code 22$/) expect(check.decisions).toEqual([false]) - expect(createBuddyToolPresentation({ toolName: dialect, arguments: { command: 'fixture' }, isError: true, result: { content: [{ type: 'text', text: error.message }] } })).toMatchObject({ card: 'terminal', exitCode: 22 }) + expect(createBuddyToolPresentation({ toolName: dialect, arguments: { command: 'fixture' }, isError: true, result })).toMatchObject({ card: 'terminal', exitCode: 22 }) }) it('keeps successful command recovery successful while reporting the refused request', async () => { @@ -102,17 +101,16 @@ describe.each(['bash', 'powershell'] as const)('%s network authorization feedbac it('keeps an ordinary failure free of network or sandbox advice', async () => { const check = await fixture({ dialect, decision: 'approved_once', exitCode: 7 }) - await expect(check.invoke()).rejects.toThrow(/^original-command-output\n\nCommand exited with code 7$/) + await expect(check.invoke()).resolves.toMatchObject({ isError: true, content: [{ type: 'text', text: 'original-command-output\n\nCommand exited with code 7' }], structuredContent: { exit_code: 7 } }) expect(check.decisions).toEqual([true]) }) it('keeps background unavailability distinct from user refusal', async () => { const check = await fixture({ dialect, background: true }) - const error = await check.invoke().catch(error => error as Error) - expect(error).toBeInstanceOf(Error) - if (!(error instanceof Error)) - throw new Error('Expected a command failure') - expect(error.message).toContain(JSON.stringify({ ...target, code: 'APPROVAL_UNAVAILABLE_IN_BACKGROUND' })) + const result = await check.invoke() + expect(result.isError).toBe(true) + const output = result.content.find(item => item.type === 'text')!.text + expect(output).toContain(JSON.stringify({ ...target, code: 'APPROVAL_UNAVAILABLE_IN_BACKGROUND' })) expect(check.decisions).toEqual([false]) }) diff --git a/patches/@earendil-works__pi-ai@0.87.1.patch b/patches/@earendil-works__pi-ai@0.99.1.patch similarity index 81% rename from patches/@earendil-works__pi-ai@0.87.1.patch rename to patches/@earendil-works__pi-ai@0.99.1.patch index 6f851b74..086c0625 100644 --- a/patches/@earendil-works__pi-ai@0.87.1.patch +++ b/patches/@earendil-works__pi-ai@0.99.1.patch @@ -1,11 +1,11 @@ diff --git a/dist/api/openai-completions.js b/dist/api/openai-completions.js -index fc8e893..04c2c95 100644 --- a/dist/api/openai-completions.js +++ b/dist/api/openai-completions.js -@@ -1123,6 +1123,14 @@ export function convertMessages(model, context, compat, options) { +@@ -1122,6 +1122,14 @@ + continue; } lastRole = msg.role; - } ++ } + if (params.some((message) => message.role === "assistant" && + typeof message.reasoning_content === "string")) { + for (const message of params) { @@ -13,15 +13,13 @@ index fc8e893..04c2c95 100644 + message.reasoning_content = ""; + } + } -+ } + } return params; } - function convertTools(tools, compat) { diff --git a/dist/api/openai-responses-shared.js b/dist/api/openai-responses-shared.js -index 53597d2..170d9a4 100644 --- a/dist/api/openai-responses-shared.js +++ b/dist/api/openai-responses-shared.js -@@ -355,7 +355,11 @@ export async function processResponsesStream(openaiStream, output, stream, model +@@ -355,7 +355,11 @@ } if (item.type === "message") { applyMessagePhaseStopReason(item); diff --git a/patches/@earendil-works__pi-coding-agent@0.87.1.patch b/patches/@earendil-works__pi-coding-agent@0.99.1.patch similarity index 92% rename from patches/@earendil-works__pi-coding-agent@0.87.1.patch rename to patches/@earendil-works__pi-coding-agent@0.99.1.patch index cc082642..11e16810 100644 --- a/patches/@earendil-works__pi-coding-agent@0.87.1.patch +++ b/patches/@earendil-works__pi-coding-agent@0.99.1.patch @@ -1,6 +1,37 @@ +diff --git a/dist/config.js b/dist/config.js +--- a/dist/config.js ++++ b/dist/config.js +@@ -9,8 +9,8 @@ + // ============================================================================= + // Package Detection + // ============================================================================= +-const __filename = fileURLToPath(import.meta.url); +-const __dirname = dirname(__filename); ++const moduleFilename = fileURLToPath(import.meta.url); ++const moduleDirectory = dirname(moduleFilename); + /** + * Detect if we're running as a Bun compiled binary. + * Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path) +@@ -45,7 +45,7 @@ + if (isBunBinary) { + return "bun-binary"; + } +- const resolvedPath = `${__dirname}\0${process.execPath || ""}`.toLowerCase().replace(/\\/g, "/"); ++ const resolvedPath = `${moduleDirectory}\0${process.execPath || ""}`.toLowerCase().replace(/\\/g, "/"); + if (resolvedPath.includes("/pnpm/") || resolvedPath.includes("/.pnpm/")) { + return "pnpm"; + } +@@ -319,7 +319,7 @@ + // Bun binary: process.execPath points to the compiled executable + return dirname(process.execPath); + } +- return findNodePackageDir(__dirname); ++ return findNodePackageDir(moduleDirectory); + } + /** + * Get path to built-in themes directory (shipped with package) diff --git a/dist/core/tools/find-execution.js b/dist/core/tools/find-execution.js new file mode 100644 -index 0000000..dd03780 --- /dev/null +++ b/dist/core/tools/find-execution.js @@ -0,0 +1,67 @@ @@ -72,7 +103,6 @@ index 0000000..dd03780 + }; +} diff --git a/dist/core/tools/find.js b/dist/core/tools/find.js -index a3bab76..de47f3e 100644 --- a/dist/core/tools/find.js +++ b/dist/core/tools/find.js @@ -1,12 +1,9 @@ @@ -90,7 +120,7 @@ index a3bab76..de47f3e 100644 /** Relativize a find result against the search root and normalize it to posix separators. */ export function relativizeFindResultPath(resultPath, searchPath, pathModule = path) { const hadTrailingSeparator = resultPath.endsWith(pathModule.sep) || (pathModule.sep === "\\" && resultPath.endsWith("/")); -@@ -26,11 +23,6 @@ export const findToolSystemPromptContribution = { +@@ -26,11 +23,6 @@ guidelines: [], }; const DEFAULT_LIMIT = 1000; @@ -102,7 +132,7 @@ index a3bab76..de47f3e 100644 export function createFindToolDefinition(cwd, options) { const customOps = options?.operations; return { -@@ -39,208 +31,8 @@ export function createFindToolDefinition(cwd, options) { +@@ -39,208 +31,8 @@ description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`, promptSnippet: findToolSystemPromptContribution.snippet, parameters: findSchema, @@ -315,7 +345,6 @@ index a3bab76..de47f3e 100644 }; diff --git a/dist/core/tools/grep-execution.js b/dist/core/tools/grep-execution.js new file mode 100644 -index 0000000..b72872c --- /dev/null +++ b/dist/core/tools/grep-execution.js @@ -0,0 +1,82 @@ @@ -402,7 +431,6 @@ index 0000000..b72872c + }; +} diff --git a/dist/core/tools/grep.js b/dist/core/tools/grep.js -index d427d07..d564283 100644 --- a/dist/core/tools/grep.js +++ b/dist/core/tools/grep.js @@ -1,13 +1,9 @@ @@ -421,7 +449,7 @@ index d427d07..d564283 100644 const grepSchema = Type.Object({ pattern: Type.String({ description: "Search pattern (regex or literal string)" }), path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })), -@@ -34,214 +30,8 @@ export function createGrepToolDefinition(cwd, options) { +@@ -34,214 +30,8 @@ description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`, promptSnippet: grepToolSystemPromptContribution.snippet, parameters: grepSchema, @@ -640,7 +668,6 @@ index d427d07..d564283 100644 }; diff --git a/dist/core/tools/search-process.js b/dist/core/tools/search-process.js new file mode 100644 -index 0000000..a897970 --- /dev/null +++ b/dist/core/tools/search-process.js @@ -0,0 +1,74 @@ @@ -719,23 +746,21 @@ index 0000000..a897970 + return Math.min(Math.max(1, value), maximum); +} diff --git a/dist/index.d.ts b/dist/index.d.ts -index 52dcaaf..8053eec 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts -@@ -3,7 +3,7 @@ export { CONFIG_DIR_NAME, getAgentDir, getDocsPath, getExamplesPath, getPackageD +@@ -3,7 +3,7 @@ export { AgentSession, type AgentSessionConfig, type AgentSessionEvent, type AgentSessionEventListener, type ModelCycleResult, type ParsedSkillBlock, type PromptOptions, parseSkillBlock, type SessionStats, } from "./core/agent-session.ts"; export { readStoredCredential } from "./core/auth-storage.ts"; export type { CacheWarmingDecision, CacheWarmingStatus } from "./core/cache-warmer.ts"; -export { type BranchPreparation, type BranchSummaryResult, type CollectEntriesResult, type CompactionResult, type CutPointResult, calculateContextTokens, collectEntriesForBranchSummary, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, type FileOperations, findCutPoint, findTurnStartIndex, type GenerateBranchSummaryOptions, generateBranchSummary, generateSummary, generateSummaryWithUsage, getLastAssistantUsage, prepareBranchEntries, serializeConversation, shouldCompact, } from "./core/compaction/index.ts"; +export { type BranchPreparation, type BranchSummaryResult, type CollectEntriesResult, type CompactionResult, type CutPointResult, calculateContextTokens, collectEntriesForBranchSummary, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, type FileOperations, findCutPoint, findTurnStartIndex, type GenerateBranchSummaryOptions, generateBranchSummary, generateSummary, generateSummaryWithUsage, getLastAssistantUsage, prepareBranchEntries, prepareCompaction, serializeConversation, shouldCompact, } from "./core/compaction/index.ts"; export { createEventBus, type EventBus, type EventBusController } from "./core/event-bus.ts"; - export type { AfterProviderResponseEvent, AgentActivityOutcome, AgentBeforeSettleEvent, AgentBeforeSettleEventResult, AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BoundaryContextPreview, BoundaryResult, BoundaryState, BuildSystemPromptOptions, CacheWarmingDecisionEvent, CacheWarmingDecisionEventResult, CompactionEntryDraft, CompactOptions, ContextEditEntryDraft, ContextEvent, ContextEventResult, ContextUsage, ContextWithSystemEvent, CustomEntryDraft, CustomMessageEntryDraft, CustomToolCallEvent, EditToolCallEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, GrepToolCallEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, MarkdownTransformContext, MarkdownTransformer, MessageEndEvent, MessageEndEventResult, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, NormalizedBuildSystemPromptOptions, PowerShellToolCallEvent, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, RegisteredCommand, RegisteredTool, ResolvedCommand, ResourcesDiscoverEvent, ResourcesDiscoverResult, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionBoundaryDraft, SessionCompactEvent, SessionCompactFailedEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SlashCommandInfo, SlashCommandSource, SourceInfo, TerminalInputHandler, ThinkingLevelSelectEvent, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TurnEndEvent, TurnEndEventResult, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, } from "./core/extensions/index.ts"; + export type { AfterProviderResponseEvent, AgentActivityOutcome, AgentBeforeSettleEvent, AgentBeforeSettleEventResult, AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BoundaryContextPreview, BoundaryResult, BoundaryState, BuildSystemPromptOptions, CacheWarmingDecisionEvent, CacheWarmingDecisionEventResult, CompactionEntryDraft, CompactOptions, ContextEditEntryDraft, ContextEvent, ContextEventResult, ContextUsage, ContextWithSystemEvent, CustomEntryDraft, CustomMessageEntryDraft, CustomToolCallEvent, EditToolCallEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, ExecuteToolOptions, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionToolContext, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionVirtualModel, ExtensionWidgetOptions, FindToolCallEvent, GrepToolCallEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, MarkdownTransformContext, MarkdownTransformer, McpServersChangeEvent, MessageEndEvent, MessageEndEventResult, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, NormalizedBuildSystemPromptOptions, PowerShellToolCallEvent, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ProviderStreamEvent, ReadToolCallEvent, RegisteredCommand, RegisteredTool, ResolvedCommand, ResourcesDiscoverEvent, ResourcesDiscoverResult, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionBoundaryDraft, SessionCompactEvent, SessionCompactFailedEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SlashCommandInfo, SlashCommandSource, SourceInfo, TerminalInputHandler, ThinkingLevelSelectEvent, ToolAnnotations, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolExposure, ToolInfo, ToolLoadout, ToolLoadoutChanges, ToolNamespace, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TurnEndEvent, TurnEndEventResult, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, } from "./core/extensions/index.ts"; export { createExtensionRuntime, defineTool, discoverAndLoadExtensions, ExtensionRunner, isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isPowerShellToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, wrapRegisteredTool, wrapRegisteredTools, } from "./core/extensions/index.ts"; diff --git a/dist/index.js b/dist/index.js -index d833966..e7503c1 100644 --- a/dist/index.js +++ b/dist/index.js -@@ -5,7 +5,7 @@ export { CONFIG_DIR_NAME, getAgentDir, getDocsPath, getExamplesPath, getPackageD +@@ -5,7 +5,7 @@ export { AgentSession, parseSkillBlock, } from "./core/agent-session.js"; export { readStoredCredential } from "./core/auth-storage.js"; // Compaction @@ -745,7 +770,6 @@ index d833966..e7503c1 100644 export { createExtensionRuntime, defineTool, discoverAndLoadExtensions, ExtensionRunner, isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isPowerShellToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, wrapRegisteredTool, wrapRegisteredTools, } from "./core/extensions/index.js"; export { convertToLlm } from "./core/messages.js"; diff --git a/dist/utils/shell.js b/dist/utils/shell.js -index 19605cc..5777711 100644 --- a/dist/utils/shell.js +++ b/dist/utils/shell.js @@ -1,5 +1,5 @@ @@ -755,7 +779,7 @@ index 19605cc..5777711 100644 import { spawn, spawnSync } from "child_process"; import { getBinDir } from "../config.js"; /** -@@ -106,6 +106,12 @@ export function getPowerShellConfig() { +@@ -106,6 +106,12 @@ if (process.platform !== "win32") { throw new Error("The powershell tool is only available on Windows."); } @@ -769,7 +793,6 @@ index 19605cc..5777711 100644 if (!shell) { throw new Error("No PowerShell executable found. Install PowerShell or add powershell.exe/pwsh.exe to PATH."); diff --git a/dist/utils/tools-manager.js b/dist/utils/tools-manager.js -index 39b1f1a..f2747a3 100644 --- a/dist/utils/tools-manager.js +++ b/dist/utils/tools-manager.js @@ -1,7 +1,7 @@ @@ -781,7 +804,7 @@ index 39b1f1a..f2747a3 100644 import { Readable } from "stream"; import { pipeline } from "stream/promises"; import { APP_NAME, getBinDir } from "../config.js"; -@@ -76,6 +76,16 @@ export function getToolPath(tool) { +@@ -76,6 +76,16 @@ const config = TOOLS[tool]; if (!config) return null; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 310554a2..0256d6d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -197,8 +197,8 @@ overrides: patchedDependencies: '@anthropic-ai/sandbox-runtime@0.0.77': b859621ca2a537d1865ef159feeccc66fb80b1470d4aef8748318d6cb84fdc50 - '@earendil-works/pi-ai@0.87.1': 04aa4388e40703a06f1c3debff41d4a87d179a34d8be0a556b7107f087a2cee8 - '@earendil-works/pi-coding-agent@0.87.1': a3e0da5c3f8c02ff6a83eddb0eea6eee46f9971338376bcf34e02078f8bdcb2a + '@earendil-works/pi-ai@0.99.1': f20f416f2738f663e6eb60ce983d36b4be33c3276d9b28350a2a3c6f8d73243b + '@earendil-works/pi-coding-agent@0.99.1': 1c4545af2877ed7c0f54954cd45a6aef3bb7a2b243ec7218787ff198481fcba3 importers: @@ -414,11 +414,11 @@ importers: specifier: 0.0.77 version: 0.0.77(patch_hash=b859621ca2a537d1865ef159feeccc66fb80b1470d4aef8748318d6cb84fdc50) '@earendil-works/pi-ai': - specifier: ^0.87.1 - version: 0.87.1(patch_hash=04aa4388e40703a06f1c3debff41d4a87d179a34d8be0a556b7107f087a2cee8)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) + specifier: ^0.99.1 + version: 0.99.1(patch_hash=f20f416f2738f663e6eb60ce983d36b4be33c3276d9b28350a2a3c6f8d73243b)(@aws-sdk/credential-provider-node@3.972.83)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(@smithy/signature-v4@5.7.0)(supports-color@8.1.1)(undici@8.10.2)(ws@8.21.0)(zod@4.4.3) '@earendil-works/pi-coding-agent': - specifier: ^0.87.1 - version: 0.87.1(patch_hash=a3e0da5c3f8c02ff6a83eddb0eea6eee46f9971338376bcf34e02078f8bdcb2a)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) + specifier: ^0.99.1 + version: 0.99.1(patch_hash=1c4545af2877ed7c0f54954cd45a6aef3bb7a2b243ec7218787ff198481fcba3)(@aws-sdk/credential-provider-node@3.972.83)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(@smithy/signature-v4@5.7.0)(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) '@js-temporal/polyfill': specifier: ^0.5.1 version: 0.5.1 @@ -1440,34 +1440,42 @@ packages: oxlint: optional: true - '@earendil-works/chord@0.87.1': - resolution: {integrity: sha512-bg7IkJGFcEaMqqYgOGUiq5Ky9RghpRfrlZ8I/v/1b4bBZ02A7t3E+6uhPRbadwWb/kWsnVFbZsqOKRN4a3LLCg==} + '@earendil-works/chord@0.99.1': + resolution: {integrity: sha512-4xyn0IBzJ+Xu/iOGi2hjXJGAR61QEhEWZsIqTDqr+GmItdquYwBO5jYFnqGiBaTqlY12/EpM7QHoEKSHbyvOug==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-agent-core@0.87.1': - resolution: {integrity: sha512-Zev3B0HK7YS5A4EZQ2XnEqiJuirx6QBiltJ+LpmjV5a/+2IU0cfKtIfnkNkORK707XOvKBY2WRtk7cAwHpbh2Q==} + '@earendil-works/pi-agent-core@0.99.1': + resolution: {integrity: sha512-zywvWnj5FujeuFI/x/CJHwwxhcLIQgjqseTA+bQgX4O8gJTcgjRd/I8SZnQDqJvxC9QcV12ujiGLviv6EgwcCg==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.87.1': - resolution: {integrity: sha512-X/3PfQBnnoeVdO9Cv8zHghUMglzlgNZYGNzoPnbRoGnHl3Rw3TlA2UKSUB7BRHUOxMryHXYa8dnjWZlbRheDZA==} + '@earendil-works/pi-ai@0.99.1': + resolution: {integrity: sha512-4nV9JKc94iPX8bwdGPc2nTuVPKIPsffhnp3WoN9NYCNqbtoOF8LhYcIs/+Sn/alroqJK/5QRu6/Z6Ck+n0hyBA==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-coding-agent@0.87.1': - resolution: {integrity: sha512-m8ArJUtVcQMSe1lLE/Ei7vX/JV7O39sWmWBsXV2NOU70F0qCp8GubA24pT3LnwTmM6LL2xV80/h6sQg85n69ew==} + '@earendil-works/pi-codemode@0.99.1': + resolution: {integrity: sha512-oh8TMsBI3SWTN3xTQtX8u5n+BKhnVXcFagroWumfn6/WWfBnDYL/LmeQtjLb83WRTb9rcu+ZdK8rFa4vggvCJg==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-coding-agent@0.99.1': + resolution: {integrity: sha512-cWUrTOqA5M73cOYMgsh9PlhDrsBhavd+n5kVY6F7BGbGl1RjqCteVCoeVMVqhngoGACVDyw1tbLjajL8l9jrHg==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-telemetry@0.87.1': - resolution: {integrity: sha512-MC6TRQH5lgMXpcN+Vku2WMI2T8BsiUPzMQHGo81uqFZD3/9O79WWJAysEDGuzduP6R4tvtgwMLwmqIxynM10JQ==} + '@earendil-works/pi-mcp@0.99.1': + resolution: {integrity: sha512-YCFGPkmDzLwQuIzwfbP6Vuk/g/ukKpZhwTpbcfzomuI1Fkiu6hHRkOGwAqsO3G8cTkZWkM8vmOkFJjStQNC4qA==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-telemetry@0.99.1': + resolution: {integrity: sha512-9PBPjGk+TXRtuMianpqBbHBpYpyKusESF6rwdmgD0WTZSTUQXhcKEO0hAINRLuSwy4V7yPvXV+EVV0ONY7mbpQ==} engines: {node: '>=22.19.0'} '@earendil-works/pi-tui@0.78.0': resolution: {integrity: sha512-3a705FnsVVUhAyceShNB3kS2rpxcxLcx+hqB0u6MMMpHwQGbW+m++MqA6r7eOzq/8FLx5e3vDh38h/SVTk2qzw==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-tui@0.87.1': - resolution: {integrity: sha512-YEH2vRyOeiO7hhN6j6AE6YwKSq2Kz2f3XR8bj1TbR+aGE/JsnY1hLPMI2pvaZfRM1n9Y00tejxFQ4zbzvF7nkQ==} + '@earendil-works/pi-tui@0.99.1': + resolution: {integrity: sha512-gZp0Guat96Fr1AuC/xqVz5B2lulZakp/PxD1lXx3lSgBdjiqmwYhJbcQ0HRrGAfy0WtMGn9b05RJr5qJf7oIuw==} engines: {node: '>=22.19.0'} '@electric-sql/pglite-socket@0.1.3': @@ -6984,12 +6992,25 @@ packages: zod: optional: true - openai@6.40.0: - resolution: {integrity: sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==} + openai@7.19.0: + resolution: {integrity: sha512-MX2s3u2L5racTO0CC/SWpCOasJQBCJrqLKXK+l82cAhdeF8mPMBEe/gxMm0ZFa2xpKpOFLRjxv5afYEZbBXmbQ==} + engines: {node: '>=22.0.0'} peerDependencies: - ws: ^8.18.0 + '@aws-sdk/credential-provider-node': '>=3.972.0 <4' + '@smithy/hash-node': '>=4.3.0 <5' + '@smithy/signature-v4': '>=5.4.0 <6' + undici: 8.10.2 + ws: ^8.21.0 zod: ^3.25 || ^4.0 peerDependenciesMeta: + '@aws-sdk/credential-provider-node': + optional: true + '@smithy/hash-node': + optional: true + '@smithy/signature-v4': + optional: true + undici: + optional: true ws: optional: true zod: @@ -7504,6 +7525,9 @@ packages: quickjs-wasi@3.0.0: resolution: {integrity: sha512-X7ouKC4ZVf9bXQ8rsE7+L6TeBbesejAJH61x16xRaGAQGfBHHRcniWgzJZZVtHc8rS9yVsY+Tvk8/usAosg4bg==} + quickjs-wasi@3.6.2: + resolution: {integrity: sha512-FCqGtGOrMgzUiIrMNMA2YnsOxCNwo31dzqXvclXUC6xeT35NJLKXQJsvbeCTjvoFAwZgEAPg8U6+KAPDGXn8Mg==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -10023,55 +10047,69 @@ snapshots: optionalDependencies: eslint: 10.4.0(jiti@2.7.0)(supports-color@8.1.1) - '@earendil-works/chord@0.87.1': + '@earendil-works/chord@0.99.1': dependencies: esbuild: 0.28.2 - '@earendil-works/pi-agent-core@0.87.1(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-agent-core@0.99.1(@aws-sdk/credential-provider-node@3.972.83)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(@smithy/signature-v4@5.7.0)(supports-color@8.1.1)(undici@8.10.2)(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/chord': 0.87.1 - '@earendil-works/pi-ai': 0.87.1(patch_hash=04aa4388e40703a06f1c3debff41d4a87d179a34d8be0a556b7107f087a2cee8)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-telemetry': 0.87.1 + '@earendil-works/chord': 0.99.1 + '@earendil-works/pi-ai': 0.99.1(patch_hash=f20f416f2738f663e6eb60ce983d36b4be33c3276d9b28350a2a3c6f8d73243b)(@aws-sdk/credential-provider-node@3.972.83)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(@smithy/signature-v4@5.7.0)(supports-color@8.1.1)(undici@8.10.2)(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-telemetry': 0.99.1 diff: 8.0.4 ignore: 7.0.8 typebox: 1.3.27 yaml: 2.9.0 transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' - '@modelcontextprotocol/sdk' + - '@smithy/hash-node' + - '@smithy/signature-v4' - bufferutil - kerberos - supports-color + - undici - utf-8-validate - ws - zod - '@earendil-works/pi-ai@0.87.1(patch_hash=04aa4388e40703a06f1c3debff41d4a87d179a34d8be0a556b7107f087a2cee8)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-ai@0.99.1(patch_hash=f20f416f2738f663e6eb60ce983d36b4be33c3276d9b28350a2a3c6f8d73243b)(@aws-sdk/credential-provider-node@3.972.83)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(@smithy/signature-v4@5.7.0)(supports-color@8.1.1)(undici@8.10.2)(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.124.0(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1127.0 - '@earendil-works/pi-telemetry': 0.87.1 + '@earendil-works/pi-telemetry': 0.99.1 '@google/genai': 2.21.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1) '@smithy/node-http-handler': 4.12.1 http-proxy-agent: 9.1.0(supports-color@8.1.1) https-proxy-agent: 9.1.0(supports-color@8.1.1) - openai: 6.40.0(ws@8.21.0)(zod@4.4.3) + openai: 7.19.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.0)(undici@8.10.2)(ws@8.21.0)(zod@4.4.3) partial-json: 0.1.7 typebox: 1.3.27 transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' - '@modelcontextprotocol/sdk' + - '@smithy/hash-node' + - '@smithy/signature-v4' - bufferutil - kerberos - supports-color + - undici - utf-8-validate - ws - zod - '@earendil-works/pi-coding-agent@0.87.1(patch_hash=a3e0da5c3f8c02ff6a83eddb0eea6eee46f9971338376bcf34e02078f8bdcb2a)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-codemode@0.99.1': + dependencies: + quickjs-wasi: 3.6.2 + + '@earendil-works/pi-coding-agent@0.99.1(patch_hash=1c4545af2877ed7c0f54954cd45a6aef3bb7a2b243ec7218787ff198481fcba3)(@aws-sdk/credential-provider-node@3.972.83)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(@smithy/signature-v4@5.7.0)(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/chord': 0.87.1 - '@earendil-works/pi-agent-core': 0.87.1(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-ai': 0.87.1(patch_hash=04aa4388e40703a06f1c3debff41d4a87d179a34d8be0a556b7107f087a2cee8)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-tui': 0.87.1 + '@earendil-works/chord': 0.99.1 + '@earendil-works/pi-agent-core': 0.99.1(@aws-sdk/credential-provider-node@3.972.83)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(@smithy/signature-v4@5.7.0)(supports-color@8.1.1)(undici@8.10.2)(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.99.1(patch_hash=f20f416f2738f663e6eb60ce983d36b4be33c3276d9b28350a2a3c6f8d73243b)(@aws-sdk/credential-provider-node@3.972.83)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(@smithy/signature-v4@5.7.0)(supports-color@8.1.1)(undici@8.10.2)(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-codemode': 0.99.1 + '@earendil-works/pi-mcp': 0.99.1 + '@earendil-works/pi-tui': 0.99.1 '@silvia-odwyer/photon-node': 0.3.4 chalk: 6.0.0 cross-spawn: 7.0.6 @@ -10083,12 +10121,16 @@ snapshots: jiti: 2.7.0 minimatch: 10.2.6 proper-lockfile: 4.1.2 + quickjs-wasi: 3.6.2 semver: 7.8.5 typebox: 1.3.27 undici: 8.10.2 yaml: 2.9.0 transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' - '@modelcontextprotocol/sdk' + - '@smithy/hash-node' + - '@smithy/signature-v4' - bufferutil - kerberos - supports-color @@ -10096,14 +10138,18 @@ snapshots: - ws - zod - '@earendil-works/pi-telemetry@0.87.1': {} + '@earendil-works/pi-mcp@0.99.1': + dependencies: + cross-spawn: 7.0.6 + + '@earendil-works/pi-telemetry@0.99.1': {} '@earendil-works/pi-tui@0.78.0': dependencies: get-east-asian-width: 1.6.0 marked: 15.0.12 - '@earendil-works/pi-tui@0.87.1': + '@earendil-works/pi-tui@0.99.1': dependencies: get-east-asian-width: 1.6.0 marked: 18.0.11 @@ -15901,8 +15947,11 @@ snapshots: ws: 8.21.0 zod: 4.4.3 - openai@6.40.0(ws@8.21.0)(zod@4.4.3): + openai@7.19.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.0)(undici@8.10.2)(ws@8.21.0)(zod@4.4.3): optionalDependencies: + '@aws-sdk/credential-provider-node': 3.972.83 + '@smithy/signature-v4': 5.7.0 + undici: 8.10.2 ws: 8.21.0 zod: 4.4.3 @@ -16523,6 +16572,8 @@ snapshots: quickjs-wasi@3.0.0: {} + quickjs-wasi@3.6.2: {} + range-parser@1.2.1: {} rastermill@0.3.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 656a36fd..268690b4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -40,8 +40,8 @@ allowBuilds: vue-demi: true patchedDependencies: '@anthropic-ai/sandbox-runtime@0.0.77': patches/@anthropic-ai__sandbox-runtime@0.0.77.patch - '@earendil-works/pi-ai@0.87.1': patches/@earendil-works__pi-ai@0.87.1.patch - '@earendil-works/pi-coding-agent@0.87.1': patches/@earendil-works__pi-coding-agent@0.87.1.patch + '@earendil-works/pi-ai@0.99.1': patches/@earendil-works__pi-ai@0.99.1.patch + '@earendil-works/pi-coding-agent@0.99.1': patches/@earendil-works__pi-coding-agent@0.99.1.patch catalog: '@types/node': ^25.6.0 dotenv: ^17.4.2 @@ -51,3 +51,12 @@ catalog: pino-pretty: ^13.1.3 typescript: ^6.0.3 zod: ^4.3.6 +minimumReleaseAgeExclude: + - '@earendil-works/chord@0.99.1' + - '@earendil-works/pi-agent-core@0.99.1' + - '@earendil-works/pi-ai@0.99.1' + - '@earendil-works/pi-codemode@0.99.1' + - '@earendil-works/pi-coding-agent@0.99.1' + - '@earendil-works/pi-mcp@0.99.1' + - '@earendil-works/pi-telemetry@0.99.1' + - '@earendil-works/pi-tui@0.99.1'