mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 08:02:38 +00:00
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.
202 lines
7.6 KiB
TypeScript
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')
|
|
}
|