/**
 * Per-tool contract for `modify_skill`.
 *
 * Skill directories are mounted read-only in Modal bash. This host-mediated
 * tool is the single approved write path, so the LLM API intentionally mirrors
 * the paths already shown in the system prompt instead of asking the model to
 * split paths into storage scope/team fields.
 */
import { Type } from '@sinclair/typebox';
import { HostToolResultStatus, buildRendererArtifact } from '../../../contracts/host-tool.js';
import { withAbort } from '../../../utils/abort.js';
import { truncateOutput } from '../../../utils/truncate.js';
import { MODIFY_SKILL } from '../tool-names.js';
export const MODIFY_SKILL_KIND = 'modifySkill';
export const ModifySkillStorageContext = {
    Team: 'team',
    User: 'user'
};
export const MODIFY_SKILL_TOOL_DESCRIPTION = `
Create or edit a user/team skill. 
Use the skill path shown in the Skills section, such as /skills/user/foo or /skills/team/<team-uuid>/foo.
writeFiles and deleteFiles are relative to the skill folder. Include SKILL.md and any helper files the skill needs.
`.trim();
const relativePathSchema = Type.String({
    description: 'Path relative to the skill folder, e.g. SKILL.md or scripts/helper.py.'
});
const modifySkillLlmSchema = Type.Object({
    skillPath: Type.String({
        description: 'Skill folder path from the system prompt, e.g. /skills/user/foo or /skills/team/<team-uuid>/foo.'
    }),
    writeFiles: Type.Optional(Type.Array(Type.Object({
        path: relativePathSchema,
        content: Type.String({ description: 'Full replacement content for this file.' })
    }), { description: 'Files to create or replace under the skill folder.' })),
    deleteFiles: Type.Optional(Type.Array(relativePathSchema, {
        description: 'Files to delete under the skill folder.'
    }))
});
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
// Team owner ids are the path/resource-id segment emitted in the system
// prompt. They may be UUIDs in production or symbolic ids in local fixtures, so
// this mirrors client/shared/src/lib/skills/resource-id.ts instead of imposing
// a hidden stricter format after the prompt already exposed the path.
//
// The prompt builder in client/shared/src/lib/shortcutxl/per-user-context/
// skills-format.ts is intentionally stricter (UUID-only) when deciding which
// team paths to *show*. This parser stays permissive on purpose: server-v1 is
// the authorization source of truth, so accepting whatever path the prompt
// exposed avoids a hidden second gate here.
const TEAM_OWNER_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
/** Validate a file path relative to a skill folder (no absolute or `.`/`..` escapes). */
export function isValidRelativePath(value) {
    if (typeof value !== 'string' || value.length === 0 || value.length > 512)
        return false;
    if (value.startsWith('/') || value.includes('\\'))
        return false;
    const parts = value.split('/');
    return parts.every((part) => part.length > 0 && part !== '.' && part !== '..');
}
/**
 * Parse only the runtime paths the system prompt exposes for skills.
 * This is intentionally stricter than a generic path check: writes are only
 * allowed to a single user/team skill folder, never to `/skills` broadly.
 * Shared by both `modify_skill` and `read_skill`.
 */
export function parseModifySkillPath(value) {
    if (typeof value !== 'string')
        return null;
    const normalized = value.replace(/\/+$/, '');
    const userMatch = normalized.match(/^\/skills\/user\/([^/]+)$/);
    if (userMatch?.[1] && SKILL_NAME_PATTERN.test(userMatch[1])) {
        return {
            storageContext: ModifySkillStorageContext.User,
            skillName: userMatch[1],
            skillPath: normalized
        };
    }
    const teamMatch = normalized.match(/^\/skills\/team\/([^/]+)\/([^/]+)$/);
    if (teamMatch?.[1] &&
        teamMatch?.[2] &&
        TEAM_OWNER_ID_PATTERN.test(teamMatch[1]) &&
        SKILL_NAME_PATTERN.test(teamMatch[2])) {
        return {
            storageContext: ModifySkillStorageContext.Team,
            teamId: teamMatch[1],
            skillName: teamMatch[2],
            skillPath: normalized
        };
    }
    return null;
}
function hasOperations(candidate) {
    return ((Array.isArray(candidate.writeFiles) && candidate.writeFiles.length > 0) ||
        (Array.isArray(candidate.deleteFiles) && candidate.deleteFiles.length > 0));
}
function validateWriteFile(value) {
    if (!value || typeof value !== 'object')
        return false;
    const candidate = value;
    return isValidRelativePath(candidate.path) && typeof candidate.content === 'string';
}
function validateParams(value) {
    if (!value || typeof value !== 'object')
        return false;
    const candidate = value;
    return (parseModifySkillPath(candidate.skillPath) !== null &&
        (candidate.writeFiles === undefined ||
            (Array.isArray(candidate.writeFiles) && candidate.writeFiles.every(validateWriteFile))) &&
        (candidate.deleteFiles === undefined ||
            (Array.isArray(candidate.deleteFiles) && candidate.deleteFiles.every(isValidRelativePath))) &&
        hasOperations(candidate));
}
function validateResultDetails(value) {
    if (value === undefined)
        return true;
    if (!value || typeof value !== 'object')
        return false;
    const candidate = value;
    return ((candidate.deleted_count === undefined || typeof candidate.deleted_count === 'number') &&
        (candidate.failed_count === undefined || typeof candidate.failed_count === 'number') &&
        (candidate.file_results === undefined ||
            (Array.isArray(candidate.file_results) &&
                candidate.file_results.every((file) => file !== null &&
                    typeof file === 'object' &&
                    typeof file.path === 'string' &&
                    (file.operation === 'write' || file.operation === 'delete') &&
                    (file.status === 'succeeded' || file.status === 'failed') &&
                    (file.error === undefined || typeof file.error === 'string')))) &&
        (candidate.skill_name === undefined || typeof candidate.skill_name === 'string') &&
        (candidate.skill_path === undefined || typeof candidate.skill_path === 'string') &&
        (candidate.storage_context === undefined ||
            candidate.storage_context === ModifySkillStorageContext.User ||
            candidate.storage_context === ModifySkillStorageContext.Team) &&
        (candidate.team_id === undefined || typeof candidate.team_id === 'string') &&
        (candidate.touched_files === undefined ||
            (Array.isArray(candidate.touched_files) &&
                candidate.touched_files.every((file) => file !== null &&
                    typeof file === 'object' &&
                    typeof file.path === 'string' &&
                    (file.operation === 'added' ||
                        file.operation === 'modified' ||
                        file.operation === 'deleted') &&
                    typeof file.added_lines === 'number' &&
                    typeof file.removed_lines === 'number'))) &&
        (candidate.written_count === undefined || typeof candidate.written_count === 'number'));
}
function normalizeResultDetails(value) {
    return value === undefined ? undefined : { ...value };
}
function getResultObservabilityFields(details) {
    if (!validateResultDetails(details) || details === undefined)
        return undefined;
    return {
        ...(details.storage_context !== undefined ? { storage_context: details.storage_context } : {}),
        ...(details.written_count !== undefined ? { written_count: details.written_count } : {}),
        ...(details.deleted_count !== undefined ? { deleted_count: details.deleted_count } : {}),
        ...(details.failed_count !== undefined ? { failed_count: details.failed_count } : {})
    };
}
function evaluateApproval() {
    return {
        required: true,
        reason: 'Skill modifications require user approval.'
    };
}
function formatResult(details, isError, userMessage) {
    if (isError) {
        return {
            content: [
                {
                    type: 'text',
                    text: truncateOutput(userMessage ?? 'Skill modification failed.')
                }
            ],
            details: buildRendererArtifact(details, userMessage),
            isError: true
        };
    }
    const written = details?.written_count ?? 0;
    const deleted = details?.deleted_count ?? 0;
    const failed = details?.failed_count ?? 0;
    return {
        content: [
            {
                type: 'text',
                text: `Skill file operations complete. Files written: ${written}. Files deleted: ${deleted}. Failed: ${failed}.`
            }
        ],
        details: details ?? {}
    };
}
function createLlmTool(options) {
    return {
        name: MODIFY_SKILL,
        label: MODIFY_SKILL,
        isReadOnly: false,
        description: MODIFY_SKILL_TOOL_DESCRIPTION,
        parameters: modifySkillLlmSchema,
        async execute(toolCallId, llmArgs, signal) {
            const args = llmArgs;
            const params = {
                skillPath: args.skillPath,
                writeFiles: args.writeFiles,
                deleteFiles: args.deleteFiles
            };
            const request = {
                approval: evaluateApproval(),
                heartbeat: {
                    label: 'Modifying skill...',
                    toolName: MODIFY_SKILL
                },
                kind: MODIFY_SKILL_KIND,
                params,
                requestId: toolCallId
            };
            const result = await withAbort(signal, async () => options.pendingHostToolRequests.request(request));
            const details = result.details;
            const isError = result.status === HostToolResultStatus.Failed ||
                result.status === HostToolResultStatus.Cancelled;
            return formatResult(details, isError, result.userMessage);
        }
    };
}
export const modifySkillContract = {
    kind: MODIFY_SKILL_KIND,
    createLlmTool,
    evaluateApproval,
    getResultObservabilityFields,
    validateParams,
    validateResultDetails,
    normalizeResultDetails
};
//# sourceMappingURL=contract.js.map