Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | 3x 3x 3x 3x 30x 25x 25x 25x 23x 23x 1x 7x 3x 9x 9x 8x 8x 10x 10x 8x 3x 5x 5x 4x 4x 5x 5x 5x 4x 3x 6x 10x 6x 6x 6x 3x 6x 6x | /**
* Pure adapters between MCP tool results and the watch core (issue #483):
* unwrap an MCP `CallToolResult` ({content:[{text}]}) into its JSON payload,
* parse a `browse_pipelines`/`jobs` result into `JobState[]`, and render a
* `WatchEvent` into a channel message (content + meta). No I/O.
*/
import * as z from 'zod';
import type { JobState, WatchEvent } from './watch';
const JobSchema = z.object({
id: z.number(),
name: z.string(),
stage: z.string(),
status: z.string(),
});
const DeploymentSchema = z.object({
id: z.number(),
status: z.string(),
environment: z.object({ name: z.string() }).optional(),
});
/** MCP tool results wrap JSON in a text content block; unwrap it, else pass through. */
export function parseToolResult(result: unknown): unknown {
if (result && typeof result === 'object' && 'content' in result) {
const content = (result as { content?: Array<{ type?: string; text?: string }> }).content;
const text = Array.isArray(content) ? content.find((c) => c.type === 'text')?.text : undefined;
if (typeof text === 'string') {
try {
return JSON.parse(text);
} catch {
return null;
}
}
}
return result;
}
/** Parse a jobs result into JobState[], silently dropping malformed entries. */
export function parseJobs(result: unknown): JobState[] {
const data = parseToolResult(result);
if (!Array.isArray(data)) return [];
const out: JobState[] = [];
for (const item of data) {
const parsed = JobSchema.safeParse(item);
if (parsed.success) out.push(parsed.data);
}
return out;
}
/**
* Parse a deployments list into pseudo-jobs the watch core can treat uniformly:
* one JobState per deployment, named by its environment. Lets a deployment be
* watched with the same aggregate/diff/terminal machinery as a pipeline.
*/
export function parseDeployments(result: unknown): JobState[] {
const data = parseToolResult(result);
if (!Array.isArray(data)) return [];
const out: JobState[] = [];
for (const item of data) {
const parsed = DeploymentSchema.safeParse(item);
Eif (parsed.success) {
out.push({
id: parsed.data.id,
name: parsed.data.environment?.name ?? `deployment-${parsed.data.id}`,
stage: 'deploy',
status: parsed.data.status,
});
}
}
return out;
}
/**
* Render a watch event into a channel message. `content` is the human-readable
* body the agent sees; `meta` keys are identifiers ([a-z0-9_]) per the channel
* protocol (hyphens in keys would be dropped, so only stable keys are used).
*/
export function formatEvent(event: WatchEvent): { content: string; meta: Record<string, string> } {
const { target, pipelineState, jobs, transitions, terminal } = event;
const jobsLine = jobs.map((j) => `${j.name}:${j.status}`).join(' ');
// A deployment is watched through the same job machinery, but the channel
// message and meta key must name it for what it is, not as a pipeline.
const label = target.kind === 'deployment' ? 'Deployment' : 'Pipeline';
const idKey = target.kind === 'deployment' ? 'deployment_id' : 'pipeline_id';
const content = terminal
? `${label} #${target.id} (project ${target.projectId}) finished: ${pipelineState}. Jobs: ${jobsLine}`
: `${label} #${target.id} (project ${target.projectId}) ${transitions
.map((t) => `${t.name} ${t.from ?? 'new'}->${t.to}`)
.join(', ')}. Now: ${jobsLine}`;
const meta: Record<string, string> = {
[idKey]: String(target.id),
project_id: target.projectId,
kind: target.kind,
state: pipelineState,
terminal: String(terminal),
};
return { content, meta };
}
|