import { randomUUID } from "node:crypto"; import { appendFileSync, statSync } from "node:fs"; export type CliInvocationObservation = { version: 1; id: string; tool: string; startedAt: number; endedAt: number; durationMs: number; exitCode: number; errorType?: string; retryable?: boolean; }; const SAFE_FILE = /^\/tmp\/dench-harness-a\/[a-f0-9-]{36}\/workspace\/\.dench\/observations\/[a-zA-Z0-9._-]+\.jsonl$/; const SAFE_NAME = /^[a-z][a-z0-9_.-]{0,119}$/; const SIDECAR_LIMIT = 65_536; const KNOWN_ROOTS = new Set([ "ads", "agent", "agents", "approval", "approvals", "artifacts", "backend", "billing", "browser", "campaign", "chat", "companies", "context", "create", "crm", "cron", "email", "env", "files", "fs", "help", "identity", "image", "instructions", "integrations", "jobs", "leads", "lists", "logout", "meetings", "members", "mem", "memory", "model", "notifications", "onboard", "organisation", "project", "reference", "register", "request", "secrets", "sessions", "signin", "status", "settings", "setup", "suggested-work", "tool", "tools", "upgrade", "use", "user", "version", "web", "what-can-i-do", ]); const KNOWN_SUBCOMMANDS = new Set([ "add", "approve", "archive", "cancel", "clear", "clone", "collapse", "complete", "compute", "configure", "create", "create-many", "create-subtask", "delete", "disable", "draft", "edit", "enable", "expand", "export", "follow", "get", "help", "hide", "history", "import", "info", "install", "list", "me", "mine", "move", "new", "parent", "pause", "poll", "presence", "preview", "priority", "query", "read", "register", "reject", "remove", "rename", "reopen", "reorder", "repeat", "resize", "resume", "roles", "run", "save", "scope", "search", "send", "series", "set", "show", "status", "stop", "subtasks", "sync", "tree", "unarchive", "uninstall", "update", "validate", "watch", ]); const NESTED_NAMESPACES = new Set([ "actions", "analytics", "builder", "cells", "docs", "drafts", "entries", "fields", "inbox", "objects", "reports", "sidebar", "sources", "statuses", "tasks", "transactions", "views", ]); function words(argv: readonly string[]) { const result: string[] = []; for (let index = 0; index < argv.length; index++) { const value = argv[index]; if (!value || value === "--") break; if (value.startsWith("-")) { if (!["--json", "--help", "-h", "--staging", "--prod"].includes(value)) index++; continue; } result.push(value.toLowerCase().replace(/[^a-z0-9_-]/g, "-")); } return result; } export function normalizedCliTool(argv: readonly string[]) { const [root = "unknown", ...rest] = words(argv); const canonicalRoot = root === "campaigns" ? "campaign" : root === "routine" || root === "routines" ? "cron" : root === "ad" || root === "adline" ? "ads" : root; const parts = [ "dench", KNOWN_ROOTS.has(canonicalRoot) ? canonicalRoot : "unknown", ]; if (KNOWN_SUBCOMMANDS.has(rest[0]) || NESTED_NAMESPACES.has(rest[0])) parts.push(rest[0]); if (NESTED_NAMESPACES.has(rest[0]) && KNOWN_SUBCOMMANDS.has(rest[1])) parts.push(rest[1]); const name = parts.join("."); return SAFE_NAME.test(name) ? name : "dench.unknown"; } function safeErrorType(value: unknown) { const raw = typeof value === "string" ? value.toLowerCase() : "cli_error"; return SAFE_NAME.test(raw) ? raw : "cli_error"; } export function beginCliInvocation( argv: readonly string[], env: NodeJS.ProcessEnv = process.env, ) { const file = env.DENCH_HARNESS_OBSERVATION_FILE?.trim(); if (!file || !SAFE_FILE.test(file)) return null; return { file, id: randomUUID(), tool: normalizedCliTool(argv), startedAt: Date.now(), }; } export function finishCliInvocation( started: ReturnType, result: { exitCode: number; errorType?: unknown; retryable?: unknown }, ) { if (!started) return; const endedAt = Date.now(); const observation: CliInvocationObservation = { version: 1, id: started.id, tool: started.tool, startedAt: started.startedAt, endedAt, durationMs: Math.max(0, endedAt - started.startedAt), exitCode: Number.isSafeInteger(result.exitCode) ? result.exitCode : 1, ...(result.exitCode === 0 ? {} : { errorType: safeErrorType(result.errorType) }), ...(typeof result.retryable === "boolean" ? { retryable: result.retryable } : {}), }; try { const line = `${JSON.stringify(observation)}\n`; let size = 0; try { size = statSync(started.file).size; } catch {} if (size + Buffer.byteLength(line) > SIDECAR_LIMIT) return; appendFileSync(started.file, line, { encoding: "utf8", mode: 0o600, }); } catch { // Harness telemetry must never change CLI behavior. } }