@wrongstack/webui 0.273.1 → 0.275.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-BG4jUAmc.js +141 -0
- package/dist/assets/index-Cw32ELnp.css +2 -0
- package/dist/assets/{vendor-P9eRrO6V.js → vendor-Cl_sFcw4.js} +251 -251
- package/dist/index.html +3 -3
- package/dist/index.js +9111 -6139
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +1537 -246
- package/dist/server/entry.js.map +1 -1
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/index.d.ts +217 -18
- package/dist/server/index.js +1473 -242
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +275 -5
- package/package.json +7 -7
- package/dist/assets/index-BGzM4-Zu.css +0 -2
- package/dist/assets/index-D0dNaLPf.js +0 -140
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/ws-payload-validation.ts","../../src/server/handlers/worklist-handlers.ts"],"sourcesContent":["export type PayloadValidationResult<T> = { ok: true; value: T } | { ok: false; message: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport interface ModelSwitchPayload {\n provider: string;\n model: string;\n}\n\nexport function validateModelSwitchPayload(\n payload: unknown,\n): PayloadValidationResult<ModelSwitchPayload> {\n if (!isRecord(payload)) {\n return {\n ok: false,\n message: 'model.switch payload must be an object with string provider and model',\n };\n }\n const provider = payload['provider'];\n const model = payload['model'];\n if (typeof provider !== 'string' || provider.trim().length === 0) {\n return { ok: false, message: 'model.switch payload.provider must be a non-empty string' };\n }\n if (typeof model !== 'string' || model.trim().length === 0) {\n return { ok: false, message: 'model.switch payload.model must be a non-empty string' };\n }\n return { ok: true, value: { provider, model } };\n}\n\nexport interface PrefsUpdatePayload {\n prefs: Record<string, unknown>;\n}\n\nconst AUTONOMY_VALUES = new Set(['off', 'suggest', 'auto', 'eternal', 'eternal-parallel']);\n\nexport interface MailboxMessagesPayload {\n limit?: number;\n agentId?: string;\n unreadOnly?: boolean;\n}\n\nexport function validateMailboxMessagesPayload(\n payload: unknown,\n): PayloadValidationResult<MailboxMessagesPayload | undefined> {\n if (payload === undefined) return { ok: true, value: undefined };\n if (!isRecord(payload)) {\n return { ok: false, message: 'mailbox.messages payload must be an object when provided' };\n }\n const limit = payload['limit'];\n const agentId = payload['agentId'];\n const unreadOnly = payload['unreadOnly'];\n if (limit !== undefined && (typeof limit !== 'number' || !Number.isFinite(limit) || limit < 1)) {\n return {\n ok: false,\n message: 'mailbox.messages payload.limit must be a positive number when provided',\n };\n }\n if (agentId !== undefined && typeof agentId !== 'string') {\n return {\n ok: false,\n message: 'mailbox.messages payload.agentId must be a string when provided',\n };\n }\n if (unreadOnly !== undefined && typeof unreadOnly !== 'boolean') {\n return {\n ok: false,\n message: 'mailbox.messages payload.unreadOnly must be a boolean when provided',\n };\n }\n return { ok: true, value: { limit, agentId, unreadOnly } };\n}\n\nexport interface MailboxAgentsPayload {\n onlineOnly?: boolean;\n}\n\nexport function validateMailboxAgentsPayload(\n payload: unknown,\n): PayloadValidationResult<MailboxAgentsPayload | undefined> {\n if (payload === undefined) return { ok: true, value: undefined };\n if (!isRecord(payload)) {\n return { ok: false, message: 'mailbox.agents payload must be an object when provided' };\n }\n const onlineOnly = payload['onlineOnly'];\n if (onlineOnly !== undefined && typeof onlineOnly !== 'boolean') {\n return {\n ok: false,\n message: 'mailbox.agents payload.onlineOnly must be a boolean when provided',\n };\n }\n return { ok: true, value: { onlineOnly } };\n}\n\nexport interface MailboxPurgePayload {\n completedMaxAgeMs?: number;\n incompleteMaxAgeMs?: number;\n}\n\nexport function validateMailboxPurgePayload(\n payload: unknown,\n): PayloadValidationResult<MailboxPurgePayload | undefined> {\n if (payload === undefined) return { ok: true, value: undefined };\n if (!isRecord(payload)) {\n return { ok: false, message: 'mailbox.purge payload must be an object when provided' };\n }\n const completedMaxAgeMs = payload['completedMaxAgeMs'];\n const incompleteMaxAgeMs = payload['incompleteMaxAgeMs'];\n if (\n completedMaxAgeMs !== undefined &&\n (typeof completedMaxAgeMs !== 'number' ||\n !Number.isFinite(completedMaxAgeMs) ||\n completedMaxAgeMs < 0)\n ) {\n return {\n ok: false,\n message:\n 'mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided',\n };\n }\n if (\n incompleteMaxAgeMs !== undefined &&\n (typeof incompleteMaxAgeMs !== 'number' ||\n !Number.isFinite(incompleteMaxAgeMs) ||\n incompleteMaxAgeMs < 0)\n ) {\n return {\n ok: false,\n message:\n 'mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided',\n };\n }\n return { ok: true, value: { completedMaxAgeMs, incompleteMaxAgeMs } };\n}\n\nexport interface BrainRiskPayload {\n level: string;\n}\n\nconst BRAIN_RISK_VALUES = new Set(['off', 'low', 'medium', 'high', 'all']);\n\nexport function validateBrainRiskPayload(\n payload: unknown,\n): PayloadValidationResult<BrainRiskPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'brain.risk payload must be an object with string level' };\n }\n const level = payload['level'];\n if (typeof level !== 'string' || !BRAIN_RISK_VALUES.has(level)) {\n return {\n ok: false,\n message: 'brain.risk payload.level must be one of off, low, medium, high, all',\n };\n }\n return { ok: true, value: { level } };\n}\n\nexport interface BrainAskPayload {\n question: string;\n}\n\nexport function validateBrainAskPayload(\n payload: unknown,\n): PayloadValidationResult<BrainAskPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'brain.ask payload must be an object with string question' };\n }\n const question = payload['question'];\n if (typeof question !== 'string' || question.trim().length === 0) {\n return { ok: false, message: 'brain.ask payload.question must be a non-empty string' };\n }\n return { ok: true, value: { question: question.trim() } };\n}\n\nexport interface AutonomySwitchPayload {\n mode: string;\n}\n\nexport function validateAutonomySwitchPayload(\n payload: unknown,\n): PayloadValidationResult<AutonomySwitchPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'autonomy.switch payload must be an object with string mode' };\n }\n const mode = payload['mode'];\n if (typeof mode !== 'string' || !AUTONOMY_VALUES.has(mode)) {\n return { ok: false, message: 'autonomy.switch payload.mode must be a valid autonomy mode' };\n }\n return { ok: true, value: { mode } };\n}\n\nexport interface PlanTemplateUsePayload {\n template: string;\n}\n\nexport function validatePlanTemplateUsePayload(\n payload: unknown,\n): PayloadValidationResult<PlanTemplateUsePayload> {\n if (!isRecord(payload)) {\n return {\n ok: false,\n message: 'plan.template_use payload must be an object with string template',\n };\n }\n const template = payload['template'];\n if (typeof template !== 'string' || template.trim().length === 0) {\n return { ok: false, message: 'plan.template_use payload.template must be a non-empty string' };\n }\n return { ok: true, value: { template } };\n}\nconst CONTEXT_STRATEGY_VALUES = new Set(['hybrid', 'intelligent', 'selective']);\nconst CONTEXT_MODE_VALUES = new Set(['balanced', 'frugal', 'deep', 'archival']);\nconst TOKEN_SAVING_TIER_VALUES = new Set(['off', 'minimal', 'light', 'medium', 'aggressive']);\nconst ENHANCE_LANGUAGE_VALUES = new Set(['original', 'english']);\nconst LOG_LEVEL_VALUES = new Set(['debug', 'info', 'warn', 'error']);\nconst AUDIT_LEVEL_VALUES = new Set(['minimal', 'standard', 'full']);\nconst REASONING_MODE_VALUES = new Set(['auto', 'on', 'off']);\nconst REASONING_EFFORT_VALUES = new Set([\n 'none',\n 'minimal',\n 'low',\n 'medium',\n 'high',\n 'xhigh',\n 'max',\n]);\nconst CACHE_TTL_VALUES = new Set(['default', '5m', '1h']);\n\nconst BOOLEAN_PREF_KEYS = new Set([\n 'yolo',\n 'chime',\n 'confirmExit',\n 'streamFleet',\n 'nextPrediction',\n 'titleAnimation',\n 'enhanceEnabled',\n 'featureMcp',\n 'featurePlugins',\n 'featureMemory',\n 'featureSkills',\n 'featureModelsRegistry',\n 'indexOnStart',\n 'contextAutoCompact',\n 'tgSessionEnd',\n 'tgDelegate',\n 'reasoningPreserve',\n 'hqEnabled',\n 'hqRawContent',\n 'fallbackAuto',\n]);\n\n/** Keys whose value must be an array of strings (e.g. an ordered model list). */\nconst STRING_ARRAY_PREF_KEYS = new Set(['fallbackModels']);\n\nconst NUMBER_PREF_KEYS = new Set([\n 'autonomyDelayMs',\n 'autoProceedMaxIterations',\n 'maxIterations',\n 'maxConcurrent',\n 'enhanceDelayMs',\n 'tgLongToolMs',\n]);\n\nconst STRING_PREF_KEYS = new Set(['hqUrl', 'hqToken']);\n\nconst ENUM_PREF_KEYS: Record<string, Set<string>> = {\n autonomy: AUTONOMY_VALUES,\n contextStrategy: CONTEXT_STRATEGY_VALUES,\n contextMode: CONTEXT_MODE_VALUES,\n tokenSavingTier: TOKEN_SAVING_TIER_VALUES,\n enhanceLanguage: ENHANCE_LANGUAGE_VALUES,\n logLevel: LOG_LEVEL_VALUES,\n auditLevel: AUDIT_LEVEL_VALUES,\n reasoningMode: REASONING_MODE_VALUES,\n reasoningEffort: REASONING_EFFORT_VALUES,\n cacheTtl: CACHE_TTL_VALUES,\n};\n\nfunction validatePreferenceValue(key: string, value: unknown): string | null {\n if (BOOLEAN_PREF_KEYS.has(key)) {\n return typeof value === 'boolean' ? null : `prefs.update payload.${key} must be a boolean`;\n }\n if (NUMBER_PREF_KEYS.has(key)) {\n return typeof value === 'number' && Number.isFinite(value)\n ? null\n : `prefs.update payload.${key} must be a finite number`;\n }\n if (STRING_PREF_KEYS.has(key)) {\n return typeof value === 'string' ? null : `prefs.update payload.${key} must be a string`;\n }\n if (STRING_ARRAY_PREF_KEYS.has(key)) {\n return Array.isArray(value) && value.every((v) => typeof v === 'string')\n ? null\n : `prefs.update payload.${key} must be an array of strings`;\n }\n const allowed = ENUM_PREF_KEYS[key];\n if (allowed) {\n return typeof value === 'string' && allowed.has(value)\n ? null\n : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(', ')}`;\n }\n return `prefs.update payload contains unknown preference key: ${key}`;\n}\n\nexport function validatePrefsUpdatePayload(\n payload: unknown,\n): PayloadValidationResult<PrefsUpdatePayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'prefs.update payload must be an object' };\n }\n for (const [key, value] of Object.entries(payload)) {\n const error = validatePreferenceValue(key, value);\n if (error) return { ok: false, message: error };\n }\n return { ok: true, value: { prefs: payload } };\n}\n\nexport interface SkillsCreatePayload {\n name: string;\n description: string;\n scope: 'project' | 'global';\n}\n\nexport function validateSkillsCreatePayload(\n payload: unknown,\n): PayloadValidationResult<SkillsCreatePayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'skills.create payload must be an object' };\n }\n const name = payload['name'];\n const description = payload['description'];\n const scope = payload['scope'];\n if (typeof name !== 'string' || name.trim().length === 0) {\n return { ok: false, message: 'Skill name is required' };\n }\n if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name.trim())) {\n return { ok: false, message: 'Skill name must be kebab-case (e.g. my-new-skill)' };\n }\n if (typeof description !== 'string' || description.trim().length === 0) {\n return { ok: false, message: 'Description/trigger is required' };\n }\n if (scope !== 'project' && scope !== 'global') {\n return { ok: false, message: 'skills.create payload.scope must be project or global' };\n }\n return { ok: true, value: { name, description, scope } };\n}\n\nexport interface SkillsEditPayload {\n name: string;\n body: string;\n}\n\nexport function validateSkillsEditPayload(\n payload: unknown,\n): PayloadValidationResult<SkillsEditPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'skills.edit payload must be an object' };\n }\n const name = payload['name'];\n const body = payload['body'];\n if (typeof name !== 'string' || name.trim().length === 0) {\n return { ok: false, message: 'Skill name is required' };\n }\n if (typeof body !== 'string' || body.length === 0) {\n return { ok: false, message: 'Skill body is required' };\n }\n return { ok: true, value: { name, body } };\n}\n\nexport interface ProcessKillPayload {\n pid: number;\n}\n\nexport function validateProcessKillPayload(\n payload: unknown,\n): PayloadValidationResult<ProcessKillPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'process.kill payload must be an object with numeric pid' };\n }\n const pid = payload['pid'];\n if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0) {\n return { ok: false, message: 'process.kill payload.pid must be a positive integer' };\n }\n return { ok: true, value: { pid } };\n}\n\nexport interface WorkingDirSetPayload {\n path: string;\n}\n\nexport function validateWorkingDirSetPayload(\n payload: unknown,\n): PayloadValidationResult<WorkingDirSetPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'working_dir.set payload must be an object with string path' };\n }\n const newPath = payload['path'];\n if (typeof newPath !== 'string' || newPath.trim().length === 0) {\n return { ok: false, message: 'working_dir.set payload.path must be a non-empty string' };\n }\n return { ok: true, value: { path: newPath } };\n}\n\nexport interface ModeSwitchPayload {\n id: string;\n}\n\nexport function validateModeSwitchPayload(\n payload: unknown,\n): PayloadValidationResult<ModeSwitchPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'mode.switch payload must be an object with string id' };\n }\n const id = payload['id'];\n if (typeof id !== 'string' || id.trim().length === 0) {\n return { ok: false, message: 'mode.switch payload.id must be a non-empty string' };\n }\n return { ok: true, value: { id } };\n}\n\nexport interface ContextModeIdPayload {\n id: string;\n}\n\nfunction validateContextModeIdPayload(\n payload: unknown,\n type: 'context.mode.switch' | 'context.mode.delete',\n): PayloadValidationResult<ContextModeIdPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: `${type} payload must be an object with string id` };\n }\n const id = payload['id'];\n if (typeof id !== 'string' || id.trim().length === 0) {\n return { ok: false, message: `${type} payload.id must be a non-empty string` };\n }\n return { ok: true, value: { id } };\n}\n\nexport function validateContextModeSwitchPayload(\n payload: unknown,\n): PayloadValidationResult<ContextModeIdPayload> {\n return validateContextModeIdPayload(payload, 'context.mode.switch');\n}\n\nexport function validateContextModeDeletePayload(\n payload: unknown,\n): PayloadValidationResult<ContextModeIdPayload> {\n return validateContextModeIdPayload(payload, 'context.mode.delete');\n}\n\nexport interface ContextModeCreatePayload {\n id: string;\n name: string;\n description: string;\n thresholds: { warn: number; soft: number; hard: number };\n preserveK: number;\n eliseThreshold: number;\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value);\n}\n\nexport function validateContextModeCreatePayload(\n payload: unknown,\n): PayloadValidationResult<ContextModeCreatePayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'context.mode.create payload must be an object' };\n }\n const id = payload['id'];\n const name = payload['name'];\n const description = payload['description'];\n const thresholds = payload['thresholds'];\n const preserveK = payload['preserveK'];\n const eliseThreshold = payload['eliseThreshold'];\n\n if (typeof id !== 'string' || id.trim().length === 0) {\n return { ok: false, message: 'context.mode.create payload.id must be a non-empty string' };\n }\n if (typeof name !== 'string' || name.trim().length === 0) {\n return { ok: false, message: 'context.mode.create payload.name must be a non-empty string' };\n }\n if (typeof description !== 'string') {\n return { ok: false, message: 'context.mode.create payload.description must be a string' };\n }\n if (!isRecord(thresholds)) {\n return {\n ok: false,\n message:\n 'context.mode.create payload.thresholds must be an object with warn/soft/hard numbers',\n };\n }\n if (\n !isFiniteNumber(thresholds['warn']) ||\n !isFiniteNumber(thresholds['soft']) ||\n !isFiniteNumber(thresholds['hard'])\n ) {\n return {\n ok: false,\n message: 'context.mode.create payload.thresholds.warn/soft/hard must be finite numbers',\n };\n }\n if (!isFiniteNumber(preserveK)) {\n return { ok: false, message: 'context.mode.create payload.preserveK must be a finite number' };\n }\n if (!isFiniteNumber(eliseThreshold)) {\n return {\n ok: false,\n message: 'context.mode.create payload.eliseThreshold must be a finite number',\n };\n }\n return {\n ok: true,\n value: {\n id,\n name,\n description,\n thresholds: { warn: thresholds['warn'], soft: thresholds['soft'], hard: thresholds['hard'] },\n preserveK,\n eliseThreshold,\n },\n };\n}\n\nexport interface ContextModeUpdatePayload {\n id: string;\n name?: string;\n description?: string;\n thresholds?: { warn?: number; soft?: number; hard?: number };\n preserveK?: number;\n eliseThreshold?: number;\n}\n\nexport function validateContextModeUpdatePayload(\n payload: unknown,\n): PayloadValidationResult<ContextModeUpdatePayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'context.mode.update payload must be an object' };\n }\n const id = payload['id'];\n if (typeof id !== 'string' || id.trim().length === 0) {\n return { ok: false, message: 'context.mode.update payload.id must be a non-empty string' };\n }\n\n const name = payload['name'];\n if (name !== undefined && typeof name !== 'string') {\n return {\n ok: false,\n message: 'context.mode.update payload.name must be a string when provided',\n };\n }\n\n const description = payload['description'];\n if (description !== undefined && typeof description !== 'string') {\n return {\n ok: false,\n message: 'context.mode.update payload.description must be a string when provided',\n };\n }\n\n const thresholds = payload['thresholds'];\n let validatedThresholds: ContextModeUpdatePayload['thresholds'];\n if (thresholds !== undefined) {\n if (!isRecord(thresholds)) {\n return {\n ok: false,\n message: 'context.mode.update payload.thresholds must be an object when provided',\n };\n }\n for (const key of ['warn', 'soft', 'hard'] as const) {\n const val = thresholds[key];\n if (val !== undefined && !isFiniteNumber(val)) {\n return {\n ok: false,\n message: `context.mode.update payload.thresholds.${key} must be a finite number when provided`,\n };\n }\n }\n validatedThresholds = {\n warn: typeof thresholds['warn'] === 'number' ? thresholds['warn'] : undefined,\n soft: typeof thresholds['soft'] === 'number' ? thresholds['soft'] : undefined,\n hard: typeof thresholds['hard'] === 'number' ? thresholds['hard'] : undefined,\n };\n }\n\n const preserveK = payload['preserveK'];\n if (preserveK !== undefined && !isFiniteNumber(preserveK)) {\n return {\n ok: false,\n message: 'context.mode.update payload.preserveK must be a finite number when provided',\n };\n }\n\n const eliseThreshold = payload['eliseThreshold'];\n if (eliseThreshold !== undefined && !isFiniteNumber(eliseThreshold)) {\n return {\n ok: false,\n message: 'context.mode.update payload.eliseThreshold must be a finite number when provided',\n };\n }\n\n return {\n ok: true,\n value: {\n id,\n name: typeof name === 'string' ? name : undefined,\n description: typeof description === 'string' ? description : undefined,\n thresholds: validatedThresholds,\n preserveK: typeof preserveK === 'number' ? preserveK : undefined,\n eliseThreshold: typeof eliseThreshold === 'number' ? eliseThreshold : undefined,\n },\n };\n}\n\nexport interface ShellOpenPayload {\n path: string;\n target?: 'file' | 'terminal';\n}\n\nexport function validateShellOpenPayload(\n payload: unknown,\n): PayloadValidationResult<ShellOpenPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'shell.open payload must be an object with string path' };\n }\n const path = payload['path'];\n if (typeof path !== 'string' || path.trim().length === 0) {\n return { ok: false, message: 'shell.open payload.path must be a non-empty string' };\n }\n const target = payload['target'];\n if (target !== undefined && target !== 'file' && target !== 'terminal') {\n return {\n ok: false,\n message: 'shell.open payload.target must be \"file\" or \"terminal\" when provided',\n };\n }\n return { ok: true, value: { path, target: target as ShellOpenPayload['target'] } };\n}\n\nexport interface GitDiffPayload {\n path: string;\n}\n\nexport function validateGitDiffPayload(payload: unknown): PayloadValidationResult<GitDiffPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'git.diff payload must be an object' };\n }\n const path = payload['path'];\n if (path === undefined || path === null) {\n return { ok: true, value: { path: '' } };\n }\n if (typeof path !== 'string') {\n return { ok: false, message: 'git.diff payload.path must be a string when provided' };\n }\n return { ok: true, value: { path } };\n}\n\nexport interface ProjectsAddPayload {\n root: string;\n name?: string;\n}\n\nexport function validateProjectsAddPayload(\n payload: unknown,\n): PayloadValidationResult<ProjectsAddPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'projects.add payload must be an object with string root' };\n }\n const root = payload['root'];\n if (typeof root !== 'string' || root.trim().length === 0) {\n return { ok: false, message: 'projects.add payload.root must be a non-empty string' };\n }\n const name = payload['name'];\n if (name !== undefined && typeof name !== 'string') {\n return { ok: false, message: 'projects.add payload.name must be a string when provided' };\n }\n return { ok: true, value: { root, name: typeof name === 'string' ? name : undefined } };\n}\n\nexport interface ProjectsSelectPayload {\n root: string;\n name?: string;\n}\n\nexport function validateProjectsSelectPayload(\n payload: unknown,\n): PayloadValidationResult<ProjectsSelectPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'projects.select payload must be an object with string root' };\n }\n const root = payload['root'];\n if (typeof root !== 'string' || root.trim().length === 0) {\n return { ok: false, message: 'projects.select payload.root must be a non-empty string' };\n }\n const name = payload['name'];\n if (name !== undefined && typeof name !== 'string') {\n return { ok: false, message: 'projects.select payload.name must be a string when provided' };\n }\n return { ok: true, value: { root, name: typeof name === 'string' ? name : undefined } };\n}\n","// ── Shared Worklist Handlers ─────────────────────────────────────────────────\n// Extracted from standalone server (packages/webui/src/server/index.ts) and CLI\n// embedded server (packages/cli/src/webui-server/). Both servers use these\n// handlers for todos, tasks, and plan operations. Keep them in sync.\n//\n// Message types handled here:\n// todos.get | todos.clear | todos.remove | todo.update\n// tasks.get | task.update\n// plan.get | plan.template_use | plan.item.update\n// ─────────────────────────────────────────────────────────────────────────────\n\nimport type { WebSocket } from 'ws';\nimport type { TodoItem } from '@wrongstack/core';\nimport { validatePlanTemplateUsePayload } from '../ws-payload-validation.js';\n\n// ── Shared result helper ───────────────────────────────────────────────────────\n\nfunction sendResult(\n ws: WebSocket,\n ctx: WorklistContext,\n ok: boolean,\n message: string,\n): void {\n ctx.send(ws, { type: ok ? 'ok' : 'error', message });\n}\n\n// ── Context interface ─────────────────────────────────────────────────────────\n// Both servers satisfy this with their own local state.\n\nexport interface WorklistContext {\n context: {\n todos: TodoItem[];\n meta: Record<string, unknown>;\n session: { id: string } | null;\n state?: unknown;\n };\n send: (ws: WebSocket, msg: object) => void;\n broadcast: (msg: object) => void;\n /**\n * Optional mutator for in-memory todo state. Servers that manage live\n * agent state (e.g. the CLI embedded server) provide this so handlers\n * can update the agent's todo list directly. Standalone server may omit.\n */\n replaceTodos?: (todos: TodoItem[]) => void;\n}\n\n// ── Todos ─────────────────────────────────────────────────────────────────────\n\nexport function handleTodosGet(ctx: WorklistContext, ws: WebSocket): void {\n ctx.send(ws, { type: 'todos.updated', payload: { todos: ctx.context.todos } });\n}\n\nexport function handleTodosClear(ctx: WorklistContext, ws: WebSocket): void {\n ctx.replaceTodos?.([]);\n ctx.broadcast({ type: 'todos.cleared' });\n sendResult(ws, ctx, true, 'Todo board cleared.');\n}\n\nexport function handleTodosRemove(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: { id?: string; index?: number } | undefined,\n): void {\n if (!payload || (payload.id === undefined && payload.index === undefined)) {\n sendResult(ws, ctx, false, 'todos.remove requires id or index.');\n return;\n }\n const next =\n payload.id !== undefined\n ? ctx.context.todos.filter((t) => t.id !== payload.id)\n : ctx.context.todos.filter((_, i) => i !== (payload.index as number));\n ctx.replaceTodos?.(next);\n ctx.broadcast({ type: 'todos.updated', payload: { todos: next } });\n sendResult(ws, ctx, true, 'Todo item removed.');\n}\n\nexport function handleTodoUpdate(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: { id: string; status?: TodoItem['status']; activeForm?: string },\n): void {\n const todo = ctx.context.todos.find((t) => t.id === payload.id);\n if (!todo) {\n sendResult(ws, ctx, false, `No todo with id \"${payload.id}\".`);\n return;\n }\n const next = ctx.context.todos.map((t) =>\n t.id === payload.id\n ? { ...t, ...(payload.status !== undefined && { status: payload.status }), ...(payload.activeForm !== undefined && { activeForm: payload.activeForm }) }\n : t,\n );\n ctx.replaceTodos?.(next);\n ctx.broadcast({ type: 'todos.updated', payload: { todos: next } });\n sendResult(ws, ctx, true, `Todo \"${todo.content}\" updated.`);\n}\n\n// ── Tasks ─────────────────────────────────────────────────────────────────────\n\nexport async function handleTasksGet(ctx: WorklistContext, ws: WebSocket): Promise<void> {\n const taskPath = ctx.context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n try {\n const { loadTasks } = await import('@wrongstack/core');\n const file = await loadTasks(taskPath);\n ctx.send(ws, { type: 'tasks.updated', payload: { tasks: file?.tasks ?? [] } });\n } catch {\n ctx.send(ws, { type: 'tasks.updated', payload: { tasks: [] } });\n }\n } else {\n ctx.send(ws, {\n type: 'tasks.updated',\n payload: { tasks: [], error: 'Task storage not configured.' },\n });\n }\n}\n\nexport async function handleTaskUpdate(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: {\n id: string;\n status: 'pending' | 'in_progress' | 'blocked' | 'failed' | 'review' | 'completed';\n },\n): Promise<void> {\n const taskPath = ctx.context.meta['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) {\n sendResult(ws, ctx, false, 'Task storage is not configured for this session.');\n return;\n }\n try {\n const { loadTasks, saveTasks } = await import('@wrongstack/core');\n const file = await loadTasks(taskPath);\n if (!file) {\n sendResult(ws, ctx, false, 'No task file found.');\n return;\n }\n const idx = file.tasks.findIndex((t) => t.id === payload.id);\n if (idx === -1) {\n sendResult(ws, ctx, false, `Task \"${payload.id}\" not found.`);\n return;\n }\n file.tasks[idx] = { ...file.tasks[idx], status: payload.status };\n await saveTasks(taskPath, file);\n ctx.broadcast({ type: 'tasks.updated', payload: { tasks: file.tasks } });\n sendResult(ws, ctx, true, `Task \"${payload.id}\" marked ${payload.status}.`);\n } catch (err) {\n sendResult(ws, ctx, false, String(err));\n }\n}\n\n// ── Plan ───────────────────────────────────────────────────────────────────────\n\nexport async function handlePlanGet(ctx: WorklistContext, ws: WebSocket): Promise<void> {\n const planPath = ctx.context.meta['plan.path'];\n const sessionId = ctx.context.session?.id ?? '';\n if (typeof planPath === 'string' && planPath) {\n try {\n const { loadPlan } = await import('@wrongstack/core');\n const plan = await loadPlan(planPath);\n ctx.send(ws, {\n type: 'plan.updated',\n payload: {\n plan: plan ?? {\n version: 1,\n sessionId,\n updatedAt: new Date().toISOString(),\n items: [],\n },\n },\n });\n } catch {\n ctx.send(ws, {\n type: 'plan.updated',\n payload: {\n plan: {\n version: 1,\n sessionId,\n updatedAt: new Date().toISOString(),\n items: [],\n },\n },\n });\n }\n } else {\n ctx.send(ws, {\n type: 'plan.updated',\n payload: { plan: null, error: 'Plan storage is not configured for this session.' },\n });\n }\n}\n\nexport async function handlePlanTemplateUse(ctx: WorklistContext, ws: WebSocket, template: string): Promise<void> {\n const planPath = ctx.context.meta['plan.path'];\n const sessionId = ctx.context.session?.id ?? '';\n if (typeof planPath !== 'string' || !planPath) {\n sendResult(ws, ctx, false, 'Plan storage is not configured for this session.');\n return;\n }\n try {\n const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import('@wrongstack/core');\n const tpl = getPlanTemplate(template);\n if (!tpl) {\n sendResult(ws, ctx, false, `Unknown template \"${template}\".`);\n return;\n }\n let plan = (await loadPlan(planPath)) ?? emptyPlan(sessionId);\n for (const item of tpl.items) {\n ({ plan } = addPlanItem(plan, item.title, item.details));\n }\n await savePlan(planPath, plan);\n sendResult(ws, ctx, true, `Applied template \"${tpl.name}\" — ${tpl.items.length} items added.`);\n ctx.broadcast({ type: 'plan.updated', payload: { plan } });\n } catch (err) {\n sendResult(ws, ctx, false, String(err));\n }\n}\n\nexport async function handlePlanItemUpdate(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: { target: string; status: 'open' | 'in_progress' | 'done' },\n): Promise<void> {\n const planPath = ctx.context.meta['plan.path'];\n const sessionId = ctx.context.session?.id ?? '';\n if (typeof planPath !== 'string' || !planPath) {\n sendResult(ws, ctx, false, 'Plan storage is not configured for this session.');\n return;\n }\n try {\n const { mutatePlan, setPlanItemStatus } = await import('@wrongstack/core');\n let changed = false;\n const plan = await mutatePlan(planPath, sessionId, async (p) => {\n const before = p.updatedAt;\n const updated = setPlanItemStatus(p, payload.target, payload.status);\n changed = updated.updatedAt !== before;\n return updated;\n });\n if (!changed) {\n sendResult(ws, ctx, false, `No plan item matched \"${payload.target}\".`);\n return;\n }\n sendResult(ws, ctx, true, `Plan item status updated to \"${payload.status}\".`);\n ctx.broadcast({ type: 'plan.updated', payload: { plan } });\n } catch (err) {\n sendResult(ws, ctx, false, String(err));\n }\n}\n\n// ── Dispatcher ──────────────────────────────────────────────────────────────────\n// Single entry point for the nine worklist message types, so the host server's\n// switch delegates one grouped case here instead of repeating the per-type\n// `makeWorklistContext()` boilerplate. Unknown types are a no-op (the caller\n// only routes worklist types to this function).\n\n/** Loosely-typed worklist WS message — payload shapes are narrowed per case. */\nexport interface WorklistMessage {\n type: string;\n payload?: unknown;\n}\n\nexport async function handleWorklistMessage(\n ctx: WorklistContext,\n ws: WebSocket,\n msg: WorklistMessage,\n): Promise<void> {\n switch (msg.type) {\n case 'todos.get':\n handleTodosGet(ctx, ws);\n return;\n case 'todos.clear':\n handleTodosClear(ctx, ws);\n return;\n case 'todos.remove':\n handleTodosRemove(ctx, ws, msg.payload as { id?: string; index?: number } | undefined);\n return;\n case 'todo.update':\n handleTodoUpdate(\n ctx,\n ws,\n msg.payload as { id: string; status?: TodoItem['status']; activeForm?: string },\n );\n return;\n case 'tasks.get':\n await handleTasksGet(ctx, ws);\n return;\n case 'task.update':\n await handleTaskUpdate(\n ctx,\n ws,\n msg.payload as {\n id: string;\n status: 'pending' | 'in_progress' | 'blocked' | 'failed' | 'review' | 'completed';\n },\n );\n return;\n case 'plan.get':\n await handlePlanGet(ctx, ws);\n return;\n case 'plan.template_use': {\n const parsed = validatePlanTemplateUsePayload(msg.payload);\n if (!parsed.ok) {\n sendResult(ws, ctx, false, parsed.message);\n return;\n }\n await handlePlanTemplateUse(ctx, ws, parsed.value.template);\n return;\n }\n case 'plan.item.update':\n await handlePlanItemUpdate(\n ctx,\n ws,\n msg.payload as { target: string; status: 'open' | 'in_progress' | 'done' },\n );\n return;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/ws-payload-validation.ts","../../src/server/handlers/worklist-handlers.ts"],"sourcesContent":["export type PayloadValidationResult<T> = { ok: true; value: T } | { ok: false; message: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport interface ModelSwitchPayload {\n provider: string;\n model: string;\n}\n\nexport function validateModelSwitchPayload(\n payload: unknown,\n): PayloadValidationResult<ModelSwitchPayload> {\n if (!isRecord(payload)) {\n return {\n ok: false,\n message: 'model.switch payload must be an object with string provider and model',\n };\n }\n const provider = payload['provider'];\n const model = payload['model'];\n if (typeof provider !== 'string' || provider.trim().length === 0) {\n return { ok: false, message: 'model.switch payload.provider must be a non-empty string' };\n }\n if (typeof model !== 'string' || model.trim().length === 0) {\n return { ok: false, message: 'model.switch payload.model must be a non-empty string' };\n }\n return { ok: true, value: { provider, model } };\n}\n\nexport interface PrefsUpdatePayload {\n prefs: Record<string, unknown>;\n}\n\nconst AUTONOMY_VALUES = new Set(['off', 'suggest', 'auto', 'eternal', 'eternal-parallel']);\n\nexport interface MailboxMessagesPayload {\n limit?: number;\n agentId?: string;\n unreadOnly?: boolean;\n}\n\nexport function validateMailboxMessagesPayload(\n payload: unknown,\n): PayloadValidationResult<MailboxMessagesPayload | undefined> {\n if (payload === undefined) return { ok: true, value: undefined };\n if (!isRecord(payload)) {\n return { ok: false, message: 'mailbox.messages payload must be an object when provided' };\n }\n const limit = payload['limit'];\n const agentId = payload['agentId'];\n const unreadOnly = payload['unreadOnly'];\n if (limit !== undefined && (typeof limit !== 'number' || !Number.isFinite(limit) || limit < 1)) {\n return {\n ok: false,\n message: 'mailbox.messages payload.limit must be a positive number when provided',\n };\n }\n if (agentId !== undefined && typeof agentId !== 'string') {\n return {\n ok: false,\n message: 'mailbox.messages payload.agentId must be a string when provided',\n };\n }\n if (unreadOnly !== undefined && typeof unreadOnly !== 'boolean') {\n return {\n ok: false,\n message: 'mailbox.messages payload.unreadOnly must be a boolean when provided',\n };\n }\n return { ok: true, value: { limit, agentId, unreadOnly } };\n}\n\nexport interface MailboxAgentsPayload {\n onlineOnly?: boolean;\n}\n\nexport function validateMailboxAgentsPayload(\n payload: unknown,\n): PayloadValidationResult<MailboxAgentsPayload | undefined> {\n if (payload === undefined) return { ok: true, value: undefined };\n if (!isRecord(payload)) {\n return { ok: false, message: 'mailbox.agents payload must be an object when provided' };\n }\n const onlineOnly = payload['onlineOnly'];\n if (onlineOnly !== undefined && typeof onlineOnly !== 'boolean') {\n return {\n ok: false,\n message: 'mailbox.agents payload.onlineOnly must be a boolean when provided',\n };\n }\n return { ok: true, value: { onlineOnly } };\n}\n\nexport interface MailboxPurgePayload {\n completedMaxAgeMs?: number;\n incompleteMaxAgeMs?: number;\n}\n\nexport function validateMailboxPurgePayload(\n payload: unknown,\n): PayloadValidationResult<MailboxPurgePayload | undefined> {\n if (payload === undefined) return { ok: true, value: undefined };\n if (!isRecord(payload)) {\n return { ok: false, message: 'mailbox.purge payload must be an object when provided' };\n }\n const completedMaxAgeMs = payload['completedMaxAgeMs'];\n const incompleteMaxAgeMs = payload['incompleteMaxAgeMs'];\n if (\n completedMaxAgeMs !== undefined &&\n (typeof completedMaxAgeMs !== 'number' ||\n !Number.isFinite(completedMaxAgeMs) ||\n completedMaxAgeMs < 0)\n ) {\n return {\n ok: false,\n message:\n 'mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided',\n };\n }\n if (\n incompleteMaxAgeMs !== undefined &&\n (typeof incompleteMaxAgeMs !== 'number' ||\n !Number.isFinite(incompleteMaxAgeMs) ||\n incompleteMaxAgeMs < 0)\n ) {\n return {\n ok: false,\n message:\n 'mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided',\n };\n }\n return { ok: true, value: { completedMaxAgeMs, incompleteMaxAgeMs } };\n}\n\nexport interface BrainRiskPayload {\n level: string;\n}\n\nconst BRAIN_RISK_VALUES = new Set(['off', 'low', 'medium', 'high', 'all']);\n\nexport function validateBrainRiskPayload(\n payload: unknown,\n): PayloadValidationResult<BrainRiskPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'brain.risk payload must be an object with string level' };\n }\n const level = payload['level'];\n if (typeof level !== 'string' || !BRAIN_RISK_VALUES.has(level)) {\n return {\n ok: false,\n message: 'brain.risk payload.level must be one of off, low, medium, high, all',\n };\n }\n return { ok: true, value: { level } };\n}\n\nexport interface BrainAskPayload {\n question: string;\n}\n\nexport function validateBrainAskPayload(\n payload: unknown,\n): PayloadValidationResult<BrainAskPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'brain.ask payload must be an object with string question' };\n }\n const question = payload['question'];\n if (typeof question !== 'string' || question.trim().length === 0) {\n return { ok: false, message: 'brain.ask payload.question must be a non-empty string' };\n }\n return { ok: true, value: { question: question.trim() } };\n}\n\nexport interface AutonomySwitchPayload {\n mode: string;\n}\n\nexport function validateAutonomySwitchPayload(\n payload: unknown,\n): PayloadValidationResult<AutonomySwitchPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'autonomy.switch payload must be an object with string mode' };\n }\n const mode = payload['mode'];\n if (typeof mode !== 'string' || !AUTONOMY_VALUES.has(mode)) {\n return { ok: false, message: 'autonomy.switch payload.mode must be a valid autonomy mode' };\n }\n return { ok: true, value: { mode } };\n}\n\nexport interface PlanTemplateUsePayload {\n template: string;\n}\n\nexport function validatePlanTemplateUsePayload(\n payload: unknown,\n): PayloadValidationResult<PlanTemplateUsePayload> {\n if (!isRecord(payload)) {\n return {\n ok: false,\n message: 'plan.template_use payload must be an object with string template',\n };\n }\n const template = payload['template'];\n if (typeof template !== 'string' || template.trim().length === 0) {\n return { ok: false, message: 'plan.template_use payload.template must be a non-empty string' };\n }\n return { ok: true, value: { template } };\n}\nconst CONTEXT_STRATEGY_VALUES = new Set(['hybrid', 'intelligent', 'selective']);\nconst CONTEXT_MODE_VALUES = new Set(['balanced', 'frugal', 'deep', 'archival']);\nconst TOKEN_SAVING_TIER_VALUES = new Set(['off', 'minimal', 'light', 'medium', 'aggressive']);\nconst ENHANCE_LANGUAGE_VALUES = new Set(['original', 'english']);\nconst LOG_LEVEL_VALUES = new Set(['debug', 'info', 'warn', 'error']);\nconst AUDIT_LEVEL_VALUES = new Set(['minimal', 'standard', 'full']);\nconst REASONING_MODE_VALUES = new Set(['auto', 'on', 'off']);\nconst REASONING_EFFORT_VALUES = new Set([\n 'none',\n 'minimal',\n 'low',\n 'medium',\n 'high',\n 'xhigh',\n 'max',\n]);\nconst CACHE_TTL_VALUES = new Set(['default', '5m', '1h']);\n\nconst BOOLEAN_PREF_KEYS = new Set([\n 'yolo',\n 'chime',\n 'confirmExit',\n 'streamFleet',\n 'nextPrediction',\n 'titleAnimation',\n 'enhanceEnabled',\n 'featureMcp',\n 'featurePlugins',\n 'featureMemory',\n 'featureSkills',\n 'featureModelsRegistry',\n 'indexOnStart',\n 'contextAutoCompact',\n 'tgSessionEnd',\n 'tgDelegate',\n 'reasoningPreserve',\n 'hqEnabled',\n 'hqRawContent',\n 'fallbackAuto',\n 'favoriteModelsOnly',\n]);\n\n/** Keys whose value must be an array of strings (e.g. an ordered model list). */\nconst STRING_ARRAY_PREF_KEYS = new Set(['fallbackModels', 'favoriteModels']);\nconst STRING_ARRAY_RECORD_PREF_KEYS = new Set(['fallbackProfiles']);\nconst MODEL_MATRIX_PREF_KEYS = new Set(['modelMatrix']);\n\nconst NUMBER_PREF_KEYS = new Set([\n 'autonomyDelayMs',\n 'autoProceedMaxIterations',\n 'maxIterations',\n 'maxConcurrent',\n 'enhanceDelayMs',\n 'tgLongToolMs',\n]);\n\nconst STRING_PREF_KEYS = new Set(['hqUrl', 'hqToken']);\n\nconst ENUM_PREF_KEYS: Record<string, Set<string>> = {\n autonomy: AUTONOMY_VALUES,\n contextStrategy: CONTEXT_STRATEGY_VALUES,\n contextMode: CONTEXT_MODE_VALUES,\n tokenSavingTier: TOKEN_SAVING_TIER_VALUES,\n enhanceLanguage: ENHANCE_LANGUAGE_VALUES,\n logLevel: LOG_LEVEL_VALUES,\n auditLevel: AUDIT_LEVEL_VALUES,\n reasoningMode: REASONING_MODE_VALUES,\n reasoningEffort: REASONING_EFFORT_VALUES,\n cacheTtl: CACHE_TTL_VALUES,\n};\n\nfunction validatePreferenceValue(key: string, value: unknown): string | null {\n if (BOOLEAN_PREF_KEYS.has(key)) {\n return typeof value === 'boolean' ? null : `prefs.update payload.${key} must be a boolean`;\n }\n if (NUMBER_PREF_KEYS.has(key)) {\n return typeof value === 'number' && Number.isFinite(value)\n ? null\n : `prefs.update payload.${key} must be a finite number`;\n }\n if (STRING_PREF_KEYS.has(key)) {\n return typeof value === 'string' ? null : `prefs.update payload.${key} must be a string`;\n }\n if (STRING_ARRAY_PREF_KEYS.has(key)) {\n return Array.isArray(value) && value.every((v) => typeof v === 'string')\n ? null\n : `prefs.update payload.${key} must be an array of strings`;\n }\n if (STRING_ARRAY_RECORD_PREF_KEYS.has(key)) {\n return isRecord(value) &&\n Object.values(value).every(\n (v) => Array.isArray(v) && v.every((item) => typeof item === 'string'),\n )\n ? null\n : `prefs.update payload.${key} must be an object of string arrays`;\n }\n if (MODEL_MATRIX_PREF_KEYS.has(key)) {\n if (!isRecord(value)) return `prefs.update payload.${key} must be an object`;\n for (const entry of Object.values(value)) {\n if (!isRecord(entry)) return `prefs.update payload.${key} entries must be objects`;\n const provider = entry['provider'];\n const model = entry['model'];\n const fallbackProfile = entry['fallbackProfile'];\n if (provider !== undefined && typeof provider !== 'string') {\n return `prefs.update payload.${key}.provider must be a string when provided`;\n }\n if (model !== undefined && typeof model !== 'string') {\n return `prefs.update payload.${key}.model must be a string when provided`;\n }\n if (fallbackProfile !== undefined && typeof fallbackProfile !== 'string') {\n return `prefs.update payload.${key}.fallbackProfile must be a string when provided`;\n }\n if (model === undefined && fallbackProfile === undefined) {\n return `prefs.update payload.${key} entries require model or fallbackProfile`;\n }\n }\n return null;\n }\n const allowed = ENUM_PREF_KEYS[key];\n if (allowed) {\n return typeof value === 'string' && allowed.has(value)\n ? null\n : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(', ')}`;\n }\n return `prefs.update payload contains unknown preference key: ${key}`;\n}\n\nexport function validatePrefsUpdatePayload(\n payload: unknown,\n): PayloadValidationResult<PrefsUpdatePayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'prefs.update payload must be an object' };\n }\n for (const [key, value] of Object.entries(payload)) {\n const error = validatePreferenceValue(key, value);\n if (error) return { ok: false, message: error };\n }\n return { ok: true, value: { prefs: payload } };\n}\n\nexport interface SkillsCreatePayload {\n name: string;\n description: string;\n scope: 'project' | 'global';\n}\n\nexport function validateSkillsCreatePayload(\n payload: unknown,\n): PayloadValidationResult<SkillsCreatePayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'skills.create payload must be an object' };\n }\n const name = payload['name'];\n const description = payload['description'];\n const scope = payload['scope'];\n if (typeof name !== 'string' || name.trim().length === 0) {\n return { ok: false, message: 'Skill name is required' };\n }\n if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name.trim())) {\n return { ok: false, message: 'Skill name must be kebab-case (e.g. my-new-skill)' };\n }\n if (typeof description !== 'string' || description.trim().length === 0) {\n return { ok: false, message: 'Description/trigger is required' };\n }\n if (scope !== 'project' && scope !== 'global') {\n return { ok: false, message: 'skills.create payload.scope must be project or global' };\n }\n return { ok: true, value: { name, description, scope } };\n}\n\nexport interface SkillsEditPayload {\n name: string;\n body: string;\n}\n\nexport function validateSkillsEditPayload(\n payload: unknown,\n): PayloadValidationResult<SkillsEditPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'skills.edit payload must be an object' };\n }\n const name = payload['name'];\n const body = payload['body'];\n if (typeof name !== 'string' || name.trim().length === 0) {\n return { ok: false, message: 'Skill name is required' };\n }\n if (typeof body !== 'string' || body.length === 0) {\n return { ok: false, message: 'Skill body is required' };\n }\n return { ok: true, value: { name, body } };\n}\n\nexport interface ProcessKillPayload {\n pid: number;\n}\n\nexport function validateProcessKillPayload(\n payload: unknown,\n): PayloadValidationResult<ProcessKillPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'process.kill payload must be an object with numeric pid' };\n }\n const pid = payload['pid'];\n if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0) {\n return { ok: false, message: 'process.kill payload.pid must be a positive integer' };\n }\n return { ok: true, value: { pid } };\n}\n\nexport interface WorkingDirSetPayload {\n path: string;\n}\n\nexport function validateWorkingDirSetPayload(\n payload: unknown,\n): PayloadValidationResult<WorkingDirSetPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'working_dir.set payload must be an object with string path' };\n }\n const newPath = payload['path'];\n if (typeof newPath !== 'string' || newPath.trim().length === 0) {\n return { ok: false, message: 'working_dir.set payload.path must be a non-empty string' };\n }\n return { ok: true, value: { path: newPath } };\n}\n\nexport interface ModeSwitchPayload {\n id: string;\n}\n\nexport function validateModeSwitchPayload(\n payload: unknown,\n): PayloadValidationResult<ModeSwitchPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'mode.switch payload must be an object with string id' };\n }\n const id = payload['id'];\n if (typeof id !== 'string' || id.trim().length === 0) {\n return { ok: false, message: 'mode.switch payload.id must be a non-empty string' };\n }\n return { ok: true, value: { id } };\n}\n\nexport interface ContextModeIdPayload {\n id: string;\n}\n\nfunction validateContextModeIdPayload(\n payload: unknown,\n type: 'context.mode.switch' | 'context.mode.delete',\n): PayloadValidationResult<ContextModeIdPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: `${type} payload must be an object with string id` };\n }\n const id = payload['id'];\n if (typeof id !== 'string' || id.trim().length === 0) {\n return { ok: false, message: `${type} payload.id must be a non-empty string` };\n }\n return { ok: true, value: { id } };\n}\n\nexport function validateContextModeSwitchPayload(\n payload: unknown,\n): PayloadValidationResult<ContextModeIdPayload> {\n return validateContextModeIdPayload(payload, 'context.mode.switch');\n}\n\nexport function validateContextModeDeletePayload(\n payload: unknown,\n): PayloadValidationResult<ContextModeIdPayload> {\n return validateContextModeIdPayload(payload, 'context.mode.delete');\n}\n\nexport interface ContextModeCreatePayload {\n id: string;\n name: string;\n description: string;\n thresholds: { warn: number; soft: number; hard: number };\n preserveK: number;\n eliseThreshold: number;\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value);\n}\n\nexport function validateContextModeCreatePayload(\n payload: unknown,\n): PayloadValidationResult<ContextModeCreatePayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'context.mode.create payload must be an object' };\n }\n const id = payload['id'];\n const name = payload['name'];\n const description = payload['description'];\n const thresholds = payload['thresholds'];\n const preserveK = payload['preserveK'];\n const eliseThreshold = payload['eliseThreshold'];\n\n if (typeof id !== 'string' || id.trim().length === 0) {\n return { ok: false, message: 'context.mode.create payload.id must be a non-empty string' };\n }\n if (typeof name !== 'string' || name.trim().length === 0) {\n return { ok: false, message: 'context.mode.create payload.name must be a non-empty string' };\n }\n if (typeof description !== 'string') {\n return { ok: false, message: 'context.mode.create payload.description must be a string' };\n }\n if (!isRecord(thresholds)) {\n return {\n ok: false,\n message:\n 'context.mode.create payload.thresholds must be an object with warn/soft/hard numbers',\n };\n }\n if (\n !isFiniteNumber(thresholds['warn']) ||\n !isFiniteNumber(thresholds['soft']) ||\n !isFiniteNumber(thresholds['hard'])\n ) {\n return {\n ok: false,\n message: 'context.mode.create payload.thresholds.warn/soft/hard must be finite numbers',\n };\n }\n if (!isFiniteNumber(preserveK)) {\n return { ok: false, message: 'context.mode.create payload.preserveK must be a finite number' };\n }\n if (!isFiniteNumber(eliseThreshold)) {\n return {\n ok: false,\n message: 'context.mode.create payload.eliseThreshold must be a finite number',\n };\n }\n return {\n ok: true,\n value: {\n id,\n name,\n description,\n thresholds: { warn: thresholds['warn'], soft: thresholds['soft'], hard: thresholds['hard'] },\n preserveK,\n eliseThreshold,\n },\n };\n}\n\nexport interface ContextModeUpdatePayload {\n id: string;\n name?: string;\n description?: string;\n thresholds?: { warn?: number; soft?: number; hard?: number };\n preserveK?: number;\n eliseThreshold?: number;\n}\n\nexport function validateContextModeUpdatePayload(\n payload: unknown,\n): PayloadValidationResult<ContextModeUpdatePayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'context.mode.update payload must be an object' };\n }\n const id = payload['id'];\n if (typeof id !== 'string' || id.trim().length === 0) {\n return { ok: false, message: 'context.mode.update payload.id must be a non-empty string' };\n }\n\n const name = payload['name'];\n if (name !== undefined && typeof name !== 'string') {\n return {\n ok: false,\n message: 'context.mode.update payload.name must be a string when provided',\n };\n }\n\n const description = payload['description'];\n if (description !== undefined && typeof description !== 'string') {\n return {\n ok: false,\n message: 'context.mode.update payload.description must be a string when provided',\n };\n }\n\n const thresholds = payload['thresholds'];\n let validatedThresholds: ContextModeUpdatePayload['thresholds'];\n if (thresholds !== undefined) {\n if (!isRecord(thresholds)) {\n return {\n ok: false,\n message: 'context.mode.update payload.thresholds must be an object when provided',\n };\n }\n for (const key of ['warn', 'soft', 'hard'] as const) {\n const val = thresholds[key];\n if (val !== undefined && !isFiniteNumber(val)) {\n return {\n ok: false,\n message: `context.mode.update payload.thresholds.${key} must be a finite number when provided`,\n };\n }\n }\n validatedThresholds = {\n warn: typeof thresholds['warn'] === 'number' ? thresholds['warn'] : undefined,\n soft: typeof thresholds['soft'] === 'number' ? thresholds['soft'] : undefined,\n hard: typeof thresholds['hard'] === 'number' ? thresholds['hard'] : undefined,\n };\n }\n\n const preserveK = payload['preserveK'];\n if (preserveK !== undefined && !isFiniteNumber(preserveK)) {\n return {\n ok: false,\n message: 'context.mode.update payload.preserveK must be a finite number when provided',\n };\n }\n\n const eliseThreshold = payload['eliseThreshold'];\n if (eliseThreshold !== undefined && !isFiniteNumber(eliseThreshold)) {\n return {\n ok: false,\n message: 'context.mode.update payload.eliseThreshold must be a finite number when provided',\n };\n }\n\n return {\n ok: true,\n value: {\n id,\n name: typeof name === 'string' ? name : undefined,\n description: typeof description === 'string' ? description : undefined,\n thresholds: validatedThresholds,\n preserveK: typeof preserveK === 'number' ? preserveK : undefined,\n eliseThreshold: typeof eliseThreshold === 'number' ? eliseThreshold : undefined,\n },\n };\n}\n\nexport interface ShellOpenPayload {\n path: string;\n target?: 'file' | 'terminal';\n}\n\nexport function validateShellOpenPayload(\n payload: unknown,\n): PayloadValidationResult<ShellOpenPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'shell.open payload must be an object with string path' };\n }\n const path = payload['path'];\n if (typeof path !== 'string' || path.trim().length === 0) {\n return { ok: false, message: 'shell.open payload.path must be a non-empty string' };\n }\n const target = payload['target'];\n if (target !== undefined && target !== 'file' && target !== 'terminal') {\n return {\n ok: false,\n message: 'shell.open payload.target must be \"file\" or \"terminal\" when provided',\n };\n }\n return { ok: true, value: { path, target: target as ShellOpenPayload['target'] } };\n}\n\nexport interface GitDiffPayload {\n path: string;\n}\n\nexport function validateGitDiffPayload(payload: unknown): PayloadValidationResult<GitDiffPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'git.diff payload must be an object' };\n }\n const path = payload['path'];\n if (path === undefined || path === null) {\n return { ok: true, value: { path: '' } };\n }\n if (typeof path !== 'string') {\n return { ok: false, message: 'git.diff payload.path must be a string when provided' };\n }\n return { ok: true, value: { path } };\n}\n\nexport interface ProjectsAddPayload {\n root: string;\n name?: string;\n}\n\nexport function validateProjectsAddPayload(\n payload: unknown,\n): PayloadValidationResult<ProjectsAddPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'projects.add payload must be an object with string root' };\n }\n const root = payload['root'];\n if (typeof root !== 'string' || root.trim().length === 0) {\n return { ok: false, message: 'projects.add payload.root must be a non-empty string' };\n }\n const name = payload['name'];\n if (name !== undefined && typeof name !== 'string') {\n return { ok: false, message: 'projects.add payload.name must be a string when provided' };\n }\n return { ok: true, value: { root, name: typeof name === 'string' ? name : undefined } };\n}\n\nexport interface ProjectsSelectPayload {\n root: string;\n name?: string;\n}\n\nexport function validateProjectsSelectPayload(\n payload: unknown,\n): PayloadValidationResult<ProjectsSelectPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'projects.select payload must be an object with string root' };\n }\n const root = payload['root'];\n if (typeof root !== 'string' || root.trim().length === 0) {\n return { ok: false, message: 'projects.select payload.root must be a non-empty string' };\n }\n const name = payload['name'];\n if (name !== undefined && typeof name !== 'string') {\n return { ok: false, message: 'projects.select payload.name must be a string when provided' };\n }\n return { ok: true, value: { root, name: typeof name === 'string' ? name : undefined } };\n}\n","// ── Shared Worklist Handlers ─────────────────────────────────────────────────\n// Extracted from standalone server (packages/webui/src/server/index.ts) and CLI\n// embedded server (packages/cli/src/webui-server/). Both servers use these\n// handlers for todos, tasks, and plan operations. Keep them in sync.\n//\n// Message types handled here:\n// todos.get | todos.clear | todos.remove | todo.update\n// tasks.get | task.update\n// plan.get | plan.template_use | plan.item.update\n// ─────────────────────────────────────────────────────────────────────────────\n\nimport type { WebSocket } from 'ws';\nimport type { TodoItem } from '@wrongstack/core';\nimport { validatePlanTemplateUsePayload } from '../ws-payload-validation.js';\n\n// ── Shared result helper ───────────────────────────────────────────────────────\n\nfunction sendResult(\n ws: WebSocket,\n ctx: WorklistContext,\n ok: boolean,\n message: string,\n): void {\n ctx.send(ws, { type: ok ? 'ok' : 'error', message });\n}\n\n// ── Context interface ─────────────────────────────────────────────────────────\n// Both servers satisfy this with their own local state.\n\nexport interface WorklistContext {\n context: {\n todos: TodoItem[];\n meta: Record<string, unknown>;\n session: { id: string } | null;\n state?: unknown;\n };\n send: (ws: WebSocket, msg: object) => void;\n broadcast: (msg: object) => void;\n /**\n * Optional mutator for in-memory todo state. Servers that manage live\n * agent state (e.g. the CLI embedded server) provide this so handlers\n * can update the agent's todo list directly. Standalone server may omit.\n */\n replaceTodos?: (todos: TodoItem[]) => void;\n}\n\n// ── Todos ─────────────────────────────────────────────────────────────────────\n\nexport function handleTodosGet(ctx: WorklistContext, ws: WebSocket): void {\n ctx.send(ws, { type: 'todos.updated', payload: { todos: ctx.context.todos } });\n}\n\nexport function handleTodosClear(ctx: WorklistContext, ws: WebSocket): void {\n ctx.replaceTodos?.([]);\n ctx.broadcast({ type: 'todos.cleared' });\n sendResult(ws, ctx, true, 'Todo board cleared.');\n}\n\nexport function handleTodosRemove(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: { id?: string; index?: number } | undefined,\n): void {\n if (!payload || (payload.id === undefined && payload.index === undefined)) {\n sendResult(ws, ctx, false, 'todos.remove requires id or index.');\n return;\n }\n const next =\n payload.id !== undefined\n ? ctx.context.todos.filter((t) => t.id !== payload.id)\n : ctx.context.todos.filter((_, i) => i !== (payload.index as number));\n ctx.replaceTodos?.(next);\n ctx.broadcast({ type: 'todos.updated', payload: { todos: next } });\n sendResult(ws, ctx, true, 'Todo item removed.');\n}\n\nexport function handleTodoUpdate(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: { id: string; status?: TodoItem['status']; activeForm?: string },\n): void {\n const todo = ctx.context.todos.find((t) => t.id === payload.id);\n if (!todo) {\n sendResult(ws, ctx, false, `No todo with id \"${payload.id}\".`);\n return;\n }\n const next = ctx.context.todos.map((t) =>\n t.id === payload.id\n ? { ...t, ...(payload.status !== undefined && { status: payload.status }), ...(payload.activeForm !== undefined && { activeForm: payload.activeForm }) }\n : t,\n );\n ctx.replaceTodos?.(next);\n ctx.broadcast({ type: 'todos.updated', payload: { todos: next } });\n sendResult(ws, ctx, true, `Todo \"${todo.content}\" updated.`);\n}\n\n// ── Tasks ─────────────────────────────────────────────────────────────────────\n\nexport async function handleTasksGet(ctx: WorklistContext, ws: WebSocket): Promise<void> {\n const taskPath = ctx.context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n try {\n const { loadTasks } = await import('@wrongstack/core');\n const file = await loadTasks(taskPath);\n ctx.send(ws, { type: 'tasks.updated', payload: { tasks: file?.tasks ?? [] } });\n } catch {\n ctx.send(ws, { type: 'tasks.updated', payload: { tasks: [] } });\n }\n } else {\n ctx.send(ws, {\n type: 'tasks.updated',\n payload: { tasks: [], error: 'Task storage not configured.' },\n });\n }\n}\n\nexport async function handleTaskUpdate(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: {\n id: string;\n status: 'pending' | 'in_progress' | 'blocked' | 'failed' | 'review' | 'completed';\n },\n): Promise<void> {\n const taskPath = ctx.context.meta['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) {\n sendResult(ws, ctx, false, 'Task storage is not configured for this session.');\n return;\n }\n try {\n const { loadTasks, saveTasks } = await import('@wrongstack/core');\n const file = await loadTasks(taskPath);\n if (!file) {\n sendResult(ws, ctx, false, 'No task file found.');\n return;\n }\n const idx = file.tasks.findIndex((t) => t.id === payload.id);\n if (idx === -1) {\n sendResult(ws, ctx, false, `Task \"${payload.id}\" not found.`);\n return;\n }\n file.tasks[idx] = { ...file.tasks[idx], status: payload.status };\n await saveTasks(taskPath, file);\n ctx.broadcast({ type: 'tasks.updated', payload: { tasks: file.tasks } });\n sendResult(ws, ctx, true, `Task \"${payload.id}\" marked ${payload.status}.`);\n } catch (err) {\n sendResult(ws, ctx, false, String(err));\n }\n}\n\n// ── Plan ───────────────────────────────────────────────────────────────────────\n\nexport async function handlePlanGet(ctx: WorklistContext, ws: WebSocket): Promise<void> {\n const planPath = ctx.context.meta['plan.path'];\n const sessionId = ctx.context.session?.id ?? '';\n if (typeof planPath === 'string' && planPath) {\n try {\n const { loadPlan } = await import('@wrongstack/core');\n const plan = await loadPlan(planPath);\n ctx.send(ws, {\n type: 'plan.updated',\n payload: {\n plan: plan ?? {\n version: 1,\n sessionId,\n updatedAt: new Date().toISOString(),\n items: [],\n },\n },\n });\n } catch {\n ctx.send(ws, {\n type: 'plan.updated',\n payload: {\n plan: {\n version: 1,\n sessionId,\n updatedAt: new Date().toISOString(),\n items: [],\n },\n },\n });\n }\n } else {\n ctx.send(ws, {\n type: 'plan.updated',\n payload: { plan: null, error: 'Plan storage is not configured for this session.' },\n });\n }\n}\n\nexport async function handlePlanTemplateUse(ctx: WorklistContext, ws: WebSocket, template: string): Promise<void> {\n const planPath = ctx.context.meta['plan.path'];\n const sessionId = ctx.context.session?.id ?? '';\n if (typeof planPath !== 'string' || !planPath) {\n sendResult(ws, ctx, false, 'Plan storage is not configured for this session.');\n return;\n }\n try {\n const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import('@wrongstack/core');\n const tpl = getPlanTemplate(template);\n if (!tpl) {\n sendResult(ws, ctx, false, `Unknown template \"${template}\".`);\n return;\n }\n let plan = (await loadPlan(planPath)) ?? emptyPlan(sessionId);\n for (const item of tpl.items) {\n ({ plan } = addPlanItem(plan, item.title, item.details));\n }\n await savePlan(planPath, plan);\n sendResult(ws, ctx, true, `Applied template \"${tpl.name}\" — ${tpl.items.length} items added.`);\n ctx.broadcast({ type: 'plan.updated', payload: { plan } });\n } catch (err) {\n sendResult(ws, ctx, false, String(err));\n }\n}\n\nexport async function handlePlanItemUpdate(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: { target: string; status: 'open' | 'in_progress' | 'done' },\n): Promise<void> {\n const planPath = ctx.context.meta['plan.path'];\n const sessionId = ctx.context.session?.id ?? '';\n if (typeof planPath !== 'string' || !planPath) {\n sendResult(ws, ctx, false, 'Plan storage is not configured for this session.');\n return;\n }\n try {\n const { mutatePlan, setPlanItemStatus } = await import('@wrongstack/core');\n let changed = false;\n const plan = await mutatePlan(planPath, sessionId, async (p) => {\n const before = p.updatedAt;\n const updated = setPlanItemStatus(p, payload.target, payload.status);\n changed = updated.updatedAt !== before;\n return updated;\n });\n if (!changed) {\n sendResult(ws, ctx, false, `No plan item matched \"${payload.target}\".`);\n return;\n }\n sendResult(ws, ctx, true, `Plan item status updated to \"${payload.status}\".`);\n ctx.broadcast({ type: 'plan.updated', payload: { plan } });\n } catch (err) {\n sendResult(ws, ctx, false, String(err));\n }\n}\n\n// ── Dispatcher ──────────────────────────────────────────────────────────────────\n// Single entry point for the nine worklist message types, so the host server's\n// switch delegates one grouped case here instead of repeating the per-type\n// `makeWorklistContext()` boilerplate. Unknown types are a no-op (the caller\n// only routes worklist types to this function).\n\n/** Loosely-typed worklist WS message — payload shapes are narrowed per case. */\nexport interface WorklistMessage {\n type: string;\n payload?: unknown;\n}\n\nexport async function handleWorklistMessage(\n ctx: WorklistContext,\n ws: WebSocket,\n msg: WorklistMessage,\n): Promise<void> {\n switch (msg.type) {\n case 'todos.get':\n handleTodosGet(ctx, ws);\n return;\n case 'todos.clear':\n handleTodosClear(ctx, ws);\n return;\n case 'todos.remove':\n handleTodosRemove(ctx, ws, msg.payload as { id?: string; index?: number } | undefined);\n return;\n case 'todo.update':\n handleTodoUpdate(\n ctx,\n ws,\n msg.payload as { id: string; status?: TodoItem['status']; activeForm?: string },\n );\n return;\n case 'tasks.get':\n await handleTasksGet(ctx, ws);\n return;\n case 'task.update':\n await handleTaskUpdate(\n ctx,\n ws,\n msg.payload as {\n id: string;\n status: 'pending' | 'in_progress' | 'blocked' | 'failed' | 'review' | 'completed';\n },\n );\n return;\n case 'plan.get':\n await handlePlanGet(ctx, ws);\n return;\n case 'plan.template_use': {\n const parsed = validatePlanTemplateUsePayload(msg.payload);\n if (!parsed.ok) {\n sendResult(ws, ctx, false, parsed.message);\n return;\n }\n await handlePlanTemplateUse(ctx, ws, parsed.value.template);\n return;\n }\n case 'plan.item.update':\n await handlePlanItemUpdate(\n ctx,\n ws,\n msg.payload as { target: string; status: 'open' | 'in_progress' | 'done' },\n );\n return;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WebSocket } from 'ws';
|
|
2
|
-
import { Agent, EventBus, SessionStore, ToolRegistry, ModelsRegistry, ConfigStore, SecretVault, JournalEntry, Logger, Provider, Tool, Context, MemoryStore, ProviderConfig, ProviderApiKey, SddInterviewDriver, AgentFactory, BrainArbiter, SkillLoader } from '@wrongstack/core';
|
|
2
|
+
import { Agent, EventBus, SessionStore, ToolRegistry, ModelsRegistry, ConfigStore, SecretVault, JournalEntry, Logger, Provider, Tool, Context, MemoryStore, ProviderConfig, ProviderApiKey, SddInterviewDriver, AgentFactory, BrainArbiter, SkillLoader, PromptLoader, PromptUsageStore } from '@wrongstack/core';
|
|
3
3
|
import * as http from 'node:http';
|
|
4
4
|
import { MCPRegistry } from '@wrongstack/mcp';
|
|
5
5
|
import { SkillInstaller } from '@wrongstack/core/skills';
|
|
@@ -13,8 +13,23 @@ interface WSClientMessage {
|
|
|
13
13
|
payload?: unknown | undefined;
|
|
14
14
|
}
|
|
15
15
|
interface WebUIOptions {
|
|
16
|
+
/** HTTP frontend port. Prefer `httpPort`; `port` is kept for compatibility. */
|
|
16
17
|
port?: number | undefined;
|
|
18
|
+
/** HTTP frontend port. */
|
|
19
|
+
httpPort?: number | undefined;
|
|
17
20
|
webuiPort?: number | undefined;
|
|
21
|
+
/** WebSocket backend port. */
|
|
22
|
+
wsPort?: number | undefined;
|
|
23
|
+
/** Host/interface to bind. */
|
|
24
|
+
wsHost?: string | undefined;
|
|
25
|
+
/** Fixed access token/password. Defaults to WEBUI_TOKEN or random per process. */
|
|
26
|
+
accessToken?: string | undefined;
|
|
27
|
+
/** Browser-facing HTTP URL, used in startup output and instance registry. */
|
|
28
|
+
publicUrl?: string | undefined;
|
|
29
|
+
/** Browser-facing WebSocket URL, injected into the frontend for tunnels/proxies. */
|
|
30
|
+
publicWsUrl?: string | undefined;
|
|
31
|
+
/** Force token/password protection even on loopback binds. */
|
|
32
|
+
requireToken?: boolean | undefined;
|
|
18
33
|
/**
|
|
19
34
|
* Pre-built backend services. When provided, `startWebUI` skips its
|
|
20
35
|
* default agent/event-bus/session/store construction and wires the
|
|
@@ -103,6 +118,11 @@ interface CreateHttpServerOptions {
|
|
|
103
118
|
* is allowed to open a WebSocket back to the local server.
|
|
104
119
|
*/
|
|
105
120
|
wsPort: number;
|
|
121
|
+
/**
|
|
122
|
+
* Public WebSocket URL injected into the frontend. Use this behind tunnels or
|
|
123
|
+
* reverse proxies where the browser-facing WS URL differs from host:wsPort.
|
|
124
|
+
*/
|
|
125
|
+
publicWsUrl?: string | undefined;
|
|
106
126
|
/**
|
|
107
127
|
* Path to the global WrongStack root (~/.wrongstack). Used by the
|
|
108
128
|
* /api/sessions and /api/sessions/:id/agents endpoints to read the
|
|
@@ -110,11 +130,13 @@ interface CreateHttpServerOptions {
|
|
|
110
130
|
*/
|
|
111
131
|
globalRoot?: string | undefined;
|
|
112
132
|
/**
|
|
113
|
-
* Shared auth token for
|
|
114
|
-
* binds (LAN exposure). Loopback binds accept
|
|
133
|
+
* Shared auth token for HTTP and WS access. Required for non-loopback
|
|
134
|
+
* binds (LAN exposure). Loopback binds accept local browser access without
|
|
115
135
|
* a token (the WS path's loopback-bootstrap policy — see ws-auth.ts).
|
|
116
136
|
*/
|
|
117
137
|
apiToken?: string | undefined;
|
|
138
|
+
/** Force HTTP token auth even on loopback binds, useful behind public tunnels. */
|
|
139
|
+
requireToken?: boolean | undefined;
|
|
118
140
|
/**
|
|
119
141
|
* If true, the `/ws-auth` endpoint exchanges a `?token=` query param (or
|
|
120
142
|
* `X-WS-Token` header) for an `HttpOnly` auth cookie. The cookie is then
|
|
@@ -149,7 +171,7 @@ interface CreateHttpServerOptions {
|
|
|
149
171
|
*/
|
|
150
172
|
declare function injectWsPort(html: string, wsPort: number): string;
|
|
151
173
|
/** Build the Content-Security-Policy value for the given WS port. */
|
|
152
|
-
declare function buildCspHeader(wsPort: number): string;
|
|
174
|
+
declare function buildCspHeader(wsPort: number, requestHost?: string | undefined, publicWsUrl?: string | undefined): string;
|
|
153
175
|
/**
|
|
154
176
|
* Create the static-file HTTP server. Returns the `http.Server` (not
|
|
155
177
|
* listening yet) so the caller can attach to a `shutdown()` hook and
|
|
@@ -201,6 +223,11 @@ declare function browserOpenCommand(url: string, platform?: NodeJS.Platform): {
|
|
|
201
223
|
* process so it survives kill/killAll. Never throws. */
|
|
202
224
|
declare function openBrowser(url: string, platform?: NodeJS.Platform): void;
|
|
203
225
|
|
|
226
|
+
interface WorktreeManagementDeps {
|
|
227
|
+
projectRoot: string;
|
|
228
|
+
/** Board snapshot dir — powers the cross-process liveness guard on cleanup. */
|
|
229
|
+
boardsDir: string;
|
|
230
|
+
}
|
|
204
231
|
/**
|
|
205
232
|
* WorktreeWebSocketHandler — mirrors AutoPhaseWebSocketHandler. Subscribes to
|
|
206
233
|
* the shared EventBus `worktree.*` lifecycle events, keeps a live snapshot of
|
|
@@ -211,14 +238,44 @@ declare function openBrowser(url: string, platform?: NodeJS.Platform): void;
|
|
|
211
238
|
declare class WorktreeWebSocketHandler {
|
|
212
239
|
private readonly events;
|
|
213
240
|
private readonly logger;
|
|
241
|
+
private readonly management?;
|
|
214
242
|
private readonly clients;
|
|
215
243
|
private readonly handles;
|
|
216
244
|
private baseBranch;
|
|
217
245
|
private broadcastInterval;
|
|
218
246
|
private readonly offs;
|
|
219
|
-
constructor(events: EventBus, logger: Logger);
|
|
247
|
+
constructor(events: EventBus, logger: Logger, management?: WorktreeManagementDeps | undefined);
|
|
220
248
|
addClient(ws: WebSocket): void;
|
|
249
|
+
/** Handle worktree-panel control messages (scan / clean / per-row ops). */
|
|
250
|
+
handleMessage(msg: {
|
|
251
|
+
type: string;
|
|
252
|
+
payload?: Record<string, unknown>;
|
|
253
|
+
}): Promise<boolean>;
|
|
221
254
|
dispose(): void;
|
|
255
|
+
/** Absolute managed-worktrees root for this project. */
|
|
256
|
+
private worktreesRoot;
|
|
257
|
+
/** True iff `dir` resolves strictly inside the managed worktrees root. */
|
|
258
|
+
private underRoot;
|
|
259
|
+
/** Branches of worktrees a live in-session run currently owns. */
|
|
260
|
+
private liveActiveBranches;
|
|
261
|
+
/**
|
|
262
|
+
* Scan the disk for managed worktrees/branches NOT owned by a live in-session
|
|
263
|
+
* run and broadcast them as orphans, with whether it is safe to clean now.
|
|
264
|
+
* No-op (empty inventory) when management deps were not wired.
|
|
265
|
+
*/
|
|
266
|
+
private scanAndBroadcast;
|
|
267
|
+
/**
|
|
268
|
+
* Force-remove every orphaned worktree + branch. Refused while a run is live —
|
|
269
|
+
* in this session (active handles) OR another process (the SDD board liveness
|
|
270
|
+
* guard inside cleanupStaleSddWorktrees). Best-effort; reports the outcome.
|
|
271
|
+
*/
|
|
272
|
+
private cleanupOrphans;
|
|
273
|
+
/** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
|
|
274
|
+
private removeOne;
|
|
275
|
+
/** Squash-merge ONE branch into base. Refused while a live run owns it. */
|
|
276
|
+
private mergeBranch;
|
|
277
|
+
/** Compact change summary for one worktree checkout. */
|
|
278
|
+
private diffOne;
|
|
222
279
|
private subscribe;
|
|
223
280
|
private upsert;
|
|
224
281
|
private patch;
|
|
@@ -277,7 +334,7 @@ declare function messagePreview(content: unknown): string;
|
|
|
277
334
|
/**
|
|
278
335
|
* Running-instance registry for the standalone WebUI server.
|
|
279
336
|
*
|
|
280
|
-
* Every live `
|
|
337
|
+
* Every live `wstackui` process records itself in a single JSON file under the
|
|
281
338
|
* wstack home dir (`~/.wrongstack/webui-instances.json`) so a user running
|
|
282
339
|
* several instances (one per project, or several per project on different
|
|
283
340
|
* ports) can see at a glance which ports are open for which path.
|
|
@@ -327,7 +384,7 @@ declare function registerInstance(record: WebUIInstanceRecord, baseDir?: string)
|
|
|
327
384
|
declare function unregisterInstance(pid: number, baseDir?: string): Promise<void>;
|
|
328
385
|
/** List live instances, pruning any dead entries encountered. */
|
|
329
386
|
declare function listInstances(baseDir?: string): Promise<WebUIInstanceRecord[]>;
|
|
330
|
-
/** Human-readable table of running instances for `
|
|
387
|
+
/** Human-readable table of running instances for `wstackui --list`. */
|
|
331
388
|
declare function formatInstances(instances: WebUIInstanceRecord[]): string;
|
|
332
389
|
|
|
333
390
|
type EternalSubscribe = (fn: (entry: JournalEntry) => void) => () => void;
|
|
@@ -374,6 +431,16 @@ declare function errMessage(err: unknown): string;
|
|
|
374
431
|
* Shared between standalone and CLI-embedded WebUI servers.
|
|
375
432
|
*/
|
|
376
433
|
declare function generateAuthToken(): string;
|
|
434
|
+
declare function resolveAuthToken(explicit?: string | undefined): string;
|
|
435
|
+
declare function hostForBrowserUrl(bindHost: string): string;
|
|
436
|
+
declare function buildWebUIAccessUrl(opts: {
|
|
437
|
+
host: string;
|
|
438
|
+
port: number;
|
|
439
|
+
token?: string | undefined;
|
|
440
|
+
protocol?: 'http' | 'https' | undefined;
|
|
441
|
+
publicUrl?: string | undefined;
|
|
442
|
+
}): string;
|
|
443
|
+
declare function envFlag(name: string): boolean;
|
|
377
444
|
|
|
378
445
|
/**
|
|
379
446
|
* Shared file-operation WebSocket handlers for both the standalone WebUI
|
|
@@ -525,7 +592,7 @@ declare function handleMemoryForget(ws: WebSocket, msg: unknown, memoryStore: Me
|
|
|
525
592
|
|
|
526
593
|
/**
|
|
527
594
|
* MCP management handlers for the WebUI server (both the standalone
|
|
528
|
-
* `
|
|
595
|
+
* `wstackui` server and the CLI's embedded `--webui` server).
|
|
529
596
|
*
|
|
530
597
|
* These are thin WebSocket translators over the shared, surface-agnostic
|
|
531
598
|
* management core in `@wrongstack/mcp` (`manage.ts`) — the SAME core the REPL
|
|
@@ -620,6 +687,7 @@ declare function extractToken(url: string): string | undefined;
|
|
|
620
687
|
declare function hostHeaderOk(input: {
|
|
621
688
|
hostHeader: string | undefined;
|
|
622
689
|
wsHost: string;
|
|
690
|
+
allowedHostnames?: readonly string[] | undefined;
|
|
623
691
|
}): boolean;
|
|
624
692
|
interface VerifyClientInput {
|
|
625
693
|
/** Browser `Origin` header, or undefined for non-browser clients. */
|
|
@@ -637,6 +705,12 @@ interface VerifyClientInput {
|
|
|
637
705
|
wsHost: string;
|
|
638
706
|
/** The server's generated auth token. */
|
|
639
707
|
expectedToken: string;
|
|
708
|
+
/** Force token auth even for loopback binds, useful behind public tunnels. */
|
|
709
|
+
requireToken?: boolean | undefined;
|
|
710
|
+
/** Extra Host header names allowed on loopback binds, e.g. a tunnel hostname. */
|
|
711
|
+
allowedHostnames?: readonly string[] | undefined;
|
|
712
|
+
/** Allow browser WS URL tokens for explicit public WS URLs where cookies cannot cross hostnames. */
|
|
713
|
+
allowBrowserUrlToken?: boolean | undefined;
|
|
640
714
|
}
|
|
641
715
|
/**
|
|
642
716
|
* Decide whether to accept an incoming WebSocket handshake. Pure mirror of the
|
|
@@ -759,19 +833,49 @@ declare class AutoPhaseWebSocketHandler {
|
|
|
759
833
|
private store;
|
|
760
834
|
private clients;
|
|
761
835
|
private broadcastInterval;
|
|
762
|
-
/** Aborts in-flight task agents when the run is stopped. */
|
|
836
|
+
/** Aborts in-flight task agents AND the planning turn when the run is stopped. */
|
|
763
837
|
private abort;
|
|
838
|
+
/** Set the instant a stop/clear/revert is requested, so a planning turn that
|
|
839
|
+
* resolves afterwards never launches the orchestrator (the abort alone can't
|
|
840
|
+
* cover the window between the LLM call resolving and the orchestrator start). */
|
|
841
|
+
private stopping;
|
|
764
842
|
/** Optional per-phase git-worktree isolation (lazily created at start). */
|
|
765
843
|
private worktrees;
|
|
844
|
+
/** Base branch + tip SHA captured at run start so a revert can git-revert the
|
|
845
|
+
* run's squash commits (history-preserving) instead of a destructive reset. */
|
|
846
|
+
private runBase;
|
|
766
847
|
/** Per-run worker identities so the board can show "who is on what". */
|
|
767
848
|
private usedNicknames;
|
|
768
849
|
constructor(agent: Agent, context: Context, logger: Logger, storeDir: string, events?: EventBus | undefined, projectRoot?: string | undefined);
|
|
769
850
|
addClient(ws: WebSocket): void;
|
|
770
851
|
handleMessage(msg: AutoPhaseWSMessage): Promise<void>;
|
|
771
852
|
private handleStart;
|
|
853
|
+
/**
|
|
854
|
+
* Halt the run NOW — at any phase. Sets `stopping` (so a planning turn that
|
|
855
|
+
* resolves afterwards bails), aborts in-flight agents, stops the orchestrator
|
|
856
|
+
* tick, and ends the live broadcast. The board is kept for review; use
|
|
857
|
+
* `autophase.clear` to reset or `autophase.revert` to undo the changes.
|
|
858
|
+
*/
|
|
859
|
+
private handleStop;
|
|
860
|
+
/**
|
|
861
|
+
* Stop + wipe: tear down phase worktrees and reset to an empty board so the UI
|
|
862
|
+
* returns to the start screen ("new one"). Does NOT touch already-merged commits
|
|
863
|
+
* on the base branch — that is `autophase.revert`.
|
|
864
|
+
*/
|
|
865
|
+
private handleClear;
|
|
866
|
+
/**
|
|
867
|
+
* Stop + undo: remove phase worktrees, then history-preservingly `git revert`
|
|
868
|
+
* every commit this run landed on the base branch (captured `runBase`..HEAD),
|
|
869
|
+
* then reset to an empty board. Refuses (reports a reason) on a dirty tree or a
|
|
870
|
+
* conflicting revert rather than leaving the tree half-reverted.
|
|
871
|
+
*/
|
|
872
|
+
private handleRevert;
|
|
772
873
|
/** Generic fallback phases when the LLM planner produces nothing usable. */
|
|
773
874
|
private defaultPhases;
|
|
774
|
-
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
875
|
+
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
876
|
+
* The caller passes the run's abort signal so a stop during planning cancels
|
|
877
|
+
* the LLM turn (the previous fresh, never-aborted controller made planning
|
|
878
|
+
* uninterruptible). */
|
|
775
879
|
private planPhases;
|
|
776
880
|
private executeTaskWithAgent;
|
|
777
881
|
/** Persist + broadcast after an interactive board mutation. */
|
|
@@ -822,6 +926,16 @@ interface SddBoardWSMessage {
|
|
|
822
926
|
type: string;
|
|
823
927
|
payload?: Record<string, unknown>;
|
|
824
928
|
}
|
|
929
|
+
/** Project paths the handler needs to apply lifecycle ops directly. */
|
|
930
|
+
interface SddBoardLifecycleDeps {
|
|
931
|
+
projectRoot: string;
|
|
932
|
+
paths: {
|
|
933
|
+
projectSpecs: string;
|
|
934
|
+
projectTaskGraphs: string;
|
|
935
|
+
projectSddSession: string;
|
|
936
|
+
projectSddBoards: string;
|
|
937
|
+
};
|
|
938
|
+
}
|
|
825
939
|
/**
|
|
826
940
|
* SddBoardWebSocketHandler — streams the live SDD multi-agent board to clients
|
|
827
941
|
* and relays control commands back to the CLI-owned run.
|
|
@@ -839,12 +953,20 @@ interface SddBoardWSMessage {
|
|
|
839
953
|
declare class SddBoardWebSocketHandler {
|
|
840
954
|
private readonly store;
|
|
841
955
|
private readonly clients;
|
|
956
|
+
private readonly lifecycle?;
|
|
842
957
|
private latest;
|
|
843
958
|
private poll;
|
|
844
959
|
private unsub;
|
|
845
|
-
constructor(boardsDir: string, events?: EventBus);
|
|
960
|
+
constructor(boardsDir: string, events?: EventBus, lifecycle?: SddBoardLifecycleDeps);
|
|
846
961
|
addClient(ws: WebSocket): void;
|
|
847
962
|
handleMessage(msg: SddBoardWSMessage): Promise<void>;
|
|
963
|
+
/**
|
|
964
|
+
* Apply a cleanup/rollback/destroy from disk and broadcast a structured
|
|
965
|
+
* `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
|
|
966
|
+
* the user must stop it first; the UI gates the buttons on `!active` and the
|
|
967
|
+
* Destroy flow auto-stops then waits before sending `destroy`.
|
|
968
|
+
*/
|
|
969
|
+
private applyLifecycle;
|
|
848
970
|
dispose(): void;
|
|
849
971
|
private pollLatest;
|
|
850
972
|
private sendCurrent;
|
|
@@ -883,6 +1005,8 @@ interface SddWizardDeps {
|
|
|
883
1005
|
defaultModel?: string | undefined;
|
|
884
1006
|
defaultProvider?: string | undefined;
|
|
885
1007
|
fallbackModels?: string[] | undefined;
|
|
1008
|
+
/** Per-run worktree-isolation override; undefined → env default. */
|
|
1009
|
+
worktrees?: boolean | undefined;
|
|
886
1010
|
}) => Promise<{
|
|
887
1011
|
runId: string;
|
|
888
1012
|
}>;
|
|
@@ -937,12 +1061,6 @@ interface SddWizardWiringOptions {
|
|
|
937
1061
|
projectDir: string;
|
|
938
1062
|
};
|
|
939
1063
|
}
|
|
940
|
-
/**
|
|
941
|
-
* Build the {@link SddWizardDeps} shared by both webui servers from a single
|
|
942
|
-
* per-task `subagentFactory`. The factory drives BOTH the interview agent (an
|
|
943
|
-
* isolated turn off the main chat bus) and the real multi-agent run, so each
|
|
944
|
-
* server only has to supply the right factory for its process.
|
|
945
|
-
*/
|
|
946
1064
|
declare function buildSddWizardDeps(opts: SddWizardWiringOptions): SddWizardDeps;
|
|
947
1065
|
|
|
948
1066
|
/**
|
|
@@ -1010,10 +1128,91 @@ declare function handleSkillsEdit(ws: WebSocket, ctx: SkillsContext, msg: unknow
|
|
|
1010
1128
|
*/
|
|
1011
1129
|
declare function handleSkillsExport(ws: WebSocket, ctx: SkillsContext): Promise<void>;
|
|
1012
1130
|
|
|
1131
|
+
/**
|
|
1132
|
+
* Shared prompt-library WebSocket handlers for BOTH the standalone WebUI server
|
|
1133
|
+
* (`packages/webui/src/server/index.ts`) and the CLI's `--webui` embedded server
|
|
1134
|
+
* (`packages/cli/src/webui-server.ts`). One source of truth so the two servers
|
|
1135
|
+
* never drift (the lesson from skills-handlers).
|
|
1136
|
+
*
|
|
1137
|
+
* Each function handles one request→response cycle; callers drop them into their
|
|
1138
|
+
* switch:
|
|
1139
|
+
*
|
|
1140
|
+
* case 'prompts.search': return handlePromptsSearch(ws, promptsCtx, msg);
|
|
1141
|
+
*
|
|
1142
|
+
* The prompt library is read across three layers (builtin + user + project) by
|
|
1143
|
+
* the injected `PromptLoader`; writes (create/favorite) go to the user layer
|
|
1144
|
+
* with copy-on-write for builtins. Treat synced/builtin content as DATA — these
|
|
1145
|
+
* handlers never execute it; the client inserts a chosen prompt into the chat
|
|
1146
|
+
* input as an ordinary user turn.
|
|
1147
|
+
*/
|
|
1148
|
+
|
|
1149
|
+
interface PromptsContext {
|
|
1150
|
+
/** Backs all prompt ops. Absent ⇒ feature unavailable. */
|
|
1151
|
+
promptLoader: PromptLoader | undefined;
|
|
1152
|
+
/** Records per-slug insert counts (shared with CLI `/prompt recent`). */
|
|
1153
|
+
promptUsage?: PromptUsageStore | undefined;
|
|
1154
|
+
}
|
|
1155
|
+
declare function handlePromptsList(ws: WSLike, ctx: PromptsContext): Promise<void>;
|
|
1156
|
+
declare function handlePromptsSearch(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
1157
|
+
declare function handlePromptsContent(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
1158
|
+
declare function handlePromptsFavorite(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
1159
|
+
declare function handlePromptsCreate(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
1160
|
+
/** Record that a prompt was inserted (best-effort; feeds CLI `/prompt recent`). */
|
|
1161
|
+
declare function handlePromptsUsed(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
1162
|
+
/** Return recently-inserted prompt slugs (most-recent first) for the modal's Recent view. */
|
|
1163
|
+
declare function handlePromptsRecent(ws: WSLike, ctx: PromptsContext): Promise<void>;
|
|
1164
|
+
/** Minimal structural type for the ws.send sink (matches `ws` WebSocket). */
|
|
1165
|
+
type WSLike = Parameters<typeof send>[0];
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Shared Design Studio WebSocket handlers for both the standalone WebUI server
|
|
1169
|
+
* (`packages/webui/src/server/index.ts`) and the CLI's `--webui` embedded
|
|
1170
|
+
* server (`packages/cli/src/webui-server.ts`). One source of truth keeps the two
|
|
1171
|
+
* servers at parity (enforced by ws-handler-parity.test.ts).
|
|
1172
|
+
*
|
|
1173
|
+
* case 'design.list': return handleDesignList(ws, designCtx);
|
|
1174
|
+
* case 'design.use': return handleDesignUse(ws, designCtx, msg);
|
|
1175
|
+
* case 'design.state': return handleDesignState(ws, designCtx);
|
|
1176
|
+
* case 'design.set': return handleDesignSet(ws, designCtx, msg);
|
|
1177
|
+
* case 'design.materialize': return handleDesignMaterialize(ws, designCtx, msg);
|
|
1178
|
+
*
|
|
1179
|
+
* Browsing + customization of curated UI design kits; `design.use` pins the
|
|
1180
|
+
* active kit, `design.set` records color/token overrides, `design.materialize`
|
|
1181
|
+
* writes the (override-applied) tokens to a real theme file on disk.
|
|
1182
|
+
*/
|
|
1183
|
+
|
|
1184
|
+
interface DesignContext {
|
|
1185
|
+
projectRoot: string;
|
|
1186
|
+
/** Live agent context whose `meta.designStudio` we read/pin. Optional. */
|
|
1187
|
+
agentMeta?: {
|
|
1188
|
+
meta: Record<string, unknown>;
|
|
1189
|
+
} | undefined;
|
|
1190
|
+
}
|
|
1191
|
+
declare function handleDesignList(ws: WebSocket, ctx: DesignContext): Promise<void>;
|
|
1192
|
+
declare function handleDesignState(ws: WebSocket, ctx: DesignContext): Promise<void>;
|
|
1193
|
+
declare function handleDesignUse(ws: WebSocket, ctx: DesignContext, msg: {
|
|
1194
|
+
payload?: unknown;
|
|
1195
|
+
}): Promise<void>;
|
|
1196
|
+
/** Record structured color/token overrides without changing the pinned kit. */
|
|
1197
|
+
declare function handleDesignSet(ws: WebSocket, ctx: DesignContext, msg: {
|
|
1198
|
+
payload?: unknown;
|
|
1199
|
+
}): Promise<void>;
|
|
1200
|
+
/** Write the active kit's (override-applied) tokens to a real theme file. */
|
|
1201
|
+
declare function handleDesignMaterialize(ws: WebSocket, ctx: DesignContext, msg: {
|
|
1202
|
+
payload?: unknown;
|
|
1203
|
+
}): Promise<void>;
|
|
1204
|
+
/** Scan project UI files for off-palette colors against the active kit. */
|
|
1205
|
+
declare function handleDesignVerify(ws: WebSocket, ctx: DesignContext): Promise<void>;
|
|
1206
|
+
|
|
1013
1207
|
declare function startWebUI(opts?: WebUIOptions & {
|
|
1014
1208
|
wsPort?: number | undefined;
|
|
1015
1209
|
wsHost?: string | undefined;
|
|
1210
|
+
httpPort?: number | undefined;
|
|
1211
|
+
accessToken?: string | undefined;
|
|
1212
|
+
publicUrl?: string | undefined;
|
|
1213
|
+
publicWsUrl?: string | undefined;
|
|
1214
|
+
requireToken?: boolean | undefined;
|
|
1016
1215
|
open?: boolean | undefined;
|
|
1017
1216
|
}): Promise<void>;
|
|
1018
1217
|
|
|
1019
|
-
export { AutoPhaseWebSocketHandler, type BackendServices, type CompletionHandlerOptions, type CompletionItemKind, type CompletionSuggestion, type ConnectedClient, type ContextBreakdown, type CreateHttpServerOptions, type CustomContextMode, type CustomModeStore, type EternalBroadcast, type EternalSubscribe, type EternalSubscription, type KeyOpResult, type LspCompletionSource, type LspCompletionSourceRequest, type MessageTokenEntry, type ProvidersRecord, SddBoardWebSocketHandler, type SddWizardDeps, SddWizardWebSocketHandler, type SddWizardWiringOptions, type ShellOpenRequest, type ShellOpenResult, type ShellOpenTarget, type SkillsContext, SpecsWebSocketHandler, type ToolTokenEntry, type VerifyClientInput, type WSClientMessage, type WSServerMessage, type WebUIInstanceRecord, type WebUIOptions, WorktreeWebSocketHandler, addProvider, broadcast, browserOpenCommand, buildCspHeader, buildSddWizardDeps, createCustomModeStore, createEternalSubscription, createHttpServer, createProviderConfigIO, createToolLspCompletionSource, defaultBaseDir, deleteKey, errMessage, estimateTokens, extractToken, findFreePort, formatInstances, generateAuthToken, handleCompletionRequest, handleFilesList, handleFilesRead, handleFilesTree, handleFilesWrite, handleGitChanges, handleGitDiff, handleGitInfo, handleMcpAdd, handleMcpDisable, handleMcpDiscover, handleMcpEnable, handleMcpList, handleMcpRemove, handleMcpRestart, handleMcpSleep, handleMcpUpdate, handleMcpWake, handleMemoryForget, handleMemoryList, handleMemoryRemember, handleShellOpen, handleSkillsContent, handleSkillsCreate, handleSkillsEdit, handleSkillsExport, handleSkillsInstall, handleSkillsUninstall, handleSkillsUpdate, hostHeaderOk, injectWsPort, isLoopbackBind, isLoopbackHostname, isPortFree, listInstances, loadSavedProviders, maskedKey, messagePreview, messageTokens, normalizeKeys, openBrowser, registerInstance, registryPath, removeProvider, saveProviders, send, sendResult, setActiveKey, startWebUI, stringifyContent, tokenMatches, unregisterInstance, upsertKey, verifyClient, writeKeysBack };
|
|
1218
|
+
export { AutoPhaseWebSocketHandler, type BackendServices, type CompletionHandlerOptions, type CompletionItemKind, type CompletionSuggestion, type ConnectedClient, type ContextBreakdown, type CreateHttpServerOptions, type CustomContextMode, type CustomModeStore, type DesignContext, type EternalBroadcast, type EternalSubscribe, type EternalSubscription, type KeyOpResult, type LspCompletionSource, type LspCompletionSourceRequest, type MessageTokenEntry, type PromptsContext, type ProvidersRecord, SddBoardWebSocketHandler, type SddWizardDeps, SddWizardWebSocketHandler, type SddWizardWiringOptions, type ShellOpenRequest, type ShellOpenResult, type ShellOpenTarget, type SkillsContext, SpecsWebSocketHandler, type ToolTokenEntry, type VerifyClientInput, type WSClientMessage, type WSServerMessage, type WebUIInstanceRecord, type WebUIOptions, WorktreeWebSocketHandler, addProvider, broadcast, browserOpenCommand, buildCspHeader, buildSddWizardDeps, buildWebUIAccessUrl, createCustomModeStore, createEternalSubscription, createHttpServer, createProviderConfigIO, createToolLspCompletionSource, defaultBaseDir, deleteKey, envFlag, errMessage, estimateTokens, extractToken, findFreePort, formatInstances, generateAuthToken, handleCompletionRequest, handleDesignList, handleDesignMaterialize, handleDesignSet, handleDesignState, handleDesignUse, handleDesignVerify, handleFilesList, handleFilesRead, handleFilesTree, handleFilesWrite, handleGitChanges, handleGitDiff, handleGitInfo, handleMcpAdd, handleMcpDisable, handleMcpDiscover, handleMcpEnable, handleMcpList, handleMcpRemove, handleMcpRestart, handleMcpSleep, handleMcpUpdate, handleMcpWake, handleMemoryForget, handleMemoryList, handleMemoryRemember, handlePromptsContent, handlePromptsCreate, handlePromptsFavorite, handlePromptsList, handlePromptsRecent, handlePromptsSearch, handlePromptsUsed, handleShellOpen, handleSkillsContent, handleSkillsCreate, handleSkillsEdit, handleSkillsExport, handleSkillsInstall, handleSkillsUninstall, handleSkillsUpdate, hostForBrowserUrl, hostHeaderOk, injectWsPort, isLoopbackBind, isLoopbackHostname, isPortFree, listInstances, loadSavedProviders, maskedKey, messagePreview, messageTokens, normalizeKeys, openBrowser, registerInstance, registryPath, removeProvider, resolveAuthToken, saveProviders, send, sendResult, setActiveKey, startWebUI, stringifyContent, tokenMatches, unregisterInstance, upsertKey, verifyClient, writeKeysBack };
|