import { Type } from "typebox"; import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { loadConfig } from "../config"; import { dockerLogs, listContainers, resolveWorkerContainer, grepLogs, truncateOutput } from "../helpers"; // ─── Container Logs ──────────────────────────────────────────────────────────── export const containerLogsTool = { name: "docker_container_logs" as const, label: "Get Docker Container Logs", description: "Fetch logs from a Docker container by name using `docker logs`. Supports tail, since, and text search filtering. Works with any container on the Docker host.", parameters: Type.Object({ container: Type.String({ description: "Docker container name (e.g. 'ai-factory-worker-1', 'ai-factory-orchestrator')", }), tail: Type.Optional(Type.Number({ description: "Number of lines to tail (default: 200)" })), since: Type.Optional(Type.String({ description: "Only logs since (e.g. '10m', '1h', ISO timestamp)" })), search: Type.Optional(Type.String({ description: "Filter lines containing this text (case-insensitive)" })), }), async execute(_id: string, params: any, _s: any, _u: any, ctx: ExtensionContext) { const config = loadConfig(ctx.cwd); const tail = params.tail || config.tail; const result = await dockerLogs(params.container, tail, params.since); if (!result.ok) { return { content: [ { type: "text", text: [ `❌ Failed to get logs for "${params.container}":`, ` ${result.error}`, "", `Check: Is Docker running? Is the container name correct?`, `Try: docker_container_list() to see running containers.`, ].join("\n"), }, ], isError: true, details: { container: params.container }, }; } let text = result.text!; let grepInfo = ""; if (params.search) { text = grepLogs(text, params.search); grepInfo = ` (filtered by "${params.search}")`; } const lines = text.split("\n").filter(Boolean); if (lines.length === 0) { return { content: [ { type: "text", text: `📋 No log entries found for "${params.container}"${grepInfo}.`, }, ], details: { container: params.container, count: 0 }, }; } const output = truncateOutput(text, tail); let header = `📋 ${params.container} — ${lines.length} lines${grepInfo}`; if (output.truncated) { header += ` (showing truncated view, total: ${output.totalLines} lines)`; } return { content: [{ type: "text", text: `${header}\n\n${output.text}` }], details: { container: params.container, count: lines.length, totalLines: output.totalLines, truncated: output.truncated, }, }; }, }; // ─── Worker Logs ─────────────────────────────────────────────────────────────── export const workerLogsTool = { name: "docker_worker_logs" as const, label: "Get Worker Container Logs", description: "Fetch logs from a wrok.in factory worker container. Resolves worker numbers (1-4) to container names automatically.", parameters: Type.Object({ worker: Type.String({ description: 'Worker identifier: "1" through "4", "worker-2", or full container name', }), tail: Type.Optional(Type.Number({ description: "Number of lines to tail (default: 200)" })), since: Type.Optional(Type.String({ description: "Only logs since (e.g. '10m', '1h')" })), search: Type.Optional(Type.String({ description: "Filter lines containing this text" })), }), async execute(_id: string, params: any, _s: any, _u: any, ctx: ExtensionContext) { const config = loadConfig(ctx.cwd); const container = resolveWorkerContainer(params.worker); if (!container) { return { content: [ { type: "text", text: `❌ Could not resolve worker "${params.worker}" to a container name.\n Try: "1", "worker-2", or "ai-factory-worker-3".`, }, ], isError: true, details: { worker: params.worker }, }; } const tail = params.tail || config.tail; const result = await dockerLogs(container, tail, params.since); if (!result.ok) { return { content: [ { type: "text", text: [ `❌ Failed to get logs for worker "${params.worker}" (container: ${container}):`, ` ${result.error}`, ].join("\n"), }, ], isError: true, details: { worker: params.worker, container }, }; } let text = result.text!; let grepInfo = ""; if (params.search) { text = grepLogs(text, params.search); grepInfo = ` (filtered by "${params.search}")`; } const lines = text.split("\n").filter(Boolean); if (lines.length === 0) { return { content: [ { type: "text", text: `📋 No log entries for worker ${params.worker}${grepInfo}.`, }, ], details: { worker: params.worker, container, count: 0 }, }; } const output = truncateOutput(text, tail); let header = `📋 Worker ${params.worker} (${container}) — ${lines.length} lines${grepInfo}`; if (output.truncated) { header += ` (showing truncated view, total: ${output.totalLines} lines)`; } return { content: [{ type: "text", text: `${header}\n\n${output.text}` }], details: { worker: params.worker, container, count: lines.length, totalLines: output.totalLines, truncated: output.truncated, }, }; }, }; // ─── List Containers ─────────────────────────────────────────────────────────── export const listContainersTool = { name: "docker_container_list" as const, label: "List Factory Containers", description: "List running Docker containers related to the AI factory. Shows container names so you know what to pass to docker_container_logs().", parameters: Type.Object({}), async execute(_id: string, _params: any, _s: any, _u: any, ctx: ExtensionContext) { const result = await listContainers(); if (!result.ok) { return { content: [ { type: "text", text: `❌ Failed to list containers: ${result.error}`, }, ], isError: true, details: {}, }; } const containers = result.containers!; if (containers.length === 0) { return { content: [ { type: "text", text: "No factory containers running. Is the factory started? (`docker compose up -d` in the wrok.in directory)", }, ], details: { count: 0 }, }; } const lines = [`🐳 Running Factory Containers (${containers.length})`, ""]; for (const c of containers) { const type = c.includes("worker") ? "🔵 Worker" : c.includes("orchestrator") ? "🟡 Orchestrator" : c.includes("gitea") ? "📦 Gitea" : c.includes("postgres") ? "🗄️ Postgres" : c.includes("loki") ? "📊 Loki" : c.includes("grafana") ? "📈 Grafana" : "⚪ Other"; lines.push(` ${type} ${c}`); } return { content: [{ type: "text", text: lines.join("\n") }], details: { containers, count: containers.length }, }; }, };