{"version":3,"file":"ipc-client-C6NryMMb.mjs","names":[],"sources":["../src/daemon/ipc-client.ts"],"sourcesContent":["/**\n * ipc-client.ts — IPC client for the PAI Daemon MCP shim\n *\n * PaiClient connects to the Unix Domain Socket served by daemon.ts\n * and forwards tool calls to the daemon. Uses a fresh socket connection per\n * call (connect → write JSON + newline → read response line → parse → destroy).\n * This keeps the client stateless and avoids connection management complexity.\n *\n * Adapted from the Coogle ipc-client pattern (which was adapted from Whazaa).\n */\n\nimport { connect, Socket } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport type {\n  NotificationConfig,\n  NotificationMode,\n  NotificationEvent,\n  SendResult,\n} from \"../notifications/types.js\";\nimport type { TopicCheckParams, TopicCheckResult } from \"../topics/detector.js\";\nimport type { AutoRouteResult } from \"../session/auto-route.js\";\nimport { paiSocketPath } from \"../runtime-paths.js\";\n\n// ---------------------------------------------------------------------------\n// Protocol types\n// ---------------------------------------------------------------------------\n\n/** Default socket path */\nexport const IPC_SOCKET_PATH = paiSocketPath();\n\n/** Timeout for IPC calls (60 seconds) */\nconst IPC_TIMEOUT_MS = 60_000;\n\ninterface IpcRequest {\n  id: string;\n  method: string;\n  params: Record<string, unknown>;\n}\n\ninterface IpcResponse {\n  id: string;\n  ok: boolean;\n  result?: unknown;\n  error?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Client\n// ---------------------------------------------------------------------------\n\n/**\n * Thin IPC proxy that forwards tool calls to pai-daemon over a Unix\n * Domain Socket. Each call opens a fresh connection, sends one NDJSON request,\n * reads the response, and closes. Stateless and simple.\n */\nexport class PaiClient {\n  private readonly socketPath: string;\n\n  constructor(socketPath?: string) {\n    this.socketPath = socketPath ?? IPC_SOCKET_PATH;\n  }\n\n  /**\n   * Call a PAI tool by name with the given params.\n   * Returns the tool result or throws on error.\n   *\n   * `timeoutMs` overrides the default 60s wait — for a caller on a hook's\n   * critical path (e.g. the threshold-triggered handover enqueue in\n   * `cli/commands/session/autosave.ts`) where the actual work happens later,\n   * asynchronously, in the daemon's worker loop, and only the cheap\n   * enqueue handshake itself should ever be waited on.\n   */\n  async call(method: string, params: Record<string, unknown>, timeoutMs?: number): Promise<unknown> {\n    return this.send(method, params, timeoutMs);\n  }\n\n  /**\n   * Check daemon status.\n   */\n  async status(): Promise<Record<string, unknown>> {\n    const result = await this.send(\"status\", {});\n    return result as Record<string, unknown>;\n  }\n\n  /**\n   * Trigger an immediate index run.\n   */\n  async triggerIndex(): Promise<void> {\n    await this.send(\"index_now\", {});\n  }\n\n  // -------------------------------------------------------------------------\n  // Notification methods\n  // -------------------------------------------------------------------------\n\n  /**\n   * Get the current notification config from the daemon.\n   */\n  async getNotificationConfig(): Promise<{\n    config: NotificationConfig;\n    activeChannels: string[];\n  }> {\n    const result = await this.send(\"notification_get_config\", {});\n    return result as { config: NotificationConfig; activeChannels: string[] };\n  }\n\n  /**\n   * Patch the notification config on the daemon (and persist to disk).\n   */\n  async setNotificationConfig(patch: {\n    mode?: NotificationMode;\n    channels?: Partial<NotificationConfig[\"channels\"]>;\n    routing?: Partial<NotificationConfig[\"routing\"]>;\n  }): Promise<{ config: NotificationConfig }> {\n    const result = await this.send(\"notification_set_config\", patch as Record<string, unknown>);\n    return result as { config: NotificationConfig };\n  }\n\n  /**\n   * Send a notification via the daemon (routes to configured channels).\n   */\n  async sendNotification(payload: {\n    event: NotificationEvent;\n    message: string;\n    title?: string;\n  }): Promise<SendResult> {\n    const result = await this.send(\"notification_send\", payload as Record<string, unknown>);\n    return result as SendResult;\n  }\n\n  // -------------------------------------------------------------------------\n  // Topic detection methods\n  // -------------------------------------------------------------------------\n\n  /**\n   * Check whether the provided context text has drifted to a different project\n   * than the session's current routing.\n   */\n  async topicCheck(params: TopicCheckParams): Promise<TopicCheckResult> {\n    const result = await this.send(\"topic_check\", params as unknown as Record<string, unknown>);\n    return result as TopicCheckResult;\n  }\n\n  // -------------------------------------------------------------------------\n  // Session routing methods\n  // -------------------------------------------------------------------------\n\n  /**\n   * Automatically detect which project a session belongs to.\n   * Tries path match, PAI.md marker walk, then topic detection (if context given).\n   */\n  async sessionAutoRoute(params: {\n    cwd?: string;\n    context?: string;\n  }): Promise<AutoRouteResult | null> {\n    // session_auto_route returns a ToolResult (content array). Extract the text\n    // and parse JSON from it.\n    const result = await this.send(\"session_auto_route\", params as Record<string, unknown>);\n    const toolResult = result as { content?: Array<{ text: string }>; isError?: boolean };\n    if (toolResult.isError) return null;\n    const text = toolResult.content?.[0]?.text ?? \"\";\n    // Text is either JSON (on match) or a human-readable \"no match\" message\n    try {\n      return JSON.parse(text) as AutoRouteResult;\n    } catch {\n      return null;\n    }\n  }\n\n  // -------------------------------------------------------------------------\n  // Internal transport\n  // -------------------------------------------------------------------------\n\n  /**\n   * Send a single IPC request and wait for the response.\n   * Opens a new socket connection per call — simple and reliable.\n   */\n  private send(\n    method: string,\n    params: Record<string, unknown>,\n    timeoutMs: number = IPC_TIMEOUT_MS\n  ): Promise<unknown> {\n    const socketPath = this.socketPath;\n\n    return new Promise((resolve, reject) => {\n      let socket: Socket | null = null;\n      let done = false;\n      let buffer = \"\";\n      let timer: ReturnType<typeof setTimeout> | null = null;\n\n      function finish(error: Error | null, value?: unknown): void {\n        if (done) return;\n        done = true;\n        if (timer !== null) {\n          clearTimeout(timer);\n          timer = null;\n        }\n        try {\n          socket?.destroy();\n        } catch {\n          // ignore\n        }\n        if (error) {\n          reject(error);\n        } else {\n          resolve(value);\n        }\n      }\n\n      socket = connect(socketPath, () => {\n        const request: IpcRequest = {\n          id: randomUUID(),\n          method,\n          params,\n        };\n        socket!.write(JSON.stringify(request) + \"\\n\");\n      });\n\n      socket.on(\"data\", (chunk: Buffer) => {\n        buffer += chunk.toString();\n        const nl = buffer.indexOf(\"\\n\");\n        if (nl === -1) return;\n\n        const line = buffer.slice(0, nl);\n        buffer = buffer.slice(nl + 1);\n\n        let response: IpcResponse;\n        try {\n          response = JSON.parse(line) as IpcResponse;\n        } catch {\n          finish(new Error(`IPC parse error: ${line}`));\n          return;\n        }\n\n        if (!response.ok) {\n          finish(new Error(response.error ?? \"IPC call failed\"));\n        } else {\n          finish(null, response.result);\n        }\n      });\n\n      socket.on(\"error\", (e: NodeJS.ErrnoException) => {\n        if (e.code === \"ENOENT\" || e.code === \"ECONNREFUSED\") {\n          finish(\n            new Error(\n              \"PAI daemon not running. Start it with: pai daemon serve\"\n            )\n          );\n        } else {\n          finish(e);\n        }\n      });\n\n      socket.on(\"end\", () => {\n        if (!done) {\n          finish(new Error(\"IPC connection closed before response\"));\n        }\n      });\n\n      timer = setTimeout(() => {\n        finish(new Error(`IPC call timed out after ${timeoutMs}ms`));\n      }, timeoutMs);\n    });\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA4BA,MAAa,kBAAkB,eAAe;;AAG9C,MAAM,iBAAiB;;;;;;AAwBvB,IAAa,YAAb,MAAuB;CACrB,AAAiB;CAEjB,YAAY,YAAqB;AAC/B,OAAK,aAAa,cAAc;;;;;;;;;;;;CAalC,MAAM,KAAK,QAAgB,QAAiC,WAAsC;AAChG,SAAO,KAAK,KAAK,QAAQ,QAAQ,UAAU;;;;;CAM7C,MAAM,SAA2C;AAE/C,SADe,MAAM,KAAK,KAAK,UAAU,EAAE,CAAC;;;;;CAO9C,MAAM,eAA8B;AAClC,QAAM,KAAK,KAAK,aAAa,EAAE,CAAC;;;;;CAUlC,MAAM,wBAGH;AAED,SADe,MAAM,KAAK,KAAK,2BAA2B,EAAE,CAAC;;;;;CAO/D,MAAM,sBAAsB,OAIgB;AAE1C,SADe,MAAM,KAAK,KAAK,2BAA2B,MAAiC;;;;;CAO7F,MAAM,iBAAiB,SAIC;AAEtB,SADe,MAAM,KAAK,KAAK,qBAAqB,QAAmC;;;;;;CAYzF,MAAM,WAAW,QAAqD;AAEpE,SADe,MAAM,KAAK,KAAK,eAAe,OAA6C;;;;;;CAY7F,MAAM,iBAAiB,QAGa;EAIlC,MAAM,aADS,MAAM,KAAK,KAAK,sBAAsB,OAAkC;AAEvF,MAAI,WAAW,QAAS,QAAO;EAC/B,MAAM,OAAO,WAAW,UAAU,IAAI,QAAQ;AAE9C,MAAI;AACF,UAAO,KAAK,MAAM,KAAK;UACjB;AACN,UAAO;;;;;;;CAYX,AAAQ,KACN,QACA,QACA,YAAoB,gBACF;EAClB,MAAM,aAAa,KAAK;AAExB,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,IAAI,SAAwB;GAC5B,IAAI,OAAO;GACX,IAAI,SAAS;GACb,IAAI,QAA8C;GAElD,SAAS,OAAO,OAAqB,OAAuB;AAC1D,QAAI,KAAM;AACV,WAAO;AACP,QAAI,UAAU,MAAM;AAClB,kBAAa,MAAM;AACnB,aAAQ;;AAEV,QAAI;AACF,aAAQ,SAAS;YACX;AAGR,QAAI,MACF,QAAO,MAAM;QAEb,SAAQ,MAAM;;AAIlB,YAAS,QAAQ,kBAAkB;IACjC,MAAM,UAAsB;KAC1B,IAAI,YAAY;KAChB;KACA;KACD;AACD,WAAQ,MAAM,KAAK,UAAU,QAAQ,GAAG,KAAK;KAC7C;AAEF,UAAO,GAAG,SAAS,UAAkB;AACnC,cAAU,MAAM,UAAU;IAC1B,MAAM,KAAK,OAAO,QAAQ,KAAK;AAC/B,QAAI,OAAO,GAAI;IAEf,MAAM,OAAO,OAAO,MAAM,GAAG,GAAG;AAChC,aAAS,OAAO,MAAM,KAAK,EAAE;IAE7B,IAAI;AACJ,QAAI;AACF,gBAAW,KAAK,MAAM,KAAK;YACrB;AACN,4BAAO,IAAI,MAAM,oBAAoB,OAAO,CAAC;AAC7C;;AAGF,QAAI,CAAC,SAAS,GACZ,QAAO,IAAI,MAAM,SAAS,SAAS,kBAAkB,CAAC;QAEtD,QAAO,MAAM,SAAS,OAAO;KAE/B;AAEF,UAAO,GAAG,UAAU,MAA6B;AAC/C,QAAI,EAAE,SAAS,YAAY,EAAE,SAAS,eACpC,wBACE,IAAI,MACF,0DACD,CACF;QAED,QAAO,EAAE;KAEX;AAEF,UAAO,GAAG,aAAa;AACrB,QAAI,CAAC,KACH,wBAAO,IAAI,MAAM,wCAAwC,CAAC;KAE5D;AAEF,WAAQ,iBAAiB;AACvB,2BAAO,IAAI,MAAM,4BAA4B,UAAU,IAAI,CAAC;MAC3D,UAAU;IACb"}