diff --git a/.pi/extensions/prod-logs/index.ts b/.pi/extensions/prod-logs/index.ts index 577939ed..6b52a0f6 100644 --- a/.pi/extensions/prod-logs/index.ts +++ b/.pi/extensions/prod-logs/index.ts @@ -14,6 +14,13 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { BorderedLoader, DynamicBorder } from "@mariozechner/pi-coding-agent"; +import { + truncateTail, + formatSize, + DEFAULT_MAX_BYTES, + DEFAULT_MAX_LINES, +} from "@mariozechner/pi-coding-agent"; +import { Type } from "typebox"; import { matchesKey, Key, @@ -309,6 +316,143 @@ async function fetchLogs( // ── Extension ─────────────────────────────────────────────────────────────── export default function prodLogsExtension(pi: ExtensionAPI) { + // ── fetch_production_logs tool ───────────────────────────────────────── + + pi.registerTool({ + name: "fetch_production_logs", + label: "Fetch Production Logs", + description: + "Fetch recent production logs from the deployed application via the Coolify API. " + + "Use this tool when investigating production errors, checking server behavior, " + + "debugging deployed issues, or when the user asks about production status, " + + "logs, errors, or application behavior in the deployed environment. " + + "Output is truncated at 50KB / 2000 lines — use a smaller 'tail' value or " + + "a narrower 'grep' pattern to reduce output if truncation occurs.", + promptSnippet: + "Fetch recent production logs from deployed app (param: tail, grep)", + promptGuidelines: [ + "Use fetch_production_logs when investigating production errors, checking deployed application behavior, or when the user asks about production status or recent activity.", + "Use the 'grep' parameter to filter logs by a case-insensitive substring (e.g., grep: 'error', grep: 'timeout'). This reduces output size and helps find relevant entries.", + "Start with a small 'tail' value (e.g., tail: 50) and increase only if needed. Large tail values may hit the 50KB/2000-line truncation limit.", + ], + parameters: Type.Object({ + tail: Type.Optional( + Type.Number({ + description: + "Number of most recent log lines to return. Default: 200. Use a smaller value to reduce output size.", + }), + ), + grep: Type.Optional( + Type.String({ + description: + "Case-insensitive substring filter for log lines. Only lines containing this string are returned. Use to narrow results to relevant entries.", + }), + ), + }), + async execute( + _toolCallId, + params, + signal, + _onUpdate, + _ctx, + ) { + // Early abort check + if (signal?.aborted) { + return { content: [{ type: "text", text: "Cancelled" }] }; + } + + // Validate credentials + const host = resolveVar("coolify_host"); + const appUuid = resolveVar("coolify_app_uuid"); + const token = resolveVar("coolify_token"); + + const missing: string[] = []; + if (!host) missing.push("HURL_VARIABLE_coolify_host"); + if (!appUuid) missing.push("HURL_VARIABLE_coolify_app_uuid"); + if (!token) missing.push("HURL_VARIABLE_coolify_token"); + + if (missing.length > 0) { + return { + content: [ + { + type: "text", + text: + `Cannot fetch production logs: the following environment variables are not set:\n` + + missing.map((v) => ` - ${v}`).join("\n") + + `\n\nEnsure these are configured in your pi environment.`, + }, + ], + isError: true, + }; + } + + // Fetch logs + let lines: string[]; + try { + lines = await fetchLogs(host!, appUuid!, token!, signal); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { + content: [ + { + type: "text", + text: `Failed to fetch production logs: ${message}`, + }, + ], + isError: true, + }; + } + + // Handle empty logs + if (lines.length === 0) { + return { + content: [{ type: "text", text: "No log entries found." }], + }; + } + + // Recent entries first (consistent with the /prod-logs viewer) + lines.reverse(); + + // Apply grep filter if provided + if (params.grep) { + const pattern = params.grep.toLowerCase(); + lines = lines.filter((line) => line.toLowerCase().includes(pattern)); + } + + // Record pre-tail line count for details + const lineCount = lines.length; + + // Apply tail limit + const tail = params.tail ?? 200; + lines = lines.slice(0, tail); + + // Join into a single string + let output = lines.join("\n"); + + // Apply output truncation + const truncation = truncateTail(output, { + maxLines: DEFAULT_MAX_LINES, + maxBytes: DEFAULT_MAX_BYTES, + }); + + if (truncation.truncated) { + output = + truncation.content + + `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). ` + + `Use a smaller 'tail' value or narrower 'grep' pattern to reduce output.]`; + } else { + output = truncation.content; + } + + return { + content: [{ type: "text", text: output }], + details: { lineCount }, + }; + }, + }); + + // ── /prod-logs interactive command ───────────────────────────────────── + pi.registerCommand("prod-logs", { description: "View production application logs via Coolify API", handler: async (_args, ctx) => { diff --git a/backlog/tasks/ml-160 - Programmatic-access-to-production-logs.md b/backlog/tasks/ml-160 - Programmatic-access-to-production-logs.md index 93f0165a..ba021ef2 100644 --- a/backlog/tasks/ml-160 - Programmatic-access-to-production-logs.md +++ b/backlog/tasks/ml-160 - Programmatic-access-to-production-logs.md @@ -1,10 +1,10 @@ --- id: ML-160 title: Programmatic access to production logs -status: To Do +status: Done assignee: [] created_date: '2026-05-04 06:42' -updated_date: '2026-05-04 06:52' +updated_date: '2026-05-04 07:33' labels: [] dependencies: [] references: @@ -20,13 +20,13 @@ Evaluate and implement the best approach for the LLM to access production logs. ## Acceptance Criteria -- [ ] #1 The LLM can fetch production logs without user intervention by calling the fetch_production_logs tool -- [ ] #2 The tool supports a `tail` parameter to limit the number of log lines returned (default: 200) -- [ ] #3 The tool supports a `grep` parameter for case-insensitive filtering of log lines -- [ ] #4 The tool returns log lines as text the LLM can read and analyze directly in context -- [ ] #5 When Coolify credentials are missing, the tool returns a clear error message listing which environment variables are not set -- [ ] #6 The existing /prod-logs interactive command continues to work unchanged -- [ ] #7 The tool description and guidelines teach the LLM when and how to use it effectively +- [x] #1 The LLM can fetch production logs without user intervention by calling the fetch_production_logs tool +- [x] #2 The tool supports a `tail` parameter to limit the number of log lines returned (default: 200) +- [x] #3 The tool supports a `grep` parameter for case-insensitive filtering of log lines +- [x] #4 The tool returns log lines as text the LLM can read and analyze directly in context +- [x] #5 When Coolify credentials are missing, the tool returns a clear error message listing which environment variables are not set +- [x] #6 The existing /prod-logs interactive command continues to work unchanged +- [x] #7 The tool description and guidelines teach the LLM when and how to use it effectively ## Implementation Plan @@ -143,3 +143,39 @@ The implementation is self-documenting: the tool's `description`, `promptSnippet - `truncateTail`, `formatSize`, `DEFAULT_MAX_BYTES`, `DEFAULT_MAX_LINES` — all from `@mariozechner/pi-coding-agent`, a pi built-in - No new npm dependencies needed + +## Implementation Notes + + +Added `fetch_production_logs` tool to the existing `.pi/extensions/prod-logs/index.ts` extension. The tool reuses the existing `fetchLogs()` and `resolveVar()` helper functions. Implementation follows the plan exactly: + +- Added imports for `Type` (typebox) and `truncateTail`, `formatSize`, `DEFAULT_MAX_BYTES`, `DEFAULT_MAX_LINES` (@mariozechner/pi-coding-agent) +- Registered tool before the existing `pi.registerCommand("prod-logs", ...)` call +- Tool supports `tail` (default 200) and `grep` parameters +- Handler: credential check → fetch → error handling → empty check → reverse → grep → tail → join → truncate → return +- Truncation via `truncateTail` with 50KB/2000-line limit, with clear truncation note in output +- Existing `/prod-logs` command code is completely untouched + + +## Final Summary + + +Added a `fetch_production_logs` tool to `.pi/extensions/prod-logs/index.ts` using `pi.registerTool()`. The tool gives the LLM programmatic access to production logs without user intervention. + +**What changed:** +- Added imports for `Type` (typebox) and `truncateTail`, `formatSize`, `DEFAULT_MAX_BYTES`, `DEFAULT_MAX_LINES` from `@mariozechner/pi-coding-agent` +- Registered `fetch_production_logs` tool with `tail` (default 200) and `grep` parameters +- Tool reuses the existing `fetchLogs()` and `resolveVar()` module-scope functions from the `/prod-logs` extension +- Handler: credential validation → fetch → error handling → empty check → reverse (newest first) → grep filter → tail slice → join → `truncateTail` truncation with clear truncation note +- On missing credentials, returns a clear error listing which env vars are not set + +**What didn't change:** +- The existing `/prod-logs` interactive command code is completely untouched +- No new dependencies, env vars, infrastructure changes, or Elixir module changes + +**Verification (requires `/reload` in pi):** +1. Ask the LLM about available tools for production logs — should describe `fetch_production_logs` +2. Ask the LLM to fetch logs with `tail: 50` or `grep: "error"` — should work +3. Unset a credential and try — should get clear error message +4. Run `/prod-logs` — should still work unchanged +