import type { CodingProjectFileContent, CodingProjectFileEntry, CodingTextSearchResult, ConversationChangesSnapshot, ProductCodingCommand, ProductCodingSkill, ProductPiCommandInput, } from '../../shared/coding-product-tools'; import type { CodingAttachmentStore } from '../coding-projects/attachment-store'; import { readCodingProjectConfigV2 } from '../coding-projects/project-config'; import { CodingProjectFileService } from '../coding-projects/project-files'; import { CodingProjectServiceError, type CodingProjectService, } from '../coding-projects/project-service'; import type { CodingConversationService } from '../coding-runtime/conversation-service'; import type { CodingConversationRuntime } from '../coding-runtime/contracts'; import type { PiProductTools } from '../coding-runtime/pi/product-tools'; export interface ActiveCodingProject { id: string; path: string; } export interface CodingProductHost { fileStatus(): Promise; findFiles(query: string, limit?: number): Promise; fileContent(path: string): Promise; searchText(pattern: string): Promise; listSkills(agentId?: string): Promise; listCommands(conversationId: string): Promise; getChanges(conversationId: string): Promise; } export interface CodingProductComposition { attachments: CodingAttachmentStore; productTools: PiProductTools; projects: CodingProjectService; conversations: CodingConversationService; runtime: CodingConversationRuntime; host: CodingProductHost; sleep(reason: 'background_sleep' | 'auth_cleanup'): Promise; shutdown(): Promise; } export class CodingProductHostError extends Error { constructor( readonly status: 404 | 409, readonly code: string, message: string, ) { super(message); } } export interface CodingProductHostOptions { projects: Pick; productTools: PiProductTools; files?: CodingProjectFileService; listPiCommands?(conversationId: string): Promise; } function normalizePiCommands(value: unknown): ProductPiCommandInput[] { const record = value && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; const candidates = Array.isArray(value) ? value : Array.isArray(record?.commands) ? record.commands : Array.isArray(record?.data) ? record.data : []; return candidates.flatMap((candidate) => { if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return []; const command = candidate as Record; if (typeof command.name !== 'string') return []; return [{ name: command.name, ...(typeof command.description === 'string' ? { description: command.description } : {}), }]; }); } export function createCodingProductHost(options: CodingProductHostOptions): CodingProductHost { const files = options.files ?? new CodingProjectFileService(); async function activeProject(): Promise { const project = await options.projects.getActiveProject(); if (!project) { throw new CodingProductHostError( 409, 'CODING_ACTIVE_PROJECT_REQUIRED', 'No active coding project is selected', ); } return project; } async function selectedSkillIds( projectPath: string, agentId?: string, ): Promise { if (!agentId) return []; const config = await readCodingProjectConfigV2(projectPath); if (config.status !== 'valid') { throw new CodingProductHostError( 409, 'CODING_PROJECT_CONFIG_INVALID', 'Coding project configuration is unavailable', ); } const agent = config.config.agents.find((candidate) => ( candidate.id === agentId && candidate.enabled && !candidate.archivedAt )); if (!agent) { throw new CodingProductHostError( 404, 'CODING_AGENT_NOT_FOUND', 'Coding project Agent does not exist', ); } return agent.skillIds; } async function conversationContext(conversationId: string): Promise<{ project: ActiveCodingProject; skillIds: readonly string[]; }> { let context; try { context = await options.projects.findActiveConversation(conversationId); } catch (error) { if (error instanceof CodingProjectServiceError && (error.status === 404 || error.status === 409)) { throw new CodingProductHostError(error.status, error.code, error.message); } throw error; } const { project, conversation } = context; return { project, skillIds: await selectedSkillIds(project.path, conversation.agentId), }; } return { async fileStatus() { const project = await activeProject(); return await files.status(project.path); }, async findFiles(query, limit) { const project = await activeProject(); return await files.find(project.path, query, limit); }, async fileContent(filePath) { const project = await activeProject(); return await files.content(project.path, filePath); }, async searchText(pattern) { const project = await activeProject(); return await files.search(project.path, pattern); }, async listSkills(agentId) { const project = await activeProject(); return await options.productTools.listSkills( await selectedSkillIds(project.path, agentId), ); }, async listCommands(conversationId) { const context = await conversationContext(conversationId); const piCommands = options.listPiCommands ? normalizePiCommands(await options.listPiCommands(conversationId)) : []; return await options.productTools.listCommands(context.skillIds, piCommands); }, async getChanges(conversationId) { await conversationContext(conversationId); return options.productTools.getChanges(conversationId); }, }; }