From d235e3c873eaa3620fdea09ff1f86db3781c704e Mon Sep 17 00:00:00 2001 From: William Valentin Date: Fri, 20 Mar 2026 11:17:26 -0700 Subject: [PATCH] feat(hooks): add telemetry handlers for codex/copilot/gemini --- .github/hooks/scripts/session-end.sh | 6 + .github/hooks/scripts/session-start.sh | 6 + .gitignore | 3 + hooks/agentmon/handler.js | 295 ++++++++++++++++ hooks/claude-code/hooks.json | 114 ++++++ hooks/claude-code/package-lock.json | 463 +++++++++++++++++++++++++ hooks/claude-code/package.json | 18 + hooks/codex/handler.js | 271 +++++++++++++++ hooks/codex/handler.ts | 324 +++++++++++++++++ hooks/codex/hooks.json | 22 ++ hooks/codex/package-lock.json | 463 +++++++++++++++++++++++++ hooks/codex/package.json | 18 + hooks/copilot/handler.js | 327 +++++++++++++++++ hooks/copilot/handler.ts | 384 ++++++++++++++++++++ hooks/copilot/hooks.json | 34 ++ hooks/copilot/package-lock.json | 463 +++++++++++++++++++++++++ hooks/copilot/package.json | 18 + hooks/gemini/handler.js | 296 ++++++++++++++++ hooks/gemini/handler.ts | 350 +++++++++++++++++++ hooks/gemini/hooks.json | 28 ++ hooks/gemini/package-lock.json | 463 +++++++++++++++++++++++++ hooks/gemini/package.json | 18 + hooks/opencode/plugin.ts | 122 +++++++ 23 files changed, 4506 insertions(+) create mode 100644 .github/hooks/scripts/session-end.sh create mode 100644 .github/hooks/scripts/session-start.sh create mode 100644 hooks/agentmon/handler.js create mode 100644 hooks/claude-code/hooks.json create mode 100644 hooks/claude-code/package-lock.json create mode 100644 hooks/claude-code/package.json create mode 100755 hooks/codex/handler.js create mode 100644 hooks/codex/handler.ts create mode 100644 hooks/codex/hooks.json create mode 100644 hooks/codex/package-lock.json create mode 100644 hooks/codex/package.json create mode 100755 hooks/copilot/handler.js create mode 100644 hooks/copilot/handler.ts create mode 100644 hooks/copilot/hooks.json create mode 100644 hooks/copilot/package-lock.json create mode 100644 hooks/copilot/package.json create mode 100755 hooks/gemini/handler.js create mode 100644 hooks/gemini/handler.ts create mode 100644 hooks/gemini/hooks.json create mode 100644 hooks/gemini/package-lock.json create mode 100644 hooks/gemini/package.json create mode 100644 hooks/opencode/plugin.ts diff --git a/.github/hooks/scripts/session-end.sh b/.github/hooks/scripts/session-end.sh new file mode 100644 index 0000000..772f530 --- /dev/null +++ b/.github/hooks/scripts/session-end.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +INPUT="$(cat)" + +~/.local/bin/agentmon-copilot-handler stop <<< "$INPUT" diff --git a/.github/hooks/scripts/session-start.sh b/.github/hooks/scripts/session-start.sh new file mode 100644 index 0000000..2ca6b86 --- /dev/null +++ b/.github/hooks/scripts/session-start.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +INPUT="$(cat)" + +~/.local/bin/agentmon-copilot-handler start <<< "$INPUT" diff --git a/.gitignore b/.gitignore index 48d471b..222c7e9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ /ingest-gateway /query-api /web-ui + +/swarm-monitor +/hooks/*/node_modules/ diff --git a/hooks/agentmon/handler.js b/hooks/agentmon/handler.js new file mode 100644 index 0000000..d64eea4 --- /dev/null +++ b/hooks/agentmon/handler.js @@ -0,0 +1,295 @@ +import { randomUUID } from "node:crypto"; +import { hostname } from "node:os"; +const INGEST_URL = process.env.AGENTMON_INGEST_URL || "http://192.168.122.1:8080"; +const VM_NAME = process.env.AGENTMON_VM_NAME || hostname(); +const BATCH_SIZE = 10; +const FLUSH_MS = 2e3; +const FETCH_TIMEOUT_MS = 500; +let buffer = []; +let flushTimer = null; +let isFlushing = false; +const activeRuns = /* @__PURE__ */ new Map(); +function isRecord(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} +function pickString(...values) { + for (const value of values) { + if (typeof value === "string" && value.trim() !== "") { + return value; + } + } + return void 0; +} +function pickNumber(...values) { + for (const value of values) { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + } + return void 0; +} +function truncate(value, limit) { + if (value === void 0 || value === null) { + return void 0; + } + const text = typeof value === "string" ? value : safeJSONStringify(value); + if (!text) { + return void 0; + } + if (text.length <= limit) { + return text; + } + return text.slice(0, limit) + "..."; +} +function safeJSONStringify(value) { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} +function getEventName(input) { + const direct = pickString(input.name, input.event); + if (direct) { + return direct; + } + if (typeof input.type === "string" && input.type.includes(":")) { + return input.type; + } + if (typeof input.type === "string" && typeof input.action === "string") { + return `${input.type}:${input.action}`; + } + if (typeof input.type === "string") { + return input.type; + } + return ""; +} +function getContext(input) { + return isRecord(input.context) ? input.context : {}; +} +function getSessionKey(input, context) { + return pickString( + input.sessionKey, + context.sessionKey, + context.session_id, + input.session_id, + isRecord(input.session) ? input.session.key : void 0, + isRecord(context.session) ? context.session.key : void 0 + ); +} +function buildEnvelope(type, sessionKey, opts = {}) { + const correlation = {}; + if (sessionKey) { + correlation.session_id = sessionKey; + } + if (opts.runId) { + correlation.run_id = opts.runId; + } + if (opts.spanId) { + correlation.span_id = opts.spanId; + } + if (opts.parentSpanId) { + correlation.parent_span_id = opts.parentSpanId; + } + const envelope = { + schema: { name: "agentmon.event", version: 1 }, + event: { + id: randomUUID(), + type, + ts: (/* @__PURE__ */ new Date()).toISOString(), + source: { + framework: "openclaw", + client_id: VM_NAME, + host: VM_NAME + } + } + }; + if (Object.keys(correlation).length > 0) { + envelope.correlation = correlation; + } + if (opts.attributes && Object.keys(opts.attributes).length > 0) { + envelope.attributes = opts.attributes; + } + if (opts.payload && Object.keys(opts.payload).length > 0) { + envelope.payload = opts.payload; + } + return envelope; +} +function scheduleFlush() { + if (!flushTimer) { + flushTimer = setTimeout(() => { + void flush(); + }, FLUSH_MS); + } +} +function enqueue(event) { + buffer.push(event); + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } +} +async function postBatch(batch) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + await fetch(`${INGEST_URL}/v1/events`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(batch), + signal: controller.signal + }); + } finally { + clearTimeout(timeout); + } +} +async function flush() { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } + if (isFlushing || buffer.length === 0) { + return; + } + isFlushing = true; + const batch = buffer.splice(0, BATCH_SIZE); + try { + await postBatch(batch); + } catch { + console.debug(`[agentmon] failed to flush ${batch.length} events`); + } finally { + isFlushing = false; + if (buffer.length > 0) { + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } + } + } +} +function emitError(sessionKey, runId, spanId, errorValue) { + if (errorValue === void 0 || errorValue === null || errorValue === false) { + return; + } + const errorRecord = isRecord(errorValue) ? errorValue : {}; + const message = pickString(errorRecord.message, errorRecord.error, errorValue) || "unknown"; + const errType = pickString(errorRecord.type, errorRecord.code) || "openclaw"; + enqueue(buildEnvelope("error", sessionKey, { + runId, + spanId, + payload: { + error: { + type: errType, + message + } + } + })); +} +function buildRunPayload(context, success) { + const payload = { + status: success ? "success" : "error" + }; + const duration = pickNumber(context.duration_ms, context.durationMs, context.elapsed_ms); + if (duration !== void 0) { + payload.duration_ms = duration; + } + const usage = isRecord(context.usage) ? context.usage : void 0; + if (usage) { + payload.usage = usage; + } + const errorMessage = pickString(context.error, isRecord(context.result) ? context.result.error : void 0); + if (errorMessage) { + payload.error = errorMessage; + } + return payload; +} +const handler = async (rawEvent) => { + if (!isRecord(rawEvent)) { + return; + } + const context = getContext(rawEvent); + const eventName = getEventName(rawEvent); + const sessionKey = getSessionKey(rawEvent, context); + try { + if (eventName === "command:new") { + enqueue(buildEnvelope("session.start", sessionKey)); + return; + } + if (eventName === "command:stop") { + enqueue(buildEnvelope("session.end", sessionKey)); + if (sessionKey) { + activeRuns.delete(sessionKey); + } + return; + } + if (eventName === "command:reset") { + enqueue(buildEnvelope("session.end", sessionKey)); + enqueue(buildEnvelope("session.start", sessionKey)); + if (sessionKey) { + activeRuns.delete(sessionKey); + } + return; + } + if (eventName === "agent:bootstrap") { + const existingRunId = sessionKey ? activeRuns.get(sessionKey) : void 0; + if (!existingRunId) { + const runId = randomUUID(); + if (sessionKey) { + activeRuns.set(sessionKey, runId); + } + enqueue(buildEnvelope("run.start", sessionKey, { + runId, + attributes: { + agent_id: pickString(context.agentId), + run_kind: "embedded" + } + })); + } + return; + } + if (eventName === "message:received") { + const runId = randomUUID(); + if (sessionKey) { + activeRuns.set(sessionKey, runId); + } + enqueue(buildEnvelope("run.start", sessionKey, { + runId, + attributes: { + channel: pickString(context.channelId, context.channel_id), + from: pickString(context.from, context.sender) + }, + payload: { + message_preview: truncate( + pickString(context.content, context.message, context.text) || context.input, + 200 + ) + } + })); + return; + } + if (eventName === "message:sent") { + const runId = sessionKey ? activeRuns.get(sessionKey) : void 0; + const success = context.success !== false && !context.error; + enqueue(buildEnvelope("run.end", sessionKey, { + runId, + attributes: { + channel: pickString(context.channelId, context.channel_id), + to: pickString(context.to, context.recipient) + }, + payload: buildRunPayload(context, success) + })); + if (!success) { + emitError(sessionKey, runId, void 0, context.error); + } + return; + } + } catch { + console.debug("[agentmon] handler error"); + } +}; +var handler_default = handler; +export { + handler_default as default +}; diff --git a/hooks/claude-code/hooks.json b/hooks/claude-code/hooks.json new file mode 100644 index 0000000..9c3dbec --- /dev/null +++ b/hooks/claude-code/hooks.json @@ -0,0 +1,114 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "npx --yes agentmon-handler start" + } + ] + } + ], + "Stop": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "npx --yes agentmon-handler stop" + } + ] + } + ], + "SubagentStop": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "npx --yes agentmon-handler subagent-stop" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "npx --yes agentmon-handler tool-start" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "npx --yes agentmon-handler tool-end" + } + ] + } + ], + "UserPromptSubmit": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "npx --yes agentmon-handler prompt" + } + ] + } + ], + "PreCompact": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "npx --yes agentmon-handler compact-start" + } + ] + } + ], + "PostCompact": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "npx --yes agentmon-handler compact-end" + } + ] + } + ], + "SubagentStart": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "npx --yes agentmon-handler subagent-start" + } + ] + } + ], + "Notification": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "npx --yes agentmon-handler notification" + } + ] + } + ] + } +} diff --git a/hooks/claude-code/package-lock.json b/hooks/claude-code/package-lock.json new file mode 100644 index 0000000..4d6e141 --- /dev/null +++ b/hooks/claude-code/package-lock.json @@ -0,0 +1,463 @@ +{ + "name": "@anthropic-ai/agentmon-claude-code", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@anthropic-ai/agentmon-claude-code", + "version": "1.0.0", + "bin": { + "agentmon-handler": "handler.js" + }, + "devDependencies": { + "esbuild": "^0.20.0", + "typescript": "^5.3.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/hooks/claude-code/package.json b/hooks/claude-code/package.json new file mode 100644 index 0000000..9cda803 --- /dev/null +++ b/hooks/claude-code/package.json @@ -0,0 +1,18 @@ +{ + "name": "@anthropic-ai/agentmon-claude-code", + "version": "1.0.0", + "description": "agentmon hook handler for Claude Code", + "main": "handler.js", + "type": "module", + "bin": { + "agentmon-handler": "./handler.js" + }, + "scripts": { + "build": "npx esbuild handler.ts --platform=node --format=esm --outfile=handler.js" + }, + "dependencies": {}, + "devDependencies": { + "esbuild": "^0.20.0", + "typescript": "^5.3.0" + } +} diff --git a/hooks/codex/handler.js b/hooks/codex/handler.js new file mode 100755 index 0000000..567ce7c --- /dev/null +++ b/hooks/codex/handler.js @@ -0,0 +1,271 @@ +#!/usr/bin/env node +import { randomUUID } from "node:crypto"; +import { hostname } from "node:os"; +const INGEST_URL = process.env.AGENTMON_INGEST_URL || "http://localhost:8080"; +const FRAMEWORK = process.env.AGENTMON_FRAMEWORK || "codex"; +const HOST = process.env.AGENTMON_HOST || hostname(); +const BATCH_SIZE = 10; +const FLUSH_MS = 2e3; +const FETCH_TIMEOUT_MS = 500; +let buffer = []; +let flushTimer = null; +let isFlushing = false; +const activeRuns = /* @__PURE__ */ new Map(); +function isRecord(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} +function pickString(...values) { + for (const value of values) { + if (typeof value === "string" && value.trim() !== "") { + return value; + } + } + return void 0; +} +function pickNumber(...values) { + for (const value of values) { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + } + return void 0; +} +function truncate(value, limit) { + if (value === void 0 || value === null) { + return void 0; + } + const text = typeof value === "string" ? value : safeJSONStringify(value); + if (!text) { + return void 0; + } + if (text.length <= limit) { + return text; + } + return text.slice(0, limit) + "..."; +} +function safeJSONStringify(value) { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} +function getSessionKey(input) { + return pickString( + input.sessionId, + input.session_id, + input.conversationId, + input.conversation_id + ); +} +function getUsage(input) { + const usage = isRecord(input.usage) ? input.usage : isRecord(input.llm) ? input.llm : isRecord(input.tokens) ? input.tokens : void 0; + if (!usage) + return void 0; + const result = {}; + if (usage.input !== void 0) + result.input_tokens = usage.input; + if (usage.output !== void 0) + result.output_tokens = usage.output; + if (usage.total !== void 0) + result.total_tokens = usage.total; + if (usage.cost !== void 0) + result.total_cost = usage.cost; + return Object.keys(result).length > 0 ? result : void 0; +} +function buildEnvelope(type, sessionKey, opts = {}) { + const correlation = {}; + if (sessionKey) { + correlation.session_id = sessionKey; + } + if (opts.runId) { + correlation.run_id = opts.runId; + } + if (opts.spanId) { + correlation.span_id = opts.spanId; + } + if (opts.parentSpanId) { + correlation.parent_span_id = opts.parentSpanId; + } + const envelope = { + schema: { name: "agentmon.event", version: 1 }, + event: { + id: randomUUID(), + type, + ts: (/* @__PURE__ */ new Date()).toISOString(), + source: { + framework: FRAMEWORK, + client_id: HOST, + host: HOST + } + } + }; + if (Object.keys(correlation).length > 0) { + envelope.correlation = correlation; + } + if (opts.attributes && Object.keys(opts.attributes).length > 0) { + envelope.attributes = opts.attributes; + } + if (opts.payload && Object.keys(opts.payload).length > 0) { + envelope.payload = opts.payload; + } + return envelope; +} +function scheduleFlush() { + if (!flushTimer) { + flushTimer = setTimeout(() => { + void flush(); + }, FLUSH_MS); + } +} +function enqueue(event) { + buffer.push(event); + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } +} +async function postBatch(batch) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + await fetch(`${INGEST_URL}/v1/events`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(batch), + signal: controller.signal + }); + } finally { + clearTimeout(timeout); + } +} +async function flush() { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } + if (isFlushing || buffer.length === 0) { + return; + } + isFlushing = true; + const batch = buffer.splice(0, BATCH_SIZE); + try { + await postBatch(batch); + } catch { + console.debug(`[agentmon] failed to flush ${batch.length} events`); + } finally { + isFlushing = false; + if (buffer.length > 0) { + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } + } + } +} +async function handleSessionStart(input) { + const sessionKey = getSessionKey(input) || randomUUID(); + const runId = randomUUID(); + activeRuns.set(sessionKey, runId); + enqueue(buildEnvelope("session.start", sessionKey)); + enqueue(buildEnvelope("run.start", sessionKey, { + runId, + attributes: { + trigger: pickString(input.trigger_type, input.trigger) + }, + payload: { + prompt_preview: truncate(input.prompt, 200) + } + })); + await flush(); +} +async function handleSessionEnd(input) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : void 0; + const usage = getUsage(input); + const duration = pickNumber(input.duration_ms, input.elapsed_ms, input.duration); + if (runId) { + enqueue(buildEnvelope("run.end", sessionKey, { + runId, + payload: { + status: "success", + duration_ms: duration, + ...usage && { usage } + } + })); + } + enqueue(buildEnvelope("session.end", sessionKey, { + payload: usage ? { usage } : void 0 + })); + activeRuns.delete(sessionKey || ""); + await flush(); +} +async function handleNotification(input) { + const sessionKey = getSessionKey(input); + const notificationType = pickString(input.type, input.notification_type); + const usage = getUsage(input); + const duration = pickNumber(input.duration_ms, input.elapsed_ms); + if (notificationType === "agent-turn-complete" || notificationType === "Done") { + const runId = sessionKey ? activeRuns.get(sessionKey) : void 0; + if (runId) { + enqueue(buildEnvelope("run.end", sessionKey, { + runId, + payload: { + status: "success", + duration_ms: duration, + ...usage && { usage } + } + })); + } + const newRunId = randomUUID(); + if (sessionKey) { + activeRuns.set(sessionKey, newRunId); + } + enqueue(buildEnvelope("run.start", sessionKey, { + runId: newRunId + })); + } + await flush(); +} +const handler = async () => { + const args = process.argv.slice(2); + const hookType = args[0] || "unknown"; + let input = {}; + try { + const stdin = await readStdin(); + if (stdin) { + input = JSON.parse(stdin); + } + } catch { + input = {}; + } + try { + switch (hookType) { + case "start": + await handleSessionStart(input); + break; + case "stop": + await handleSessionEnd(input); + break; + case "notification": + await handleNotification(input); + break; + default: + console.debug(`[agentmon] unknown hook type: ${hookType}`); + } + } catch (err) { + console.debug("[agentmon] handler error:", err); + } +}; +async function readStdin() { + return new Promise((resolve) => { + let data = ""; + process.stdin.on("data", (chunk) => data += chunk); + process.stdin.on("end", () => resolve(data)); + process.stdin.on("error", () => resolve("")); + setTimeout(() => resolve(data), 100); + }); +} +handler(); diff --git a/hooks/codex/handler.ts b/hooks/codex/handler.ts new file mode 100644 index 0000000..5787963 --- /dev/null +++ b/hooks/codex/handler.ts @@ -0,0 +1,324 @@ +#!/usr/bin/env node +import { randomUUID } from 'node:crypto'; +import { hostname } from 'node:os'; + +const INGEST_URL = process.env.AGENTMON_INGEST_URL || 'http://localhost:8080'; +const FRAMEWORK = process.env.AGENTMON_FRAMEWORK || 'codex'; +const HOST = process.env.AGENTMON_HOST || hostname(); +const BATCH_SIZE = 10; +const FLUSH_MS = 2000; +const FETCH_TIMEOUT_MS = 500; + +interface Dict { [key: string]: any } + +let buffer: Dict[] = []; +let flushTimer: ReturnType | null = null; +let isFlushing = false; + +const activeRuns = new Map(); + +function isRecord(value: unknown): value is Dict { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function pickString(...values: unknown[]): string | undefined { + for (const value of values) { + if (typeof value === 'string' && value.trim() !== '') { + return value; + } + } + return undefined; +} + +function pickNumber(...values: unknown[]): number | undefined { + for (const value of values) { + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + } + return undefined; +} + +function truncate(value: unknown, limit: number): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + + const text = typeof value === 'string' ? value : safeJSONStringify(value); + if (!text) { + return undefined; + } + + if (text.length <= limit) { + return text; + } + return text.slice(0, limit) + '...'; +} + +function safeJSONStringify(value: unknown): string { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function getSessionKey(input: Dict): string | undefined { + return pickString( + input.sessionId, + input.session_id, + input.conversationId, + input.conversation_id, + ); +} + +function getUsage(input: Dict): Dict | undefined { + const usage = isRecord(input.usage) ? input.usage : + isRecord(input.llm) ? input.llm : + isRecord(input.tokens) ? input.tokens : undefined; + if (!usage) return undefined; + + const result: Dict = {}; + if (usage.input !== undefined) result.input_tokens = usage.input; + if (usage.output !== undefined) result.output_tokens = usage.output; + if (usage.total !== undefined) result.total_tokens = usage.total; + if (usage.cost !== undefined) result.total_cost = usage.cost; + + return Object.keys(result).length > 0 ? result : undefined; +} + +function buildEnvelope( + type: string, + sessionKey?: string, + opts: { + runId?: string; + spanId?: string; + parentSpanId?: string; + attributes?: Dict; + payload?: Dict; + } = {}, +): Dict { + const correlation: Dict = {}; + if (sessionKey) { + correlation.session_id = sessionKey; + } + if (opts.runId) { + correlation.run_id = opts.runId; + } + if (opts.spanId) { + correlation.span_id = opts.spanId; + } + if (opts.parentSpanId) { + correlation.parent_span_id = opts.parentSpanId; + } + + const envelope: Dict = { + schema: { name: 'agentmon.event', version: 1 }, + event: { + id: randomUUID(), + type, + ts: new Date().toISOString(), + source: { + framework: FRAMEWORK, + client_id: HOST, + host: HOST, + }, + }, + }; + + if (Object.keys(correlation).length > 0) { + envelope.correlation = correlation; + } + if (opts.attributes && Object.keys(opts.attributes).length > 0) { + envelope.attributes = opts.attributes; + } + if (opts.payload && Object.keys(opts.payload).length > 0) { + envelope.payload = opts.payload; + } + + return envelope; +} + +function scheduleFlush() { + if (!flushTimer) { + flushTimer = setTimeout(() => { + void flush(); + }, FLUSH_MS); + } +} + +function enqueue(event: Dict) { + buffer.push(event); + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } +} + +async function postBatch(batch: Dict[]) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + + try { + await fetch(`${INGEST_URL}/v1/events`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(batch), + signal: controller.signal, + }); + } finally { + clearTimeout(timeout); + } +} + +async function flush() { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } + if (isFlushing || buffer.length === 0) { + return; + } + + isFlushing = true; + const batch = buffer.splice(0, BATCH_SIZE); + + try { + await postBatch(batch); + } catch { + console.debug(`[agentmon] failed to flush ${batch.length} events`); + } finally { + isFlushing = false; + if (buffer.length > 0) { + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } + } + } +} + +async function handleSessionStart(input: Dict) { + const sessionKey = getSessionKey(input) || randomUUID(); + const runId = randomUUID(); + activeRuns.set(sessionKey, runId); + + enqueue(buildEnvelope('session.start', sessionKey)); + + enqueue(buildEnvelope('run.start', sessionKey, { + runId, + attributes: { + trigger: pickString(input.trigger_type, input.trigger), + }, + payload: { + prompt_preview: truncate(input.prompt, 200), + }, + })); + + await flush(); +} + +async function handleSessionEnd(input: Dict) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : undefined; + const usage = getUsage(input); + const duration = pickNumber(input.duration_ms, input.elapsed_ms, input.duration); + + if (runId) { + enqueue(buildEnvelope('run.end', sessionKey, { + runId, + payload: { + status: 'success', + duration_ms: duration, + ...(usage && { usage }), + }, + })); + } + + enqueue(buildEnvelope('session.end', sessionKey, { + payload: usage ? { usage } : undefined, + })); + + activeRuns.delete(sessionKey || ''); + await flush(); +} + +async function handleNotification(input: Dict) { + const sessionKey = getSessionKey(input); + const notificationType = pickString(input.type, input.notification_type); + const usage = getUsage(input); + const duration = pickNumber(input.duration_ms, input.elapsed_ms); + + if (notificationType === 'agent-turn-complete' || notificationType === 'Done') { + const runId = sessionKey ? activeRuns.get(sessionKey) : undefined; + + if (runId) { + enqueue(buildEnvelope('run.end', sessionKey, { + runId, + payload: { + status: 'success', + duration_ms: duration, + ...(usage && { usage }), + }, + })); + } + + const newRunId = randomUUID(); + if (sessionKey) { + activeRuns.set(sessionKey, newRunId); + } + + enqueue(buildEnvelope('run.start', sessionKey, { + runId: newRunId, + })); + } + + await flush(); +} + +const handler = async () => { + const args = process.argv.slice(2); + const hookType = args[0] || 'unknown'; + + let input: Dict = {}; + try { + const stdin = await readStdin(); + if (stdin) { + input = JSON.parse(stdin); + } + } catch { + input = {}; + } + + try { + switch (hookType) { + case 'start': + await handleSessionStart(input); + break; + case 'stop': + await handleSessionEnd(input); + break; + case 'notification': + await handleNotification(input); + break; + default: + console.debug(`[agentmon] unknown hook type: ${hookType}`); + } + } catch (err) { + console.debug('[agentmon] handler error:', err); + } +}; + +async function readStdin(): Promise { + return new Promise((resolve) => { + let data = ''; + process.stdin.on('data', (chunk) => data += chunk); + process.stdin.on('end', () => resolve(data)); + process.stdin.on('error', () => resolve('')); + setTimeout(() => resolve(data), 100); + }); +} + +handler(); diff --git a/hooks/codex/hooks.json b/hooks/codex/hooks.json new file mode 100644 index 0000000..5b1dea6 --- /dev/null +++ b/hooks/codex/hooks.json @@ -0,0 +1,22 @@ +{ + "hooks": { + "sessionStart": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-codex-handler start" + } + ], + "sessionEnd": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-codex-handler stop" + } + ], + "notify": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-codex-handler notification" + } + ] + } +} diff --git a/hooks/codex/package-lock.json b/hooks/codex/package-lock.json new file mode 100644 index 0000000..411440f --- /dev/null +++ b/hooks/codex/package-lock.json @@ -0,0 +1,463 @@ +{ + "name": "@anthropic-ai/agentmon-codex", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@anthropic-ai/agentmon-codex", + "version": "1.0.0", + "bin": { + "agentmon-handler": "handler.js" + }, + "devDependencies": { + "esbuild": "^0.20.0", + "typescript": "^5.3.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/hooks/codex/package.json b/hooks/codex/package.json new file mode 100644 index 0000000..5fe6a99 --- /dev/null +++ b/hooks/codex/package.json @@ -0,0 +1,18 @@ +{ + "name": "@anthropic-ai/agentmon-codex", + "version": "1.0.0", + "description": "agentmon hook handler for Codex CLI", + "main": "handler.js", + "type": "module", + "bin": { + "agentmon-handler": "./handler.js" + }, + "scripts": { + "build": "npx esbuild handler.ts --platform=node --format=esm --outfile=handler.js" + }, + "dependencies": {}, + "devDependencies": { + "esbuild": "^0.20.0", + "typescript": "^5.3.0" + } +} diff --git a/hooks/copilot/handler.js b/hooks/copilot/handler.js new file mode 100755 index 0000000..43a0fee --- /dev/null +++ b/hooks/copilot/handler.js @@ -0,0 +1,327 @@ +#!/usr/bin/env node +import { randomUUID } from "node:crypto"; +import { hostname } from "node:os"; +const INGEST_URL = process.env.AGENTMON_INGEST_URL || "http://localhost:8080"; +const FRAMEWORK = process.env.AGENTMON_FRAMEWORK || "copilot"; +const HOST = process.env.AGENTMON_HOST || hostname(); +const BATCH_SIZE = 10; +const FLUSH_MS = 2e3; +const FETCH_TIMEOUT_MS = 500; +let buffer = []; +let flushTimer = null; +let isFlushing = false; +const activeRuns = /* @__PURE__ */ new Map(); +const activeSpans = /* @__PURE__ */ new Map(); +function isRecord(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} +function pickString(...values) { + for (const value of values) { + if (typeof value === "string" && value.trim() !== "") { + return value; + } + } + return void 0; +} +function pickNumber(...values) { + for (const value of values) { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + } + return void 0; +} +function truncate(value, limit) { + if (value === void 0 || value === null) { + return void 0; + } + const text = typeof value === "string" ? value : safeJSONStringify(value); + if (!text) { + return void 0; + } + if (text.length <= limit) { + return text; + } + return text.slice(0, limit) + "..."; +} +function safeJSONStringify(value) { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} +function getSessionKey(input) { + return pickString( + input.sessionId, + input.session_id, + input.conversationId, + input.conversation_id + ); +} +function getUsage(input) { + const usage = isRecord(input.usage) ? input.usage : isRecord(input.tokens) ? input.tokens : isRecord(input.llm_usage) ? input.llm_usage : void 0; + if (!usage) + return void 0; + const result = {}; + if (usage.input_tokens !== void 0) + result.input_tokens = usage.input_tokens; + if (usage.prompt_tokens !== void 0) + result.input_tokens = usage.prompt_tokens; + if (usage.output_tokens !== void 0) + result.output_tokens = usage.output_tokens; + if (usage.completion_tokens !== void 0) + result.output_tokens = usage.completion_tokens; + if (usage.total_tokens !== void 0) + result.total_tokens = usage.total_tokens; + if (usage.cost !== void 0) + result.total_cost = usage.cost; + return Object.keys(result).length > 0 ? result : void 0; +} +function buildEnvelope(type, sessionKey, opts = {}) { + const correlation = {}; + if (sessionKey) { + correlation.session_id = sessionKey; + } + if (opts.runId) { + correlation.run_id = opts.runId; + } + if (opts.spanId) { + correlation.span_id = opts.spanId; + } + if (opts.parentSpanId) { + correlation.parent_span_id = opts.parentSpanId; + } + const envelope = { + schema: { name: "agentmon.event", version: 1 }, + event: { + id: randomUUID(), + type, + ts: (/* @__PURE__ */ new Date()).toISOString(), + source: { + framework: FRAMEWORK, + client_id: HOST, + host: HOST + } + } + }; + if (Object.keys(correlation).length > 0) { + envelope.correlation = correlation; + } + if (opts.attributes && Object.keys(opts.attributes).length > 0) { + envelope.attributes = opts.attributes; + } + if (opts.payload && Object.keys(opts.payload).length > 0) { + envelope.payload = opts.payload; + } + return envelope; +} +function scheduleFlush() { + if (!flushTimer) { + flushTimer = setTimeout(() => { + void flush(); + }, FLUSH_MS); + } +} +function enqueue(event) { + buffer.push(event); + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } +} +async function postBatch(batch) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + await fetch(`${INGEST_URL}/v1/events`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(batch), + signal: controller.signal + }); + } finally { + clearTimeout(timeout); + } +} +async function flush() { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } + if (isFlushing || buffer.length === 0) { + return; + } + isFlushing = true; + const batch = buffer.splice(0, BATCH_SIZE); + try { + await postBatch(batch); + } catch { + console.debug(`[agentmon] failed to flush ${batch.length} events`); + } finally { + isFlushing = false; + if (buffer.length > 0) { + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } + } + } +} +async function handleSessionStart(input) { + const sessionKey = getSessionKey(input) || randomUUID(); + const runId = randomUUID(); + activeRuns.set(sessionKey, runId); + enqueue(buildEnvelope("session.start", sessionKey)); + enqueue(buildEnvelope("run.start", sessionKey, { + runId, + payload: { + prompt_preview: truncate(input.prompt, 200) + } + })); + await flush(); +} +async function handleSessionEnd(input) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : void 0; + const usage = getUsage(input); + const duration = pickNumber(input.duration_ms, input.elapsed_ms); + if (runId) { + enqueue(buildEnvelope("run.end", sessionKey, { + runId, + payload: { + status: "success", + duration_ms: duration, + ...usage && { usage } + } + })); + } + enqueue(buildEnvelope("session.end", sessionKey, { + payload: usage ? { usage } : void 0 + })); + activeRuns.delete(sessionKey || ""); + activeSpans.clear(); + await flush(); +} +async function handlePromptSubmit(input) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : void 0; + const prompt = pickString(input.prompt, input.text, input.message); + if (runId && prompt) { + enqueue(buildEnvelope("run.end", sessionKey, { + runId, + payload: { + status: "success", + duration_ms: pickNumber(input.elapsed_ms, input.duration_ms) + } + })); + } + const newRunId = randomUUID(); + if (sessionKey) { + activeRuns.set(sessionKey, newRunId); + } + enqueue(buildEnvelope("run.start", sessionKey, { + runId: newRunId, + payload: { + prompt_preview: truncate(prompt, 200) + } + })); + await flush(); +} +async function handleToolStart(input) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : void 0; + const toolName = pickString(input.tool, input.tool_name, input.name) || "unknown"; + const toolInput = isRecord(input.input) ? input.input : {}; + const spanId = randomUUID(); + if (sessionKey) { + activeSpans.set(sessionKey + ":" + toolName, spanId); + } + enqueue(buildEnvelope("span.start", sessionKey, { + runId, + spanId, + attributes: { + span_kind: "tool", + name: toolName + }, + payload: { + input: truncate(toolInput, 200) + } + })); + await flush(); +} +async function handleToolEnd(input) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : void 0; + const toolName = pickString(input.tool, input.tool_name, input.name) || "unknown"; + const spanKey = sessionKey ? sessionKey + ":" + toolName : void 0; + const spanId = spanKey ? activeSpans.get(spanKey) : void 0; + const result = isRecord(input.result) ? input.result : isRecord(input.output) ? input.output : {}; + const success = !result.error && input.success !== false; + const duration = pickNumber(input.duration_ms, input.elapsed_ms); + const usage = getUsage(input); + enqueue(buildEnvelope("span.end", sessionKey, { + runId, + spanId, + attributes: { + span_kind: "tool", + name: toolName + }, + payload: { + status: success ? "success" : "error", + result_preview: truncate(result.output ?? result.error ?? result, 500), + duration_ms: duration, + ...usage && { usage } + } + })); + activeSpans.delete(spanKey || ""); + await flush(); +} +const handler = async () => { + const args = process.argv.slice(2); + const hookType = args[0] || "unknown"; + let input = {}; + try { + const stdin = await readStdin(); + if (stdin) { + input = JSON.parse(stdin); + } + } catch { + input = {}; + } + try { + switch (hookType) { + case "start": + await handleSessionStart(input); + break; + case "stop": + await handleSessionEnd(input); + break; + case "prompt": + await handlePromptSubmit(input); + break; + case "tool-start": + await handleToolStart(input); + break; + case "tool-end": + await handleToolEnd(input); + break; + default: + console.debug(`[agentmon] unknown hook type: ${hookType}`); + } + } catch (err) { + console.debug("[agentmon] handler error:", err); + } +}; +async function readStdin() { + return new Promise((resolve) => { + let data = ""; + process.stdin.on("data", (chunk) => data += chunk); + process.stdin.on("end", () => resolve(data)); + process.stdin.on("error", () => resolve("")); + setTimeout(() => resolve(data), 100); + }); +} +handler(); diff --git a/hooks/copilot/handler.ts b/hooks/copilot/handler.ts new file mode 100644 index 0000000..75de55a --- /dev/null +++ b/hooks/copilot/handler.ts @@ -0,0 +1,384 @@ +#!/usr/bin/env node +import { randomUUID } from 'node:crypto'; +import { hostname } from 'node:os'; + +const INGEST_URL = process.env.AGENTMON_INGEST_URL || 'http://localhost:8080'; +const FRAMEWORK = process.env.AGENTMON_FRAMEWORK || 'copilot'; +const HOST = process.env.AGENTMON_HOST || hostname(); +const BATCH_SIZE = 10; +const FLUSH_MS = 2000; +const FETCH_TIMEOUT_MS = 500; + +interface Dict { [key: string]: any } + +let buffer: Dict[] = []; +let flushTimer: ReturnType | null = null; +let isFlushing = false; + +const activeRuns = new Map(); +const activeSpans = new Map(); + +function isRecord(value: unknown): value is Dict { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function pickString(...values: unknown[]): string | undefined { + for (const value of values) { + if (typeof value === 'string' && value.trim() !== '') { + return value; + } + } + return undefined; +} + +function pickNumber(...values: unknown[]): number | undefined { + for (const value of values) { + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + } + return undefined; +} + +function truncate(value: unknown, limit: number): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + + const text = typeof value === 'string' ? value : safeJSONStringify(value); + if (!text) { + return undefined; + } + + if (text.length <= limit) { + return text; + } + return text.slice(0, limit) + '...'; +} + +function safeJSONStringify(value: unknown): string { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function getSessionKey(input: Dict): string | undefined { + return pickString( + input.sessionId, + input.session_id, + input.conversationId, + input.conversation_id, + ); +} + +function getUsage(input: Dict): Dict | undefined { + const usage = isRecord(input.usage) ? input.usage : + isRecord(input.tokens) ? input.tokens : + isRecord(input.llm_usage) ? input.llm_usage : undefined; + if (!usage) return undefined; + + const result: Dict = {}; + if (usage.input_tokens !== undefined) result.input_tokens = usage.input_tokens; + if (usage.prompt_tokens !== undefined) result.input_tokens = usage.prompt_tokens; + if (usage.output_tokens !== undefined) result.output_tokens = usage.output_tokens; + if (usage.completion_tokens !== undefined) result.output_tokens = usage.completion_tokens; + if (usage.total_tokens !== undefined) result.total_tokens = usage.total_tokens; + if (usage.cost !== undefined) result.total_cost = usage.cost; + + return Object.keys(result).length > 0 ? result : undefined; +} + +function buildEnvelope( + type: string, + sessionKey?: string, + opts: { + runId?: string; + spanId?: string; + parentSpanId?: string; + attributes?: Dict; + payload?: Dict; + } = {}, +): Dict { + const correlation: Dict = {}; + if (sessionKey) { + correlation.session_id = sessionKey; + } + if (opts.runId) { + correlation.run_id = opts.runId; + } + if (opts.spanId) { + correlation.span_id = opts.spanId; + } + if (opts.parentSpanId) { + correlation.parent_span_id = opts.parentSpanId; + } + + const envelope: Dict = { + schema: { name: 'agentmon.event', version: 1 }, + event: { + id: randomUUID(), + type, + ts: new Date().toISOString(), + source: { + framework: FRAMEWORK, + client_id: HOST, + host: HOST, + }, + }, + }; + + if (Object.keys(correlation).length > 0) { + envelope.correlation = correlation; + } + if (opts.attributes && Object.keys(opts.attributes).length > 0) { + envelope.attributes = opts.attributes; + } + if (opts.payload && Object.keys(opts.payload).length > 0) { + envelope.payload = opts.payload; + } + + return envelope; +} + +function scheduleFlush() { + if (!flushTimer) { + flushTimer = setTimeout(() => { + void flush(); + }, FLUSH_MS); + } +} + +function enqueue(event: Dict) { + buffer.push(event); + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } +} + +async function postBatch(batch: Dict[]) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + + try { + await fetch(`${INGEST_URL}/v1/events`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(batch), + signal: controller.signal, + }); + } finally { + clearTimeout(timeout); + } +} + +async function flush() { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } + if (isFlushing || buffer.length === 0) { + return; + } + + isFlushing = true; + const batch = buffer.splice(0, BATCH_SIZE); + + try { + await postBatch(batch); + } catch { + console.debug(`[agentmon] failed to flush ${batch.length} events`); + } finally { + isFlushing = false; + if (buffer.length > 0) { + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } + } + } +} + +async function handleSessionStart(input: Dict) { + const sessionKey = getSessionKey(input) || randomUUID(); + const runId = randomUUID(); + activeRuns.set(sessionKey, runId); + + enqueue(buildEnvelope('session.start', sessionKey)); + + enqueue(buildEnvelope('run.start', sessionKey, { + runId, + payload: { + prompt_preview: truncate(input.prompt, 200), + }, + })); + + await flush(); +} + +async function handleSessionEnd(input: Dict) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : undefined; + const usage = getUsage(input); + const duration = pickNumber(input.duration_ms, input.elapsed_ms); + + if (runId) { + enqueue(buildEnvelope('run.end', sessionKey, { + runId, + payload: { + status: 'success', + duration_ms: duration, + ...(usage && { usage }), + }, + })); + } + + enqueue(buildEnvelope('session.end', sessionKey, { + payload: usage ? { usage } : undefined, + })); + + activeRuns.delete(sessionKey || ''); + activeSpans.clear(); + await flush(); +} + +async function handlePromptSubmit(input: Dict) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : undefined; + const prompt = pickString(input.prompt, input.text, input.message); + + if (runId && prompt) { + enqueue(buildEnvelope('run.end', sessionKey, { + runId, + payload: { + status: 'success', + duration_ms: pickNumber(input.elapsed_ms, input.duration_ms), + }, + })); + } + + const newRunId = randomUUID(); + if (sessionKey) { + activeRuns.set(sessionKey, newRunId); + } + + enqueue(buildEnvelope('run.start', sessionKey, { + runId: newRunId, + payload: { + prompt_preview: truncate(prompt, 200), + }, + })); + + await flush(); +} + +async function handleToolStart(input: Dict) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : undefined; + const toolName = pickString(input.tool, input.tool_name, input.name) || 'unknown'; + const toolInput = isRecord(input.input) ? input.input : {}; + const spanId = randomUUID(); + + if (sessionKey) { + activeSpans.set(sessionKey + ':' + toolName, spanId); + } + + enqueue(buildEnvelope('span.start', sessionKey, { + runId, + spanId, + attributes: { + span_kind: 'tool', + name: toolName, + }, + payload: { + input: truncate(toolInput, 200), + }, + })); + + await flush(); +} + +async function handleToolEnd(input: Dict) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : undefined; + const toolName = pickString(input.tool, input.tool_name, input.name) || 'unknown'; + const spanKey = sessionKey ? sessionKey + ':' + toolName : undefined; + const spanId = spanKey ? activeSpans.get(spanKey) : undefined; + const result = isRecord(input.result) ? input.result : isRecord(input.output) ? input.output : {}; + const success = !result.error && input.success !== false; + const duration = pickNumber(input.duration_ms, input.elapsed_ms); + const usage = getUsage(input); + + enqueue(buildEnvelope('span.end', sessionKey, { + runId, + spanId, + attributes: { + span_kind: 'tool', + name: toolName, + }, + payload: { + status: success ? 'success' : 'error', + result_preview: truncate(result.output ?? result.error ?? result, 500), + duration_ms: duration, + ...(usage && { usage }), + }, + })); + + activeSpans.delete(spanKey || ''); + await flush(); +} + +const handler = async () => { + const args = process.argv.slice(2); + const hookType = args[0] || 'unknown'; + + let input: Dict = {}; + try { + const stdin = await readStdin(); + if (stdin) { + input = JSON.parse(stdin); + } + } catch { + input = {}; + } + + try { + switch (hookType) { + case 'start': + await handleSessionStart(input); + break; + case 'stop': + await handleSessionEnd(input); + break; + case 'prompt': + await handlePromptSubmit(input); + break; + case 'tool-start': + await handleToolStart(input); + break; + case 'tool-end': + await handleToolEnd(input); + break; + default: + console.debug(`[agentmon] unknown hook type: ${hookType}`); + } + } catch (err) { + console.debug('[agentmon] handler error:', err); + } +}; + +async function readStdin(): Promise { + return new Promise((resolve) => { + let data = ''; + process.stdin.on('data', (chunk) => data += chunk); + process.stdin.on('end', () => resolve(data)); + process.stdin.on('error', () => resolve('')); + setTimeout(() => resolve(data), 100); + }); +} + +handler(); diff --git a/hooks/copilot/hooks.json b/hooks/copilot/hooks.json new file mode 100644 index 0000000..3ccf118 --- /dev/null +++ b/hooks/copilot/hooks.json @@ -0,0 +1,34 @@ +{ + "hooks": { + "sessionStart": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-copilot-handler start" + } + ], + "sessionEnd": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-copilot-handler stop" + } + ], + "userPromptSubmitted": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-copilot-handler prompt" + } + ], + "preToolUse": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-copilot-handler tool-start" + } + ], + "postToolUse": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-copilot-handler tool-end" + } + ] + } +} diff --git a/hooks/copilot/package-lock.json b/hooks/copilot/package-lock.json new file mode 100644 index 0000000..c96aabb --- /dev/null +++ b/hooks/copilot/package-lock.json @@ -0,0 +1,463 @@ +{ + "name": "@anthropic-ai/agentmon-copilot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@anthropic-ai/agentmon-copilot", + "version": "1.0.0", + "bin": { + "agentmon-handler": "handler.js" + }, + "devDependencies": { + "esbuild": "^0.20.0", + "typescript": "^5.3.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/hooks/copilot/package.json b/hooks/copilot/package.json new file mode 100644 index 0000000..4e3ee7c --- /dev/null +++ b/hooks/copilot/package.json @@ -0,0 +1,18 @@ +{ + "name": "@anthropic-ai/agentmon-copilot", + "version": "1.0.0", + "description": "agentmon hook handler for GitHub Copilot CLI", + "main": "handler.js", + "type": "module", + "bin": { + "agentmon-handler": "./handler.js" + }, + "scripts": { + "build": "npx esbuild handler.ts --platform=node --format=esm --outfile=handler.js" + }, + "dependencies": {}, + "devDependencies": { + "esbuild": "^0.20.0", + "typescript": "^5.3.0" + } +} diff --git a/hooks/gemini/handler.js b/hooks/gemini/handler.js new file mode 100755 index 0000000..abe7fd8 --- /dev/null +++ b/hooks/gemini/handler.js @@ -0,0 +1,296 @@ +#!/usr/bin/env node +import { randomUUID } from "node:crypto"; +import { hostname } from "node:os"; +const INGEST_URL = process.env.AGENTMON_INGEST_URL || "http://localhost:8080"; +const FRAMEWORK = process.env.AGENTMON_FRAMEWORK || "gemini"; +const HOST = process.env.AGENTMON_HOST || hostname(); +const BATCH_SIZE = 10; +const FLUSH_MS = 2e3; +const FETCH_TIMEOUT_MS = 500; +let buffer = []; +let flushTimer = null; +let isFlushing = false; +const activeRuns = /* @__PURE__ */ new Map(); +const activeSpans = /* @__PURE__ */ new Map(); +function isRecord(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} +function pickString(...values) { + for (const value of values) { + if (typeof value === "string" && value.trim() !== "") { + return value; + } + } + return void 0; +} +function pickNumber(...values) { + for (const value of values) { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + } + return void 0; +} +function truncate(value, limit) { + if (value === void 0 || value === null) { + return void 0; + } + const text = typeof value === "string" ? value : safeJSONStringify(value); + if (!text) { + return void 0; + } + if (text.length <= limit) { + return text; + } + return text.slice(0, limit) + "..."; +} +function safeJSONStringify(value) { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} +function getSessionKey(input) { + return pickString( + input.sessionId, + input.session_id, + input.conversationId, + input.conversation_id, + input.sessionKey + ); +} +function getUsage(input) { + const usage = isRecord(input.usage) ? input.usage : isRecord(input.tokens) ? input.tokens : isRecord(input.llm_usage) ? input.llm_usage : void 0; + if (!usage) + return void 0; + const result = {}; + if (usage.prompt_tokens !== void 0) + result.input_tokens = usage.prompt_tokens; + if (usage.completion_tokens !== void 0) + result.output_tokens = usage.completion_tokens; + if (usage.total_tokens !== void 0) + result.total_tokens = usage.total_tokens; + if (usage.cost !== void 0) + result.total_cost = usage.cost; + return Object.keys(result).length > 0 ? result : void 0; +} +function buildEnvelope(type, sessionKey, opts = {}) { + const correlation = {}; + if (sessionKey) { + correlation.session_id = sessionKey; + } + if (opts.runId) { + correlation.run_id = opts.runId; + } + if (opts.spanId) { + correlation.span_id = opts.spanId; + } + if (opts.parentSpanId) { + correlation.parent_span_id = opts.parentSpanId; + } + const envelope = { + schema: { name: "agentmon.event", version: 1 }, + event: { + id: randomUUID(), + type, + ts: (/* @__PURE__ */ new Date()).toISOString(), + source: { + framework: FRAMEWORK, + client_id: HOST, + host: HOST + } + } + }; + if (Object.keys(correlation).length > 0) { + envelope.correlation = correlation; + } + if (opts.attributes && Object.keys(opts.attributes).length > 0) { + envelope.attributes = opts.attributes; + } + if (opts.payload && Object.keys(opts.payload).length > 0) { + envelope.payload = opts.payload; + } + return envelope; +} +function scheduleFlush() { + if (!flushTimer) { + flushTimer = setTimeout(() => { + void flush(); + }, FLUSH_MS); + } +} +function enqueue(event) { + buffer.push(event); + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } +} +async function postBatch(batch) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + await fetch(`${INGEST_URL}/v1/events`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(batch), + signal: controller.signal + }); + } finally { + clearTimeout(timeout); + } +} +async function flush() { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } + if (isFlushing || buffer.length === 0) { + return; + } + isFlushing = true; + const batch = buffer.splice(0, BATCH_SIZE); + try { + await postBatch(batch); + } catch { + console.debug(`[agentmon] failed to flush ${batch.length} events`); + } finally { + isFlushing = false; + if (buffer.length > 0) { + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } + } + } +} +async function handleSessionStart(input) { + const sessionKey = getSessionKey(input) || randomUUID(); + const runId = randomUUID(); + activeRuns.set(sessionKey, runId); + enqueue(buildEnvelope("session.start", sessionKey)); + enqueue(buildEnvelope("run.start", sessionKey, { + runId, + payload: { + prompt_preview: truncate(input.prompt, 200) + } + })); + await flush(); +} +async function handleSessionEnd(input) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : void 0; + const usage = getUsage(input); + const duration = pickNumber(input.duration_ms, input.elapsed_ms); + if (runId) { + enqueue(buildEnvelope("run.end", sessionKey, { + runId, + payload: { + status: "success", + duration_ms: duration, + ...usage && { usage } + } + })); + } + enqueue(buildEnvelope("session.end", sessionKey, { + payload: usage ? { usage } : void 0 + })); + activeRuns.delete(sessionKey || ""); + activeSpans.clear(); + await flush(); +} +async function handleToolCall(input) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : void 0; + const toolName = pickString(input.tool, input.tool_name, input.name) || "unknown"; + const toolInput = isRecord(input.input) ? input.input : {}; + const spanId = randomUUID(); + if (sessionKey) { + activeSpans.set(sessionKey + ":" + toolName, spanId); + } + enqueue(buildEnvelope("span.start", sessionKey, { + runId, + spanId, + attributes: { + span_kind: "tool", + name: toolName + }, + payload: { + input: truncate(toolInput, 200) + } + })); + await flush(); +} +async function handleToolResult(input) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : void 0; + const toolName = pickString(input.tool, input.tool_name, input.name) || "unknown"; + const spanKey = sessionKey ? sessionKey + ":" + toolName : void 0; + const spanId = spanKey ? activeSpans.get(spanKey) : void 0; + const result = isRecord(input.result) ? input.result : isRecord(input.output) ? input.output : {}; + const success = !result.error && input.success !== false; + const duration = pickNumber(input.duration_ms, input.elapsed_ms); + const usage = getUsage(input); + enqueue(buildEnvelope("span.end", sessionKey, { + runId, + spanId, + attributes: { + span_kind: "tool", + name: toolName + }, + payload: { + status: success ? "success" : "error", + result_preview: truncate(result.output ?? result.error ?? result, 500), + duration_ms: duration, + ...usage && { usage } + } + })); + activeSpans.delete(spanKey || ""); + await flush(); +} +const handler = async () => { + const args = process.argv.slice(2); + const hookType = args[0] || "unknown"; + let input = {}; + try { + const stdin = await readStdin(); + if (stdin) { + input = JSON.parse(stdin); + } + } catch { + input = {}; + } + try { + switch (hookType) { + case "start": + await handleSessionStart(input); + break; + case "stop": + await handleSessionEnd(input); + break; + case "tool-call": + await handleToolCall(input); + break; + case "tool-result": + await handleToolResult(input); + break; + default: + console.debug(`[agentmon] unknown hook type: ${hookType}`); + } + } catch (err) { + console.debug("[agentmon] handler error:", err); + } +}; +async function readStdin() { + return new Promise((resolve) => { + let data = ""; + process.stdin.on("data", (chunk) => data += chunk); + process.stdin.on("end", () => resolve(data)); + process.stdin.on("error", () => resolve("")); + setTimeout(() => resolve(data), 100); + }); +} +handler(); diff --git a/hooks/gemini/handler.ts b/hooks/gemini/handler.ts new file mode 100644 index 0000000..ad9e10d --- /dev/null +++ b/hooks/gemini/handler.ts @@ -0,0 +1,350 @@ +#!/usr/bin/env node +import { randomUUID } from 'node:crypto'; +import { hostname } from 'node:os'; + +const INGEST_URL = process.env.AGENTMON_INGEST_URL || 'http://localhost:8080'; +const FRAMEWORK = process.env.AGENTMON_FRAMEWORK || 'gemini'; +const HOST = process.env.AGENTMON_HOST || hostname(); +const BATCH_SIZE = 10; +const FLUSH_MS = 2000; +const FETCH_TIMEOUT_MS = 500; + +interface Dict { [key: string]: any } + +let buffer: Dict[] = []; +let flushTimer: ReturnType | null = null; +let isFlushing = false; + +const activeRuns = new Map(); +const activeSpans = new Map(); + +function isRecord(value: unknown): value is Dict { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function pickString(...values: unknown[]): string | undefined { + for (const value of values) { + if (typeof value === 'string' && value.trim() !== '') { + return value; + } + } + return undefined; +} + +function pickNumber(...values: unknown[]): number | undefined { + for (const value of values) { + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + } + return undefined; +} + +function truncate(value: unknown, limit: number): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + + const text = typeof value === 'string' ? value : safeJSONStringify(value); + if (!text) { + return undefined; + } + + if (text.length <= limit) { + return text; + } + return text.slice(0, limit) + '...'; +} + +function safeJSONStringify(value: unknown): string { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function getSessionKey(input: Dict): string | undefined { + return pickString( + input.sessionId, + input.session_id, + input.conversationId, + input.conversation_id, + input.sessionKey, + ); +} + +function getUsage(input: Dict): Dict | undefined { + const usage = isRecord(input.usage) ? input.usage : + isRecord(input.tokens) ? input.tokens : + isRecord(input.llm_usage) ? input.llm_usage : undefined; + if (!usage) return undefined; + + const result: Dict = {}; + if (usage.prompt_tokens !== undefined) result.input_tokens = usage.prompt_tokens; + if (usage.completion_tokens !== undefined) result.output_tokens = usage.completion_tokens; + if (usage.total_tokens !== undefined) result.total_tokens = usage.total_tokens; + if (usage.cost !== undefined) result.total_cost = usage.cost; + + return Object.keys(result).length > 0 ? result : undefined; +} + +function buildEnvelope( + type: string, + sessionKey?: string, + opts: { + runId?: string; + spanId?: string; + parentSpanId?: string; + attributes?: Dict; + payload?: Dict; + } = {}, +): Dict { + const correlation: Dict = {}; + if (sessionKey) { + correlation.session_id = sessionKey; + } + if (opts.runId) { + correlation.run_id = opts.runId; + } + if (opts.spanId) { + correlation.span_id = opts.spanId; + } + if (opts.parentSpanId) { + correlation.parent_span_id = opts.parentSpanId; + } + + const envelope: Dict = { + schema: { name: 'agentmon.event', version: 1 }, + event: { + id: randomUUID(), + type, + ts: new Date().toISOString(), + source: { + framework: FRAMEWORK, + client_id: HOST, + host: HOST, + }, + }, + }; + + if (Object.keys(correlation).length > 0) { + envelope.correlation = correlation; + } + if (opts.attributes && Object.keys(opts.attributes).length > 0) { + envelope.attributes = opts.attributes; + } + if (opts.payload && Object.keys(opts.payload).length > 0) { + envelope.payload = opts.payload; + } + + return envelope; +} + +function scheduleFlush() { + if (!flushTimer) { + flushTimer = setTimeout(() => { + void flush(); + }, FLUSH_MS); + } +} + +function enqueue(event: Dict) { + buffer.push(event); + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } +} + +async function postBatch(batch: Dict[]) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + + try { + await fetch(`${INGEST_URL}/v1/events`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(batch), + signal: controller.signal, + }); + } finally { + clearTimeout(timeout); + } +} + +async function flush() { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } + if (isFlushing || buffer.length === 0) { + return; + } + + isFlushing = true; + const batch = buffer.splice(0, BATCH_SIZE); + + try { + await postBatch(batch); + } catch { + console.debug(`[agentmon] failed to flush ${batch.length} events`); + } finally { + isFlushing = false; + if (buffer.length > 0) { + if (buffer.length >= BATCH_SIZE) { + void flush(); + } else { + scheduleFlush(); + } + } + } +} + +async function handleSessionStart(input: Dict) { + const sessionKey = getSessionKey(input) || randomUUID(); + const runId = randomUUID(); + activeRuns.set(sessionKey, runId); + + enqueue(buildEnvelope('session.start', sessionKey)); + + enqueue(buildEnvelope('run.start', sessionKey, { + runId, + payload: { + prompt_preview: truncate(input.prompt, 200), + }, + })); + + await flush(); +} + +async function handleSessionEnd(input: Dict) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : undefined; + const usage = getUsage(input); + const duration = pickNumber(input.duration_ms, input.elapsed_ms); + + if (runId) { + enqueue(buildEnvelope('run.end', sessionKey, { + runId, + payload: { + status: 'success', + duration_ms: duration, + ...(usage && { usage }), + }, + })); + } + + enqueue(buildEnvelope('session.end', sessionKey, { + payload: usage ? { usage } : undefined, + })); + + activeRuns.delete(sessionKey || ''); + activeSpans.clear(); + await flush(); +} + +async function handleToolCall(input: Dict) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : undefined; + const toolName = pickString(input.tool, input.tool_name, input.name) || 'unknown'; + const toolInput = isRecord(input.input) ? input.input : {}; + const spanId = randomUUID(); + + if (sessionKey) { + activeSpans.set(sessionKey + ':' + toolName, spanId); + } + + enqueue(buildEnvelope('span.start', sessionKey, { + runId, + spanId, + attributes: { + span_kind: 'tool', + name: toolName, + }, + payload: { + input: truncate(toolInput, 200), + }, + })); + + await flush(); +} + +async function handleToolResult(input: Dict) { + const sessionKey = getSessionKey(input); + const runId = sessionKey ? activeRuns.get(sessionKey) : undefined; + const toolName = pickString(input.tool, input.tool_name, input.name) || 'unknown'; + const spanKey = sessionKey ? sessionKey + ':' + toolName : undefined; + const spanId = spanKey ? activeSpans.get(spanKey) : undefined; + const result = isRecord(input.result) ? input.result : isRecord(input.output) ? input.output : {}; + const success = !result.error && input.success !== false; + const duration = pickNumber(input.duration_ms, input.elapsed_ms); + const usage = getUsage(input); + + enqueue(buildEnvelope('span.end', sessionKey, { + runId, + spanId, + attributes: { + span_kind: 'tool', + name: toolName, + }, + payload: { + status: success ? 'success' : 'error', + result_preview: truncate(result.output ?? result.error ?? result, 500), + duration_ms: duration, + ...(usage && { usage }), + }, + })); + + activeSpans.delete(spanKey || ''); + await flush(); +} + +const handler = async () => { + const args = process.argv.slice(2); + const hookType = args[0] || 'unknown'; + + let input: Dict = {}; + try { + const stdin = await readStdin(); + if (stdin) { + input = JSON.parse(stdin); + } + } catch { + input = {}; + } + + try { + switch (hookType) { + case 'start': + await handleSessionStart(input); + break; + case 'stop': + await handleSessionEnd(input); + break; + case 'tool-call': + await handleToolCall(input); + break; + case 'tool-result': + await handleToolResult(input); + break; + default: + console.debug(`[agentmon] unknown hook type: ${hookType}`); + } + } catch (err) { + console.debug('[agentmon] handler error:', err); + } +}; + +async function readStdin(): Promise { + return new Promise((resolve) => { + let data = ''; + process.stdin.on('data', (chunk) => data += chunk); + process.stdin.on('end', () => resolve(data)); + process.stdin.on('error', () => resolve('')); + setTimeout(() => resolve(data), 100); + }); +} + +handler(); diff --git a/hooks/gemini/hooks.json b/hooks/gemini/hooks.json new file mode 100644 index 0000000..c5cae66 --- /dev/null +++ b/hooks/gemini/hooks.json @@ -0,0 +1,28 @@ +{ + "hooks": { + "onStart": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-gemini-handler start" + } + ], + "onStop": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-gemini-handler stop" + } + ], + "onToolCall": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-gemini-handler tool-call" + } + ], + "onToolResult": [ + { + "type": "command", + "command": "~/.local/bin/agentmon-gemini-handler tool-result" + } + ] + } +} diff --git a/hooks/gemini/package-lock.json b/hooks/gemini/package-lock.json new file mode 100644 index 0000000..aa8df77 --- /dev/null +++ b/hooks/gemini/package-lock.json @@ -0,0 +1,463 @@ +{ + "name": "@anthropic-ai/agentmon-gemini", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@anthropic-ai/agentmon-gemini", + "version": "1.0.0", + "bin": { + "agentmon-handler": "handler.js" + }, + "devDependencies": { + "esbuild": "^0.20.0", + "typescript": "^5.3.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/hooks/gemini/package.json b/hooks/gemini/package.json new file mode 100644 index 0000000..9b5e168 --- /dev/null +++ b/hooks/gemini/package.json @@ -0,0 +1,18 @@ +{ + "name": "@anthropic-ai/agentmon-gemini", + "version": "1.0.0", + "description": "agentmon hook handler for Gemini CLI", + "main": "handler.js", + "type": "module", + "bin": { + "agentmon-handler": "./handler.js" + }, + "scripts": { + "build": "npx esbuild handler.ts --platform=node --format=esm --outfile=handler.js" + }, + "dependencies": {}, + "devDependencies": { + "esbuild": "^0.20.0", + "typescript": "^5.3.0" + } +} diff --git a/hooks/opencode/plugin.ts b/hooks/opencode/plugin.ts new file mode 100644 index 0000000..b7bb755 --- /dev/null +++ b/hooks/opencode/plugin.ts @@ -0,0 +1,122 @@ +import { Plugin } from "@opencode-ai/plugin"; + +const INGEST_URL = process.env.AGENTMON_INGEST_URL || "http://localhost:8080"; + +let buffer: any[] = []; +let flushTimer: NodeJS.Timeout | null = null; +const BATCH_SIZE = 10; +const FLUSH_MS = 2000; + +let currentSessionId: string | undefined; +let currentRunId: string | undefined; + +function enqueue(event: any) { + buffer.push(event); + if (buffer.length >= BATCH_SIZE) { + flush(); + } else if (!flushTimer) { + flushTimer = setTimeout(flush, FLUSH_MS); + } +} + +async function flush() { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } + if (buffer.length === 0) return; + + const batch = buffer.splice(0, BATCH_SIZE); + try { + await fetch(`${INGEST_URL}/v1/events`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(batch), + }); + } catch (e) { + console.debug("[agentmon] failed to flush events"); + } +} + +function buildEnvelope(type: string, data: any) { + return { + schema: { name: "agentmon.event", version: 1 }, + event: { + id: crypto.randomUUID(), + type, + ts: new Date().toISOString(), + source: { + framework: "opencode", + client_id: "opencode", + host: require("os").hostname(), + }, + }, + ...(data.correlation && { correlation: data.correlation }), + ...(data.payload && { payload: data.payload }), + ...(data.attributes && { attributes: data.attributes }), + }; +} + +export const agentmon: Plugin = async (input) => { + return { + "tool.execute.before": async (data: any) => { + if (!currentSessionId) { + currentSessionId = crypto.randomUUID(); + currentRunId = crypto.randomUUID(); + + enqueue(buildEnvelope("session.start", { + correlation: { session_id: currentSessionId }, + })); + + enqueue(buildEnvelope("run.start", { + correlation: { session_id: currentSessionId, run_id: currentRunId }, + })); + } + + const spanId = crypto.randomUUID(); + enqueue(buildEnvelope("span.start", { + correlation: { session_id: currentSessionId, run_id: currentRunId, span_id: spanId }, + attributes: { span_kind: "tool", name: data.tool }, + payload: { input: (JSON.stringify(data.input) ?? "").substring(0, 200) }, + })); + }, + + "tool.execute.after": async (data: any) => { + if (!currentSessionId || !currentRunId) return; + + enqueue(buildEnvelope("span.end", { + correlation: { session_id: currentSessionId, run_id: currentRunId }, + attributes: { span_kind: "tool", name: data.tool }, + payload: { + status: data.error ? "error" : "success", + result_preview: data.result ? JSON.stringify(data.result).substring(0, 500) : undefined, + duration_ms: data.duration, + }, + })); + }, + + "chat.message": async (data: any) => { + if (!currentSessionId) { + currentSessionId = data.sessionID || crypto.randomUUID(); + currentRunId = crypto.randomUUID(); + + enqueue(buildEnvelope("session.start", { + correlation: { session_id: currentSessionId }, + })); + + enqueue(buildEnvelope("run.start", { + correlation: { session_id: currentSessionId, run_id: currentRunId }, + })); + } + }, + + "experimental.session.compacting": async (data: any) => { + if (currentSessionId && currentRunId) { + enqueue(buildEnvelope("span.start", { + correlation: { session_id: currentSessionId, run_id: currentRunId }, + attributes: { span_kind: "internal", name: "context_compaction" }, + })); + } + }, + }; +};