@kb-labs/workflow-entry 2.118.2 → 2.119.0-canary.0c078654e
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/commands/health.js.map +1 -1
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/runs-approve.js.map +1 -1
- package/dist/commands/runs-cancel.js.map +1 -1
- package/dist/commands/runs-list.js.map +1 -1
- package/dist/commands/runs-logs.js.map +1 -1
- package/dist/commands/runs-rerun.js.map +1 -1
- package/dist/commands/runs-restart.js.map +1 -1
- package/dist/commands/runs-status.js +2 -0
- package/dist/commands/runs-status.js.map +1 -1
- package/dist/commands/runs-view.js +4 -0
- package/dist/commands/runs-view.js.map +1 -1
- package/dist/commands/runs-watch.js +3 -1
- package/dist/commands/runs-watch.js.map +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/workflow-run.js.map +1 -1
- package/dist/index.js +2 -10
- package/dist/index.js.map +1 -1
- package/dist/manifest.js +2 -10
- package/dist/manifest.js.map +1 -1
- package/dist/rest/cron-list-handler.js.map +1 -1
- package/dist/rest/index.js.map +1 -1
- package/dist/rest/job-cancel-handler.js.map +1 -1
- package/dist/rest/job-detail-handler.js.map +1 -1
- package/dist/rest/job-logs-handler.js.map +1 -1
- package/dist/rest/job-steps-handler.js.map +1 -1
- package/dist/rest/jobs-list-handler.js.map +1 -1
- package/dist/rest/pending-approvals-handler.js.map +1 -1
- package/dist/rest/resolve-approval-handler.js.map +1 -1
- package/dist/rest/run-detail-handler.js.map +1 -1
- package/dist/rest/runs-list-handler.js.map +1 -1
- package/dist/rest/stats-handler.js.map +1 -1
- package/dist/rest/workflow-detail-handler.js.map +1 -1
- package/dist/rest/workflow-run-cancel-handler.js.map +1 -1
- package/dist/rest/workflow-run-handler.js.map +1 -1
- package/dist/rest/workflow-runs-handler.js.map +1 -1
- package/dist/rest/workflows-list-handler.js.map +1 -1
- package/dist/ws/logs-channel.js +66 -26
- package/dist/ws/logs-channel.js.map +1 -1
- package/dist/ws/progress-channel.js +42 -12
- package/dist/ws/progress-channel.js.map +1 -1
- package/package.json +10 -8
- package/dist/manifest.json +0 -901
- package/dist/widgets/117.js +0 -25
- package/dist/widgets/165.js +0 -1
- package/dist/widgets/326.js +0 -1
- package/dist/widgets/370.js +0 -1
- package/dist/widgets/405.js +0 -1
- package/dist/widgets/46.js +0 -445
- package/dist/widgets/50.js +0 -1
- package/dist/widgets/546.js +0 -1
- package/dist/widgets/62.js +0 -96
- package/dist/widgets/648.js +0 -1
- package/dist/widgets/669.js +0 -1
- package/dist/widgets/707.js +0 -1
- package/dist/widgets/716.js +0 -1
- package/dist/widgets/775.js +0 -1
- package/dist/widgets/78.js +0 -1
- package/dist/widgets/875.js +0 -25
- package/dist/widgets/945.js +0 -1
- package/dist/widgets/968.js +0 -9
- package/dist/widgets/__federation_expose_Crons.js +0 -1
- package/dist/widgets/__federation_expose_Dashboard.js +0 -1
- package/dist/widgets/__federation_expose_DefinitionDetail.js +0 -1
- package/dist/widgets/__federation_expose_Definitions.js +0 -1
- package/dist/widgets/__federation_expose_Jobs.js +0 -1
- package/dist/widgets/__federation_expose_RunDetail.js +0 -109
- package/dist/widgets/__federation_expose_Runs.js +0 -1
- package/dist/widgets/mf-manifest.json +0 -357
- package/dist/widgets/mf-stats.json +0 -397
- package/dist/widgets/remoteEntry.js +0 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/workflow-run-handler.ts"],"names":[],"mappings":";;;AA2HA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACpHA,IAAO,+BAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EAC4B;AAC5B,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,EAAA,EAAG,GAAI,KAAA,CAAM,MAAA;AAErB,IAAA,IAAI,CAAC,EAAA,EAAI;AACP,MAAA,MAAM,IAAI,MAAM,sBAAsB,CAAA;AAAA,IACxC;AAEA,IAAA,MAAM,IAAA,GAA2B,KAAA,CAAM,IAAA,IAAQ,EAAC;AAEhD,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,kBAAA,EAAqB,kBAAA,CAAmB,EAAE,CAAC,CAAA,IAAA,CAAA;AACnE,IAAA,GAAA,CAAI,SAAS,MAAA,CAAO,IAAA,CAAK,2CAA2C,EAAE,CAAA,KAAA,EAAQ,GAAG,CAAA,CAAE,CAAA;AAEnF,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,cAAA,EAAgB;AAAA,SAClB;AAAA,QACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI;AAAA,OAC1B,CAAA;AAED,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,oBAAoB,CAAA;AAAA,QACtC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,wBAAwB,CAAA;AAAA,MAC1D;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,+CAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,cAAA,CAAe,KAAK,CAAA;AAAA,IACtB;AAAA,EACF;AACF,CAAC","file":"workflow-run-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRerunRequest,\n WorkflowRestartRequest,\n WorkflowRestartResponse,\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Workflow run summary returned by GET /api/v1/runs */\nexport interface WorkflowRunSummary {\n id: string;\n name: string;\n version?: string;\n status: 'queued' | 'running' | 'success' | 'failed' | 'cancelled';\n trigger?: {\n type: string;\n actor?: string;\n payload?: unknown;\n parentRunId?: string;\n parentJobId?: string;\n parentStepId?: string;\n };\n createdAt?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n inputs?: Record<string, unknown>;\n hasPendingApproval?: boolean;\n currentStepName?: string;\n}\n\n/** Full workflow run detail returned by GET /api/v1/runs/:runId */\nexport interface WorkflowRunDetail extends WorkflowRunSummary {\n env?: Record<string, string>;\n metadata?: Record<string, unknown>;\n result?: {\n status: string;\n summary?: string;\n error?: { message: string; code?: string };\n outputs?: Record<string, unknown>;\n };\n jobs?: Array<{\n id: string;\n jobName?: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n error?: { message: string; code?: string; stack?: string } | string;\n steps?: Array<{\n id: string;\n name?: string;\n index?: number;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n outputs?: Record<string, unknown>;\n resolvedInputs?: Record<string, unknown>;\n error?: { message: string; code?: string; details?: Record<string, unknown> } | string;\n spec?: Record<string, unknown>;\n }>;\n }>;\n}\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * List workflow runs with optional status filter\n */\n async listRuns(params: {\n status?: string;\n limit?: number;\n workflowId?: string;\n } = {}): Promise<WorkflowRunSummary[]> {\n const query = new URLSearchParams();\n if (params.status) { query.set('status', params.status); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.workflowId) { query.set('workflowId', params.workflowId); }\n\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs${qs ? `?${qs}` : ''}`);\n if (!response.ok) {\n throw new Error(`Failed to list runs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ runs?: WorkflowRunSummary[] }>(data);\n return unwrapped.runs ?? (Array.isArray(data) ? data as WorkflowRunSummary[] : []);\n }\n\n /**\n * Get a specific workflow run with full detail\n */\n async getRun(runId: string): Promise<WorkflowRunDetail> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n // Response shape: { ok: true, data: { run: {...} } }\n const unwrapped = this.unwrapData<{ run?: WorkflowRunDetail } | WorkflowRunDetail>(data);\n return ('run' in (unwrapped as object) ? (unwrapped as { run: WorkflowRunDetail }).run : unwrapped) as WorkflowRunDetail;\n }\n\n /**\n * Get run logs, optionally filtered to failed steps only\n */\n async getRunLogs(runId: string, params: {\n stepId?: string;\n level?: string;\n limit?: number;\n failedOnly?: boolean;\n } = {}): Promise<Array<{ level: string; message: string; timestamp: string; stepId?: string; stepName?: string; stream?: string; [key: string]: unknown }>> {\n const query = new URLSearchParams();\n if (params.stepId) { query.set('stepId', params.stepId); }\n if (params.level) { query.set('level', params.level); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.failedOnly) { query.set('failedOnly', 'true'); }\n\n const encodedId = encodeURIComponent(runId);\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/logs${qs ? `?${qs}` : ''}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ logs?: unknown[] }>(data);\n return (unwrapped.logs ?? []) as Array<{ level: string; message: string; timestamp: string; [key: string]: unknown }>;\n }\n\n /**\n * Stream run events via SSE (returns a ReadableStream or EventSource-compatible URL)\n */\n getRunEventsUrl(runId: string): string {\n return `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/events`;\n }\n\n /**\n * List pending approval steps for a run\n */\n async listPendingApprovals(runId: string): Promise<{\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals`);\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to list pending approvals: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: {\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Resolve (approve or reject) a pending approval step\n */\n async resolveApproval(\n runId: string,\n jobId: string,\n stepId: string,\n action: 'approve' | 'reject',\n comment?: string,\n data?: Record<string, unknown>\n ): Promise<{ runId: string; jobId: string; stepId: string; action: string; resolved: boolean }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals/resolve`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jobId, stepId, action, comment, data }),\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to resolve approval: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: { runId: string; jobId: string; stepId: string; action: string; resolved: boolean };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Cancel a workflow run\n */\n async cancelRun(runId: string): Promise<void> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/cancel`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: '{}',\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to cancel run: ${message || response.statusText || response.status}`);\n }\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Rerun a workflow run by ID, optionally only failed jobs\n */\n async rerunWorkflow(\n runId: string,\n request: WorkflowRerunRequest = {},\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/rerun`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to rerun workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Restart a run from a snapshot, optionally resuming from a specific step.\n * Preceding steps inherit their stored outputs; only the target step and beyond are re-executed.\n */\n async restartRun(\n runId: string,\n request: WorkflowRestartRequest = {},\n ): Promise<WorkflowRestartResponse> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/restart`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to restart run: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: WorkflowRestartResponse }>(response);\n return this.unwrapData<WorkflowRestartResponse>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/workflow-run-handler\n * REST handler for running a workflow\n */\n\nimport { defineHandler, rethrowForRest, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { WorkflowRunRequest } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\ninterface WorkflowRunParams {\n id: string;\n}\n\ninterface WorkflowRunResult {\n runId: string;\n status: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<WorkflowRunRequest, unknown, WorkflowRunParams>\n ): Promise<WorkflowRunResult> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { id } = input.params!;\n\n if (!id) {\n throw new Error('Missing id parameter');\n }\n\n const body: WorkflowRunRequest = input.body || {};\n\n const url = `${daemonUrl}/api/v1/workflows/${encodeURIComponent(id)}/run`;\n ctx.platform.logger.info(`[workflow-run-handler] Running workflow ${id} via ${url}`);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Workflow not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: WorkflowRunResult; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to run workflow');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[workflow-run-handler] Error running workflow',\n error instanceof Error ? error : undefined\n );\n rethrowForRest(error);\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../home/runner/work/kb-labs/kb-labs/plugins/workflow/entry/src/http-client.ts","../../../home/runner/work/kb-labs/kb-labs/plugins/workflow/entry/src/rest/workflow-run-handler.ts"],"names":[],"mappings":";;;AA2HA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACpHA,IAAO,+BAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EAC4B;AAC5B,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,EAAA,EAAG,GAAI,KAAA,CAAM,MAAA;AAErB,IAAA,IAAI,CAAC,EAAA,EAAI;AACP,MAAA,MAAM,IAAI,MAAM,sBAAsB,CAAA;AAAA,IACxC;AAEA,IAAA,MAAM,IAAA,GAA2B,KAAA,CAAM,IAAA,IAAQ,EAAC;AAEhD,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,kBAAA,EAAqB,kBAAA,CAAmB,EAAE,CAAC,CAAA,IAAA,CAAA;AACnE,IAAA,GAAA,CAAI,SAAS,MAAA,CAAO,IAAA,CAAK,2CAA2C,EAAE,CAAA,KAAA,EAAQ,GAAG,CAAA,CAAE,CAAA;AAEnF,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,cAAA,EAAgB;AAAA,SAClB;AAAA,QACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI;AAAA,OAC1B,CAAA;AAED,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,oBAAoB,CAAA;AAAA,QACtC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,wBAAwB,CAAA;AAAA,MAC1D;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,+CAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,cAAA,CAAe,KAAK,CAAA;AAAA,IACtB;AAAA,EACF;AACF,CAAC","file":"workflow-run-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRerunRequest,\n WorkflowRestartRequest,\n WorkflowRestartResponse,\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Workflow run summary returned by GET /api/v1/runs */\nexport interface WorkflowRunSummary {\n id: string;\n name: string;\n version?: string;\n status: 'queued' | 'running' | 'success' | 'failed' | 'cancelled';\n trigger?: {\n type: string;\n actor?: string;\n payload?: unknown;\n parentRunId?: string;\n parentJobId?: string;\n parentStepId?: string;\n };\n createdAt?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n inputs?: Record<string, unknown>;\n hasPendingApproval?: boolean;\n currentStepName?: string;\n}\n\n/** Full workflow run detail returned by GET /api/v1/runs/:runId */\nexport interface WorkflowRunDetail extends WorkflowRunSummary {\n env?: Record<string, string>;\n metadata?: Record<string, unknown>;\n result?: {\n status: string;\n summary?: string;\n error?: { message: string; code?: string };\n outputs?: Record<string, unknown>;\n };\n jobs?: Array<{\n id: string;\n jobName?: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n error?: { message: string; code?: string; stack?: string } | string;\n steps?: Array<{\n id: string;\n name?: string;\n index?: number;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n outputs?: Record<string, unknown>;\n resolvedInputs?: Record<string, unknown>;\n error?: { message: string; code?: string; details?: Record<string, unknown> } | string;\n spec?: Record<string, unknown>;\n }>;\n }>;\n}\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * List workflow runs with optional status filter\n */\n async listRuns(params: {\n status?: string;\n limit?: number;\n workflowId?: string;\n } = {}): Promise<WorkflowRunSummary[]> {\n const query = new URLSearchParams();\n if (params.status) { query.set('status', params.status); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.workflowId) { query.set('workflowId', params.workflowId); }\n\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs${qs ? `?${qs}` : ''}`);\n if (!response.ok) {\n throw new Error(`Failed to list runs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ runs?: WorkflowRunSummary[] }>(data);\n return unwrapped.runs ?? (Array.isArray(data) ? data as WorkflowRunSummary[] : []);\n }\n\n /**\n * Get a specific workflow run with full detail\n */\n async getRun(runId: string): Promise<WorkflowRunDetail> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n // Response shape: { ok: true, data: { run: {...} } }\n const unwrapped = this.unwrapData<{ run?: WorkflowRunDetail } | WorkflowRunDetail>(data);\n return ('run' in (unwrapped as object) ? (unwrapped as { run: WorkflowRunDetail }).run : unwrapped) as WorkflowRunDetail;\n }\n\n /**\n * Get run logs, optionally filtered to failed steps only\n */\n async getRunLogs(runId: string, params: {\n stepId?: string;\n level?: string;\n limit?: number;\n failedOnly?: boolean;\n } = {}): Promise<Array<{ level: string; message: string; timestamp: string; stepId?: string; stepName?: string; stream?: string; [key: string]: unknown }>> {\n const query = new URLSearchParams();\n if (params.stepId) { query.set('stepId', params.stepId); }\n if (params.level) { query.set('level', params.level); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.failedOnly) { query.set('failedOnly', 'true'); }\n\n const encodedId = encodeURIComponent(runId);\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/logs${qs ? `?${qs}` : ''}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ logs?: unknown[] }>(data);\n return (unwrapped.logs ?? []) as Array<{ level: string; message: string; timestamp: string; [key: string]: unknown }>;\n }\n\n /**\n * Stream run events via SSE (returns a ReadableStream or EventSource-compatible URL)\n */\n getRunEventsUrl(runId: string): string {\n return `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/events`;\n }\n\n /**\n * List pending approval steps for a run\n */\n async listPendingApprovals(runId: string): Promise<{\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals`);\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to list pending approvals: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: {\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Resolve (approve or reject) a pending approval step\n */\n async resolveApproval(\n runId: string,\n jobId: string,\n stepId: string,\n action: 'approve' | 'reject',\n comment?: string,\n data?: Record<string, unknown>\n ): Promise<{ runId: string; jobId: string; stepId: string; action: string; resolved: boolean }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals/resolve`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jobId, stepId, action, comment, data }),\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to resolve approval: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: { runId: string; jobId: string; stepId: string; action: string; resolved: boolean };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Cancel a workflow run\n */\n async cancelRun(runId: string): Promise<void> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/cancel`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: '{}',\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to cancel run: ${message || response.statusText || response.status}`);\n }\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Rerun a workflow run by ID, optionally only failed jobs\n */\n async rerunWorkflow(\n runId: string,\n request: WorkflowRerunRequest = {},\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/rerun`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to rerun workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Restart a run from a snapshot, optionally resuming from a specific step.\n * Preceding steps inherit their stored outputs; only the target step and beyond are re-executed.\n */\n async restartRun(\n runId: string,\n request: WorkflowRestartRequest = {},\n ): Promise<WorkflowRestartResponse> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/restart`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to restart run: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: WorkflowRestartResponse }>(response);\n return this.unwrapData<WorkflowRestartResponse>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/workflow-run-handler\n * REST handler for running a workflow\n */\n\nimport { defineHandler, rethrowForRest, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { WorkflowRunRequest } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\ninterface WorkflowRunParams {\n id: string;\n}\n\ninterface WorkflowRunResult {\n runId: string;\n status: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<WorkflowRunRequest, unknown, WorkflowRunParams>\n ): Promise<WorkflowRunResult> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { id } = input.params!;\n\n if (!id) {\n throw new Error('Missing id parameter');\n }\n\n const body: WorkflowRunRequest = input.body || {};\n\n const url = `${daemonUrl}/api/v1/workflows/${encodeURIComponent(id)}/run`;\n ctx.platform.logger.info(`[workflow-run-handler] Running workflow ${id} via ${url}`);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Workflow not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: WorkflowRunResult; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to run workflow');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[workflow-run-handler] Error running workflow',\n error instanceof Error ? error : undefined\n );\n rethrowForRest(error);\n }\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/workflow-runs-handler.ts"],"names":[],"mappings":";;;AA2HA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;AC7HA,IAAO,gCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACqC;AACrC,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,EAAA,EAAI,UAAA,EAAW,GAAI,KAAA,CAAM,MAAA;AACjC,IAAA,MAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,QAAO,GAAI,KAAA,CAAM,SAAS,EAAC;AAGlD,IAAA,MAAM,cAAA,GAAiB,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,KAAK,GAAA,CAAI,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA,IAAK,EAAA,EAAI,CAAC,CAAA,EAAG,GAAI,CAAA,GAAI,EAAA;AACxF,IAAA,MAAM,eAAA,GAAkB,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,QAAA,CAAS,QAAQ,EAAE,CAAA,IAAK,CAAA,EAAG,CAAC,CAAA,GAAI,CAAA;AAC1E,IAAA,MAAM,gBAAgB,CAAC,QAAA,EAAU,SAAA,EAAW,WAAA,EAAa,UAAU,WAAW,CAAA;AAC9E,IAAA,MAAM,kBAAkB,MAAA,IAAU,aAAA,CAAc,QAAA,CAAS,MAAM,IAAI,MAAA,GAAS,MAAA;AAE5E,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,IAAA,MAAA,CAAO,MAAA,CAAO,OAAA,EAAS,MAAA,CAAO,cAAc,CAAC,CAAA;AAC7C,IAAA,MAAA,CAAO,MAAA,CAAO,QAAA,EAAU,MAAA,CAAO,eAAe,CAAC,CAAA;AAC/C,IAAA,IAAI,eAAA,EAAiB;AACnB,MAAA,MAAA,CAAO,MAAA,CAAO,UAAU,eAAe,CAAA;AAAA,IACzC;AAEA,IAAA,MAAM,GAAA,GAAM,GAAG,SAAS,CAAA,kBAAA,EAAqB,mBAAmB,UAAU,CAAC,SAAS,MAAM,CAAA,CAAA;AAE1F,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,kDAAA,EAAqD,GAAG,CAAA,CAAE,CAAA;AAEnF,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,oBAAoB,CAAA;AAAA,QACtC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,sCAAsC,CAAA;AAAA,MACxE;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,6DAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,cAAA,CAAe,KAAK,CAAA;AAAA,IACtB;AAAA,EACF;AACF,CAAC","file":"workflow-runs-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRerunRequest,\n WorkflowRestartRequest,\n WorkflowRestartResponse,\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Workflow run summary returned by GET /api/v1/runs */\nexport interface WorkflowRunSummary {\n id: string;\n name: string;\n version?: string;\n status: 'queued' | 'running' | 'success' | 'failed' | 'cancelled';\n trigger?: {\n type: string;\n actor?: string;\n payload?: unknown;\n parentRunId?: string;\n parentJobId?: string;\n parentStepId?: string;\n };\n createdAt?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n inputs?: Record<string, unknown>;\n hasPendingApproval?: boolean;\n currentStepName?: string;\n}\n\n/** Full workflow run detail returned by GET /api/v1/runs/:runId */\nexport interface WorkflowRunDetail extends WorkflowRunSummary {\n env?: Record<string, string>;\n metadata?: Record<string, unknown>;\n result?: {\n status: string;\n summary?: string;\n error?: { message: string; code?: string };\n outputs?: Record<string, unknown>;\n };\n jobs?: Array<{\n id: string;\n jobName?: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n error?: { message: string; code?: string; stack?: string } | string;\n steps?: Array<{\n id: string;\n name?: string;\n index?: number;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n outputs?: Record<string, unknown>;\n resolvedInputs?: Record<string, unknown>;\n error?: { message: string; code?: string; details?: Record<string, unknown> } | string;\n spec?: Record<string, unknown>;\n }>;\n }>;\n}\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * List workflow runs with optional status filter\n */\n async listRuns(params: {\n status?: string;\n limit?: number;\n workflowId?: string;\n } = {}): Promise<WorkflowRunSummary[]> {\n const query = new URLSearchParams();\n if (params.status) { query.set('status', params.status); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.workflowId) { query.set('workflowId', params.workflowId); }\n\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs${qs ? `?${qs}` : ''}`);\n if (!response.ok) {\n throw new Error(`Failed to list runs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ runs?: WorkflowRunSummary[] }>(data);\n return unwrapped.runs ?? (Array.isArray(data) ? data as WorkflowRunSummary[] : []);\n }\n\n /**\n * Get a specific workflow run with full detail\n */\n async getRun(runId: string): Promise<WorkflowRunDetail> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n // Response shape: { ok: true, data: { run: {...} } }\n const unwrapped = this.unwrapData<{ run?: WorkflowRunDetail } | WorkflowRunDetail>(data);\n return ('run' in (unwrapped as object) ? (unwrapped as { run: WorkflowRunDetail }).run : unwrapped) as WorkflowRunDetail;\n }\n\n /**\n * Get run logs, optionally filtered to failed steps only\n */\n async getRunLogs(runId: string, params: {\n stepId?: string;\n level?: string;\n limit?: number;\n failedOnly?: boolean;\n } = {}): Promise<Array<{ level: string; message: string; timestamp: string; stepId?: string; stepName?: string; stream?: string; [key: string]: unknown }>> {\n const query = new URLSearchParams();\n if (params.stepId) { query.set('stepId', params.stepId); }\n if (params.level) { query.set('level', params.level); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.failedOnly) { query.set('failedOnly', 'true'); }\n\n const encodedId = encodeURIComponent(runId);\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/logs${qs ? `?${qs}` : ''}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ logs?: unknown[] }>(data);\n return (unwrapped.logs ?? []) as Array<{ level: string; message: string; timestamp: string; [key: string]: unknown }>;\n }\n\n /**\n * Stream run events via SSE (returns a ReadableStream or EventSource-compatible URL)\n */\n getRunEventsUrl(runId: string): string {\n return `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/events`;\n }\n\n /**\n * List pending approval steps for a run\n */\n async listPendingApprovals(runId: string): Promise<{\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals`);\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to list pending approvals: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: {\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Resolve (approve or reject) a pending approval step\n */\n async resolveApproval(\n runId: string,\n jobId: string,\n stepId: string,\n action: 'approve' | 'reject',\n comment?: string,\n data?: Record<string, unknown>\n ): Promise<{ runId: string; jobId: string; stepId: string; action: string; resolved: boolean }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals/resolve`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jobId, stepId, action, comment, data }),\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to resolve approval: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: { runId: string; jobId: string; stepId: string; action: string; resolved: boolean };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Cancel a workflow run\n */\n async cancelRun(runId: string): Promise<void> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/cancel`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: '{}',\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to cancel run: ${message || response.statusText || response.status}`);\n }\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Rerun a workflow run by ID, optionally only failed jobs\n */\n async rerunWorkflow(\n runId: string,\n request: WorkflowRerunRequest = {},\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/rerun`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to rerun workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Restart a run from a snapshot, optionally resuming from a specific step.\n * Preceding steps inherit their stored outputs; only the target step and beyond are re-executed.\n */\n async restartRun(\n runId: string,\n request: WorkflowRestartRequest = {},\n ): Promise<WorkflowRestartResponse> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/restart`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to restart run: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: WorkflowRestartResponse }>(response);\n return this.unwrapData<WorkflowRestartResponse>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/workflow-runs-handler\n * REST handler for workflow run history (proxy to daemon)\n */\n\nimport { defineHandler, rethrowForRest, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { WorkflowRunHistoryResponse } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<{ limit?: string; offset?: string; status?: string }, unknown, { id: string }>\n ): Promise<WorkflowRunHistoryResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { id: workflowId } = input.params!;\n const { limit, offset, status } = input.query || {};\n\n // Validate query parameters\n const validatedLimit = limit ? Math.min(Math.max(parseInt(limit, 10) || 50, 1), 1000) : 50;\n const validatedOffset = offset ? Math.max(parseInt(offset, 10) || 0, 0) : 0;\n const validStatuses = ['queued', 'running', 'completed', 'failed', 'cancelled'];\n const validatedStatus = status && validStatuses.includes(status) ? status : undefined;\n\n const params = new URLSearchParams();\n params.append('limit', String(validatedLimit));\n params.append('offset', String(validatedOffset));\n if (validatedStatus) {\n params.append('status', validatedStatus);\n }\n\n const url = `${daemonUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs?${params}`;\n\n ctx.platform.logger.info(`[workflow-runs-handler] Fetching run history from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Workflow not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: WorkflowRunHistoryResponse; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch workflow run history');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[workflow-runs-handler] Error fetching workflow run history',\n error instanceof Error ? error : undefined\n );\n rethrowForRest(error);\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../home/runner/work/kb-labs/kb-labs/plugins/workflow/entry/src/http-client.ts","../../../home/runner/work/kb-labs/kb-labs/plugins/workflow/entry/src/rest/workflow-runs-handler.ts"],"names":[],"mappings":";;;AA2HA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;AC7HA,IAAO,gCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACqC;AACrC,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,EAAA,EAAI,UAAA,EAAW,GAAI,KAAA,CAAM,MAAA;AACjC,IAAA,MAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,QAAO,GAAI,KAAA,CAAM,SAAS,EAAC;AAGlD,IAAA,MAAM,cAAA,GAAiB,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,KAAK,GAAA,CAAI,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA,IAAK,EAAA,EAAI,CAAC,CAAA,EAAG,GAAI,CAAA,GAAI,EAAA;AACxF,IAAA,MAAM,eAAA,GAAkB,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,QAAA,CAAS,QAAQ,EAAE,CAAA,IAAK,CAAA,EAAG,CAAC,CAAA,GAAI,CAAA;AAC1E,IAAA,MAAM,gBAAgB,CAAC,QAAA,EAAU,SAAA,EAAW,WAAA,EAAa,UAAU,WAAW,CAAA;AAC9E,IAAA,MAAM,kBAAkB,MAAA,IAAU,aAAA,CAAc,QAAA,CAAS,MAAM,IAAI,MAAA,GAAS,MAAA;AAE5E,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,IAAA,MAAA,CAAO,MAAA,CAAO,OAAA,EAAS,MAAA,CAAO,cAAc,CAAC,CAAA;AAC7C,IAAA,MAAA,CAAO,MAAA,CAAO,QAAA,EAAU,MAAA,CAAO,eAAe,CAAC,CAAA;AAC/C,IAAA,IAAI,eAAA,EAAiB;AACnB,MAAA,MAAA,CAAO,MAAA,CAAO,UAAU,eAAe,CAAA;AAAA,IACzC;AAEA,IAAA,MAAM,GAAA,GAAM,GAAG,SAAS,CAAA,kBAAA,EAAqB,mBAAmB,UAAU,CAAC,SAAS,MAAM,CAAA,CAAA;AAE1F,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,kDAAA,EAAqD,GAAG,CAAA,CAAE,CAAA;AAEnF,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,oBAAoB,CAAA;AAAA,QACtC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,sCAAsC,CAAA;AAAA,MACxE;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,6DAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,cAAA,CAAe,KAAK,CAAA;AAAA,IACtB;AAAA,EACF;AACF,CAAC","file":"workflow-runs-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRerunRequest,\n WorkflowRestartRequest,\n WorkflowRestartResponse,\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Workflow run summary returned by GET /api/v1/runs */\nexport interface WorkflowRunSummary {\n id: string;\n name: string;\n version?: string;\n status: 'queued' | 'running' | 'success' | 'failed' | 'cancelled';\n trigger?: {\n type: string;\n actor?: string;\n payload?: unknown;\n parentRunId?: string;\n parentJobId?: string;\n parentStepId?: string;\n };\n createdAt?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n inputs?: Record<string, unknown>;\n hasPendingApproval?: boolean;\n currentStepName?: string;\n}\n\n/** Full workflow run detail returned by GET /api/v1/runs/:runId */\nexport interface WorkflowRunDetail extends WorkflowRunSummary {\n env?: Record<string, string>;\n metadata?: Record<string, unknown>;\n result?: {\n status: string;\n summary?: string;\n error?: { message: string; code?: string };\n outputs?: Record<string, unknown>;\n };\n jobs?: Array<{\n id: string;\n jobName?: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n error?: { message: string; code?: string; stack?: string } | string;\n steps?: Array<{\n id: string;\n name?: string;\n index?: number;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n outputs?: Record<string, unknown>;\n resolvedInputs?: Record<string, unknown>;\n error?: { message: string; code?: string; details?: Record<string, unknown> } | string;\n spec?: Record<string, unknown>;\n }>;\n }>;\n}\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * List workflow runs with optional status filter\n */\n async listRuns(params: {\n status?: string;\n limit?: number;\n workflowId?: string;\n } = {}): Promise<WorkflowRunSummary[]> {\n const query = new URLSearchParams();\n if (params.status) { query.set('status', params.status); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.workflowId) { query.set('workflowId', params.workflowId); }\n\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs${qs ? `?${qs}` : ''}`);\n if (!response.ok) {\n throw new Error(`Failed to list runs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ runs?: WorkflowRunSummary[] }>(data);\n return unwrapped.runs ?? (Array.isArray(data) ? data as WorkflowRunSummary[] : []);\n }\n\n /**\n * Get a specific workflow run with full detail\n */\n async getRun(runId: string): Promise<WorkflowRunDetail> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n // Response shape: { ok: true, data: { run: {...} } }\n const unwrapped = this.unwrapData<{ run?: WorkflowRunDetail } | WorkflowRunDetail>(data);\n return ('run' in (unwrapped as object) ? (unwrapped as { run: WorkflowRunDetail }).run : unwrapped) as WorkflowRunDetail;\n }\n\n /**\n * Get run logs, optionally filtered to failed steps only\n */\n async getRunLogs(runId: string, params: {\n stepId?: string;\n level?: string;\n limit?: number;\n failedOnly?: boolean;\n } = {}): Promise<Array<{ level: string; message: string; timestamp: string; stepId?: string; stepName?: string; stream?: string; [key: string]: unknown }>> {\n const query = new URLSearchParams();\n if (params.stepId) { query.set('stepId', params.stepId); }\n if (params.level) { query.set('level', params.level); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.failedOnly) { query.set('failedOnly', 'true'); }\n\n const encodedId = encodeURIComponent(runId);\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/logs${qs ? `?${qs}` : ''}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ logs?: unknown[] }>(data);\n return (unwrapped.logs ?? []) as Array<{ level: string; message: string; timestamp: string; [key: string]: unknown }>;\n }\n\n /**\n * Stream run events via SSE (returns a ReadableStream or EventSource-compatible URL)\n */\n getRunEventsUrl(runId: string): string {\n return `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/events`;\n }\n\n /**\n * List pending approval steps for a run\n */\n async listPendingApprovals(runId: string): Promise<{\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals`);\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to list pending approvals: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: {\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Resolve (approve or reject) a pending approval step\n */\n async resolveApproval(\n runId: string,\n jobId: string,\n stepId: string,\n action: 'approve' | 'reject',\n comment?: string,\n data?: Record<string, unknown>\n ): Promise<{ runId: string; jobId: string; stepId: string; action: string; resolved: boolean }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals/resolve`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jobId, stepId, action, comment, data }),\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to resolve approval: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: { runId: string; jobId: string; stepId: string; action: string; resolved: boolean };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Cancel a workflow run\n */\n async cancelRun(runId: string): Promise<void> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/cancel`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: '{}',\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to cancel run: ${message || response.statusText || response.status}`);\n }\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Rerun a workflow run by ID, optionally only failed jobs\n */\n async rerunWorkflow(\n runId: string,\n request: WorkflowRerunRequest = {},\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/rerun`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to rerun workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Restart a run from a snapshot, optionally resuming from a specific step.\n * Preceding steps inherit their stored outputs; only the target step and beyond are re-executed.\n */\n async restartRun(\n runId: string,\n request: WorkflowRestartRequest = {},\n ): Promise<WorkflowRestartResponse> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/restart`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to restart run: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: WorkflowRestartResponse }>(response);\n return this.unwrapData<WorkflowRestartResponse>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/workflow-runs-handler\n * REST handler for workflow run history (proxy to daemon)\n */\n\nimport { defineHandler, rethrowForRest, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { WorkflowRunHistoryResponse } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<{ limit?: string; offset?: string; status?: string }, unknown, { id: string }>\n ): Promise<WorkflowRunHistoryResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { id: workflowId } = input.params!;\n const { limit, offset, status } = input.query || {};\n\n // Validate query parameters\n const validatedLimit = limit ? Math.min(Math.max(parseInt(limit, 10) || 50, 1), 1000) : 50;\n const validatedOffset = offset ? Math.max(parseInt(offset, 10) || 0, 0) : 0;\n const validStatuses = ['queued', 'running', 'completed', 'failed', 'cancelled'];\n const validatedStatus = status && validStatuses.includes(status) ? status : undefined;\n\n const params = new URLSearchParams();\n params.append('limit', String(validatedLimit));\n params.append('offset', String(validatedOffset));\n if (validatedStatus) {\n params.append('status', validatedStatus);\n }\n\n const url = `${daemonUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs?${params}`;\n\n ctx.platform.logger.info(`[workflow-runs-handler] Fetching run history from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Workflow not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: WorkflowRunHistoryResponse; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch workflow run history');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[workflow-runs-handler] Error fetching workflow run history',\n error instanceof Error ? error : undefined\n );\n rethrowForRest(error);\n }\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/workflows-list-handler.ts"],"names":[],"mappings":";;;AA2HA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACvHA,IAAO,iCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EAC+B;AAC/B,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAQ,MAAK,GAAI,KAAA,CAAM,SAAS,EAAC;AAEjD,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,IAAA,IAAI,MAAA,EAAQ;AAAC,MAAA,MAAA,CAAO,MAAA,CAAO,UAAU,MAAM,CAAA;AAAA,IAAE;AAC7C,IAAA,IAAI,MAAA,EAAQ;AAAC,MAAA,MAAA,CAAO,MAAA,CAAO,UAAU,MAAM,CAAA;AAAA,IAAE;AAC7C,IAAA,IAAI,IAAA,EAAM;AAAC,MAAA,MAAA,CAAO,MAAA,CAAO,QAAQ,IAAI,CAAA;AAAA,IAAE;AAEvC,IAAA,MAAM,WAAA,GAAc,OAAO,QAAA,EAAS;AACpC,IAAA,MAAM,GAAA,GAAM,GAAG,SAAS,CAAA,iBAAA,EAAoB,cAAc,CAAA,CAAA,EAAI,WAAW,KAAK,EAAE,CAAA,CAAA;AAEhF,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,iDAAA,EAAoD,GAAG,CAAA,CAAE,CAAA;AAElF,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,2BAA2B,CAAA;AAAA,MAC7D;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,mDAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,cAAA,CAAe,KAAK,CAAA;AAAA,IACtB;AAAA,EACF;AACF,CAAC","file":"workflows-list-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRerunRequest,\n WorkflowRestartRequest,\n WorkflowRestartResponse,\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Workflow run summary returned by GET /api/v1/runs */\nexport interface WorkflowRunSummary {\n id: string;\n name: string;\n version?: string;\n status: 'queued' | 'running' | 'success' | 'failed' | 'cancelled';\n trigger?: {\n type: string;\n actor?: string;\n payload?: unknown;\n parentRunId?: string;\n parentJobId?: string;\n parentStepId?: string;\n };\n createdAt?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n inputs?: Record<string, unknown>;\n hasPendingApproval?: boolean;\n currentStepName?: string;\n}\n\n/** Full workflow run detail returned by GET /api/v1/runs/:runId */\nexport interface WorkflowRunDetail extends WorkflowRunSummary {\n env?: Record<string, string>;\n metadata?: Record<string, unknown>;\n result?: {\n status: string;\n summary?: string;\n error?: { message: string; code?: string };\n outputs?: Record<string, unknown>;\n };\n jobs?: Array<{\n id: string;\n jobName?: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n error?: { message: string; code?: string; stack?: string } | string;\n steps?: Array<{\n id: string;\n name?: string;\n index?: number;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n outputs?: Record<string, unknown>;\n resolvedInputs?: Record<string, unknown>;\n error?: { message: string; code?: string; details?: Record<string, unknown> } | string;\n spec?: Record<string, unknown>;\n }>;\n }>;\n}\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * List workflow runs with optional status filter\n */\n async listRuns(params: {\n status?: string;\n limit?: number;\n workflowId?: string;\n } = {}): Promise<WorkflowRunSummary[]> {\n const query = new URLSearchParams();\n if (params.status) { query.set('status', params.status); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.workflowId) { query.set('workflowId', params.workflowId); }\n\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs${qs ? `?${qs}` : ''}`);\n if (!response.ok) {\n throw new Error(`Failed to list runs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ runs?: WorkflowRunSummary[] }>(data);\n return unwrapped.runs ?? (Array.isArray(data) ? data as WorkflowRunSummary[] : []);\n }\n\n /**\n * Get a specific workflow run with full detail\n */\n async getRun(runId: string): Promise<WorkflowRunDetail> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n // Response shape: { ok: true, data: { run: {...} } }\n const unwrapped = this.unwrapData<{ run?: WorkflowRunDetail } | WorkflowRunDetail>(data);\n return ('run' in (unwrapped as object) ? (unwrapped as { run: WorkflowRunDetail }).run : unwrapped) as WorkflowRunDetail;\n }\n\n /**\n * Get run logs, optionally filtered to failed steps only\n */\n async getRunLogs(runId: string, params: {\n stepId?: string;\n level?: string;\n limit?: number;\n failedOnly?: boolean;\n } = {}): Promise<Array<{ level: string; message: string; timestamp: string; stepId?: string; stepName?: string; stream?: string; [key: string]: unknown }>> {\n const query = new URLSearchParams();\n if (params.stepId) { query.set('stepId', params.stepId); }\n if (params.level) { query.set('level', params.level); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.failedOnly) { query.set('failedOnly', 'true'); }\n\n const encodedId = encodeURIComponent(runId);\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/logs${qs ? `?${qs}` : ''}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ logs?: unknown[] }>(data);\n return (unwrapped.logs ?? []) as Array<{ level: string; message: string; timestamp: string; [key: string]: unknown }>;\n }\n\n /**\n * Stream run events via SSE (returns a ReadableStream or EventSource-compatible URL)\n */\n getRunEventsUrl(runId: string): string {\n return `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/events`;\n }\n\n /**\n * List pending approval steps for a run\n */\n async listPendingApprovals(runId: string): Promise<{\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals`);\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to list pending approvals: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: {\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Resolve (approve or reject) a pending approval step\n */\n async resolveApproval(\n runId: string,\n jobId: string,\n stepId: string,\n action: 'approve' | 'reject',\n comment?: string,\n data?: Record<string, unknown>\n ): Promise<{ runId: string; jobId: string; stepId: string; action: string; resolved: boolean }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals/resolve`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jobId, stepId, action, comment, data }),\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to resolve approval: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: { runId: string; jobId: string; stepId: string; action: string; resolved: boolean };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Cancel a workflow run\n */\n async cancelRun(runId: string): Promise<void> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/cancel`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: '{}',\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to cancel run: ${message || response.statusText || response.status}`);\n }\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Rerun a workflow run by ID, optionally only failed jobs\n */\n async rerunWorkflow(\n runId: string,\n request: WorkflowRerunRequest = {},\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/rerun`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to rerun workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Restart a run from a snapshot, optionally resuming from a specific step.\n * Preceding steps inherit their stored outputs; only the target step and beyond are re-executed.\n */\n async restartRun(\n runId: string,\n request: WorkflowRestartRequest = {},\n ): Promise<WorkflowRestartResponse> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/restart`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to restart run: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: WorkflowRestartResponse }>(response);\n return this.unwrapData<WorkflowRestartResponse>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/workflows-list-handler\n * REST handler for listing workflow definitions\n */\n\nimport { defineHandler, rethrowForRest, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { WorkflowListResponse } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\ninterface WorkflowsListQuery {\n source?: string;\n status?: string;\n tags?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<WorkflowsListQuery>\n ): Promise<WorkflowListResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { source, status, tags } = input.query || {};\n\n const params = new URLSearchParams();\n if (source) {params.append('source', source);}\n if (status) {params.append('status', status);}\n if (tags) {params.append('tags', tags);}\n\n const queryString = params.toString();\n const url = `${daemonUrl}/api/v1/workflows${queryString ? `?${queryString}` : ''}`;\n\n ctx.platform.logger.info(`[workflows-list-handler] Fetching workflows from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: WorkflowListResponse; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch workflows');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[workflows-list-handler] Error fetching workflows',\n error instanceof Error ? error : undefined\n );\n rethrowForRest(error);\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../home/runner/work/kb-labs/kb-labs/plugins/workflow/entry/src/http-client.ts","../../../home/runner/work/kb-labs/kb-labs/plugins/workflow/entry/src/rest/workflows-list-handler.ts"],"names":[],"mappings":";;;AA2HA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACvHA,IAAO,iCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EAC+B;AAC/B,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAQ,MAAK,GAAI,KAAA,CAAM,SAAS,EAAC;AAEjD,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,IAAA,IAAI,MAAA,EAAQ;AAAC,MAAA,MAAA,CAAO,MAAA,CAAO,UAAU,MAAM,CAAA;AAAA,IAAE;AAC7C,IAAA,IAAI,MAAA,EAAQ;AAAC,MAAA,MAAA,CAAO,MAAA,CAAO,UAAU,MAAM,CAAA;AAAA,IAAE;AAC7C,IAAA,IAAI,IAAA,EAAM;AAAC,MAAA,MAAA,CAAO,MAAA,CAAO,QAAQ,IAAI,CAAA;AAAA,IAAE;AAEvC,IAAA,MAAM,WAAA,GAAc,OAAO,QAAA,EAAS;AACpC,IAAA,MAAM,GAAA,GAAM,GAAG,SAAS,CAAA,iBAAA,EAAoB,cAAc,CAAA,CAAA,EAAI,WAAW,KAAK,EAAE,CAAA,CAAA;AAEhF,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,iDAAA,EAAoD,GAAG,CAAA,CAAE,CAAA;AAElF,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,2BAA2B,CAAA;AAAA,MAC7D;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,mDAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,cAAA,CAAe,KAAK,CAAA;AAAA,IACtB;AAAA,EACF;AACF,CAAC","file":"workflows-list-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRerunRequest,\n WorkflowRestartRequest,\n WorkflowRestartResponse,\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Workflow run summary returned by GET /api/v1/runs */\nexport interface WorkflowRunSummary {\n id: string;\n name: string;\n version?: string;\n status: 'queued' | 'running' | 'success' | 'failed' | 'cancelled';\n trigger?: {\n type: string;\n actor?: string;\n payload?: unknown;\n parentRunId?: string;\n parentJobId?: string;\n parentStepId?: string;\n };\n createdAt?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n inputs?: Record<string, unknown>;\n hasPendingApproval?: boolean;\n currentStepName?: string;\n}\n\n/** Full workflow run detail returned by GET /api/v1/runs/:runId */\nexport interface WorkflowRunDetail extends WorkflowRunSummary {\n env?: Record<string, string>;\n metadata?: Record<string, unknown>;\n result?: {\n status: string;\n summary?: string;\n error?: { message: string; code?: string };\n outputs?: Record<string, unknown>;\n };\n jobs?: Array<{\n id: string;\n jobName?: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n error?: { message: string; code?: string; stack?: string } | string;\n steps?: Array<{\n id: string;\n name?: string;\n index?: number;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n attempt?: number;\n outputs?: Record<string, unknown>;\n resolvedInputs?: Record<string, unknown>;\n error?: { message: string; code?: string; details?: Record<string, unknown> } | string;\n spec?: Record<string, unknown>;\n }>;\n }>;\n}\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * List workflow runs with optional status filter\n */\n async listRuns(params: {\n status?: string;\n limit?: number;\n workflowId?: string;\n } = {}): Promise<WorkflowRunSummary[]> {\n const query = new URLSearchParams();\n if (params.status) { query.set('status', params.status); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.workflowId) { query.set('workflowId', params.workflowId); }\n\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs${qs ? `?${qs}` : ''}`);\n if (!response.ok) {\n throw new Error(`Failed to list runs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ runs?: WorkflowRunSummary[] }>(data);\n return unwrapped.runs ?? (Array.isArray(data) ? data as WorkflowRunSummary[] : []);\n }\n\n /**\n * Get a specific workflow run with full detail\n */\n async getRun(runId: string): Promise<WorkflowRunDetail> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n // Response shape: { ok: true, data: { run: {...} } }\n const unwrapped = this.unwrapData<{ run?: WorkflowRunDetail } | WorkflowRunDetail>(data);\n return ('run' in (unwrapped as object) ? (unwrapped as { run: WorkflowRunDetail }).run : unwrapped) as WorkflowRunDetail;\n }\n\n /**\n * Get run logs, optionally filtered to failed steps only\n */\n async getRunLogs(runId: string, params: {\n stepId?: string;\n level?: string;\n limit?: number;\n failedOnly?: boolean;\n } = {}): Promise<Array<{ level: string; message: string; timestamp: string; stepId?: string; stepName?: string; stream?: string; [key: string]: unknown }>> {\n const query = new URLSearchParams();\n if (params.stepId) { query.set('stepId', params.stepId); }\n if (params.level) { query.set('level', params.level); }\n if (params.limit) { query.set('limit', String(params.limit)); }\n if (params.failedOnly) { query.set('failedOnly', 'true'); }\n\n const encodedId = encodeURIComponent(runId);\n const qs = query.toString();\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/logs${qs ? `?${qs}` : ''}`);\n if (response.status === 404) {\n throw new Error(`Run ${runId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get run logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ logs?: unknown[] }>(data);\n return (unwrapped.logs ?? []) as Array<{ level: string; message: string; timestamp: string; [key: string]: unknown }>;\n }\n\n /**\n * Stream run events via SSE (returns a ReadableStream or EventSource-compatible URL)\n */\n getRunEventsUrl(runId: string): string {\n return `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/events`;\n }\n\n /**\n * List pending approval steps for a run\n */\n async listPendingApprovals(runId: string): Promise<{\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals`);\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to list pending approvals: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: {\n runId: string;\n pending: Array<{\n jobId: string;\n stepId: string;\n stepName: string;\n specId?: string;\n context: Record<string, unknown>;\n waitingSince?: string;\n }>;\n };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Resolve (approve or reject) a pending approval step\n */\n async resolveApproval(\n runId: string,\n jobId: string,\n stepId: string,\n action: 'approve' | 'reject',\n comment?: string,\n data?: Record<string, unknown>\n ): Promise<{ runId: string; jobId: string; stepId: string; action: string; resolved: boolean }> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/approvals/resolve`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jobId, stepId, action, comment, data }),\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to resolve approval: ${message || response.statusText || response.status}`);\n }\n const payload = await this.parseJsonResponse<{\n ok: boolean;\n data?: { runId: string; jobId: string; stepId: string; action: string; resolved: boolean };\n }>(response);\n return this.unwrapData(payload);\n }\n\n /**\n * Cancel a workflow run\n */\n async cancelRun(runId: string): Promise<void> {\n const encodedId = encodeURIComponent(runId);\n const response = await fetch(`${this.baseUrl}/api/v1/runs/${encodedId}/cancel`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: '{}',\n });\n if (!response.ok) {\n const message = await response\n .json()\n .then((j: { error?: string }) => j?.error ?? '')\n .catch(() => response.text().catch(() => ''));\n throw new Error(`Failed to cancel run: ${message || response.statusText || response.status}`);\n }\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Rerun a workflow run by ID, optionally only failed jobs\n */\n async rerunWorkflow(\n runId: string,\n request: WorkflowRerunRequest = {},\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/rerun`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to rerun workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n\n /**\n * Restart a run from a snapshot, optionally resuming from a specific step.\n * Preceding steps inherit their stored outputs; only the target step and beyond are re-executed.\n */\n async restartRun(\n runId: string,\n request: WorkflowRestartRequest = {},\n ): Promise<WorkflowRestartResponse> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/restart`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n },\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to restart run: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: WorkflowRestartResponse }>(response);\n return this.unwrapData<WorkflowRestartResponse>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/workflows-list-handler\n * REST handler for listing workflow definitions\n */\n\nimport { defineHandler, rethrowForRest, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { WorkflowListResponse } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\ninterface WorkflowsListQuery {\n source?: string;\n status?: string;\n tags?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<WorkflowsListQuery>\n ): Promise<WorkflowListResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { source, status, tags } = input.query || {};\n\n const params = new URLSearchParams();\n if (source) {params.append('source', source);}\n if (status) {params.append('status', status);}\n if (tags) {params.append('tags', tags);}\n\n const queryString = params.toString();\n const url = `${daemonUrl}/api/v1/workflows${queryString ? `?${queryString}` : ''}`;\n\n ctx.platform.logger.info(`[workflows-list-handler] Fetching workflows from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: WorkflowListResponse; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch workflows');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[workflows-list-handler] Error fetching workflows',\n error instanceof Error ? error : undefined\n );\n rethrowForRest(error);\n }\n },\n});\n"]}
|
package/dist/ws/logs-channel.js
CHANGED
|
@@ -8,7 +8,9 @@ function getWorkflowDaemonUrl() {
|
|
|
8
8
|
|
|
9
9
|
// src/ws/logs-channel.ts
|
|
10
10
|
var POLL_INTERVAL_MS = 1500;
|
|
11
|
-
var SubscribeMsg = defineMessage(
|
|
11
|
+
var SubscribeMsg = defineMessage(
|
|
12
|
+
"subscribe"
|
|
13
|
+
);
|
|
12
14
|
var UnsubscribeMsg = defineMessage("unsubscribe");
|
|
13
15
|
var LogMsg = defineMessage("log");
|
|
14
16
|
var ErrorMsg = defineMessage("error");
|
|
@@ -33,7 +35,14 @@ function levelMatches(logLevel, filterLevel) {
|
|
|
33
35
|
if (!filterLevel || filterLevel === "all") {
|
|
34
36
|
return true;
|
|
35
37
|
}
|
|
36
|
-
const levelOrder = {
|
|
38
|
+
const levelOrder = {
|
|
39
|
+
debug: 0,
|
|
40
|
+
trace: 0,
|
|
41
|
+
info: 1,
|
|
42
|
+
warn: 2,
|
|
43
|
+
error: 3,
|
|
44
|
+
fatal: 3
|
|
45
|
+
};
|
|
37
46
|
return (levelOrder[logLevel] ?? 1) >= (levelOrder[filterLevel] ?? 0);
|
|
38
47
|
}
|
|
39
48
|
async function fetchLogs(runId, offset, level) {
|
|
@@ -52,8 +61,20 @@ async function fetchLogs(runId, offset, level) {
|
|
|
52
61
|
}
|
|
53
62
|
async function checkRunExists(runId) {
|
|
54
63
|
const daemonUrl = getWorkflowDaemonUrl();
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(
|
|
66
|
+
`${daemonUrl}/api/v1/runs/${encodeURIComponent(runId)}`
|
|
67
|
+
);
|
|
68
|
+
if (res.status === 404) {
|
|
69
|
+
return { exists: false };
|
|
70
|
+
}
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
return { exists: false, unavailable: true };
|
|
73
|
+
}
|
|
74
|
+
return { exists: true };
|
|
75
|
+
} catch {
|
|
76
|
+
return { exists: false, unavailable: true };
|
|
77
|
+
}
|
|
57
78
|
}
|
|
58
79
|
function clearConnection(connectionId) {
|
|
59
80
|
activeSubscriptions.delete(connectionId);
|
|
@@ -65,26 +86,32 @@ function clearConnection(connectionId) {
|
|
|
65
86
|
logOffsets.delete(connectionId);
|
|
66
87
|
}
|
|
67
88
|
var logs_channel_default = defineWebSocket({
|
|
68
|
-
path: "/logs/:
|
|
89
|
+
path: "/logs/:runId",
|
|
69
90
|
description: "Real-time job logs streaming",
|
|
70
91
|
handler: {
|
|
71
92
|
async onConnect(ctx, _sender) {
|
|
72
|
-
ctx.platform.logger.info("[logs-channel] Client connected", {
|
|
93
|
+
ctx.platform.logger.info("[logs-channel] Client connected", {
|
|
94
|
+
connectionId: ctx.requestId
|
|
95
|
+
});
|
|
73
96
|
},
|
|
74
97
|
async onMessage(ctx, message, sender) {
|
|
75
98
|
const connectionId = ctx.requestId;
|
|
76
99
|
const router = new MessageRouter().on(SubscribeMsg, async (_ctx, payload) => {
|
|
77
100
|
const { jobId: runId, level } = payload;
|
|
78
|
-
const
|
|
79
|
-
if (!exists) {
|
|
80
|
-
|
|
81
|
-
|
|
101
|
+
const probe = await checkRunExists(runId);
|
|
102
|
+
if (!probe.exists) {
|
|
103
|
+
const code = probe.unavailable ? "DEPENDENCY_UNAVAILABLE" : "NOT_FOUND";
|
|
104
|
+
const error = probe.unavailable ? "Workflow daemon is unavailable; logs cannot be subscribed right now" : `Run ${runId} not found`;
|
|
105
|
+
await sender.send(ErrorMsg.create({ error, code }));
|
|
106
|
+
sender.close(probe.unavailable ? 1013 : 1008, code);
|
|
82
107
|
return;
|
|
83
108
|
}
|
|
84
109
|
clearConnection(connectionId);
|
|
85
110
|
logOffsets.set(connectionId, 0);
|
|
86
111
|
activeSubscriptions.add(connectionId);
|
|
87
|
-
const initialLogs = await fetchLogs(runId, 0, level).catch(
|
|
112
|
+
const initialLogs = await fetchLogs(runId, 0, level).catch(
|
|
113
|
+
() => []
|
|
114
|
+
);
|
|
88
115
|
if (!activeSubscriptions.has(connectionId)) {
|
|
89
116
|
return;
|
|
90
117
|
}
|
|
@@ -96,12 +123,14 @@ var logs_channel_default = defineWebSocket({
|
|
|
96
123
|
if (!activeSubscriptions.has(connectionId)) {
|
|
97
124
|
return;
|
|
98
125
|
}
|
|
99
|
-
await sender.send(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
126
|
+
await sender.send(
|
|
127
|
+
LogMsg.create({
|
|
128
|
+
timestamp: log.timestamp,
|
|
129
|
+
level: normalizeLevel(log.level),
|
|
130
|
+
message: log.message,
|
|
131
|
+
context: log.context
|
|
132
|
+
})
|
|
133
|
+
);
|
|
105
134
|
}
|
|
106
135
|
logOffsets.set(connectionId, initialLogs.length);
|
|
107
136
|
const timer = setInterval(async () => {
|
|
@@ -109,7 +138,9 @@ var logs_channel_default = defineWebSocket({
|
|
|
109
138
|
return;
|
|
110
139
|
}
|
|
111
140
|
const currentOffset = logOffsets.get(connectionId) ?? 0;
|
|
112
|
-
const newLogs = await fetchLogs(runId, currentOffset, level).catch(
|
|
141
|
+
const newLogs = await fetchLogs(runId, currentOffset, level).catch(
|
|
142
|
+
() => []
|
|
143
|
+
);
|
|
113
144
|
if (!activeSubscriptions.has(connectionId)) {
|
|
114
145
|
return;
|
|
115
146
|
}
|
|
@@ -123,12 +154,14 @@ var logs_channel_default = defineWebSocket({
|
|
|
123
154
|
if (!activeSubscriptions.has(connectionId)) {
|
|
124
155
|
return;
|
|
125
156
|
}
|
|
126
|
-
await sender.send(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
157
|
+
await sender.send(
|
|
158
|
+
LogMsg.create({
|
|
159
|
+
timestamp: log.timestamp,
|
|
160
|
+
level: normalizeLevel(log.level),
|
|
161
|
+
message: log.message,
|
|
162
|
+
context: log.context
|
|
163
|
+
})
|
|
164
|
+
).catch(() => {
|
|
132
165
|
});
|
|
133
166
|
}
|
|
134
167
|
logOffsets.set(connectionId, currentOffset + newLogs.length);
|
|
@@ -141,13 +174,20 @@ var logs_channel_default = defineWebSocket({
|
|
|
141
174
|
},
|
|
142
175
|
async onDisconnect(ctx, _code, _reason) {
|
|
143
176
|
clearConnection(ctx.requestId);
|
|
144
|
-
ctx.platform.logger.info("[logs-channel] Client disconnected", {
|
|
177
|
+
ctx.platform.logger.info("[logs-channel] Client disconnected", {
|
|
178
|
+
connectionId: ctx.requestId
|
|
179
|
+
});
|
|
145
180
|
},
|
|
146
181
|
async onError(ctx, error, sender) {
|
|
147
182
|
ctx.platform.logger.error("[logs-channel] Error", error);
|
|
148
183
|
clearConnection(ctx.requestId);
|
|
149
184
|
try {
|
|
150
|
-
await sender.send(
|
|
185
|
+
await sender.send(
|
|
186
|
+
ErrorMsg.create({
|
|
187
|
+
error: error.message,
|
|
188
|
+
code: "DEPENDENCY_UNAVAILABLE"
|
|
189
|
+
})
|
|
190
|
+
);
|
|
151
191
|
} catch {
|
|
152
192
|
}
|
|
153
193
|
}
|