/** * What this server can still do with no editor running, and what it says when * something needs one (T16). * * The claim that ue-mcp "works offline" was true and unmeasurable at the same * time. Every category tool is advertised in full whether or not an editor is * attached, which `tests/golden/editor-down.json` records: the startup contract * does not shrink when the editor is down. What shrinks is the set of calls * that can succeed, and nothing reported which set that was. An agent starting * a session against a stopped editor could see 949 actions, pick one, and learn * the difference one refusal at a time. * * There are exactly two ways an action reaches its work: * * `spec.bridge` a method name dispatched over the WebSocket to the editor. * It cannot run without one, so the answer is mechanical and * covers the overwhelming majority of the surface. * `spec.handler` a closure that runs inside this Node process. Whether it * then talks to the editor is a property of the code, not of * the declaration, so those are enumerated below by hand. * * The hand-written half is deliberate. Deriving it by scanning * `handler.toString()` for a bridge reference looks cheaper and is wrong: * `asset(migrate)` calls the bridge from a module-level helper the closure only * names, so a source scan reads it as editor-free and an agent is told a * migrate will work with the editor down. `LOCAL_ACTIONS` records the answer * once, and `tests/unit/offline.test.ts` fails when an action is added, removed * or renamed without updating it, so the table cannot silently drift away from * the graph the server dispatches from. */ import type { ActionSpec, ToolDef } from "./types.js"; /** * Whether an action can run with no editor attached. * * `always` is not a promise that the call succeeds. It is a promise that a * missing editor is not what stops it: `project(read_config)` still needs the * INI to exist, and `editor(stop_editor)` still needs a process to ask. */ export type ActionAvailability = "always" | "editor" | "unknown"; export interface ActionVerdict { tool: string; action: string; availability: ActionAvailability; /** Why, in one sentence a caller can act on. */ reason: string; /** The bridge method an editor-bound action dispatches to. */ bridgeMethod?: string; } /** * Every action whose work happens in this process rather than in the editor, * with the reason each one is in the list. * * Keyed `tool.action`. An entry omitted here for an action that has a handler * is not assumed either way: it reports `unknown` and says so, because guessing * "offline" for an unclassified action is the failure this module exists to * remove. */ export declare const LOCAL_ACTIONS: Record; /** * Actions that have a handler but still reach the editor, with the reason. * * Kept separate from `LOCAL_ACTIONS` so both halves are stated rather than one * being inferred from the other's absence. Between them they must cover every * handler-backed action in the graph, which is what the unit test enforces. */ export declare const EDITOR_BOUND_LOCAL_ACTIONS: Record; /** Where a handler-backed action's classification comes from. */ export declare function classifyAction(tool: string, action: string, spec: ActionSpec): ActionVerdict; /** Every action in a tool graph, classified. Declaration order is preserved. */ export declare function classifyGraph(graph: ToolDef[]): ActionVerdict[]; /** The `tool.action` names that run with no editor, for a graph. */ export declare function offlineActionNames(graph: ToolDef[]): string[]; export interface AvailabilityReportOptions { /** True when an editor is attached right now. */ editorConnected: boolean; /** Narrow the whole report to one category. */ category?: string; /** Which side of the line to list. */ state?: "available" | "blocked" | "all"; /** Include the per-action list, not just the counts. */ names?: boolean; } export interface AvailabilityReport { editorConnected: boolean; total: number; availableNow: number; blocked: number; /** Why the blocked ones are blocked, or undefined when nothing is. */ reason?: string; byCategory: Array<{ category: string; total: number; availableNow: number; blocked: number; }>; actions?: Array<{ tool: string; action: string; availability: ActionAvailability; availableNow: boolean; reason: string; bridgeMethod?: string; }>; } /** * What this server can serve right now. * * With an editor attached every action is available, and the classification is * still worth reporting: it is the answer to "which of these keep working after * I stop the editor to rebuild", which is the moment the question is actually * asked. */ export declare function availabilityReport(graph: ToolDef[], options: AvailabilityReportOptions): AvailabilityReport; export interface EditorDownContext { /** The bridge method the caller was trying to reach. */ method: string; /** The .uproject this connection belongs to, when one is loaded. */ projectPath: string | null; /** The port that was tried, and where the number came from. */ port: number; portSource?: string; /** What the underlying transport said, quoted rather than paraphrased. */ cause?: string; /** The editor's own account of itself, when a log or snapshot could be read. */ phase?: string; /** True when the phase says a human has to answer something first. */ blocking?: boolean; /** A modal dialog title the plugin's status file reported. */ modal?: string; /** How many actions still work with no editor. */ offlineActionCount?: number; } /** * The message a caller gets when the editor is not there. * * The transport's own answer is fast and exact ("connect ECONNREFUSED" comes * back in single-digit milliseconds, so this is never the timeout case) and it * is also unactionable on its own: it names a port and nothing else. It does * not say which project the port belongs to, whether the editor is stopped or * still starting, what to call to fix it, or that a large part of the surface * does not need an editor at all. */ export declare function editorDownMessage(ctx: EditorDownContext): string; /** * Replace a bare connection failure with the account above. * * Returns the error untouched unless it is a connection failure, so a timeout * on a live socket keeps its own message: that one already explains that the * outcome is unknown, which is the opposite of what this says. */ export declare function explainEditorDown(error: unknown, ctx: EditorDownContext): unknown; /** * The same explanation, with the evidence gathered for you. * * Reads the editor's own log and the plugin's status snapshot (both plain file * reads) and counts the offline surface out of the live tool graph. All of it * happens on a failure path that has already given up, so the cost is paid * only by a call that was going to fail anyway. The tool graph is imported * dynamically because this module sits underneath it: a static import would * close the cycle from `tools.ts` back to itself. */ export declare function explainEditorDownWithEvidence(error: unknown, ctx: Omit): Promise;