/**
 * Plugin-owned human-command registry shared by interactive UI adapters.
 * @module @deepseek-ai/dsh-commands
 */
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
    var useValue = arguments.length > 2;
    for (var i = 0; i < initializers.length; i++) {
        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
    }
    return useValue ? value : void 0;
};
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
    var _, done = false;
    for (var i = decorators.length - 1; i >= 0; i--) {
        var context = {};
        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
        if (kind === "accessor") {
            if (result === void 0) continue;
            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
            if (_ = accept(result.get)) descriptor.get = _;
            if (_ = accept(result.set)) descriptor.set = _;
            if (_ = accept(result.init)) initializers.unshift(_);
        }
        else if (_ = accept(result)) {
            if (kind === "field") initializers.unshift(_);
            else descriptor[key] = _;
        }
    }
    if (target) Object.defineProperty(target, contextIn.name, descriptor);
    done = true;
};
import { AttachmentError, admitEncodedImages } from '@deepseek-ai/dsh-attachment';
import { NamedEntries, ScopedLayers } from '@deepseek-ai/dsh-scope';
import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol';
import { CommandId } from "./brand.js";
export { CommandId } from "./brand.js";
export const name = 'commands';
const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u;
/** Shared frozen attachments value for image-free invocations. */
const NO_ATTACHMENTS = Object.freeze([]);
/** All command registrations owned by one global or scoped layer. */
class CommandLayer {
    commands;
    /**
     * Create one command layer with diagnostics specific to its ownership scope.
     * @param scope - the scoped owner, or `undefined` for global registrations.
     */
    constructor(scope) {
        this.commands = new NamedEntries(name => new Error(scope === undefined
            ? `command "${name}" is already registered (for a per-agent variant, mount a command-injected plugin under that agent's \`agent.ctx\`)`
            : `command "${name}" is already registered in this scope`));
    }
    /** @returns whether this layer owns no command registrations. */
    isEmpty() {
        return this.commands.isEmpty();
    }
}
/**
 * Parse an exact slash command without normalizing its trailing input.
 *
 * @param line - Complete candidate command line.
 * @returns The parsed command, or `undefined` when the line is not a command.
 */
export function parseCommand(line) {
    const match = /^\/([a-z][a-z0-9_-]*)(?=$|[\t\n\r ])/u.exec(line);
    if (match === null)
        return undefined;
    const name = match[1];
    /* v8 ignore next -- the first capture is required whenever the regular expression matches */
    if (name === undefined)
        return undefined;
    return Object.freeze({ name, rawInput: line.slice(match[0].length) });
}
/** Convert arbitrary abort reasons to one stable rejected Error. */
function abortError(signal) {
    if (signal.reason instanceof Error)
        return signal.reason;
    return new Error(typeof signal.reason === 'string' ? signal.reason : 'command aborted');
}
/** The signal's normalized abort error when it is already aborted. */
function cancellationOf(signal) {
    return signal.aborted ? abortError(signal) : undefined;
}
/** Render arbitrary thrown values without trusting their string coercion. */
function renderThrown(value) {
    try {
        return String(value);
    }
    catch {
        return '<unrenderable thrown value>';
    }
}
/** Stop awaiting an uncooperative handler once its owning UI request aborts. */
function withAbort(promise, signal) {
    if (signal.aborted)
        return Promise.reject(abortError(signal));
    return new Promise((resolve, reject) => {
        const onAbort = () => {
            signal.removeEventListener('abort', onAbort);
            reject(abortError(signal));
        };
        signal.addEventListener('abort', onAbort, { once: true });
        promise.then((value) => {
            signal.removeEventListener('abort', onAbort);
            resolve(value);
        }, (error) => {
            signal.removeEventListener('abort', onAbort);
            reject(error instanceof Error
                ? error
                : new Error(`command handler rejected with a non-Error value: ${renderThrown(error)}`, { cause: error }));
        });
    });
}
/** Reject invalid command metadata before it can reach a UI protocol. */
function normalizeDefinition(definition) {
    if (!COMMAND_NAME.test(definition.name)) {
        throw new TypeError(`command name "${definition.name}" must match ${String(COMMAND_NAME)}`);
    }
    if (typeof definition.description !== 'string') {
        throw new TypeError(`command "${definition.name}" description must be a string`);
    }
    if (definition.description.trim().length === 0) {
        throw new TypeError(`command "${definition.name}" description must not be empty`);
    }
    if (typeof definition.handler !== 'function') {
        throw new TypeError(`command "${definition.name}" handler must be a function`);
    }
    const rawInput = definition.input;
    let input;
    if (rawInput !== undefined) {
        if (typeof rawInput !== 'object' || rawInput === null || !('hint' in rawInput)
            || typeof rawInput.hint !== 'string') {
            throw new TypeError(`command "${definition.name}" input hint must be a string`);
        }
        if (rawInput.hint.trim().length === 0) {
            throw new TypeError(`command "${definition.name}" input hint must not be empty`);
        }
        if ('images' in rawInput && rawInput.images !== undefined && typeof rawInput.images !== 'boolean') {
            throw new TypeError(`command "${definition.name}" input images flag must be a boolean`);
        }
        input = Object.freeze({
            hint: rawInput.hint,
            ...('images' in rawInput && rawInput.images === true) ? { images: true } : {},
        });
    }
    const normalized = Object.freeze({
        name: definition.name,
        description: definition.description,
        ...input === undefined ? {} : { input },
        ...definition.recordInput === undefined ? {} : { recordInput: definition.recordInput },
        handler: definition.handler,
    });
    const descriptor = Object.freeze({
        name: normalized.name,
        description: normalized.description,
        ...normalized.input === undefined ? {} : { input: normalized.input },
    });
    return { definition: normalized, descriptor };
}
/** Validate and detach an untrusted handler result at the registry boundary. */
function normalizeResult(command, value) {
    if (typeof value !== 'object' || value === null || !('kind' in value)) {
        throw new TypeError(`command "${command}" handler must return a CommandResult`);
    }
    const result = value;
    if (result.kind === 'success') {
        if (result.text !== undefined && typeof result.text !== 'string') {
            throw new TypeError(`command "${command}" success text must be a string when supplied`);
        }
        if (result.sourceEventSeq !== undefined
            && (!Number.isSafeInteger(result.sourceEventSeq) || result.sourceEventSeq < 0)) {
            throw new TypeError(`command "${command}" success sourceEventSeq must be a non-negative safe integer when supplied`);
        }
        return Object.freeze({
            kind: 'success',
            ...result.text === undefined ? {} : { text: result.text },
            ...result.sourceEventSeq === undefined ? {} : { sourceEventSeq: result.sourceEventSeq },
        });
    }
    if (result.kind === 'error') {
        if (typeof result.text !== 'string' || result.text.trim().length === 0) {
            throw new TypeError(`command "${command}" error text must be a non-empty string`);
        }
        return Object.freeze({ kind: 'error', text: result.text });
    }
    throw new TypeError(`command "${command}" returned unknown result kind "${String(result.kind)}"`);
}
/**
 * Human-command registry. Plain-context definitions are global; definitions
 * registered through a command-injected child of an agent context shadow
 * globals for that agent.
 */
let CommandRuntime = (() => {
    let _classSuper = TypertRemoteService;
    let _instanceExtraInitializers = [];
    let _list_decorators;
    let _execute_decorators;
    return class CommandRuntime extends _classSuper {
        static {
            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
            _list_decorators = [Remote];
            _execute_decorators = [Remote];
            __esDecorate(this, null, _list_decorators, { kind: "method", name: "list", static: false, private: false, access: { has: obj => "list" in obj, get: obj => obj.list }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _execute_decorators, { kind: "method", name: "execute", static: false, private: false, access: { has: obj => "execute" in obj, get: obj => obj.execute }, metadata: _metadata }, null, _instanceExtraInitializers);
            if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        }
        layers = (__runInitializers(this, _instanceExtraInitializers), new ScopedLayers(scope => new CommandLayer(scope), () => { this.notifyChange(); }));
        /** Monotonic per-instance counter behind {@link mintCommandId}. */
        commandSeq = 0;
        /** Instance token keeping minted ids unique across process restarts over one resumed log. */
        instanceToken = crypto.randomUUID().slice(0, 8);
        constructor(ctx) {
            super(ctx, 'commands');
        }
        /**
         * Register a global or calling-agent-scoped command.
         * @param definition - discovery metadata and direct UI handler.
         * @returns the exact effect disposer that unregisters this definition.
         */
        register(definition) {
            const registered = normalizeDefinition(definition);
            return this.layers.effect(this.ctx, layer => layer.commands.insert(registered.definition.name, registered), { label: 'commands.register()' });
        }
        /**
         * List the effective immutable command descriptors for one agent.
         * @param agent - exact receiving agent and scoped-layer key.
         * @returns name-sorted descriptors after scoped shadowing.
         */
        list(agent) {
            return Object.freeze([...this.view(agent).values()]
                .map(command => command.descriptor)
                // Names are unique in the effective view, so equality is impossible.
                .sort((left, right) => left.name < right.name ? -1 : 1));
        }
        /**
         * Resolve one effective command definition.
         * @param agent - exact receiving agent and scoped-layer key.
         * @param name - command name without a slash.
         * @returns the scoped shadow or global definition.
         */
        find(agent, name) {
            return this.view(agent).get(name)?.definition;
        }
        /**
         * Parse and execute a known command without sending it to the model.
         *
         * A resolved command's lifecycle is logged: `command/run` is appended
         * before the handler is invoked and `command/done` after settlement (a
         * thrown or aborted handler settles as `kind: 'error'`). Both are direct
         * log-only appends — no turn wraps them, and persistence drains them at
         * ordinary checkpoints. Admission misses (syntax or unknown name) log
         * nothing — they never entered a handler. A `command/run` append failure
         * fails the execution loud; a `command/done` append failure on the
         * handler-failure path is contained so the handler's own error stays the
         * reported failure.
         *
         * Image admission is enforced here, not in the composer: images sent to a
         * command that does not declare `input.images`, an absent attachment store,
         * and an exceeded attachment limit each settle as an error result before
         * the handler runs, and a rejected batch publishes no durable object.
         *
         * @param agent - exact receiving agent.
         * @param line - complete slash-command line.
         * @param images - base64-encoded composer images accompanying the line, in
         *   submission order; empty for a plain invocation.
         * @param signal - cancellation signal owned by the UI request.
         * @returns the settled execution (result + lifecycle pairing id), or
         *   `undefined` when syntax or name does not resolve.
         */
        async execute(agent, line, images, signal) {
            const parsed = parseCommand(line);
            if (parsed === undefined)
                return undefined;
            const command = this.view(agent).get(parsed.name);
            if (command === undefined)
                return undefined;
            if (signal.aborted)
                throw abortError(signal);
            const commandId = this.mintCommandId();
            this.appendLifecycle(agent.session, 'command/run', {
                commandId,
                name: parsed.name,
                ...command.definition.recordInput === false ? {} : { args: parsed.rawInput },
                source: { kind: 'user' },
            });
            const settle = (result) => {
                this.appendLifecycle(agent.session, 'command/done', {
                    commandId, kind: result.kind,
                    ...result.text === undefined ? {} : { text: result.text },
                    ...result.kind === 'success' && result.sourceEventSeq !== undefined
                        ? { sourceEventSeq: result.sourceEventSeq }
                        : {},
                });
                return Object.freeze({ commandId, result: Object.freeze(result) });
            };
            let attachments = NO_ATTACHMENTS;
            if (images.length > 0) {
                if (command.definition.input?.images !== true) {
                    return settle({ kind: 'error', text: `/${parsed.name} does not accept image attachments` });
                }
                const store = this.ctx.get('attachments');
                if (store === undefined) {
                    return settle({ kind: 'error', text: `/${parsed.name}: image attachments are unavailable because no attachment store is composed` });
                }
                try {
                    const refs = await admitEncodedImages(store, images);
                    attachments = Object.freeze(refs.map(ref => Object.freeze({ type: 'image', attachment: ref })));
                }
                catch (error) {
                    if (error instanceof AttachmentError) {
                        return settle({ kind: 'error', text: error.message });
                    }
                    this.settleThrown(agent.session, parsed.name, commandId, error);
                    throw error;
                }
                // Cancellation must be honored BEFORE the handler runs: admission may
                // await slow storage, and a handler entered after the caller cancelled
                // would mutate state the retrying caller then duplicates. (The committed
                // image objects stay unreferenced and are deferred-GC territory.)
                const cancelledDuringAdmission = cancellationOf(signal);
                if (cancelledDuringAdmission !== undefined) {
                    this.settleThrown(agent.session, parsed.name, commandId, cancelledDuringAdmission);
                    throw cancelledDuringAdmission;
                }
            }
            const invocation = Object.freeze({ commandId, agent, rawInput: parsed.rawInput, attachments, signal });
            let result;
            try {
                const output = command.definition.handler(invocation);
                result = normalizeResult(parsed.name, await withAbort(Promise.resolve(output), signal));
            }
            catch (error) {
                this.settleThrown(agent.session, parsed.name, commandId, error);
                throw error;
            }
            return settle(result);
        }
        /** Contained `command/done` error append for a thrown handler or admission failure. */
        settleThrown(session, command, commandId, error) {
            try {
                this.appendLifecycle(session, 'command/done', {
                    commandId, kind: 'error',
                    text: error instanceof Error ? error.message : renderThrown(error),
                });
            }
            catch (appendError) {
                this.ctx.logger.warn(`command "${command}": command/done append failed: ${renderThrown(appendError)}`);
            }
        }
        /** Mint the next pairing id (monotonic; instance-token-prefixed so a resumed log never repeats one). */
        mintCommandId() {
            this.commandSeq += 1;
            return CommandId(`cmd-${this.instanceToken}-${this.commandSeq}`);
        }
        /**
         * Append one log-only lifecycle event directly: no turn is opened for it and
         * no flush is forced — persistence observes the eager `session/event` path
         * and drains at ordinary checkpoints and teardown, like every other
         * standalone plugin event.
         */
        appendLifecycle(session, type, data) {
            // Both admitted types are log-only (non-surface), but TypeScript does not
            // reduce Session.append's conditional rest parameter through a generic
            // type parameter. Preserve the proven two-argument call shape.
            const appendLogOnly = session.append.bind(session);
            return appendLogOnly(type, data);
        }
        /** Resolve global definitions followed by exact scoped shadows. */
        view(agent) {
            return this.layers.merge(agent, layer => layer.commands);
        }
        /** Notify every registry observer without making UI refresh load-bearing. */
        notifyChange() {
            // Cordis emit uses Array.map: one synchronous throw starves later listeners,
            // and returned promises are discarded. Registry notifications are
            // non-vetoing, so contain each callback independently.
            for (const callback of this.ctx.events.dispatch('emit', ['commands/change'])) {
                try {
                    const returned = callback();
                    void Promise.resolve(returned).catch((error) => {
                        this.ctx.logger.warn(`commands/change listener rejected: ${renderThrown(error)}`);
                    });
                }
                catch (error) {
                    this.ctx.logger.warn(`commands/change listener threw: ${renderThrown(error)}`);
                }
            }
        }
    };
})();
export { CommandRuntime };
export default CommandRuntime;
//# sourceMappingURL=index.js.map