import { AGENT_METHODS, agent, ndJsonStream } from '@agentclientprotocol/sdk' import { PiAcpAgent } from './acp/agent/index.js' import { acpClientConnection } from './acp/client-connection.js' import { exitOnCrash } from './exit-on-crash.js' import { getPiCommand, resolveShellSpawn } from './pi/command.js' // Terminal Auth entrypoint. The ACP client launches the agent with `--terminal-login`. if (process.argv.includes('--terminal-login')) { const { spawnSync } = await import('node:child_process') const cmd = getPiCommand(process.env.PI_ACP_PI_COMMAND) const target = resolveShellSpawn(cmd, []) const res = spawnSync(target.command, target.args, { stdio: 'inherit', env: process.env, shell: target.shell }) if ((res.error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') { process.stderr.write( `pi-acp-jetbrain: could not start pi (command not found: ${cmd}). Install it via \`npm install -g @earendil-works/pi-coding-agent\` or ensure \`pi\` is on your PATH.\n` ) process.exit(1) } process.exit(typeof res.status === 'number' ? res.status : 1) } const input = new WritableStream({ write(chunk) { return new Promise(resolve => { if (process.stdout.destroyed || !process.stdout.writable) return resolve() try { process.stdout.write(chunk, err => { void err resolve() }) } catch { // Common: ERR_STREAM_DESTROYED ("Cannot call write after a stream was destroyed"). resolve() } }) } }) const output = new ReadableStream({ start(controller) { process.stdin.on('data', (chunk: Buffer) => controller.enqueue(new Uint8Array(chunk))) process.stdin.on('end', () => controller.close()) process.stdin.on('error', err => controller.error(err)) } }) const stream = ndJsonStream(input, output) // Retain the agent so shutdown can dispose owned pi subprocesses. The agent // app API does not expose per-request handler instances either. let activeAgent: PiAcpAgent | null = null function impl(): PiAcpAgent { if (!activeAgent) throw new Error('PiAcpAgent not initialized yet') return activeAgent } const app = agent() // Connection-level context for calling the client at any time (streaming // session updates, tool-call updates, permission prompts), mirroring what the // deprecated AgentSideConnection used to hand to the handler factory. app.onConnect(connection => { activeAgent = new PiAcpAgent(acpClientConnection(connection.client)) }) // Request/notification handlers, mirroring the SDK's legacy Agent mapping. app.onRequest(AGENT_METHODS.initialize, ctx => impl().initialize(ctx.params)) app.onRequest(AGENT_METHODS.session_new, ctx => impl().newSession(ctx.params)) app.onRequest(AGENT_METHODS.authenticate, async ctx => (await impl().authenticate(ctx.params)) ?? {}) app.onRequest(AGENT_METHODS.session_prompt, ctx => impl().prompt(ctx.params)) app.onNotification(AGENT_METHODS.session_cancel, ctx => impl().cancel(ctx.params)) app.onRequest(AGENT_METHODS.session_load, ctx => impl().loadSession(ctx.params)) app.onRequest(AGENT_METHODS.session_list, ctx => impl().listSessions(ctx.params)) app.onRequest(AGENT_METHODS.session_delete, async ctx => (await impl().deleteSession(ctx.params)) ?? {}) app.onRequest(AGENT_METHODS.session_fork, ctx => impl().unstable_forkSession(ctx.params)) app.onRequest(AGENT_METHODS.session_resume, ctx => impl().resumeSession(ctx.params)) app.onRequest(AGENT_METHODS.session_close, async ctx => (await impl().closeSession(ctx.params)) ?? {}) app.onRequest(AGENT_METHODS.session_set_mode, async ctx => (await impl().setSessionMode(ctx.params)) ?? {}) app.onRequest(AGENT_METHODS.session_set_config_option, ctx => impl().setSessionConfigOption(ctx.params)) app.onRequest(AGENT_METHODS.providers_list, ctx => impl().unstable_listProviders(ctx.params)) // Client extension method (IntelliJ MCP bridge). The legacy adapter forwarded // every non-standard method name to the handler; the IDE only ever sends // 'mcp/message', so register it explicitly with a pass-through parser. const jsonParams = { parse: (params: unknown) => params as Record } app.onRequest('mcp/message', jsonParams, ctx => impl().handleExtMethod('mcp/message', ctx.params)) app.onNotification('mcp/message', jsonParams, ctx => impl().handleExtNotification('mcp/message', ctx.params)) app.connect(stream) let shuttingDown = false async function shutdown(): Promise { if (shuttingDown) return shuttingDown = true try { // Dispose session subprocesses (and the IDE bridge) before exiting so no // pi or MCP child is orphaned when the client disconnects. const a = activeAgent activeAgent = null if (a) await a.dispose() } catch { // ignore; the exit below is authoritative } try { process.exit(0) } catch { // ignore } } process.stdin.on('end', () => void shutdown()) process.stdin.on('close', () => void shutdown()) process.stdin.resume() process.on('SIGINT', () => void shutdown()) process.on('SIGTERM', () => void shutdown()) // Avoid crashing if the client closes stdout early. process.stdout.on('error', () => { try { process.exit(0) } catch { // ignore } }) // Last-resort crash handlers: log, dispose owned children best-effort, then exit // nonzero so the client can surface the failure (a dead-but-zero adapter would // look like an idle session). process.on('uncaughtException', error => { const a = activeAgent activeAgent = null exitOnCrash('uncaught exception', error, a ? () => a.dispose() : null) }) process.on('unhandledRejection', reason => { const a = activeAgent activeAgent = null exitOnCrash('unhandled rejection', reason, a ? () => a.dispose() : null) })