@wrongstack/webui 0.272.2 → 0.273.1
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-BGzM4-Zu.css +2 -0
- package/dist/assets/index-D0dNaLPf.js +140 -0
- package/dist/assets/vendor-Doh9e_v3.css +1 -0
- package/dist/assets/{vendor-cIhL9uWi.js → vendor-P9eRrO6V.js} +296 -296
- package/dist/index.html +4 -4
- package/dist/index.js +8175 -4852
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +1182 -413
- package/dist/server/entry.js.map +1 -1
- package/dist/server/handlers.js +5 -2
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/index.d.ts +165 -2
- package/dist/server/index.js +1187 -414
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +201 -1
- package/package.json +6 -6
- package/dist/assets/index-CTDuGGlX.css +0 -2
- package/dist/assets/index-DSfCNNx4.js +0 -122
- package/dist/assets/vendor-BzqIVtlT.css +0 -1
package/dist/server/handlers.js
CHANGED
|
@@ -4,7 +4,10 @@ function isRecord(value) {
|
|
|
4
4
|
}
|
|
5
5
|
function validatePlanTemplateUsePayload(payload) {
|
|
6
6
|
if (!isRecord(payload)) {
|
|
7
|
-
return {
|
|
7
|
+
return {
|
|
8
|
+
ok: false,
|
|
9
|
+
message: "plan.template_use payload must be an object with string template"
|
|
10
|
+
};
|
|
8
11
|
}
|
|
9
12
|
const template = payload["template"];
|
|
10
13
|
if (typeof template !== "string" || template.trim().length === 0) {
|
|
@@ -162,7 +165,7 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
|
|
|
162
165
|
return;
|
|
163
166
|
}
|
|
164
167
|
try {
|
|
165
|
-
const {
|
|
168
|
+
const { mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
|
|
166
169
|
let changed = false;
|
|
167
170
|
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
168
171
|
const before = p.updatedAt;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/ws-payload-validation.ts","../../src/server/handlers/worklist-handlers.ts"],"sourcesContent":["export type PayloadValidationResult<T> =\n | { ok: true; value: T }\n | { 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(payload: unknown): PayloadValidationResult<ModelSwitchPayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'model.switch payload must be an object with string provider and model' };\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(payload: unknown): 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 { ok: false, message: 'mailbox.messages payload.limit must be a positive number when provided' };\n }\n if (agentId !== undefined && typeof agentId !== 'string') {\n return { ok: false, message: 'mailbox.messages payload.agentId must be a string when provided' };\n }\n if (unreadOnly !== undefined && typeof unreadOnly !== 'boolean') {\n return { ok: false, message: 'mailbox.messages payload.unreadOnly must be a boolean when provided' };\n }\n return { ok: true, value: { limit, agentId, unreadOnly } };\n}\n\nexport interface MailboxAgentsPayload {\n onlineOnly?: boolean;\n}\n\nexport function validateMailboxAgentsPayload(payload: unknown): 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 { ok: false, message: 'mailbox.agents payload.onlineOnly must be a boolean when provided' };\n }\n return { ok: true, value: { onlineOnly } };\n}\n\nexport interface MailboxPurgePayload {\n completedMaxAgeMs?: number;\n incompleteMaxAgeMs?: number;\n}\n\nexport function validateMailboxPurgePayload(payload: unknown): 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 (completedMaxAgeMs !== undefined && (typeof completedMaxAgeMs !== 'number' || !Number.isFinite(completedMaxAgeMs) || completedMaxAgeMs < 0)) {\n return { ok: false, message: 'mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided' };\n }\n if (incompleteMaxAgeMs !== undefined && (typeof incompleteMaxAgeMs !== 'number' || !Number.isFinite(incompleteMaxAgeMs) || incompleteMaxAgeMs < 0)) {\n return { ok: false, message: 'mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided' };\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(payload: unknown): 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 { ok: false, message: 'brain.risk payload.level must be one of off, low, medium, high, all' };\n }\n return { ok: true, value: { level } };\n}\n\nexport interface BrainAskPayload {\n question: string;\n}\n\nexport function validateBrainAskPayload(payload: unknown): 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(payload: unknown): 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(payload: unknown): PayloadValidationResult<PlanTemplateUsePayload> {\n if (!isRecord(payload)) {\n return { ok: false, message: 'plan.template_use payload must be an object with string template' };\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(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);\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]);\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 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(payload: unknown): 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(payload: unknown): 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(payload: unknown): 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(payload: unknown): 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(payload: unknown): 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(payload: unknown): 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(payload: unknown): PayloadValidationResult<ContextModeIdPayload> {\n return validateContextModeIdPayload(payload, 'context.mode.switch');\n}\n\nexport function validateContextModeDeletePayload(payload: unknown): 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(payload: unknown): 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 { ok: false, message: 'context.mode.create payload.thresholds must be an object with warn/soft/hard numbers' };\n }\n if (!isFiniteNumber(thresholds['warn']) || !isFiniteNumber(thresholds['soft']) || !isFiniteNumber(thresholds['hard'])) {\n return { ok: false, message: 'context.mode.create payload.thresholds.warn/soft/hard must be finite numbers' };\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 { ok: false, message: 'context.mode.create payload.eliseThreshold must be a finite number' };\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(payload: unknown): 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 { ok: false, message: 'context.mode.update payload.name must be a string when provided' };\n }\n\n const description = payload['description'];\n if (description !== undefined && typeof description !== 'string') {\n return { ok: false, message: 'context.mode.update payload.description must be a string when provided' };\n }\n\n const thresholds = payload['thresholds'];\n let validatedThresholds: ContextModeUpdatePayload['thresholds'];\n if (thresholds !== undefined) {\n if (!isRecord(thresholds)) {\n return { ok: false, message: 'context.mode.update payload.thresholds must be an object when provided' };\n }\n for (const key of ['warn', 'soft', 'hard'] as const) {\n const val = thresholds[key];\n if (val !== undefined && !isFiniteNumber(val)) {\n return { ok: false, message: `context.mode.update payload.thresholds.${key} must be a finite number when provided` };\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 { ok: false, message: 'context.mode.update payload.preserveK must be a finite number when provided' };\n }\n\n const eliseThreshold = payload['eliseThreshold'];\n if (eliseThreshold !== undefined && !isFiniteNumber(eliseThreshold)) {\n return { ok: false, message: 'context.mode.update payload.eliseThreshold must be a finite number when provided' };\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(payload: unknown): 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 { ok: false, message: 'shell.open payload.target must be \"file\" or \"terminal\" when provided' };\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(payload: unknown): 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(payload: unknown): 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\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 { loadPlan, savePlan, 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]);\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":[]}
|
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, 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 } 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';
|
|
@@ -763,6 +763,8 @@ declare class AutoPhaseWebSocketHandler {
|
|
|
763
763
|
private abort;
|
|
764
764
|
/** Optional per-phase git-worktree isolation (lazily created at start). */
|
|
765
765
|
private worktrees;
|
|
766
|
+
/** Per-run worker identities so the board can show "who is on what". */
|
|
767
|
+
private usedNicknames;
|
|
766
768
|
constructor(agent: Agent, context: Context, logger: Logger, storeDir: string, events?: EventBus | undefined, projectRoot?: string | undefined);
|
|
767
769
|
addClient(ws: WebSocket): void;
|
|
768
770
|
handleMessage(msg: AutoPhaseWSMessage): Promise<void>;
|
|
@@ -772,6 +774,8 @@ declare class AutoPhaseWebSocketHandler {
|
|
|
772
774
|
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure. */
|
|
773
775
|
private planPhases;
|
|
774
776
|
private executeTaskWithAgent;
|
|
777
|
+
/** Persist + broadcast after an interactive board mutation. */
|
|
778
|
+
private afterBoardMutation;
|
|
775
779
|
private handleTaskStatusChange;
|
|
776
780
|
private startBroadcast;
|
|
777
781
|
private stopBroadcast;
|
|
@@ -782,6 +786,165 @@ declare class AutoPhaseWebSocketHandler {
|
|
|
782
786
|
private send;
|
|
783
787
|
}
|
|
784
788
|
|
|
789
|
+
interface SpecsWSMessage {
|
|
790
|
+
type: string;
|
|
791
|
+
payload?: Record<string, unknown>;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* SpecsWebSocketHandler — read-only-ish browser of persisted SDD specs and their
|
|
795
|
+
* task graphs, rendered as a FORGE-style dependency board (topological phase
|
|
796
|
+
* columns + dependency refs). Shared by both webui servers via specs-routes.
|
|
797
|
+
*
|
|
798
|
+
* Message types:
|
|
799
|
+
* specs.list → all specs + progress
|
|
800
|
+
* specs.get { specId } → one spec's dependency board
|
|
801
|
+
* specs.taskStatus { graphId, taskId, status } → update + rebroadcast
|
|
802
|
+
*/
|
|
803
|
+
declare class SpecsWebSocketHandler {
|
|
804
|
+
private specStore;
|
|
805
|
+
private graphStore;
|
|
806
|
+
private clients;
|
|
807
|
+
constructor(specsDir: string, taskGraphsDir: string);
|
|
808
|
+
addClient(ws: WebSocket): void;
|
|
809
|
+
handleMessage(msg: SpecsWSMessage): Promise<void>;
|
|
810
|
+
private buildList;
|
|
811
|
+
private broadcastList;
|
|
812
|
+
private sendList;
|
|
813
|
+
private broadcastDetail;
|
|
814
|
+
private findGraphForSpec;
|
|
815
|
+
private buildDetail;
|
|
816
|
+
private updateTaskStatus;
|
|
817
|
+
private broadcast;
|
|
818
|
+
private send;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
interface SddBoardWSMessage {
|
|
822
|
+
type: string;
|
|
823
|
+
payload?: Record<string, unknown>;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* SddBoardWebSocketHandler — streams the live SDD multi-agent board to clients
|
|
827
|
+
* and relays control commands back to the CLI-owned run.
|
|
828
|
+
*
|
|
829
|
+
* Two observe modes (one class, shared by both webui servers):
|
|
830
|
+
* • in-process (CLI-hosted): subscribe the shared EventBus `sdd.board.snapshot`
|
|
831
|
+
* for instant updates;
|
|
832
|
+
* • standalone (separate process): poll the on-disk snapshot store (the CLI
|
|
833
|
+
* run persists JSON every change).
|
|
834
|
+
*
|
|
835
|
+
* Control is uniform + cross-process: every command is appended to the run's
|
|
836
|
+
* `<runId>.control.jsonl`, which the CLI run drains and applies — so the run
|
|
837
|
+
* stays the single driver and nothing races on shared state.
|
|
838
|
+
*/
|
|
839
|
+
declare class SddBoardWebSocketHandler {
|
|
840
|
+
private readonly store;
|
|
841
|
+
private readonly clients;
|
|
842
|
+
private latest;
|
|
843
|
+
private poll;
|
|
844
|
+
private unsub;
|
|
845
|
+
constructor(boardsDir: string, events?: EventBus);
|
|
846
|
+
addClient(ws: WebSocket): void;
|
|
847
|
+
handleMessage(msg: SddBoardWSMessage): Promise<void>;
|
|
848
|
+
dispose(): void;
|
|
849
|
+
private pollLatest;
|
|
850
|
+
private sendCurrent;
|
|
851
|
+
private broadcastCurrent;
|
|
852
|
+
private loadLatestFromDisk;
|
|
853
|
+
private broadcast;
|
|
854
|
+
private send;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
interface WizardMessage {
|
|
858
|
+
type: string;
|
|
859
|
+
payload?: Record<string, unknown>;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Dependencies each webui server supplies. The handler is deliberately
|
|
863
|
+
* agent-agnostic: every surface decides how to build a driver, how to run an
|
|
864
|
+
* interview turn (on an isolated agent, off the main chat bus), and how to
|
|
865
|
+
* start the real multi-agent run (CLI's director-backed factory vs the runtime
|
|
866
|
+
* light factory). This keeps the wizard protocol identical across both servers.
|
|
867
|
+
*/
|
|
868
|
+
interface SddWizardDeps {
|
|
869
|
+
/** Build a fresh interview driver (disk spec/graph stores + session path). */
|
|
870
|
+
makeDriver: () => SddInterviewDriver;
|
|
871
|
+
/**
|
|
872
|
+
* Run one interview turn: feed the AI prompt to an isolated agent and return
|
|
873
|
+
* its final text. MUST NOT run on the main chat agent's bus — the wizard owns
|
|
874
|
+
* this conversation, separate from the user's chat.
|
|
875
|
+
*/
|
|
876
|
+
runInterviewTurn: (prompt: string) => Promise<string>;
|
|
877
|
+
/**
|
|
878
|
+
* Start the real multi-agent SDD run for the driver's task graph. Returns the
|
|
879
|
+
* runId; the live board flows through the existing board handler.
|
|
880
|
+
*/
|
|
881
|
+
startRun: (driver: SddInterviewDriver, opts: {
|
|
882
|
+
parallelSlots?: number | undefined;
|
|
883
|
+
defaultModel?: string | undefined;
|
|
884
|
+
defaultProvider?: string | undefined;
|
|
885
|
+
fallbackModels?: string[] | undefined;
|
|
886
|
+
}) => Promise<{
|
|
887
|
+
runId: string;
|
|
888
|
+
}>;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* SddWizardWebSocketHandler — drives the interactive "New SDD Project" wizard
|
|
892
|
+
* (goal → Q&A → spec → task graph → start run) over WebSocket. Shared by both
|
|
893
|
+
* webui servers; server-specific construction (agent, factory) is injected via
|
|
894
|
+
* {@link SddWizardDeps}.
|
|
895
|
+
*/
|
|
896
|
+
declare class SddWizardWebSocketHandler {
|
|
897
|
+
private readonly deps;
|
|
898
|
+
private readonly clients;
|
|
899
|
+
private driver;
|
|
900
|
+
/** The agent's most recent question — paired with the next user answer. */
|
|
901
|
+
private lastAgentText;
|
|
902
|
+
/** Guards against overlapping interview turns (one in flight at a time). */
|
|
903
|
+
private busy;
|
|
904
|
+
constructor(deps: SddWizardDeps);
|
|
905
|
+
addClient(ws: WebSocket): void;
|
|
906
|
+
handleMessage(msg: WizardMessage): Promise<void>;
|
|
907
|
+
private onStart;
|
|
908
|
+
private onMessage;
|
|
909
|
+
private onApprove;
|
|
910
|
+
private onRunStart;
|
|
911
|
+
/** Run one interview turn against the isolated agent, then ingest + broadcast. */
|
|
912
|
+
private runTurn;
|
|
913
|
+
private snapshotMsg;
|
|
914
|
+
private broadcast;
|
|
915
|
+
private send;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
interface SddWizardWiringOptions {
|
|
919
|
+
/** Leader agent — seeds the run's default factory + project context. */
|
|
920
|
+
agent: Agent;
|
|
921
|
+
/** Shared EventBus — the board projector emits sdd.board.snapshot on it. */
|
|
922
|
+
events: EventBus;
|
|
923
|
+
projectRoot: string;
|
|
924
|
+
/** Per-task agent factory: CLI's director-backed one, or the runtime light one. */
|
|
925
|
+
subagentFactory: AgentFactory;
|
|
926
|
+
/**
|
|
927
|
+
* Decision authority for the failure supervisor (the server's bound
|
|
928
|
+
* TOKENS.BrainArbiter). Omit to run without a supervisor (plain terminal-fail,
|
|
929
|
+
* matching a bare run) — but parity with the CLI wants it wired.
|
|
930
|
+
*/
|
|
931
|
+
brain?: BrainArbiter | undefined;
|
|
932
|
+
/** Persisted-store directories (from resolveWstackPaths). */
|
|
933
|
+
paths: {
|
|
934
|
+
projectSpecs: string;
|
|
935
|
+
projectTaskGraphs: string;
|
|
936
|
+
projectSddBoards: string;
|
|
937
|
+
projectDir: string;
|
|
938
|
+
};
|
|
939
|
+
}
|
|
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
|
+
declare function buildSddWizardDeps(opts: SddWizardWiringOptions): SddWizardDeps;
|
|
947
|
+
|
|
785
948
|
/**
|
|
786
949
|
* Shared skills WebSocket handlers for both the standalone WebUI server
|
|
787
950
|
* (`packages/webui/src/server/index.ts`) and the CLI's `--webui` embedded
|
|
@@ -853,4 +1016,4 @@ declare function startWebUI(opts?: WebUIOptions & {
|
|
|
853
1016
|
open?: boolean | undefined;
|
|
854
1017
|
}): Promise<void>;
|
|
855
1018
|
|
|
856
|
-
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, type ShellOpenRequest, type ShellOpenResult, type ShellOpenTarget, type SkillsContext, type ToolTokenEntry, type VerifyClientInput, type WSClientMessage, type WSServerMessage, type WebUIInstanceRecord, type WebUIOptions, WorktreeWebSocketHandler, addProvider, broadcast, browserOpenCommand, buildCspHeader, 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 };
|
|
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 };
|