import { mkdir, rename, stat } from 'node:fs/promises'; import path from 'node:path'; import { BUNDLED_CODING_SKILL_IDS, type BundledCodingSkillId, } from '../../../shared/coding-skills'; import { atomicWriteJson, atomicWriteText } from '../../coding-projects/atomic-json'; import { validateSessionKey } from '../../coding-projects/conversation-store'; import type { PiProviderSelection } from './provider-config'; import type { PiManagedInputRevision } from './managed-input-revision'; const MANAGED_SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; 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; skillIds: readonly string[]; bundledSkillsDir: string; revision: PiManagedInputRevision; } export interface PiAgentResourceManifest { schemaVersion: 1; projectId: string; agentId: string; promptFile: string; skillIds: BundledCodingSkillId[]; revision: PiManagedInputRevision; } export interface PiAgentResourceSnapshot { paths: PiManagedPaths; projectSessionsDir: string; promptPath: string; manifestPath: string; skillIds: BundledCodingSkillId[]; skillPaths: string[]; revision: PiManagedInputRevision; summary: { projectId: string; agentId: string; skillIds: BundledCodingSkillId[]; revision: PiManagedInputRevision; }; } 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 normalizeSkillIds(skillIds: readonly string[]): BundledCodingSkillId[] { const result: BundledCodingSkillId[] = []; const seen = new Set(); for (const rawSkillId of skillIds) { const skillId = rawSkillId.trim(); if (!BUNDLED_CODING_SKILL_IDS.includes(skillId as BundledCodingSkillId)) { throw new Error(`Unknown bundled coding skill: ${skillId || '(empty)'}`); } if (seen.has(skillId)) continue; seen.add(skillId); result.push(skillId as BundledCodingSkillId); } return result; } export async function resolveExplicitCodingSkillPaths( bundledSkillsDir: string, skillIds: readonly string[], ): Promise<{ skillIds: BundledCodingSkillId[]; skillPaths: string[] }> { const normalizedSkillIds = normalizeSkillIds(skillIds); const root = path.resolve(bundledSkillsDir); const skillPaths = normalizedSkillIds.map((skillId) => path.join(root, skillId, 'SKILL.md')); await Promise.all(skillPaths.map(async (skillPath) => { const metadata = await stat(skillPath); if (!metadata.isFile()) throw new Error(`Bundled coding skill entry is not a file: ${skillPath}`); })); return { skillIds: normalizedSkillIds, 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, skillPaths } = await resolveExplicitCodingSkillPaths( options.bundledSkillsDir, options.skillIds, ); const promptPath = path.join(projectPromptsDir, `${agentId}.md`); const manifestPath = path.join(projectPromptsDir, `${agentId}.manifest.json`); const manifest: PiAgentResourceManifest = { schemaVersion: 1, projectId, agentId, promptFile: path.basename(promptPath), skillIds: [...skillIds], revision: { ...options.revision }, }; await atomicWriteText(promptPath, options.prompt); await atomicWriteJson(manifestPath, manifest); return { paths, projectSessionsDir, promptPath, manifestPath, skillIds: [...skillIds], skillPaths, revision: { ...options.revision }, summary: { projectId, agentId, skillIds: [...skillIds], revision: { ...options.revision }, }, }; } export function buildPiManagedInputArgs( selection: PiProviderSelection, resources: PiAgentResourceSnapshot, ): string[] { const args = [ '--provider', selection.runtimeProviderId, '--model', selection.modelId, '--thinking', selection.thinkingLevel, '--system-prompt', resources.promptPath, ]; for (const skillPath of resources.skillPaths) args.push('--skill', skillPath); return args; }