import { mkdir, rename, stat } from 'node:fs/promises'; import path from 'node:path'; import { atomicWriteJson, atomicWriteText } from '../../coding-projects/atomic-json'; import { validateSessionKey } from '../../coding-projects/conversation-store'; import { resolveBundledCodingPluginRootPaths } from '../../coding-plugins/manifest'; import type { PiProviderSelection } from './provider-config'; import type { PiManagedInputRevision } from './managed-input-revision'; import type { EffectivePluginSnapshot } from '../../coding-plugins/effective-resolver'; const MANAGED_SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; export interface PiSkillEntry { id: string; entryPath: string; packageRoot?: string; } export const MAKELORE_DEFAULT_LANGUAGE_PROMPT = `你正在 Makelore 中工作,产品默认服务中文用户。 语言要求: - 默认使用简体中文进行所有自然语言表达,包括可见的思考过程、进度说明、工具调用说明、提问、总结和最终回复。 - 代码、命令、标识符、文件路径、日志、错误原文,以及为保证准确性必须保留原文的引用,可以使用其原始语言。 - 只有当用户或项目智能体的自定义系统指令明确要求其他语言时,才改用对应语言。`; export interface BundledCodingSkillsPathInput { isPackaged: boolean; resourcesPath: string; appPath: string; } export interface PiManagedPaths { rootDir: string; configDir: string; modelsFile: string; sessionsDir: string; promptsDir: string; extensionsDir: string; logsDir: string; trashDir: string; } export interface MaterializePiAgentResourcesOptions { userDataDir: string; projectId: string; agentId: string; prompt: string; skillEntries: readonly PiSkillEntry[]; catalogRevision: number; bundledSkillsDir: string; /** Main-owned installed package roots for effective Marketplace Skills. */ skillRoots?: readonly string[]; /** The exact resolver output for this worker generation. */ effectiveSnapshot?: EffectivePluginSnapshot; revision: PiManagedInputRevision; } export interface PiAgentResourceManifest { schemaVersion: 1; projectId: string; agentId: string; promptFile: string; languagePromptFile: string; skillIds: string[]; skillEntries: PiSkillEntry[]; catalogRevision: number; revision: PiManagedInputRevision; effectivePluginSnapshot?: EffectivePluginSnapshot; } export interface PiAgentResourceSnapshot { paths: PiManagedPaths; projectSessionsDir: string; promptPath: string; languagePromptPath: string; manifestPath: string; skillIds: string[]; skillEntries: PiSkillEntry[]; skillPaths: string[]; catalogRevision: number; revision: PiManagedInputRevision; effectivePluginSnapshot?: EffectivePluginSnapshot; summary: { projectId: string; agentId: string; skillIds: string[]; skillEntries: PiSkillEntry[]; catalogRevision: number; revision: PiManagedInputRevision; effectivePluginSnapshot?: EffectivePluginSnapshot; }; } function managedSegment(value: string, name: string): string { const normalized = value.trim(); if (!MANAGED_SEGMENT_PATTERN.test(normalized)) { throw new Error(`${name} is not a valid managed resource segment`); } return normalized; } export function resolveBundledCodingSkillsDir(input: BundledCodingSkillsPathInput): string { return input.isPackaged ? path.join(input.resourcesPath, 'resources', 'coding-skills') : path.join(input.appPath, 'resources', 'coding-skills'); } export function getPiManagedPaths(userDataDir: string): PiManagedPaths { const rootDir = path.join(path.resolve(userDataDir), 'coding-runtime', 'pi'); const configDir = path.join(rootDir, 'config'); return { rootDir, configDir, modelsFile: path.join(configDir, 'models.json'), sessionsDir: path.join(rootDir, 'sessions'), promptsDir: path.join(rootDir, 'prompts'), extensionsDir: path.join(rootDir, 'extensions'), logsDir: path.join(rootDir, 'logs'), trashDir: path.join(rootDir, 'trash'), }; } export async function ensurePiManagedPaths(userDataDir: string): Promise { const paths = getPiManagedPaths(userDataDir); await Promise.all([ mkdir(paths.configDir, { recursive: true }), mkdir(paths.sessionsDir, { recursive: true }), mkdir(paths.promptsDir, { recursive: true }), mkdir(paths.extensionsDir, { recursive: true }), mkdir(paths.logsDir, { recursive: true }), mkdir(paths.trashDir, { recursive: true }), ]); return paths; } export async function archivePiConversationSession(input: { userDataDir: string; projectId: string; sessionKey: string; }): Promise { const projectId = managedSegment(input.projectId, 'Project id'); const sessionKey = validateSessionKey(input.sessionKey); const paths = getPiManagedPaths(input.userDataDir); const source = path.join(paths.sessionsDir, projectId, `${sessionKey}.jsonl`); const targetDirectory = path.join(paths.trashDir, projectId); const target = path.join(targetDirectory, `${sessionKey}.jsonl`); await mkdir(targetDirectory, { recursive: true }); try { await rename(source, target); return target; } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; const alreadyArchived = await stat(target).catch((targetError: NodeJS.ErrnoException) => { if (targetError.code === 'ENOENT') return null; throw targetError; }); return alreadyArchived?.isFile() ? target : null; } } function normalizeSkillEntries(skillEntries: readonly PiSkillEntry[]): PiSkillEntry[] { const result: PiSkillEntry[] = []; const seen = new Set(); for (const rawEntry of skillEntries) { if (!rawEntry || typeof rawEntry.id !== 'string' || typeof rawEntry.entryPath !== 'string') { throw new Error('Effective coding Skill entry is invalid'); } const skillId = rawEntry.id.trim(); const entryPath = rawEntry.entryPath.trim().replaceAll('\\', '/'); if (!skillId || !entryPath) throw new Error('Effective coding Skill entry is invalid'); const packageRoot = typeof rawEntry.packageRoot === 'string' ? rawEntry.packageRoot.trim() : undefined; if (rawEntry.packageRoot !== undefined && !packageRoot) { throw new Error('Effective coding Skill package root is invalid'); } if (entryPath.startsWith('/') || /^[A-Za-z]:\//u.test(entryPath) || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(entryPath)) { throw new Error(`Effective coding Skill entry path must be relative: ${entryPath}`); } const relative = path.posix.normalize(entryPath); if (!relative || relative === '.' || relative === '..' || relative.startsWith('../')) { throw new Error(`Effective coding Skill entry path escapes its package: ${entryPath}`); } if (seen.has(skillId)) continue; seen.add(skillId); result.push({ id: skillId, entryPath: relative, ...(packageRoot ? { packageRoot: path.resolve(packageRoot) } : {}), }); } return result; } function catalogRevision(value: number): number { if (!Number.isSafeInteger(value) || value < 0) { throw new Error('Catalog revision must be a non-negative safe integer'); } return value; } function pathWithin(root: string, entryPath: string): string | null { const resolvedRoot = path.resolve(root); const resolved = path.resolve(resolvedRoot, ...entryPath.split('/')); const relative = path.relative(resolvedRoot, resolved); if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) { return null; } return resolved; } async function resolveSkillEntryPath( bundledSkillsDir: string, entry: PiSkillEntry, skillRoots: readonly string[] = [], ): Promise { const roots = entry.packageRoot ? [path.resolve(entry.packageRoot)] : [ path.resolve(bundledSkillsDir), ...resolveBundledCodingPluginRootPaths(path.join( path.dirname(path.resolve(bundledSkillsDir)), 'coding-plugins', )), ...skillRoots.map((root) => path.resolve(root)), ]; for (const root of roots) { const candidate = pathWithin(root, entry.entryPath); if (!candidate) continue; try { const metadata = await stat(candidate); if (metadata.isFile()) return candidate; } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } } throw new Error(`Bundled coding Skill entry is not a file: ${entry.entryPath}`); } export async function resolveExplicitCodingSkillPaths( bundledSkillsDir: string, skillEntries: readonly PiSkillEntry[], skillRoots: readonly string[] = [], ): Promise<{ skillIds: string[]; skillEntries: PiSkillEntry[]; skillPaths: string[] }> { const normalizedEntries = normalizeSkillEntries(skillEntries); const skillPaths = await Promise.all(normalizedEntries.map((entry) => ( resolveSkillEntryPath(bundledSkillsDir, entry, skillRoots) ))); return { skillIds: normalizedEntries.map(({ id }) => id), skillEntries: normalizedEntries, skillPaths, }; } export async function materializePiAgentResources( options: MaterializePiAgentResourcesOptions, ): Promise { const projectId = managedSegment(options.projectId, 'Project id'); const agentId = managedSegment(options.agentId, 'Agent id'); const paths = await ensurePiManagedPaths(options.userDataDir); const projectSessionsDir = path.join(paths.sessionsDir, projectId); const projectPromptsDir = path.join(paths.promptsDir, projectId); await Promise.all([ mkdir(projectSessionsDir, { recursive: true }), mkdir(projectPromptsDir, { recursive: true }), ]); const { skillIds, skillEntries, skillPaths } = await resolveExplicitCodingSkillPaths( options.bundledSkillsDir, options.skillEntries, options.skillRoots, ); const resolvedCatalogRevision = catalogRevision(options.catalogRevision); const promptPath = path.join(projectPromptsDir, `${agentId}.md`); const languagePromptPath = path.join(projectPromptsDir, `${agentId}.language.md`); const manifestPath = path.join(projectPromptsDir, `${agentId}.manifest.json`); const manifest: PiAgentResourceManifest = { schemaVersion: 1, projectId, agentId, promptFile: path.basename(promptPath), languagePromptFile: path.basename(languagePromptPath), skillIds: [...skillIds], skillEntries: structuredClone(skillEntries), catalogRevision: resolvedCatalogRevision, revision: { ...options.revision }, ...(options.effectiveSnapshot ? { effectivePluginSnapshot: structuredClone(options.effectiveSnapshot) } : {}), }; await Promise.all([ atomicWriteText(promptPath, options.prompt), atomicWriteText(languagePromptPath, MAKELORE_DEFAULT_LANGUAGE_PROMPT), ]); await atomicWriteJson(manifestPath, manifest); return { paths, projectSessionsDir, promptPath, languagePromptPath, manifestPath, skillIds: [...skillIds], skillEntries: structuredClone(skillEntries), skillPaths, catalogRevision: resolvedCatalogRevision, revision: { ...options.revision }, ...(options.effectiveSnapshot ? { effectivePluginSnapshot: structuredClone(options.effectiveSnapshot) } : {}), summary: { projectId, agentId, skillIds: [...skillIds], skillEntries: structuredClone(skillEntries), catalogRevision: resolvedCatalogRevision, revision: { ...options.revision }, ...(options.effectiveSnapshot ? { effectivePluginSnapshot: structuredClone(options.effectiveSnapshot) } : {}), }, }; } export function buildPiManagedInputArgs( selection: PiProviderSelection, resources: PiAgentResourceSnapshot, ): string[] { const args = [ '--provider', selection.runtimeProviderId, '--model', selection.modelId, '--thinking', selection.thinkingLevel, '--system-prompt', resources.promptPath, '--append-system-prompt', resources.languagePromptPath, ]; for (const skillPath of resources.skillPaths) args.push('--skill', skillPath); return args; }