183 lines
6.0 KiB
TypeScript
183 lines
6.0 KiB
TypeScript
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<CodingProjectFileEntry[]>;
|
|
findFiles(query: string, limit?: number): Promise<CodingProjectFileEntry[]>;
|
|
fileContent(path: string): Promise<CodingProjectFileContent>;
|
|
searchText(pattern: string): Promise<CodingTextSearchResult[]>;
|
|
listSkills(agentId?: string): Promise<ProductCodingSkill[]>;
|
|
listCommands(conversationId: string): Promise<ProductCodingCommand[]>;
|
|
getChanges(conversationId: string): Promise<ConversationChangesSnapshot | null>;
|
|
}
|
|
|
|
export interface CodingProductComposition {
|
|
attachments: CodingAttachmentStore;
|
|
productTools: PiProductTools;
|
|
projects: CodingProjectService;
|
|
conversations: CodingConversationService;
|
|
runtime: CodingConversationRuntime;
|
|
host: CodingProductHost;
|
|
shutdown(): Promise<void>;
|
|
}
|
|
|
|
export class CodingProductHostError extends Error {
|
|
constructor(
|
|
readonly status: 404 | 409,
|
|
readonly code: string,
|
|
message: string,
|
|
) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
export interface CodingProductHostOptions {
|
|
projects: Pick<CodingProjectService, 'getActiveProject' | 'findActiveConversation'>;
|
|
productTools: PiProductTools;
|
|
files?: CodingProjectFileService;
|
|
listPiCommands?(conversationId: string): Promise<unknown>;
|
|
}
|
|
|
|
function normalizePiCommands(value: unknown): ProductPiCommandInput[] {
|
|
const record = value && typeof value === 'object' && !Array.isArray(value)
|
|
? value as Record<string, unknown>
|
|
: 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<string, unknown>;
|
|
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<ActiveCodingProject> {
|
|
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<readonly string[]> {
|
|
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);
|
|
},
|
|
};
|
|
}
|