/**
 * Graph Management Tools for Pi-Mono Agent
 *
 * These tools allow the agent to interact with the knowledge graph:
 * - Create, read, update, delete entities
 * - Search and filter
 * - Manage wikilinks
 * - Upload assets
 */
import { Type as T } from 'typebox';
import { ENTITY_SCHEMAS } from '../../core/validation.js';
import { describeZodObject } from '../../core/schema-registry.js';
import { lintWorkspace } from '../../services/lint-service.js';
import { findOrphans } from '../../services/orphan-service.js';
import * as access from '../../services/access-control.js';
import { isCuratedEntityType } from '../../core/entity-types-catalog.js';
import { getAgentAllowedEntityTypes } from '../prompts/entity-types-section.js';
import { BridgeError, assertCanChangeUserHandle, validateUserHandleInPayload } from '../../services/user-person-bridge.js';
import { buildActor, prepareCreatePayload, prepareUpdatePayload, diagnosedUpdate, DroppedAssetError, } from '../../services/entity-mutations.js';
import { listRepoAssets } from '../../services/asset-listing.js';
import { assetEmbed } from '../../core/embeds.js';
/**
 * Helper to wrap tool results in AgentToolResult format
 */
export function wrapToolResult(result) {
    const text = JSON.stringify(result, null, 2);
    return {
        content: [{ type: 'text', text }],
        details: result,
    };
}
/**
 * Strip entity_type/entity_id from data to avoid confusing the LLM
 * (these are already surfaced as top-level entityType/entityId fields)
 */
function cleanData(data) {
    const { entity_type, entity_id, ...rest } = data;
    return rest;
}
/**
 * Undo JSON-string escapes that sometimes leak into content when the LLM
 * serializes markdown as a quoted string instead of raw text. Handles `\n`,
 * `\t`, `\r`, `\"`, `\\` in a single pass so escape sequences aren't
 * re-processed. Also strips stray surrounding quotes from a fully-quoted
 * payload.
 */
function sanitizeLlmContent(content) {
    if (!content)
        return '';
    const unescaped = content.replace(/\\([ntr"\\])/g, (_, c) => {
        if (c === 'n')
            return '\n';
        if (c === 't')
            return '\t';
        if (c === 'r')
            return '\r';
        return c;
    });
    return unescaped.replace(/^"+|"+$/g, '');
}
/**
 * Per-request user context for folder access filtering.
 * When getCurrentUser returns a user, tools filter results to only
 * folders the user is a member of (via folder_access).
 */
function getAccessibleRepoNames(workspaceManager, getCurrentUser, authService) {
    const user = getCurrentUser();
    if (!user)
        return null; // API key / open mode → no filtering
    if (!authService)
        return null;
    // Delegate to the shared helper so personal-folder restrictions and
    // owner-on-shared-folder semantics apply consistently with the HTTP layer.
    return new Set(access.getAccessibleRepos(user, workspaceManager, authService).map(r => r.name));
}
function canAccessRepo(repoName, workspaceManager, getCurrentUser, authService) {
    const user = getCurrentUser();
    if (!user)
        return true;
    if (!authService)
        return true;
    return access.hasRepoAccess(user, repoName, workspaceManager, authService);
}
export function createGraphTools(workspaceManager, gitUser, workspacePath, onHighlight, onMutation, getCurrentUser, authService, 
/**
 * Called at the start (`enter`) and end (`leave`) of any mutation tool
 * call. Surfaces the active-agent chip in the sidebar via the WS hub. The
 * orchestrator wires this through to wsHub.handleAgentActivity.
 */
onAgentActivity) {
    const getUser = getCurrentUser ?? (() => null);
    const authSvc = authService ?? null;
    /**
     * Resolve the identity to stamp on writes. If the tool call is running on
     * behalf of a real user (chat / quick-capture from the web), credit them
     * by their handle via the shared `buildActor` so MCP and agent surfaces
     * produce byte-identical git authors for the same user. Fall back to the
     * orchestrator-level gitUser for unauthenticated contexts (CLI, tests,
     * server-only automations).
     */
    function effectiveGitUser() {
        const user = getUser();
        if (user?.handle)
            return buildActor(user);
        return gitUser;
    }
    /** Resolve "private" or "entries" to the current user's actual private folder name. */
    function resolvePrivateRepo(repoName) {
        if (repoName !== 'private' && repoName !== 'entries')
            return repoName;
        const user = getUser();
        if (!user)
            return repoName;
        const personalRepo = workspaceManager.getUserPersonalRepo(user.id);
        return personalRepo?.name ?? repoName;
    }
    /**
     * Eager skills are inlined into every system prompt for everyone with folder
     * access. Creating one in a shared folder requires admin/owner so a rogue
     * member can't bloat everyone's context. Eager skills in the user's own
     * private folder are fine — they only bloat that user's own prompt.
     * Returns an error string or null if the operation is allowed.
     */
    function checkEagerSkillPermission(entityType, data, repoName) {
        if (entityType !== 'skill')
            return null;
        if (!data || data.loading !== 'eager')
            return null;
        const user = getUser();
        if (!user)
            return null; // non-authenticated contexts (CLI, tests) bypass
        const personalRepo = workspaceManager.getUserPersonalRepo(user.id);
        if (personalRepo && personalRepo.name === repoName)
            return null;
        if (authSvc?.isFolderAdmin(user.id, repoName))
            return null;
        if (user.role === 'owner')
            return null;
        return `Creating an eager skill in a shared folder requires folder admin or workspace owner. Save this skill to your private folder (repo: "private") or set loading to "on-demand".`;
    }
    // Compute the set of entity types the agent is allowed to create in this
    // workspace: the full canonical catalog plus any custom types defined via
    // schema_extensions. The launch-time allowlist was removed — the catalog
    // now grows with the user via the New Entry picker's progressive disclosure.
    const builtinTypeKeys = new Set(Object.keys(ENTITY_SCHEMAS));
    const customWorkspaceTypes = workspaceManager.getSchemaRegistry().getAllTypes()
        .filter(t => !builtinTypeKeys.has(t));
    const agentAllowedTypes = getAgentAllowedEntityTypes(customWorkspaceTypes);
    const agentAllowedTypeSet = new Set(agentAllowedTypes);
    function rejectDisallowedCreateType(entityType) {
        if (agentAllowedTypeSet.has(entityType))
            return null;
        if (entityType === 'asset') {
            // The schema enum already excludes `asset` (see
            // getAgentAllowedEntityTypes). This is the runtime defense-in-depth
            // path for clients that bypass the enum.
            return wrapToolResult({
                success: false,
                error: 'Asset entries cannot be created via create_entity — they require a binary blob plus a sidecar text file. Use the import_file tool with a local file path, or have the user upload via the workspace import dialog.',
            });
        }
        const hint = isCuratedEntityType(entityType)
            ? '' // shouldn't happen for any other curated type — they're all allowed
            : ` Allowed types: ${agentAllowedTypes.join(', ')}.`;
        return wrapToolResult({
            success: false,
            error: `Entry type "${entityType}" is not a known canonical or workspace custom type.${hint}`,
        });
    }
    return [
        // Create entity
        {
            name: 'create_entity',
            label: 'Create Entry',
            description: 'Create a new entry in a folder. Always validate required fields before calling. Folder is auto-resolved for app-defined entry types.',
            parameters: T.Object({
                repo: T.Optional(T.String({ description: 'Folder name (auto-resolved for app entity types)' })),
                entityType: T.Union(agentAllowedTypes.map(t => T.Literal(t)), { description: `Entity type. Must be one of: ${agentAllowedTypes.join(', ')}.` }),
                entityId: T.String({ description: 'Unique entity ID in kebab-case' }),
                data: T.Object({}, { description: 'Entity data (YAML frontmatter)', additionalProperties: true }),
                content: T.Optional(T.String({ description: 'Markdown content (optional)' })),
                folder_path: T.Optional(T.String({ description: 'Repo-relative subfolder for the entry (e.g. "clients/acme"). Omit to land at repo root. Parent folders are auto-created.' })),
                commitMessage: T.Optional(T.String({ description: 'Custom commit message (optional)' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    // Defense in depth: LLMs occasionally pass strings that don't match the
                    // enum schema. Runtime check ensures the launch allowlist is enforced
                    // even if the schema layer is bypassed.
                    const typeRejection = rejectDisallowedCreateType(params.entityType);
                    if (typeRejection)
                        return typeRejection;
                    let repoName = params.repo;
                    if (!repoName) {
                        return wrapToolResult({ success: false, error: `repo is required — must specify which folder to use for type "${params.entityType}"` });
                    }
                    repoName = resolvePrivateRepo(repoName);
                    if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
                    }
                    const eagerError = checkEagerSkillPermission(params.entityType, params.data, repoName);
                    if (eagerError)
                        return wrapToolResult({ success: false, error: eagerError });
                    const graph = workspaceManager.getGraph(repoName);
                    // Workspace-level dedup: warn if an entity with this ID already exists elsewhere
                    const existing = workspaceManager.findEntityById(params.entityId);
                    if (existing && existing.repoName !== repoName) {
                        return wrapToolResult({ success: false, error: `An entity with ID "${params.entityId}" already exists in the "${existing.repoName}" folder (type: ${existing.data?.entity_type}). Use a unique ID or link to the existing entity with [[${params.entityId}]].` });
                    }
                    const actor = effectiveGitUser();
                    const sanitizedContent = sanitizeLlmContent(params.content);
                    // Workspace-wide uniqueness check for `user_handle` on person
                    // entities (one user ↔ one person, workspace-wide), plus owner-
                    // only authz so a non-owner agent session can't stamp a
                    // pre-linked person via generic create_entity.
                    if (params.entityType === 'person') {
                        try {
                            const incoming = params.data ?? {};
                            validateUserHandleInPayload(workspaceManager, incoming);
                            if (Object.prototype.hasOwnProperty.call(incoming, 'user_handle')) {
                                assertCanChangeUserHandle(getUser(), '', incoming.user_handle);
                            }
                        }
                        catch (err) {
                            if (err instanceof BridgeError) {
                                return wrapToolResult({ success: false, error: err.message, code: err.code, ...err.details });
                            }
                            throw err;
                        }
                    }
                    const options = prepareCreatePayload({
                        entityType: params.entityType,
                        entityId: params.entityId,
                        data: params.data ?? {},
                        content: sanitizedContent,
                        folderPath: params.folder_path,
                        commitMessage: params.commitMessage,
                    }, actor);
                    // Surface a live "<user>'s agent is acting" chip in the sidebar for
                    // the duration of the mutation. `leave` is always emitted via the
                    // inner finally so a thrown error doesn't leave the chip stuck.
                    onAgentActivity?.('enter', repoName);
                    try {
                        const entity = await graph.create(options);
                        onMutation?.({ action: 'created', repo: repoName, entityType: params.entityType, entityId: params.entityId });
                        return wrapToolResult({
                            success: true,
                            entity: {
                                entityId: entity.id,
                                entityType: entity.entityType,
                                repo: repoName,
                                path: entity.path,
                                data: cleanData(entity.data),
                            },
                            message: `Created ${params.entityType}: ${params.entityId} in ${repoName}. Auto-committed — ready to push.`,
                        });
                    }
                    finally {
                        onAgentActivity?.('leave', repoName);
                    }
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Get entity
        {
            name: 'get_entity',
            label: 'Get Entry',
            description: 'Retrieve full entry details. Use repo, entityType, entityId from search/list results. entityType is optional — if omitted it is read from the file.',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name' }),
                entityType: T.Optional(T.String({ description: 'Entity type (optional — resolved from frontmatter if omitted)' })),
                entityId: T.String({ description: 'Entity ID' }),
            }),
            execute: async (toolCallId, params) => {
                try {
                    const repo = resolvePrivateRepo(params.repo);
                    if (!canAccessRepo(repo, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${repo}" not found` });
                    }
                    const graph = workspaceManager.getGraph(repo);
                    // If the agent passes the wrong entityType (e.g. based on stale
                    // search results), graph.get computes the wrong filesystem path
                    // and throws. Fall back to getById, which scans all type subdirs
                    // under the folder and resolves the actual location.
                    let entity;
                    if (params.entityType) {
                        try {
                            entity = graph.get(params.entityType, params.entityId);
                        }
                        catch {
                            entity = graph.getById(params.entityId);
                        }
                    }
                    else {
                        entity = graph.getById(params.entityId);
                    }
                    // Prefer the live editor body when a seeded Y.Doc is open. graph.get
                    // reads disk, which lags the editor by the autosave debounce — a
                    // user's freshly-placed image embed lives in the Y.Doc before it's
                    // committed. Returning disk content is what let the agent edit then
                    // resend a body missing those embeds, silently dropping images.
                    const liveBody = graph.getLiveBody(entity.entityType, entity.id);
                    return wrapToolResult({
                        success: true,
                        entity: {
                            entityId: entity.id,
                            entityType: entity.entityType,
                            repo,
                            path: entity.path,
                            data: cleanData(entity.data),
                            content: liveBody ?? entity.document.content,
                            wikilinks: entity.document.wikilinks,
                        },
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Update entity
        {
            name: 'update_entity',
            label: 'Update Entry',
            description: 'Update an existing entry. Folder is auto-resolved for app-defined types.',
            parameters: T.Object({
                repo: T.Optional(T.String({ description: 'Folder name (auto-resolved for app entity types)' })),
                entityType: T.String({ description: 'Entity type' }),
                entityId: T.String({ description: 'Entity ID' }),
                data: T.Optional(T.Object({}, { description: 'Updated data fields', additionalProperties: true })),
                content: T.Optional(T.String({ description: 'Updated markdown body (replaces the entire body)' })),
                commitMessage: T.Optional(T.String({ description: 'Custom commit message' })),
                allowImageRemoval: T.Optional(T.Boolean({ description: 'Set true only to deliberately delete image/asset embeds the document currently has; otherwise such a whole-body write is rejected (ASSET_REMOVAL_BLOCKED). Prefer get_entity then patch_entity for small edits.' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    let repoName = params.repo;
                    if (!repoName) {
                        return wrapToolResult({ success: false, error: `repo is required — must specify which folder to use for type "${params.entityType}"` });
                    }
                    repoName = resolvePrivateRepo(repoName);
                    if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
                    }
                    const eagerError = checkEagerSkillPermission(params.entityType, params.data, repoName);
                    if (eagerError)
                        return wrapToolResult({ success: false, error: eagerError });
                    const graph = workspaceManager.getGraph(repoName);
                    const actor = effectiveGitUser();
                    const sanitizedContent = params.content !== undefined
                        ? sanitizeLlmContent(params.content)
                        : params.content;
                    // Workspace-wide uniqueness check + owner-only authz for
                    // `user_handle` on person entities. Generic update_entity must
                    // not be a back door around the ownerOnly() gate on the dedicated
                    // link/unlink tools — otherwise a member could ask the agent to
                    // "set this person's user_handle to mine" and bypass the gate.
                    if (params.entityType === 'person' && params.data) {
                        try {
                            validateUserHandleInPayload(workspaceManager, params.data, { repo: repoName, entityId: params.entityId });
                            if (Object.prototype.hasOwnProperty.call(params.data, 'user_handle')) {
                                let currentHandle = '';
                                try {
                                    const current = graph.get('person', params.entityId);
                                    currentHandle = (current?.data?.user_handle ?? '');
                                }
                                catch { /* new entity / missing — treat as empty */ }
                                assertCanChangeUserHandle(getUser(), currentHandle, params.data.user_handle);
                            }
                        }
                        catch (err) {
                            if (err instanceof BridgeError) {
                                return wrapToolResult({ success: false, error: err.message, code: err.code, ...err.details });
                            }
                            throw err;
                        }
                    }
                    const options = prepareUpdatePayload({
                        entityType: params.entityType,
                        entityId: params.entityId,
                        data: params.data,
                        content: sanitizedContent,
                        commitMessage: params.commitMessage,
                    }, actor);
                    onAgentActivity?.('enter', repoName);
                    try {
                        // Shared diagnosed update (c6ef9297 contract): throws
                        // SilentDropError when the caller intended a change that left the
                        // file byte-identical, surfaced via the outer catch so the agent
                        // isn't misled by a success envelope.
                        const { entity, frontmatter_changed, content_changed, noop, broadcast } = await diagnosedUpdate(graph, options, { data: params.data, content: sanitizedContent }, { allowAssetRemoval: params.allowImageRemoval });
                        onMutation?.({ action: 'updated', repo: repoName, entityType: params.entityType, entityId: params.entityId });
                        return wrapToolResult({
                            success: true,
                            entity: {
                                entityId: entity.id,
                                entityType: entity.entityType,
                                repo: repoName,
                                data: cleanData(entity.data),
                            },
                            frontmatter_changed,
                            content_changed,
                            noop,
                            broadcast,
                            message: noop
                                ? `No change to ${params.entityType}: ${params.entityId} — on-disk state already matched.`
                                : `Updated ${params.entityType}: ${params.entityId}. Auto-committed — ready to push.`,
                        });
                    }
                    finally {
                        onAgentActivity?.('leave', repoName);
                    }
                }
                catch (error) {
                    if (error instanceof DroppedAssetError) {
                        return wrapToolResult({
                            success: false,
                            code: 'ASSET_REMOVAL_BLOCKED',
                            droppedAssets: error.dropped,
                            error: error.message,
                        });
                    }
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // List assets already uploaded to a folder
        {
            name: 'list_assets',
            label: 'List Assets',
            description: 'List images/files already uploaded to a folder, with a ready-to-paste markdown embed for each. ' +
                'Use this to find an asset a user placed (e.g. a screenshot) so you can embed it into an entry — ' +
                'assets live in storage (R2/local), NOT on the local filesystem, so import_file cannot find them. ' +
                'Each result has an `embed` string: use it verbatim and NEVER write ![[filename]] transclusion syntax ' +
                '(it does not render). Optionally filter by entityType/entityId. This tool only references existing ' +
                'assets — uploads happen via the workspace UI or import_file.',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name' }),
                entityType: T.Optional(T.String({ description: 'Filter to assets owned by entries of this type' })),
                entityId: T.Optional(T.String({ description: 'Filter to assets owned by this entry id' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    const repoName = resolvePrivateRepo(params.repo);
                    if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
                    }
                    let assets = await listRepoAssets(repoName, workspaceManager);
                    if (params.entityType)
                        assets = assets.filter(a => a.entityType === params.entityType);
                    if (params.entityId)
                        assets = assets.filter(a => a.entityId === params.entityId);
                    return wrapToolResult({
                        success: true,
                        repo: repoName,
                        count: assets.length,
                        assets: assets.map(a => ({
                            asset_id: a.assetId,
                            filename: a.filename,
                            mime_type: a.contentType,
                            entityType: a.entityType,
                            entityId: a.entityId,
                            entityName: a.entityName,
                            url: a.url,
                            embed: a.embed,
                        })),
                    });
                }
                catch (error) {
                    return wrapToolResult({ success: false, error: error instanceof Error ? error.message : 'Unknown error' });
                }
            },
        },
        // Embed an already-uploaded asset into an entry body
        {
            name: 'attach_asset_to_entity',
            label: 'Attach Asset',
            description: 'Embed an already-uploaded asset (from list_assets) into an entry body using its render-safe markdown, ' +
                'without rewriting the whole body. mode "append" adds it at the end; mode "replace_placeholder" swaps dead ' +
                'placeholder text (e.g. an ![[Foo.png]] written earlier) for the working embed. Drop-guarded: rejects with ' +
                'ASSET_REMOVAL_BLOCKED if it would remove other image embeds (append only — replace_placeholder is exempt ' +
                'since the placeholder it removes is the dead reference being fixed).',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name' }),
                entityType: T.String({ description: 'Entity type of the target entry' }),
                entityId: T.String({ description: 'Entity ID of the target entry' }),
                url: T.String({ description: 'Asset url, taken verbatim from list_assets' }),
                alt: T.Optional(T.String({ description: 'Alt text / caption. Defaults to the asset filename.' })),
                mode: T.Optional(T.Union([T.Literal('append'), T.Literal('replace_placeholder')], { description: 'append (default) or replace_placeholder' })),
                placeholder: T.Optional(T.String({ description: 'Required for replace_placeholder: exact text to replace' })),
                expectedCount: T.Optional(T.Number({ description: 'For replace_placeholder: require this many occurrences, else reject' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    const repoName = resolvePrivateRepo(params.repo);
                    if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
                    }
                    const url = params.url ?? '';
                    if (!url.trim())
                        return wrapToolResult({ success: false, error: 'url is required' });
                    const mode = params.mode ?? 'append';
                    if (mode === 'replace_placeholder' && !params.placeholder) {
                        return wrapToolResult({ success: false, error: 'mode "replace_placeholder" requires a non-empty placeholder' });
                    }
                    const filename = params.alt ?? (url.split('/').pop() || 'asset').split('?')[0];
                    const embed = assetEmbed(filename, url);
                    const graph = workspaceManager.getGraph(repoName);
                    const entity = graph.get(params.entityType, params.entityId);
                    const currentBody = graph.getLiveBody(params.entityType, params.entityId) ?? entity.document.content;
                    let newBody;
                    let occurrences;
                    let allowAssetRemoval = false;
                    if (mode === 'replace_placeholder') {
                        const ph = params.placeholder;
                        let count = 0;
                        for (let i = currentBody.indexOf(ph); i !== -1; i = currentBody.indexOf(ph, i + ph.length))
                            count++;
                        if (count === 0) {
                            return wrapToolResult({ success: false, code: 'NO_MATCH', error: `placeholder not present in ${params.entityType}/${params.entityId}.` });
                        }
                        if (params.expectedCount !== undefined && count !== params.expectedCount) {
                            return wrapToolResult({ success: false, code: 'AMBIGUOUS_MATCH', occurrences: count, error: `Expected ${params.expectedCount} occurrence(s) of placeholder, found ${count}.` });
                        }
                        occurrences = count;
                        newBody = currentBody.split(ph).join(embed);
                        // The placeholder we're removing is itself a recognized embed
                        // target; swapping it for the working embed is the whole point.
                        allowAssetRemoval = true;
                    }
                    else {
                        if (currentBody.includes(embed)) {
                            return wrapToolResult({ success: false, code: 'ALREADY_PRESENT', error: `Asset embed already present in ${params.entityType}/${params.entityId}.` });
                        }
                        newBody = currentBody.trimEnd() + '\n\n' + embed + '\n';
                    }
                    const options = prepareUpdatePayload({
                        entityType: params.entityType,
                        entityId: params.entityId,
                        content: newBody,
                    }, effectiveGitUser());
                    onAgentActivity?.('enter', repoName);
                    try {
                        const { entity: updated, frontmatter_changed, content_changed, noop, broadcast } = await diagnosedUpdate(graph, options, { content: newBody }, { allowAssetRemoval });
                        onMutation?.({ action: 'updated', repo: repoName, entityType: params.entityType, entityId: params.entityId });
                        return wrapToolResult({
                            success: true,
                            entity: { entityId: updated.id, entityType: updated.entityType, repo: repoName },
                            embed,
                            mode,
                            occurrences,
                            frontmatter_changed,
                            content_changed,
                            noop,
                            broadcast,
                            message: `Embedded ${filename} into ${params.entityType}: ${params.entityId}. Auto-committed — ready to push.`,
                        });
                    }
                    finally {
                        onAgentActivity?.('leave', repoName);
                    }
                }
                catch (error) {
                    if (error instanceof DroppedAssetError) {
                        return wrapToolResult({ success: false, code: 'ASSET_REMOVAL_BLOCKED', droppedAssets: error.dropped, error: error.message });
                    }
                    return wrapToolResult({ success: false, error: error instanceof Error ? error.message : 'Unknown error' });
                }
            },
        },
        // Delete entity
        {
            name: 'delete_entity',
            label: 'Delete Entry',
            description: 'Delete an entry. Use with caution! Folder is auto-resolved for app-defined types.',
            parameters: T.Object({
                repo: T.Optional(T.String({ description: 'Folder name (auto-resolved for app entity types)' })),
                entityType: T.String({ description: 'Entity type' }),
                entityId: T.String({ description: 'Entity ID' }),
                commitMessage: T.Optional(T.String({ description: 'Custom commit message' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    let repoName = params.repo;
                    if (!repoName) {
                        return wrapToolResult({ success: false, error: `repo is required — must specify which folder to use for type "${params.entityType}"` });
                    }
                    repoName = resolvePrivateRepo(repoName);
                    if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
                    }
                    const graph = workspaceManager.getGraph(repoName);
                    onAgentActivity?.('enter', repoName);
                    try {
                        await graph.delete(params.entityType, params.entityId, params.commitMessage);
                        onMutation?.({ action: 'deleted', repo: repoName, entityType: params.entityType, entityId: params.entityId });
                        return wrapToolResult({
                            success: true,
                            message: `Deleted ${params.entityType}: ${params.entityId}. Auto-committed — ready to push.`,
                        });
                    }
                    finally {
                        onAgentActivity?.('leave', repoName);
                    }
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Import file as entity
        {
            name: 'import_file',
            label: 'Import File',
            description: 'Import a local file as a Studiograph entry. Text formats become typed entries (.md/.txt → note, .csv → dataset). Binary formats (PDF, images, design files, video, audio, archives) become asset entries.',
            parameters: T.Object({
                path: T.String({ description: 'Absolute path or ~/relative path to the file' }),
                folder: T.String({ description: 'Target folder name' }),
                entityType: T.Optional(T.String({ description: 'Override entity type (default: auto-detect from extension; ignored for asset uploads)' })),
                entityId: T.Optional(T.String({ description: 'Override entity ID (default: from filename)' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    if (!canAccessRepo(params.folder, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${params.folder}" not found` });
                    }
                    const { readFileSync, existsSync } = await import('fs');
                    const { resolve, basename, sep } = await import('path');
                    const { homedir } = await import('os');
                    const { importFile, isSupportedFile } = await import('../../services/import-service.js');
                    const { isAssetUploadExtension } = await import('../../services/assets/base.js');
                    const { uploadAsset } = await import('../../services/asset-upload-service.js');
                    const filePath = params.path.startsWith('~/')
                        ? resolve(homedir(), params.path.slice(2))
                        : resolve(params.path);
                    if (!existsSync(filePath)) {
                        return wrapToolResult({ success: false, error: `File not found: ${filePath}` });
                    }
                    const graph = workspaceManager.getGraph(params.folder);
                    const fname = basename(filePath);
                    // Path-exfil guard: the agent reads bytes off disk and uploads
                    // them as a workspace asset, accessible via /api/assets/* by any
                    // folder member. Without an allowlist, an LLM prompt injection in
                    // an imported note can ask the agent to grab ~/.ssh/id_rsa, .env
                    // files, kubeconfigs, etc. by renaming them to a permitted
                    // extension. Restrict to roots a user would plausibly drop files
                    // from: the workspace itself, ~/Downloads, ~/Documents.
                    const home = homedir();
                    const workspaceRoot = graph.getWorkspaceRoot();
                    const allowedRoots = [
                        workspaceRoot,
                        resolve(home, 'Downloads'),
                        resolve(home, 'Documents'),
                    ].filter((r) => Boolean(r)).map(r => r.endsWith(sep) ? r : r + sep);
                    const isInAllowedRoot = allowedRoots.some(root => filePath === root.slice(0, -1) || filePath.startsWith(root));
                    if (!isInAllowedRoot) {
                        return wrapToolResult({
                            success: false,
                            error: `Path "${filePath}" is outside the import allowlist. Files must live under the workspace, ~/Downloads, or ~/Documents.`,
                        });
                    }
                    // Binary extensions → asset wrapper. The text-import path doesn't
                    // know how to handle .indd / .pptx / etc. and would either reject
                    // them or (for .pdf) mistakenly land them as a `note` entity with
                    // extracted body text. Route to uploadAsset for parity with the
                    // web import dispatcher.
                    if (isAssetUploadExtension(fname)) {
                        const buffer = readFileSync(filePath);
                        const result = await uploadAsset({
                            graph,
                            filename: fname,
                            buffer,
                            workspacePath: graph.getWorkspaceRoot(),
                        });
                        onMutation?.({ action: 'created', repo: params.folder, entityType: 'asset', entityId: result.entityId });
                        return wrapToolResult({
                            success: true,
                            entityType: 'asset',
                            entityId: result.entityId,
                            asset_kind: result.asset_kind,
                            asset_size: result.asset_size,
                            pages: result.pages,
                            message: `Imported ${fname} as asset/${result.entityId} in ${params.folder}`,
                        });
                    }
                    if (!isSupportedFile(filePath)) {
                        return wrapToolResult({ success: false, error: `Unsupported file type for ${fname}` });
                    }
                    const content = readFileSync(filePath);
                    const result = await importFile(graph, fname, content, {
                        entityType: params.entityType,
                        entityId: params.entityId,
                    });
                    if (result.status === 'created') {
                        onMutation?.({ action: 'created', repo: params.folder, entityType: result.entityType, entityId: result.entityId });
                    }
                    return wrapToolResult({
                        success: result.status === 'created',
                        ...result,
                        message: result.status === 'created'
                            ? `Imported ${fname} as ${result.entityType}/${result.entityId} in ${params.folder}`
                            : result.error,
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Rename entity (change ID)
        {
            name: 'rename_entity',
            label: 'Rename Entry',
            description: 'Rename an entry by changing its ID. Preserves all data, content, and source_ref. The old ID will no longer exist. Folder is auto-resolved for app-defined types.',
            parameters: T.Object({
                repo: T.Optional(T.String({ description: 'Folder name (auto-resolved for app entity types)' })),
                entityType: T.String({ description: 'Entity type' }),
                oldId: T.String({ description: 'Current entity ID' }),
                newId: T.String({ description: 'New entity ID (kebab-case)' }),
            }),
            execute: async (toolCallId, params) => {
                try {
                    let repoName = params.repo;
                    if (!repoName) {
                        return wrapToolResult({ success: false, error: `repo is required — must specify which folder to use for type "${params.entityType}"` });
                    }
                    repoName = resolvePrivateRepo(repoName);
                    if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
                    }
                    const graph = workspaceManager.getGraph(repoName);
                    const result = await graph.rename(params.entityType, params.oldId, params.newId);
                    onMutation?.({ action: 'deleted', repo: repoName, entityType: params.entityType, entityId: params.oldId });
                    onMutation?.({ action: 'created', repo: repoName, entityType: params.entityType, entityId: params.newId });
                    return wrapToolResult({
                        success: true,
                        message: `Renamed ${params.entityType}: ${params.oldId} → ${params.newId}`,
                        entity: { entityType: params.entityType, entityId: params.newId, repo: repoName },
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Transfer entity across repos (cross-repo move). Distinct from
        // folder-tools' `move_entity`, which relocates within one repo.
        {
            name: 'transfer_entity',
            label: 'Transfer Entry',
            description: 'Move an entry from one repo to another. Preserves all data, content, and wikilinks. Requires access to both folders. For relocating within the same repo (folder_path change), use move_entity instead.',
            parameters: T.Object({
                sourceRepo: T.String({ description: 'Source repo name' }),
                targetRepo: T.String({ description: 'Target repo name' }),
                entityType: T.String({ description: 'Entity type' }),
                entityId: T.String({ description: 'Entity ID' }),
                commitMessage: T.Optional(T.String({ description: 'Custom commit message (optional)' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    if (!canAccessRepo(params.sourceRepo, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${params.sourceRepo}" not found` });
                    }
                    if (!canAccessRepo(params.targetRepo, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${params.targetRepo}" not found` });
                    }
                    const sourceGraph = workspaceManager.getGraph(params.sourceRepo);
                    const targetGraph = workspaceManager.getGraph(params.targetRepo);
                    // Read the full entity from source
                    const entity = sourceGraph.get(params.entityType, params.entityId);
                    // Create in target repo
                    const actor = effectiveGitUser();
                    const now = new Date().toISOString();
                    await targetGraph.create({
                        entityType: params.entityType,
                        entityId: params.entityId,
                        data: { ...entity.data, updated_at: now, updated_by: actor.id },
                        content: entity.document.content || '',
                        commitMessage: params.commitMessage || `Move ${params.entityType} ${params.entityId} from ${params.sourceRepo} to ${params.targetRepo}`,
                        user: actor,
                    });
                    // Delete from source repo
                    await sourceGraph.delete(params.entityType, params.entityId, `Move ${params.entityType} ${params.entityId} to ${params.targetRepo}`);
                    onMutation?.({ action: 'deleted', repo: params.sourceRepo, entityType: params.entityType, entityId: params.entityId });
                    onMutation?.({ action: 'created', repo: params.targetRepo, entityType: params.entityType, entityId: params.entityId });
                    return wrapToolResult({
                        success: true,
                        message: `Moved ${params.entityType}: ${params.entityId} from ${params.sourceRepo} to ${params.targetRepo}. Changes were auto-committed in both repos — ready to push.`,
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Search entities
        {
            name: 'search_entities',
            label: 'Search Entries',
            description: 'Search for entries across folders with filters.',
            parameters: T.Object({
                query: T.Optional(T.String({ description: 'Search query (full-text)' })),
                entityType: T.Optional(T.String({ description: 'Filter by entity type' })),
                repoNames: T.Optional(T.Array(T.String(), { description: 'Limit to specific folders' })),
                visibility: T.Optional(T.Array(T.String(), { description: 'Filter by visibility (public, team, admin, restricted)' })),
                tags: T.Optional(T.Array(T.String(), { description: 'Filter by tags' })),
                dateRange: T.Optional(T.Object({
                    from: T.String({ description: 'Start date (ISO 8601)' }),
                    to: T.String({ description: 'End date (ISO 8601)' }),
                })),
                sortBy: T.Optional(T.String({ description: 'Sort by: created_at, updated_at, relevance' })),
                limit: T.Optional(T.Number({ description: 'Max results (default 10)' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    // Scope search to accessible folders
                    const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
                    let repoNames = params.repoNames?.map((r) => resolvePrivateRepo(r));
                    if (allowed) {
                        repoNames = repoNames
                            ? repoNames.filter((r) => allowed.has(r))
                            : [...allowed];
                    }
                    const results = await workspaceManager.search({
                        query: params.query,
                        entityType: params.entityType,
                        repoNames,
                        visibility: params.visibility,
                        tags: params.tags,
                        dateRange: params.dateRange,
                        sortBy: params.sortBy,
                        limit: params.limit || 10,
                    });
                    return wrapToolResult({
                        success: true,
                        count: results.length,
                        results: results.map(r => ({
                            repo: r.repoName,
                            entityId: r.id,
                            entityType: r.entityType,
                            data: cleanData(r.data),
                        })),
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // List entities
        {
            name: 'list_entities',
            label: 'List Entries',
            description: 'List all entries of a given type (or all types)',
            parameters: T.Object({
                entityType: T.Optional(T.String({ description: 'Entity type to list (omit for all)' })),
                limit: T.Optional(T.Number({ description: 'Max results' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    let results = workspaceManager.list(params.entityType, params.limit);
                    // Filter to accessible folders
                    const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
                    if (allowed) {
                        results = results.filter(r => allowed.has(r.repoName));
                    }
                    return wrapToolResult({
                        success: true,
                        count: results.length,
                        results: results.map(r => ({
                            repo: r.repoName,
                            entityId: r.id,
                            entityType: r.entityType,
                            data: cleanData(r.data),
                        })),
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Get related entities
        {
            name: 'get_related',
            label: 'Get Related Entries',
            description: 'Get entries related to a given entry via wikilinks (cross-folder supported). Pass repo, entityType, entityId from search/list results.',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name' }),
                entityType: T.String({ description: 'Entity type' }),
                entityId: T.String({ description: 'Entity ID' }),
            }),
            execute: async (toolCallId, params) => {
                try {
                    const repo = resolvePrivateRepo(params.repo);
                    if (!canAccessRepo(repo, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${repo}" not found` });
                    }
                    let related = workspaceManager.getRelated(repo, params.entityType, params.entityId);
                    // Filter cross-folder results to accessible repos
                    const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
                    if (allowed) {
                        related = related.filter(r => allowed.has(r.repoName));
                    }
                    return wrapToolResult({
                        success: true,
                        count: related.length,
                        related: related.map(r => ({
                            repo: r.repoName,
                            entityId: r.id,
                            entityType: r.entityType,
                            data: cleanData(r.data),
                        })),
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Get backlinks
        {
            name: 'get_backlinks',
            label: 'Get Backlinks',
            description: 'Get entries that link to a given entry (cross-folder supported). Pass repo, entityType, entityId from search/list results.',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name' }),
                entityType: T.String({ description: 'Entity type' }),
                entityId: T.String({ description: 'Entity ID' }),
            }),
            execute: async (toolCallId, params) => {
                try {
                    if (!canAccessRepo(params.repo, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${params.repo}" not found` });
                    }
                    let backlinks = workspaceManager.getBacklinks(params.repo, params.entityType, params.entityId);
                    // Filter cross-folder results to accessible repos
                    const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
                    if (allowed) {
                        backlinks = backlinks.filter(r => allowed.has(r.repoName));
                    }
                    return wrapToolResult({
                        success: true,
                        count: backlinks.length,
                        backlinks: backlinks.map(r => ({
                            repo: r.repoName,
                            entityId: r.id,
                            entityType: r.entityType,
                            data: cleanData(r.data),
                        })),
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Validate wikilinks
        {
            name: 'validate_wikilinks',
            label: 'Validate Wikilinks',
            description: 'Validate all wikilinks in an entry (checks same folder and cross-folder)',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name' }),
                entityType: T.String({ description: 'Entity type' }),
                entityId: T.String({ description: 'Entity ID' }),
            }),
            execute: async (toolCallId, params) => {
                try {
                    if (!canAccessRepo(params.repo, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${params.repo}" not found` });
                    }
                    const validation = workspaceManager.validateWikilinks(params.repo, params.entityType, params.entityId);
                    return wrapToolResult({
                        success: true,
                        valid: validation.valid,
                        crossRepo: validation.crossRepo,
                        broken: validation.broken,
                        summary: {
                            total: validation.valid.length + validation.crossRepo.length + validation.broken.length,
                            valid: validation.valid.length,
                            crossRepo: validation.crossRepo.length,
                            broken: validation.broken.length,
                        },
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Get workspace summary
        {
            name: 'get_workspace_summary',
            label: 'Get Workspace Summary',
            description: 'Get summary statistics for the workspace',
            parameters: T.Object({}),
            execute: async (toolCallId, params) => {
                try {
                    const summary = workspaceManager.getSummary();
                    const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
                    if (allowed) {
                        // Filter summary to accessible folders only
                        const filteredByRepo = {};
                        let filteredTotal = 0;
                        for (const [repo, count] of Object.entries(summary.entitiesByRepo)) {
                            if (allowed.has(repo)) {
                                filteredByRepo[repo] = count;
                                filteredTotal += count;
                            }
                        }
                        return wrapToolResult({
                            success: true,
                            summary: {
                                totalFolders: allowed.size,
                                totalEntities: filteredTotal,
                                entitiesByFolder: filteredByRepo,
                            },
                        });
                    }
                    return wrapToolResult({
                        success: true,
                        summary: {
                            totalFolders: summary.totalRepos,
                            totalEntities: summary.totalEntities,
                            entitiesByType: summary.entitiesByType,
                            entitiesByFolder: summary.entitiesByRepo,
                        },
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // List folders
        {
            name: 'list_repos',
            label: 'List Folders',
            description: 'List all folders in the workspace',
            parameters: T.Object({}),
            execute: async (toolCallId, params) => {
                try {
                    let repos = workspaceManager.getAllRepoConfigs();
                    const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
                    if (allowed) {
                        repos = repos.filter(r => allowed.has(r.name));
                    }
                    return wrapToolResult({
                        success: true,
                        count: repos.length,
                        folders: repos.map(r => ({
                            name: r.name,
                            type: r.type,
                            description: r.description,
                            path: r.path,
                            personal: r.personal || false,
                        })),
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Create a new folder
        {
            name: 'create_folder',
            label: 'Create Folder',
            description: 'Create a new shared folder in the workspace. Folders are git-backed containers for entries. New folders are shared with teammates who have access; the creator is granted admin. Every user already has a personal "My Folder" folder auto-provisioned — do not use this tool to create another one.',
            parameters: T.Object({
                name: T.String({ description: 'Folder name in kebab-case (e.g., "client-projects")' }),
                description: T.Optional(T.String({ description: 'Short description of the folder' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    const user = getUser();
                    if (!/^[a-z0-9][a-z0-9-]*$/.test(params.name)) {
                        return wrapToolResult({ success: false, error: 'Folder name must be kebab-case (lowercase letters, numbers, hyphens)' });
                    }
                    const { Workspace } = await import('../../core/workspace.js');
                    const workspace = new Workspace(workspacePath);
                    const repo = await workspace.registerRepo({
                        name: params.name,
                        description: params.description,
                        owner_id: user?.id,
                    });
                    workspaceManager.reloadConfig();
                    if (user && authSvc)
                        authSvc.grantFolderAccess(user.id, repo.name, 'admin');
                    onMutation?.({ action: 'created', repo: repo.name, entityType: '_folder', entityId: repo.name });
                    return wrapToolResult({
                        success: true,
                        folder: { name: repo.name, path: repo.path, description: repo.description },
                    });
                }
                catch (error) {
                    return wrapToolResult({ success: false, error: error instanceof Error ? error.message : 'Unknown error' });
                }
            },
        },
        // Query dataset rows
        {
            name: 'query_dataset',
            label: 'Query Dataset',
            description: 'Query rows from a dataset entity with optional filtering',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name' }),
                entityId: T.String({ description: 'Dataset entity ID' }),
                filter: T.Optional(T.Object({}, { description: 'Filter conditions (column: value or { gte, lte, contains })', additionalProperties: true })),
                limit: T.Optional(T.Number({ description: 'Max rows to return' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    if (!canAccessRepo(params.repo, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${params.repo}" not found` });
                    }
                    const graph = workspaceManager.getGraph(params.repo);
                    // Validates the entity exists and is a dataset before querying.
                    graph.get('dataset', params.entityId);
                    let rows = graph.getDatasetRows(params.entityId, params.filter);
                    const total = rows.length;
                    if (params.limit && params.limit > 0) {
                        rows = rows.slice(0, params.limit);
                    }
                    // Columns from the header, not the result rows — so a zero-match
                    // filter or header-only dataset still reports the real columns.
                    const columns = graph.getDatasetColumns(params.entityId);
                    return wrapToolResult({
                        success: true,
                        rows,
                        total,
                        columns,
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Append rows to dataset
        {
            name: 'append_rows',
            label: 'Append Rows',
            description: 'Append rows to a dataset entity',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name' }),
                entityId: T.String({ description: 'Dataset entity ID' }),
                rows: T.Array(T.Object({}, { additionalProperties: true }), { description: 'Rows to append' }),
                commitMessage: T.Optional(T.String({ description: 'Custom commit message' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    if (!canAccessRepo(params.repo, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${params.repo}" not found` });
                    }
                    const graph = workspaceManager.getGraph(params.repo);
                    // Get current row count before appending
                    const beforeRows = graph.getDatasetRows(params.entityId);
                    await graph.appendDatasetRows(params.entityId, params.rows, params.commitMessage);
                    const afterRows = graph.getDatasetRows(params.entityId);
                    onMutation?.({ action: 'updated', repo: params.repo, entityType: 'dataset', entityId: params.entityId });
                    return wrapToolResult({
                        success: true,
                        rowsAdded: afterRows.length - beforeRows.length,
                        totalRows: afterRows.length,
                        message: `Appended ${afterRows.length - beforeRows.length} row(s) to dataset: ${params.entityId}`,
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Get entity schema
        {
            name: 'get_entity_schema',
            label: 'Get Entry Schema',
            description: 'Get the authoritative schema for an entry type. Returns each field with its type, whether it is optional, the description from the Zod schema, the full enum value list when applicable, and any default. Call this before writing enum-shaped fields (stage, status, category, priority, etc.) — the validator only accepts the values listed under `enum`.',
            parameters: T.Object({
                entityType: T.String({ description: 'Entity type' }),
            }),
            execute: async (toolCallId, params) => {
                try {
                    const registry = workspaceManager.getSchemaRegistry();
                    const knownTypes = new Set(registry.getAllTypes());
                    if (!knownTypes.has(params.entityType)) {
                        return wrapToolResult({
                            success: false,
                            error: `Unknown entity type: ${params.entityType}. Known types: ${[...knownTypes].sort().join(', ')}`,
                        });
                    }
                    const schema = registry.getSchema(params.entityType);
                    const fields = describeZodObject(schema);
                    return wrapToolResult({
                        success: true,
                        entityType: params.entityType,
                        fields,
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Lint entities
        {
            name: 'lint_entities',
            label: 'Lint Entries',
            description: 'Check for missing recommended fields across entries. Returns incomplete entries with their missing fields. Use to discover entries that need enrichment. Optionally filter by folder or entry type.',
            parameters: T.Object({
                repo: T.Optional(T.String({ description: 'Optional: limit to specific folder' })),
                entityType: T.Optional(T.String({ description: 'Optional: limit to specific entity type (e.g. "project", "deal")' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    if (params.repo && !canAccessRepo(params.repo, workspaceManager, getUser, authSvc)) {
                        return wrapToolResult({ success: false, error: `Folder "${params.repo}" not found` });
                    }
                    let issues = await lintWorkspace(workspaceManager, {
                        repo: params.repo,
                        entityType: params.entityType,
                    });
                    const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
                    if (allowed) {
                        issues = issues.filter(i => allowed.has(i.repo));
                    }
                    // Group by entity type for easier reading
                    const grouped = new Map();
                    for (const issue of issues) {
                        const key = issue.entityType;
                        if (!grouped.has(key))
                            grouped.set(key, []);
                        grouped.get(key).push(issue);
                    }
                    const summary = {};
                    for (const [type, typeIssues] of grouped.entries()) {
                        summary[type] = {
                            count: typeIssues.length,
                            issues: typeIssues.map(i => ({
                                location: `${i.repo}/${i.entityType}/${i.entityId}`,
                                missingFields: i.missingFields,
                            })),
                        };
                    }
                    return wrapToolResult({
                        success: true,
                        total: issues.length,
                        byType: summary,
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Find orphan entities
        {
            name: 'find_orphans',
            label: 'Find Orphans',
            description: 'Find entries with zero inbound wikilink references.',
            parameters: T.Object({
                entityTypes: T.Optional(T.Array(T.String(), { description: 'Entity types to check (e.g. ["person", "client"])' })),
                minAgeDays: T.Optional(T.Number({ description: 'Only flag entities older than N days' })),
                repos: T.Optional(T.Array(T.String(), { description: 'Folder names to scan (default: all)' })),
            }),
            execute: async (toolCallId, params) => {
                try {
                    // Scope to accessible folders
                    const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
                    let repos = params.repos;
                    if (allowed) {
                        repos = repos ? repos.filter((r) => allowed.has(r)) : [...allowed];
                    }
                    const result = findOrphans(workspaceManager, {
                        entityTypes: params.entityTypes,
                        minAgeDays: params.minAgeDays,
                        repos,
                    });
                    // Group by type for summary
                    const byType = {};
                    for (const o of result.orphans) {
                        byType[o.entityType] = (byType[o.entityType] || 0) + 1;
                    }
                    return wrapToolResult({
                        success: true,
                        totalScanned: result.totalScanned,
                        totalOrphans: result.totalOrphans,
                        byType,
                        orphans: result.orphans.slice(0, 50), // limit response size
                    });
                }
                catch (error) {
                    return wrapToolResult({
                        success: false,
                        error: error instanceof Error ? error.message : 'Unknown error',
                    });
                }
            },
        },
        // Highlight entities in graph view
        {
            name: 'highlight_entities',
            label: 'Highlight Entries',
            description: 'Highlight specific entries in the graph view. Pass entry IDs to highlight them (dimming all others). Pass an empty array to clear highlights.',
            parameters: T.Object({
                entityIds: T.Array(T.String(), { description: 'Entity IDs to highlight in the graph' }),
            }),
            execute: async (_toolCallId, params) => {
                onHighlight?.(params.entityIds);
                const count = params.entityIds.length;
                return wrapToolResult({
                    highlighted: count,
                    message: count > 0
                        ? `Highlighted ${count} entities in the graph.`
                        : 'Cleared graph highlights.',
                });
            },
        },
        // Mark onboarding complete
        {
            name: 'mark_onboarded',
            label: 'Mark Onboarded',
            description: 'Mark the current user as onboarded. Call this when the interview/onboarding flow is complete.',
            parameters: T.Object({}),
            execute: async (_toolCallId) => {
                if (!authSvc)
                    return wrapToolResult({ success: false, error: 'Auth service not available' });
                const userId = getUser()?.id ?? 0;
                authSvc.setOnboarded(userId, true);
                return wrapToolResult({ success: true, message: 'User marked as onboarded.' });
            },
        },
        // Update workspace description (ABOUT.md)
        {
            name: 'update_workspace_about',
            label: 'Update Workspace Description',
            description: 'Write or update the workspace description (ABOUT.md). Use this to synthesize a description from the knowledge graph after onboarding, or when asked to sync the workspace description. The content should be a concise narrative covering the organization\'s identity, team, capabilities, and values — written in markdown.',
            parameters: T.Object({
                content: T.String({ description: 'The full markdown content for the workspace description' }),
            }),
            execute: async (_toolCallId, params) => {
                try {
                    const { Workspace } = await import('../../core/workspace.js');
                    const workspace = new Workspace(workspacePath);
                    workspace.writeAbout(params.content);
                    onMutation?.({ action: 'updated', repo: '_workspace', entityType: '_workspace', entityId: 'about' });
                    return wrapToolResult({ success: true, message: 'Workspace description updated.' });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err.message || 'Failed to write ABOUT.md' });
                }
            },
        },
        // Reset conversation
        {
            name: 'reset_conversation',
            label: 'Reset Conversation',
            description: 'Clear the current conversation and start fresh. Use when the user asks to "start over", "reset", "clear the chat", or "new conversation".',
            parameters: T.Object({}),
            execute: async (_toolCallId) => {
                onMutation?.({ action: 'deleted', repo: '_conversation', entityType: '_conversation', entityId: 'reset' });
                return wrapToolResult({ success: true, message: 'Conversation reset.' });
            },
        },
    ];
}
//# sourceMappingURL=graph-tools.js.map