import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { LoadSessionConfigOptions } from "../config/load-config.ts"; import type { ObservMeConfig } from "../config/schema.ts"; import type { LokiFetch } from "../query/loki.ts"; import { createLokiQueryClient } from "../query/loki.ts"; import type { ObsLokiLogSummaryRow, ObsLokiTimeRangeOptions } from "./obs-loki-summary.ts"; import { completeObsSubcommand, isExactObsSubcommandRequest } from "./obs-args.ts"; import { loadObsCommandConfig, notifyObsCommand } from "./obs-command-support.ts"; import { appendObsRecoveryHint, formatObsCommandFailure } from "./obs-diagnostics.ts"; import { createRecentObsLokiTimeRange, formatObsLokiWindow, normalizeObsLokiMaxLogs, renderObsLokiLogSummary, toObsLokiLogSummaryRow, } from "./obs-loki-summary.ts"; export interface ObsErrorsCommandContext { readonly cwd?: string; readonly ui: { notify: (message: string, type?: "info" | "warning" | "error") => Promise | void; }; readonly isProjectTrusted?: () => boolean | Promise; } export interface ObsErrorsSnapshot { readonly window: string; readonly query: string; readonly maxLogs: number; readonly logs: readonly ObsLokiLogSummaryRow[]; } export type ObsErrorsConfigLoader = (options: LoadSessionConfigOptions) => Promise; export type ObsErrorsProvider = (ctx: ObsErrorsCommandContext) => Promise | ObsErrorsSnapshot; export interface ObsErrorsSnapshotOptions extends ObsLokiTimeRangeOptions { readonly loadConfig?: ObsErrorsConfigLoader; readonly fetch?: LokiFetch; readonly env?: NodeJS.ProcessEnv; readonly configDirName?: string; } export interface RegisterObsErrorsCommandOptions extends ObsErrorsSnapshotOptions { readonly getErrors?: ObsErrorsProvider; } export const OBS_ERROR_EVENT_NAME_PATTERN = ".*[.]failed|.*[.]dropped|agent[.]orphaned"; export const OBS_ERRORS_LOGQL = `{service_name="observme-pi-extension", event_name=~"${OBS_ERROR_EVENT_NAME_PATTERN}"}`; const OBS_COMMAND_NAME = "obs"; const OBS_ERRORS_SUBCOMMAND = "errors"; const OBS_ERRORS_USAGE = "Usage: /obs errors"; const OBS_ERRORS_ERROR_NEXT_ACTION = "run /obs health and verify query.grafana.url, Grafana credentials, the Loki datasource UID, and service labels."; const OBS_ERRORS_NO_LOGS_NEXT_ACTION = "generate error telemetry, then verify Loki labels and datasource with /obs health."; export function registerObsErrorsCommand(pi: ExtensionAPI, options: RegisterObsErrorsCommandOptions = {}): void { const command = new ObsErrorsCommand(options); pi.registerCommand(OBS_COMMAND_NAME, { description: "Show recent ObservMe error events from Loki. Usage: /obs errors", getArgumentCompletions: getObsErrorsCommandArgumentCompletions, handler: command.handle.bind(command), }); } export async function handleObsErrorsCommand( args: string, ctx: ObsErrorsCommandContext, options: RegisterObsErrorsCommandOptions = {}, ): Promise { if (!isObsErrorsRequest(args)) { await notifyObsCommand(ctx, OBS_ERRORS_USAGE, "warning"); return; } try { const snapshot = await resolveObsErrorsSnapshot(ctx, options); await notifyObsCommand(ctx, renderObsErrors(snapshot), "info"); } catch (error) { await notifyObsCommand( ctx, formatObsCommandFailure("ObservMe errors unavailable", error, { subsystem: "Loki", nextAction: OBS_ERRORS_ERROR_NEXT_ACTION, }), "error", ); } } export function getObsErrorsCommandArgumentCompletions(prefix: string): Array<{ value: string; label: string }> | null { return completeObsSubcommand(prefix, OBS_ERRORS_SUBCOMMAND); } export async function getObsErrorsSnapshot( ctx: ObsErrorsCommandContext, options: ObsErrorsSnapshotOptions = {}, ): Promise { const config = await loadObsErrorsConfig(ctx, options); const maxLogs = normalizeObsLokiMaxLogs(config.query.maxLogs); const logs = await queryObsErrors(config, options); return { window: formatObsLokiWindow(options), query: OBS_ERRORS_LOGQL, maxLogs, logs: logs.slice(0, maxLogs).map(toObsLokiLogSummaryRow), }; } export function renderObsErrors(snapshot: ObsErrorsSnapshot): string { return renderObsLokiLogSummary({ title: "Recent error events", window: snapshot.window, maxLogs: snapshot.maxLogs, rows: snapshot.logs, emptyMessage: appendObsRecoveryHint("No error logs found.", OBS_ERRORS_NO_LOGS_NEXT_ACTION), }); } class ObsErrorsCommand { readonly #options: RegisterObsErrorsCommandOptions; constructor(options: RegisterObsErrorsCommandOptions) { this.#options = options; } async handle(args: string, ctx: ObsErrorsCommandContext): Promise { await handleObsErrorsCommand(args, ctx, this.#options); } } async function resolveObsErrorsSnapshot( ctx: ObsErrorsCommandContext, options: RegisterObsErrorsCommandOptions, ): Promise { if (options.getErrors) return options.getErrors(ctx); return getObsErrorsSnapshot(ctx, options); } async function loadObsErrorsConfig( ctx: ObsErrorsCommandContext, options: ObsErrorsSnapshotOptions, ): Promise { return loadObsCommandConfig(ctx, options); } async function queryObsErrors(config: ObservMeConfig, options: ObsErrorsSnapshotOptions) { const client = createLokiQueryClient(config, { fetch: options.fetch }); return client.queryLoki(OBS_ERRORS_LOGQL, createRecentObsLokiTimeRange(options)); } function isObsErrorsRequest(args: string): boolean { return isExactObsSubcommandRequest(args, OBS_ERRORS_SUBCOMMAND); }