Files
orca/src/main/amp/agent-status-plugin-source.ts
T
Neil bc28107864 refactor(hooks,relay): split agent hook services and relay under the max-lines budget (#14725)
The four agent hook services, the main hooks module, and the two relay modules
each carried a file-level `eslint-disable max-lines` and ran 365-628 counted
lines against a 300-line budget. AGENTS.md calls for splitting rather than
suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this
removes all seven suppressions and prunes their entries (341 -> 334).

Pure move, no behavior change. Each hook service splits into its managed script
source, its config/bundle serialization, and its remote-install path, keeping the
per-agent integrations independent: copilot, amp, antigravity and hermes each
retain their own getManagedScript rather than sharing one, because each emits a
different script body for a different agent. Merging them by name would have
been a behavior change, not a refactor.

For antigravity the suppression's stated rationale -- that local install, Windows
wrapper generation, status cleanup, and SSH remote install must share one event
list and managed-command matcher so stale-hook cleanup cannot drift by platform
-- is now enforced structurally instead: both install paths call
buildInstalledConfig + createAntigravityManagedCommandMatcher over the single
ANTIGRAVITY_EVENTS catalog, with the graph a strict DAG.

Also registers the six new antigravity/ and copilot/ modules in
config/tsconfig.cli.json. That project uses a curated `include` list rather than
a glob, so an unlisted module fails `tsc -p config/tsconfig.tc.cli.json` with
TS6307 even though the entire unit suite passes.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, green when
re-run serially), no new runtime import cycles, and no lint suppression added.
2026-08-15 18:25:37 -07:00

202 lines
7.6 KiB
TypeScript

export const AMP_PLUGIN_FILE = 'orca-agent-status.ts'
export const AMP_PLUGIN_MARKER = 'Managed by Orca. Do not edit; changes may be overwritten.'
export function getAmpPluginSource(): string {
return [
"import { readFileSync, statSync } from 'fs'",
"import type { PluginAPI } from '@ampcode/plugin'",
'',
`// ${AMP_PLUGIN_MARKER}`,
'type HookCoords = { port?: string; token?: string; env?: string; version?: string }',
'',
'let warnedBadEndpoint = false',
"let cachedEndpointKey = ''",
'let cachedEndpointValues: HookCoords | null = null',
'',
'function readEndpointFile(): HookCoords | null {',
' const endpointPath = process.env.ORCA_AGENT_HOOK_ENDPOINT',
' if (!endpointPath) return null',
' try {',
' const stat = statSync(endpointPath)',
' const cacheKey = `${stat.mtimeMs}:${stat.size}:${stat.ino}`',
' if (cacheKey === cachedEndpointKey && cachedEndpointValues) {',
' return cachedEndpointValues',
' }',
" const contents = readFileSync(endpointPath, 'utf8')",
' const out: HookCoords = {}',
' for (const line of contents.split(/\\r?\\n/)) {',
' const match = line.match(/^(?:set\\s+)?([A-Z0-9_]+)=(.*)$/)',
' if (!match) continue',
' const value = match[2].replace(/\\r$/, "")',
" if (match[1] === 'ORCA_AGENT_HOOK_PORT') out.port = value",
" if (match[1] === 'ORCA_AGENT_HOOK_TOKEN') out.token = value",
" if (match[1] === 'ORCA_AGENT_HOOK_ENV') out.env = value",
" if (match[1] === 'ORCA_AGENT_HOOK_VERSION') out.version = value",
' }',
' cachedEndpointKey = cacheKey',
' cachedEndpointValues = out',
' return out',
' } catch (error) {',
" cachedEndpointKey = ''",
' cachedEndpointValues = null',
' if ((error as { code?: unknown })?.code !== "ENOENT" && !warnedBadEndpoint) {',
' warnedBadEndpoint = true',
" console.warn('[orca-hook] failed to parse Amp endpoint file:', (error as Error).message)",
' }',
' return null',
' }',
'}',
'',
'function resolveHookCoords(): HookCoords {',
' // Why: Amp sessions can outlive an Orca restart; the endpoint file is',
' // rewritten on each start, so read it per event before falling back to env.',
' const fileEnv = readEndpointFile() ?? {}',
' return {',
' port: fileEnv.port || process.env.ORCA_AGENT_HOOK_PORT,',
' token: fileEnv.token || process.env.ORCA_AGENT_HOOK_TOKEN,',
' env: fileEnv.env || process.env.ORCA_AGENT_HOOK_ENV || "",',
' version: fileEnv.version || process.env.ORCA_AGENT_HOOK_VERSION || ""',
' }',
'}',
'',
'function previewValue(value: unknown, maxLength = 4000): string | undefined {',
' if (typeof value === "string") return value.slice(0, maxLength)',
' if (value === null || value === undefined) return undefined',
' try {',
' return JSON.stringify(value).slice(0, maxLength)',
' } catch {',
' return String(value).slice(0, maxLength)',
' }',
'}',
'',
'function jsonSafe(value: unknown, depth = 0): unknown {',
' if (value === null || value === undefined) return value',
' if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {',
' return value',
' }',
' if (typeof value === "bigint" || typeof value === "symbol" || typeof value === "function") {',
' return String(value)',
' }',
' if (depth >= 4) return previewValue(value)',
' if (Array.isArray(value)) return value.slice(0, 20).map((item) => jsonSafe(item, depth + 1))',
' if (typeof value === "object") {',
' const out: Record<string, unknown> = {}',
' for (const [key, child] of Object.entries(value).slice(0, 20)) {',
' out[key] = jsonSafe(child, depth + 1)',
' }',
' return out',
' }',
' return String(value)',
'}',
'',
'async function post(hookEventName: string, payload: Record<string, unknown>): Promise<void> {',
' const coords = resolveHookCoords()',
' const paneKey = process.env.ORCA_PANE_KEY',
' if (!coords.port || !coords.token || !paneKey) return',
' const controller = new AbortController()',
' const timeout = setTimeout(() => controller.abort(), 1000)',
' try {',
' await fetch(`http://127.0.0.1:${coords.port}/hook/amp`, {',
' method: "POST",',
' signal: controller.signal,',
' headers: {',
' "Content-Type": "application/json",',
' "X-Orca-Agent-Hook-Token": coords.token',
' },',
' body: JSON.stringify({',
' paneKey,',
' launchToken: process.env.ORCA_AGENT_LAUNCH_TOKEN || "",',
' tabId: process.env.ORCA_TAB_ID || "",',
' worktreeId: process.env.ORCA_WORKTREE_ID || "",',
' env: coords.env,',
' version: coords.version,',
' hook_event_name: hookEventName,',
' payload: { hook_event_name: hookEventName, ...payload }',
' })',
' })',
' } catch {',
' // Why: Orca status reporting must never affect the Amp run.',
' } finally {',
' clearTimeout(timeout)',
' }',
'}',
'',
'const MAX_PENDING_POSTS = 50',
'type QueuedPost = { hookEventName: string; payload: Record<string, unknown> }',
'let postQueue: QueuedPost[] = []',
'let postDraining = false',
'',
'async function drainPostQueue(): Promise<void> {',
' if (postDraining) return',
' postDraining = true',
' try {',
' while (postQueue.length > 0) {',
' const next = postQueue.shift()',
' if (!next) continue',
' await post(next.hookEventName, next.payload)',
' }',
' } finally {',
' postDraining = false',
' if (postQueue.length > 0) {',
' void drainPostQueue()',
' }',
' }',
'}',
'function enqueuePost(hookEventName: string, payload: Record<string, unknown>): void {',
' // Why: keep hook callbacks non-blocking without retaining unbounded',
' // payload closures when Orca is down and each POST waits for timeout.',
' if (postQueue.length >= MAX_PENDING_POSTS) {',
' postQueue.shift()',
' }',
' postQueue.push({ hookEventName, payload })',
' void drainPostQueue()',
'}',
'',
'export default function (amp: PluginAPI) {',
" amp.on('session.start', (event) => {",
' enqueuePost("session.start", { threadId: event.thread.id })',
' })',
'',
" amp.on('agent.start', (event) => {",
' enqueuePost("agent.start", {',
' threadId: event.thread.id,',
' id: event.id,',
' message: event.message',
' })',
' })',
'',
" amp.on('tool.call', (event) => {",
' enqueuePost("tool.call", {',
' threadId: event.thread.id,',
' toolUseId: event.toolUseID,',
' tool: event.tool,',
' input: jsonSafe(event.input)',
' })',
' return { action: "allow" }',
' })',
'',
" amp.on('tool.result', (event) => {",
' enqueuePost("tool.result", {',
' threadId: event.thread.id,',
' toolUseId: event.toolUseID,',
' tool: event.tool,',
' input: jsonSafe(event.input),',
' status: event.status,',
' error: event.error,',
' output: previewValue(event.output)',
' })',
' })',
'',
" amp.on('agent.end', (event) => {",
' enqueuePost("agent.end", {',
' threadId: event.thread.id,',
' id: event.id,',
' message: event.message,',
' status: event.status',
' })',
' })',
'}',
''
].join('\n')
}