zidane 8.2.4 → 8.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/chat.js +3 -3
  2. package/dist/daemon-cli.js +2 -2
  3. package/dist/{dispatcher-Dclv6HXn.js → dispatcher-D_9_QFO3.js} +7 -7
  4. package/dist/{dispatcher-Dclv6HXn.js.map → dispatcher-D_9_QFO3.js.map} +1 -1
  5. package/dist/{engine-C_U6kDfA.js → engine-BsWBWgTB.js} +2 -2
  6. package/dist/{engine-C_U6kDfA.js.map → engine-BsWBWgTB.js.map} +1 -1
  7. package/dist/{executor-BFaR2nfc.js → executor-BSesPPcU.js} +2 -2
  8. package/dist/{executor-BFaR2nfc.js.map → executor-BSesPPcU.js.map} +1 -1
  9. package/dist/{extensions-DIbpUj5R.js → extensions-BJof-2mi.js} +2 -2
  10. package/dist/{extensions-DIbpUj5R.js.map → extensions-BJof-2mi.js.map} +1 -1
  11. package/dist/extensions.js +1 -1
  12. package/dist/{host-signals-CQ2jZR-2.js → host-signals-CUKPZZxC.js} +5 -5
  13. package/dist/{host-signals-CQ2jZR-2.js.map → host-signals-CUKPZZxC.js.map} +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/ipc/remote.js +3 -3
  16. package/dist/ipc.js +6 -6
  17. package/dist/{links-D5itfH8P.js → links-Bto-5dVu.js} +6 -6
  18. package/dist/{links-D5itfH8P.js.map → links-Bto-5dVu.js.map} +1 -1
  19. package/dist/{oauth-Ck3wQX4Q.js → oauth-BwPiX0hL.js} +2 -2
  20. package/dist/{oauth-Ck3wQX4Q.js.map → oauth-BwPiX0hL.js.map} +1 -1
  21. package/dist/{remote-hosts-BkOwRszu.js → remote-hosts-DcCh7fqp.js} +4 -4
  22. package/dist/{remote-hosts-BkOwRszu.js.map → remote-hosts-DcCh7fqp.js.map} +1 -1
  23. package/dist/{settings-keys-QoGJ5n3e.js → settings-keys-CnrJgT1T.js} +2 -2
  24. package/dist/{settings-keys-QoGJ5n3e.js.map → settings-keys-CnrJgT1T.js.map} +1 -1
  25. package/dist/{shell-path-BXXtRmEA.js → shell-path-rJDa0OMv.js} +3 -3
  26. package/dist/{shell-path-BXXtRmEA.js.map → shell-path-rJDa0OMv.js.map} +1 -1
  27. package/dist/{transcript-anchors-D8jWLpe5.js → transcript-anchors-BcFLh3ZA.js} +3 -3
  28. package/dist/{transcript-anchors-D8jWLpe5.js.map → transcript-anchors-BcFLh3ZA.js.map} +1 -1
  29. package/dist/tui.js +8 -8
  30. package/dist/{version-DDRBX7K6.js → version-DOvIuU8M.js} +2 -2
  31. package/dist/{version-DDRBX7K6.js.map → version-DOvIuU8M.js.map} +1 -1
  32. package/dist/{workspace-exec-stream-iHv9cooF.js → workspace-exec-stream-Bzxczzu2.js} +4 -4
  33. package/dist/{workspace-exec-stream-iHv9cooF.js.map → workspace-exec-stream-Bzxczzu2.js.map} +1 -1
  34. package/dist/{ws-B2HB87nz.js → ws-CrD-4Mx6.js} +3 -3
  35. package/dist/{ws-B2HB87nz.js.map → ws-CrD-4Mx6.js.map} +1 -1
  36. package/dist/{ws-client-DPC9ZJHr.js → ws-client-Dond8vT3.js} +2 -2
  37. package/dist/{ws-client-DPC9ZJHr.js.map → ws-client-Dond8vT3.js.map} +1 -1
  38. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { t as createSyncEngine } from "./engine-C_U6kDfA.js";
2
- import { r as startWsSavedHosts } from "./ws-B2HB87nz.js";
3
- import { i as wsTransport, t as hostSignalHandlers } from "./host-signals-CQ2jZR-2.js";
1
+ import { t as createSyncEngine } from "./engine-BsWBWgTB.js";
2
+ import { r as startWsSavedHosts } from "./ws-CrD-4Mx6.js";
3
+ import { i as wsTransport, t as hostSignalHandlers } from "./host-signals-CUKPZZxC.js";
4
4
  //#region src/tui/host/remote-hosts.ts
5
5
  /** The TUI's directory, as the shared saved-hosts sync sees it. */
6
6
  function savedHostsDirectory(directory) {
@@ -95,4 +95,4 @@ function startRemoteHosts(options, registry) {
95
95
  //#endregion
96
96
  export { startRemoteHosts };
97
97
 
98
- //# sourceMappingURL=remote-hosts-BkOwRszu.js.map
98
+ //# sourceMappingURL=remote-hosts-DcCh7fqp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"remote-hosts-BkOwRszu.js","names":[],"sources":["../src/tui/host/remote-hosts.ts"],"sourcesContent":["/**\n * Saved servers in the window's host directory: loaded only when the\n * `remoteHosts` setting is on (`use-remote-hosts` imports this module\n * dynamically), so a window with it off never reads `hosts.json`, never\n * loads the WebSocket client and never dials anything.\n *\n * The registry ↔ directory sync and the connection loops are the shared\n * ones (`startWsSavedHosts`, `src/ipc/hosts`, the GUI's too); this module is\n * the TUI's glue: a directory entry per server carries a WebSocket\n * transport (the session controllers and session data dial through it),\n * and the server's presence and session changes feed the window.\n */\nimport type { HostEntry, HostRegistry } from '../../chat/hosts'\nimport type { SyncEngine, SyncEngineOptions } from '../../chat/sync/engine'\nimport type { SavedHostsController, SavedHostsDirectory } from '../../ipc/hosts'\nimport type { SessionPresence } from '../../ipc/index'\nimport type { HostDirectory } from './host-directory'\nimport { createSyncEngine } from '../../chat/sync/engine'\nimport { startWsSavedHosts } from '../../ipc/hosts'\nimport { wsTransport } from '../runtime/transport'\nimport { hostSignalHandlers } from './host-signals'\n\nexport interface RemoteHostsOptions {\n dataDir: string\n directory: HostDirectory\n onPresence?: (key: string, rows: readonly SessionPresence[]) => void\n /** A server (re)connected: its sessions can be listed. */\n onConnected?: (key: string) => void\n /** A server's sessions changed (another window created, renamed or deleted one). */\n onSessionsChanged?: (key: string) => void\n /** Settings sync to the saved servers (`src/chat/sync`): this computer's files. Absent: no sync. */\n sync?: Omit<SyncEngineOptions, 'dataDir'>\n}\n\nexport type RemoteHostsController = SavedHostsController & {\n /** Settings sync to each saved server (Settings → Hosts → a server → Sync). */\n sync?: SyncEngine | null\n}\n\n/** The TUI's directory, as the shared saved-hosts sync sees it. */\nfunction savedHostsDirectory(directory: HostDirectory): SavedHostsDirectory {\n return {\n saved: () => directory.servers().filter(host => !host.adHoc).map(host => ({ key: host.key, url: host.url ?? '' })),\n upsert: (entry: HostEntry, token) => {\n // Every dial through it (the connection loop, session data, controllers) checks it is still this server.\n const transport = wsTransport({ url: entry.url, expectHostId: entry.id, ...(token ? { token } : {}) })\n const known = directory.get(entry.id)\n if (known) {\n // In place: the entry (and the sessions routed to it) stays.\n directory.update(entry.id, { name: entry.name, url: entry.url, transport, client: null, status: 'connecting', error: undefined })\n return\n }\n directory.put({\n key: entry.id,\n kind: 'server',\n name: entry.name,\n url: entry.url,\n transport,\n client: null,\n status: 'connecting',\n ...(entry.serverName ? { serverName: entry.serverName } : {}),\n ...(entry.serverVersion ? { serverVersion: entry.serverVersion } : {}),\n })\n },\n update: (key, patch) => directory.update(key, patch),\n remove: key => directory.remove(key),\n }\n}\n\nexport function startRemoteHosts(options: RemoteHostsOptions, registry?: HostRegistry): RemoteHostsController {\n const { directory } = options\n /** `--host` already names this server: its saved entry would be a second connection to it. */\n const adHocUrl = directory.servers().find(host => host.adHoc)?.url\n const sync = options.sync ? createSyncEngine({ dataDir: options.dataDir, ...options.sync }) : null\n const handlers = hostSignalHandlers(options)\n const controller = startWsSavedHosts({\n dataDir: options.dataDir,\n ...(registry ? { registry } : {}),\n directory: savedHostsDirectory(directory),\n skip: entry => entry.url === adHocUrl,\n connect: (entry, _token, attempt) => {\n const host = directory.get(entry.id)\n if (!host)\n return Promise.reject(new Error(`${entry.name} is no longer in this window's hosts.`))\n return host.transport.connect({ appVersion: 'tui-hosts', passive: true, ...attempt })\n },\n onSignal: (key, signal) => {\n handlers.onSignal(key, signal)\n if (signal.kind === 'sync:changed')\n sync?.serverChanged(key)\n },\n onConnected: (key, client) => {\n handlers.onConnected(key, client)\n sync?.connected(key, client)\n },\n onDisconnected: (key) => {\n handlers.onDisconnected(key)\n sync?.disconnected(key)\n },\n })\n return {\n ...controller,\n // Removing a server keeps what it has; this computer stops syncing to it.\n remove: (id) => {\n const removed = controller.remove(id)\n if (removed)\n sync?.removed(id)\n return removed\n },\n stop: () => {\n sync?.stop()\n controller.stop()\n },\n sync,\n }\n}\n"],"mappings":";;;;;AAwCA,SAAS,oBAAoB,WAA+C;CAC1E,OAAO;EACL,aAAa,UAAU,QAAQ,CAAC,CAAC,QAAO,SAAQ,CAAC,KAAK,KAAK,CAAC,CAAC,KAAI,UAAS;GAAE,KAAK,KAAK;GAAK,KAAK,KAAK,OAAO;EAAG,EAAE;EACjH,SAAS,OAAkB,UAAU;GAEnC,MAAM,YAAY,YAAY;IAAE,KAAK,MAAM;IAAK,cAAc,MAAM;IAAI,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GAAG,CAAC;GAErG,IADc,UAAU,IAAI,MAAM,EAC1B,GAAG;IAET,UAAU,OAAO,MAAM,IAAI;KAAE,MAAM,MAAM;KAAM,KAAK,MAAM;KAAK;KAAW,QAAQ;KAAM,QAAQ;KAAc,OAAO,KAAA;IAAU,CAAC;IAChI;GACF;GACA,UAAU,IAAI;IACZ,KAAK,MAAM;IACX,MAAM;IACN,MAAM,MAAM;IACZ,KAAK,MAAM;IACX;IACA,QAAQ;IACR,QAAQ;IACR,GAAI,MAAM,aAAa,EAAE,YAAY,MAAM,WAAW,IAAI,CAAC;IAC3D,GAAI,MAAM,gBAAgB,EAAE,eAAe,MAAM,cAAc,IAAI,CAAC;GACtE,CAAC;EACH;EACA,SAAS,KAAK,UAAU,UAAU,OAAO,KAAK,KAAK;EACnD,SAAQ,QAAO,UAAU,OAAO,GAAG;CACrC;AACF;AAEA,SAAgB,iBAAiB,SAA6B,UAAgD;CAC5G,MAAM,EAAE,cAAc;;CAEtB,MAAM,WAAW,UAAU,QAAQ,CAAC,CAAC,MAAK,SAAQ,KAAK,KAAK,CAAC,EAAE;CAC/D,MAAM,OAAO,QAAQ,OAAO,iBAAiB;EAAE,SAAS,QAAQ;EAAS,GAAG,QAAQ;CAAK,CAAC,IAAI;CAC9F,MAAM,WAAW,mBAAmB,OAAO;CAC3C,MAAM,aAAa,kBAAkB;EACnC,SAAS,QAAQ;EACjB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAC/B,WAAW,oBAAoB,SAAS;EACxC,OAAM,UAAS,MAAM,QAAQ;EAC7B,UAAU,OAAO,QAAQ,YAAY;GACnC,MAAM,OAAO,UAAU,IAAI,MAAM,EAAE;GACnC,IAAI,CAAC,MACH,OAAO,QAAQ,uBAAO,IAAI,MAAM,GAAG,MAAM,KAAK,sCAAsC,CAAC;GACvF,OAAO,KAAK,UAAU,QAAQ;IAAE,YAAY;IAAa,SAAS;IAAM,GAAG;GAAQ,CAAC;EACtF;EACA,WAAW,KAAK,WAAW;GACzB,SAAS,SAAS,KAAK,MAAM;GAC7B,IAAI,OAAO,SAAS,gBAClB,MAAM,cAAc,GAAG;EAC3B;EACA,cAAc,KAAK,WAAW;GAC5B,SAAS,YAAY,KAAK,MAAM;GAChC,MAAM,UAAU,KAAK,MAAM;EAC7B;EACA,iBAAiB,QAAQ;GACvB,SAAS,eAAe,GAAG;GAC3B,MAAM,aAAa,GAAG;EACxB;CACF,CAAC;CACD,OAAO;EACL,GAAG;EAEH,SAAS,OAAO;GACd,MAAM,UAAU,WAAW,OAAO,EAAE;GACpC,IAAI,SACF,MAAM,QAAQ,EAAE;GAClB,OAAO;EACT;EACA,YAAY;GACV,MAAM,KAAK;GACX,WAAW,KAAK;EAClB;EACA;CACF;AACF"}
1
+ {"version":3,"file":"remote-hosts-DcCh7fqp.js","names":[],"sources":["../src/tui/host/remote-hosts.ts"],"sourcesContent":["/**\n * Saved servers in the window's host directory: loaded only when the\n * `remoteHosts` setting is on (`use-remote-hosts` imports this module\n * dynamically), so a window with it off never reads `hosts.json`, never\n * loads the WebSocket client and never dials anything.\n *\n * The registry ↔ directory sync and the connection loops are the shared\n * ones (`startWsSavedHosts`, `src/ipc/hosts`, the GUI's too); this module is\n * the TUI's glue: a directory entry per server carries a WebSocket\n * transport (the session controllers and session data dial through it),\n * and the server's presence and session changes feed the window.\n */\nimport type { HostEntry, HostRegistry } from '../../chat/hosts'\nimport type { SyncEngine, SyncEngineOptions } from '../../chat/sync/engine'\nimport type { SavedHostsController, SavedHostsDirectory } from '../../ipc/hosts'\nimport type { SessionPresence } from '../../ipc/index'\nimport type { HostDirectory } from './host-directory'\nimport { createSyncEngine } from '../../chat/sync/engine'\nimport { startWsSavedHosts } from '../../ipc/hosts'\nimport { wsTransport } from '../runtime/transport'\nimport { hostSignalHandlers } from './host-signals'\n\nexport interface RemoteHostsOptions {\n dataDir: string\n directory: HostDirectory\n onPresence?: (key: string, rows: readonly SessionPresence[]) => void\n /** A server (re)connected: its sessions can be listed. */\n onConnected?: (key: string) => void\n /** A server's sessions changed (another window created, renamed or deleted one). */\n onSessionsChanged?: (key: string) => void\n /** Settings sync to the saved servers (`src/chat/sync`): this computer's files. Absent: no sync. */\n sync?: Omit<SyncEngineOptions, 'dataDir'>\n}\n\nexport type RemoteHostsController = SavedHostsController & {\n /** Settings sync to each saved server (Settings → Hosts → a server → Sync). */\n sync?: SyncEngine | null\n}\n\n/** The TUI's directory, as the shared saved-hosts sync sees it. */\nfunction savedHostsDirectory(directory: HostDirectory): SavedHostsDirectory {\n return {\n saved: () => directory.servers().filter(host => !host.adHoc).map(host => ({ key: host.key, url: host.url ?? '' })),\n upsert: (entry: HostEntry, token) => {\n // Every dial through it (the connection loop, session data, controllers) checks it is still this server.\n const transport = wsTransport({ url: entry.url, expectHostId: entry.id, ...(token ? { token } : {}) })\n const known = directory.get(entry.id)\n if (known) {\n // In place: the entry (and the sessions routed to it) stays.\n directory.update(entry.id, { name: entry.name, url: entry.url, transport, client: null, status: 'connecting', error: undefined })\n return\n }\n directory.put({\n key: entry.id,\n kind: 'server',\n name: entry.name,\n url: entry.url,\n transport,\n client: null,\n status: 'connecting',\n ...(entry.serverName ? { serverName: entry.serverName } : {}),\n ...(entry.serverVersion ? { serverVersion: entry.serverVersion } : {}),\n })\n },\n update: (key, patch) => directory.update(key, patch),\n remove: key => directory.remove(key),\n }\n}\n\nexport function startRemoteHosts(options: RemoteHostsOptions, registry?: HostRegistry): RemoteHostsController {\n const { directory } = options\n /** `--host` already names this server: its saved entry would be a second connection to it. */\n const adHocUrl = directory.servers().find(host => host.adHoc)?.url\n const sync = options.sync ? createSyncEngine({ dataDir: options.dataDir, ...options.sync }) : null\n const handlers = hostSignalHandlers(options)\n const controller = startWsSavedHosts({\n dataDir: options.dataDir,\n ...(registry ? { registry } : {}),\n directory: savedHostsDirectory(directory),\n skip: entry => entry.url === adHocUrl,\n connect: (entry, _token, attempt) => {\n const host = directory.get(entry.id)\n if (!host)\n return Promise.reject(new Error(`${entry.name} is no longer in this window's hosts.`))\n return host.transport.connect({ appVersion: 'tui-hosts', passive: true, ...attempt })\n },\n onSignal: (key, signal) => {\n handlers.onSignal(key, signal)\n if (signal.kind === 'sync:changed')\n sync?.serverChanged(key)\n },\n onConnected: (key, client) => {\n handlers.onConnected(key, client)\n sync?.connected(key, client)\n },\n onDisconnected: (key) => {\n handlers.onDisconnected(key)\n sync?.disconnected(key)\n },\n })\n return {\n ...controller,\n // Removing a server keeps what it has; this computer stops syncing to it.\n remove: (id) => {\n const removed = controller.remove(id)\n if (removed)\n sync?.removed(id)\n return removed\n },\n stop: () => {\n sync?.stop()\n controller.stop()\n },\n sync,\n }\n}\n"],"mappings":";;;;;AAwCA,SAAS,oBAAoB,WAA+C;CAC1E,OAAO;EACL,aAAa,UAAU,QAAQ,CAAC,CAAC,QAAO,SAAQ,CAAC,KAAK,KAAK,CAAC,CAAC,KAAI,UAAS;GAAE,KAAK,KAAK;GAAK,KAAK,KAAK,OAAO;EAAG,EAAE;EACjH,SAAS,OAAkB,UAAU;GAEnC,MAAM,YAAY,YAAY;IAAE,KAAK,MAAM;IAAK,cAAc,MAAM;IAAI,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GAAG,CAAC;GAErG,IADc,UAAU,IAAI,MAAM,EAC1B,GAAG;IAET,UAAU,OAAO,MAAM,IAAI;KAAE,MAAM,MAAM;KAAM,KAAK,MAAM;KAAK;KAAW,QAAQ;KAAM,QAAQ;KAAc,OAAO,KAAA;IAAU,CAAC;IAChI;GACF;GACA,UAAU,IAAI;IACZ,KAAK,MAAM;IACX,MAAM;IACN,MAAM,MAAM;IACZ,KAAK,MAAM;IACX;IACA,QAAQ;IACR,QAAQ;IACR,GAAI,MAAM,aAAa,EAAE,YAAY,MAAM,WAAW,IAAI,CAAC;IAC3D,GAAI,MAAM,gBAAgB,EAAE,eAAe,MAAM,cAAc,IAAI,CAAC;GACtE,CAAC;EACH;EACA,SAAS,KAAK,UAAU,UAAU,OAAO,KAAK,KAAK;EACnD,SAAQ,QAAO,UAAU,OAAO,GAAG;CACrC;AACF;AAEA,SAAgB,iBAAiB,SAA6B,UAAgD;CAC5G,MAAM,EAAE,cAAc;;CAEtB,MAAM,WAAW,UAAU,QAAQ,CAAC,CAAC,MAAK,SAAQ,KAAK,KAAK,CAAC,EAAE;CAC/D,MAAM,OAAO,QAAQ,OAAO,iBAAiB;EAAE,SAAS,QAAQ;EAAS,GAAG,QAAQ;CAAK,CAAC,IAAI;CAC9F,MAAM,WAAW,mBAAmB,OAAO;CAC3C,MAAM,aAAa,kBAAkB;EACnC,SAAS,QAAQ;EACjB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAC/B,WAAW,oBAAoB,SAAS;EACxC,OAAM,UAAS,MAAM,QAAQ;EAC7B,UAAU,OAAO,QAAQ,YAAY;GACnC,MAAM,OAAO,UAAU,IAAI,MAAM,EAAE;GACnC,IAAI,CAAC,MACH,OAAO,QAAQ,uBAAO,IAAI,MAAM,GAAG,MAAM,KAAK,sCAAsC,CAAC;GACvF,OAAO,KAAK,UAAU,QAAQ;IAAE,YAAY;IAAa,SAAS;IAAM,GAAG;GAAQ,CAAC;EACtF;EACA,WAAW,KAAK,WAAW;GACzB,SAAS,SAAS,KAAK,MAAM;GAC7B,IAAI,OAAO,SAAS,gBAClB,MAAM,cAAc,GAAG;EAC3B;EACA,cAAc,KAAK,WAAW;GAC5B,SAAS,YAAY,KAAK,MAAM;GAChC,MAAM,UAAU,KAAK,MAAM;EAC7B;EACA,iBAAiB,QAAQ;GACvB,SAAS,eAAe,GAAG;GAC3B,MAAM,aAAa,GAAG;EACxB;CACF,CAAC;CACD,OAAO;EACL,GAAG;EAEH,SAAS,OAAO;GACd,MAAM,UAAU,WAAW,OAAO,EAAE;GACpC,IAAI,SACF,MAAM,QAAQ,EAAE;GAClB,OAAO;EACT;EACA,YAAY;GACV,MAAM,KAAK;GACX,WAAW,KAAK;EAClB;EACA;CACF;AACF"}
@@ -1,4 +1,4 @@
1
- import { n as compareVersions, t as ZIDANE_VERSION } from "./version-DDRBX7K6.js";
1
+ import { n as compareVersions, t as ZIDANE_VERSION } from "./version-DOvIuU8M.js";
2
2
  import { t as resolveStorageDirs } from "./xdg-CJS70jqe.js";
3
3
  import { n as JsonRpcRemoteError, t as JsonRpcConnectionClosedError } from "./json-rpc-G0rBfEar.js";
4
4
  import { r as isWindowOnlySetting, t as HOST_ENABLED_LISTS } from "./host-scoped-settings-DxxI_yRo.js";
@@ -388,4 +388,4 @@ function isSyncableSetting(key) {
388
388
  //#endregion
389
389
  export { createSignalListeners as a, daemonSocketName as c, legacySocketPath as d, probeSocket as f, IpcVersionMismatchError as i, defaultSocketPath as l, hashValue as n, openIpcClient as o, selfDaemonSpawnCommand as p, isRecord as r, IPC_BUILD_ID as s, isSyncableSetting as t, ensureDaemonRunning as u };
390
390
 
391
- //# sourceMappingURL=settings-keys-QoGJ5n3e.js.map
391
+ //# sourceMappingURL=settings-keys-CnrJgT1T.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings-keys-QoGJ5n3e.js","names":[],"sources":["../src/ipc/daemon/lifecycle.ts","../src/ipc/transports/client-core.ts","../src/chat/sync/hash.ts","../src/chat/sync/settings-keys.ts"],"sourcesContent":["/**\n * Daemon lifecycle: socket-path resolution, liveness probing, stale-socket\n * cleanup, and spawn-on-demand.\n *\n * The socket lives in a dedicated daemon subdir of the resolved state dir\n * (`~/.zidane/daemon/daemon.sock` by default, honoring the same\n * `ZIDANE_STORAGE_DIR` / `ZIDANE_PREFIX` env knobs every other surface\n * uses), overridable via `ZIDANE_DAEMON_SOCKET` for parallel installs and\n * tests.\n */\nimport { spawn } from 'node:child_process'\nimport { existsSync, readdirSync, statSync } from 'node:fs'\nimport { connect } from 'node:net'\nimport { basename, dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { resolveStorageDirs } from '../../chat/xdg'\n\n/**\n * Build fingerprint for the handshake. Running from source it is the newest\n * mtime under the `src/` tree at import time. Bundled, it is the mtime of this\n * module's own file. Client and daemon load the same files from disk, so a\n * daemon spawned before a rebuild (or a source edit in dev) reports a\n * different id than a freshly-started client, and the client drains +\n * respawns it. This closes the \"same protocol version, different code\"\n * gap that a hand-bumped version constant can't: during development the\n * daemon process is frozen at spawn while the tree keeps moving.\n *\n * Compiled binaries (`/$bunfs/…` virtual paths) can't be stat'ed and\n * report `'unknown'`; the check is skipped when EITHER side is unknown,\n * so released builds rely on `IPC_PROTOCOL_VERSION` alone — correct,\n * since their code only changes when the version does.\n */\nexport const IPC_BUILD_ID: string = (() => {\n try {\n const selfPath = fileURLToPath(import.meta.url)\n /*\n * Dev (running from source): fingerprint the whole source tree, not\n * just this file — a change to server.ts / engine.ts / runtime code\n * with no edit to lifecycle.ts must still retire an old daemon. Walk\n * `src/` and take the newest mtime. When the tree isn't present\n * (bundled dist, compiled binary) we fall back to this module's own\n * file mtime; in a bundle every ipc source collapses into one chunk, so\n * that single mtime already moves on any ipc change.\n */\n /*\n * Root validation is load-bearing: from a built bundle this module\n * lives flat in `dist/`, so the two-levels-up guess would be the\n * PACKAGE root — walking it would synchronously stat the whole repo\n * (node_modules, .git) at import time and fingerprint unrelated\n * churn. Only walk when the candidate directory demonstrably IS the\n * ipc source tree; a bundle falls back to this chunk's own mtime,\n * which already moves on every rebuild.\n */\n const ipcRoot = dirname(dirname(selfPath))\n if (!existsSync(join(ipcRoot, 'protocol.ts')))\n return String(Math.floor(statSync(selfPath).mtimeMs))\n\n /*\n * Dev scope: walk the whole `src/` tree, not just `src/ipc` — daemon\n * behavior also lives in agent.ts, loop.ts, and chat/** (store,\n * replay, discovery), and an edit there must retire a running dev\n * daemon just as surely as an ipc edit (the spawn-marker incident\n * lived in chat/store.ts). A few hundred stats — milliseconds.\n */\n const srcRoot = dirname(ipcRoot)\n let newest = 0\n const walk = (dir: string): void => {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const full = join(dir, entry.name)\n if (entry.isDirectory())\n walk(full)\n else\n newest = Math.max(newest, statSync(full).mtimeMs)\n }\n }\n try {\n walk(srcRoot)\n }\n catch {\n newest = statSync(selfPath).mtimeMs\n }\n return String(Math.floor(newest))\n }\n catch {\n return 'unknown'\n }\n})()\n\n/**\n * Socket file name. Dev builds (`ZIDANE_DEV=1`, set by every `dev` script\n * in the repo) get their OWN socket so a checkout's TUI, GUI and `zidane\n * daemon` pair up among themselves and never attach to — or get refused by\n * — the installed daemon. Without this, a protocol bump in the working\n * tree turned the dev GUI's dot red against the stable daemon, and a dev\n * daemon could drain the one the installed TUI relies on.\n * @param env - Environment to read `ZIDANE_DEV` from.\n * @returns `dev.sock` for a dev build, else `daemon.sock`.\n */\nexport function daemonSocketName(env: NodeJS.ProcessEnv = process.env): 'daemon.sock' | 'dev.sock' {\n return env.ZIDANE_DEV && env.ZIDANE_DEV !== '0' ? 'dev.sock' : 'daemon.sock'\n}\n\n/**\n * Path of the daemon socket this process uses.\n * The `ZIDANE_DAEMON_SOCKET` environment variable wins. Otherwise it is `<stateDir>/daemon/<socket name>`, see {@link daemonSocketName}.\n * @returns The socket path.\n */\nexport function defaultSocketPath(): string {\n const explicit = process.env.ZIDANE_DAEMON_SOCKET\n if (explicit)\n return explicit\n /*\n * The socket is transient runtime state, so it lands under the resolved\n * state dir — XDG-aware on XDG installs, `<storageDir>/<prefix>` in\n * legacy mode. Uses the same resolver every other surface reads through,\n * honoring `ZIDANE_STORAGE_DIR` / `ZIDANE_PREFIX`.\n *\n * DEDICATED `daemon/` subdir: the state dir is shared with\n * `state.json` / logs, so the daemon can't chmod it 0700 without\n * affecting unrelated files. The server hardens this subdir instead;\n * client and server must agree on this path.\n */\n const dirs = resolveStorageDirs({\n ...(process.env.ZIDANE_PREFIX !== undefined ? { prefix: process.env.ZIDANE_PREFIX } : {}),\n })\n return join(dirs.stateDir, 'daemon', daemonSocketName())\n}\n\n/**\n * Legacy socket location (`<stateDir>/daemon.sock`) for the given socket path,\n * or null when the path isn't the default `<stateDir>/daemon/daemon.sock`\n * layout (explicit `ZIDANE_DAEMON_SOCKET` users never had a migration to run).\n * A daemon still serving the old path would be ORPHANED by the move — new clients\n * never probe it, and a service-style `--idle-exit 0` daemon would hold\n * agents and run-guard markers against the same sessions.db forever —\n * so daemon startup probes this path and drains any survivor.\n */\nexport function legacySocketPath(socketPath: string): string | null {\n const dir = dirname(socketPath)\n if (basename(socketPath) !== 'daemon.sock' || basename(dir) !== 'daemon')\n return null\n return join(dirname(dir), 'daemon.sock')\n}\n\n/**\n * Spawn command that re-enters the CURRENT zidane program as `… daemon`.\n *\n * Three launch shapes:\n * - compiled Bun binary — `Bun.main` is the virtual `/$bunfs/…` entry the\n * binary self-inserts as argv[1], so the spawn must NOT repeat it\n * (verified: repeating it makes the child parse the junk path as its\n * subcommand and boot the full TUI instead of the daemon).\n * - dev (`bun run cli.ts`) — the script path must ride along.\n * - Node hosts (no `Bun` global) — fall back to the published bin.\n */\nexport function selfDaemonSpawnCommand(): string[] {\n const bunMain = (globalThis as { Bun?: { main?: string } }).Bun?.main\n // Compiled binary: the virtual entry parses its own `daemon` subcommand.\n if (bunMain?.startsWith('/$bunfs'))\n return [process.execPath, 'daemon']\n // Dev-from-source: only re-execute `Bun.main` when it's actually a\n // zidane CLI entry (`cli.ts`/`cli.js`). For any other embedder (library\n // consumer, script) re-running their entry with a `daemon` argv would\n // launch an arbitrary program — fall back to the published bin instead.\n if (bunMain && /(?:^|[/\\\\])cli\\.[cm]?[jt]s$/.test(bunMain))\n return [process.execPath, bunMain, 'daemon']\n return ['zidane-daemon']\n}\n\n/**\n * True when something accepts connections on `socketPath`. Distinguishes a\n * live daemon from a stale socket file left by a crashed one.\n * @param socketPath - Path of the socket to try.\n * @param timeoutMs - How long to wait for the connection, in milliseconds.\n */\nexport function probeSocket(socketPath: string, timeoutMs = 1000): Promise<boolean> {\n return new Promise((resolve) => {\n /*\n * Skip the connect entirely unless the path is an actual socket file.\n * A stale REGULAR file (crashed daemon) or a missing path both mean\n * \"nothing listening\" — and connecting to them can surface a noisy\n * async ENOENT/ENOTSOCK that some runtimes treat as uncaught.\n */\n try {\n if (!existsSync(socketPath) || !statSync(socketPath).isSocket()) {\n resolve(false)\n return\n }\n }\n catch {\n resolve(false)\n return\n }\n\n let socket: ReturnType<typeof connect>\n try {\n socket = connect(socketPath)\n }\n catch {\n resolve(false)\n return\n }\n const done = (alive: boolean): void => {\n socket.destroy()\n resolve(alive)\n }\n socket.once('connect', () => done(true))\n socket.once('error', () => done(false))\n socket.setTimeout(timeoutMs, () => done(false))\n })\n}\n\n/** Options for {@link ensureDaemonRunning}. */\nexport interface EnsureDaemonOptions {\n /**\n * Socket the daemon listens on.\n * @default defaultSocketPath()\n */\n socketPath?: string\n /**\n * Command used to spawn the daemon when nothing is listening. Defaults to\n * `['zidane-daemon']` (the published bin). The child is detached and\n * unref'd — it outlives the spawning client by design.\n */\n spawnCommand?: string[]\n /** Total time to wait for the daemon to accept connections. Default 8000 ms. */\n timeoutMs?: number\n}\n\n/** Daemon starts in flight, by socket path, so concurrent callers share one. */\nconst pendingStarts = new Map<string, Promise<string>>()\n\n/**\n * Makes sure a daemon is listening on the socket, spawning one when nothing is. Concurrent calls for the same socket share one start.\n * Rejects when the spawn fails or the daemon does not accept connections within `timeoutMs`.\n * @param options - Socket, spawn command and timeout.\n * @returns The socket path once a connection succeeds.\n */\nexport function ensureDaemonRunning(options: EnsureDaemonOptions = {}): Promise<string> {\n const socketPath = options.socketPath ?? defaultSocketPath()\n const pending = pendingStarts.get(socketPath)\n if (pending)\n return pending\n const start = startDaemon({ ...options, socketPath }).finally(() => pendingStarts.delete(socketPath))\n pendingStarts.set(socketPath, start)\n return start\n}\n\nasync function startDaemon(options: EnsureDaemonOptions & { socketPath: string }): Promise<string> {\n const socketPath = options.socketPath\n if (await probeSocket(socketPath))\n return socketPath\n\n const [cmd, ...args] = options.spawnCommand ?? ['zidane-daemon']\n const child = spawn(cmd!, args, {\n detached: true,\n stdio: 'ignore',\n env: { ...process.env, ZIDANE_DAEMON_SOCKET: socketPath },\n })\n /*\n * A missing binary (e.g. `zidane-daemon` not on PATH) emits an async\n * `error` event; without a listener Node rethrows it as an uncaught\n * exception and crashes the caller. Capture it and surface a clean\n * rejection through the poll loop below.\n */\n let spawnError: Error | undefined\n child.once('error', (err: Error) => {\n spawnError = err\n })\n child.unref()\n\n const deadline = Date.now() + (options.timeoutMs ?? 8000)\n for (;;) {\n if (spawnError !== undefined)\n throw new Error(`Failed to spawn zidane daemon (${cmd}): ${spawnError.message}`)\n if (await probeSocket(socketPath))\n return socketPath\n if (Date.now() > deadline)\n throw new Error(`zidane daemon did not come up on ${socketPath} within ${options.timeoutMs ?? 8000}ms.`)\n await new Promise(r => setTimeout(r, 150))\n }\n}\n","/**\n * The client side every wire transport shares (the unix socket in\n * `../client.ts`, the WebSocket in `./ws-client.ts`): the `ipc.hello`\n * handshake with its version fences, typed requests with deadlines,\n * notifications, and signal fan-out. A transport supplies a connected\n * JSON-RPC {@link ClientChannel}; this turns it into an {@link IpcClient}.\n */\nimport type { AcpPeer, JsonRpcMessage } from '../../acp/types'\nimport type { HelloResult, HostSignal, IpcMethodName, IpcParams, IpcResult, SignalMeta } from '../protocol'\nimport { JsonRpcConnectionClosedError, JsonRpcRemoteError } from '../../acp/json-rpc'\nimport { compareVersions, ZIDANE_VERSION } from '../../version'\nimport { IPC_BUILD_ID } from '../daemon/lifecycle'\nimport { IPC_PROTOCOL_VERSION, IpcErrorCode, PING_METHOD, SIGNAL_SEQ_FIELD } from '../protocol'\n\n/** A connected host client, shared by every transport. Requests are checked against {@link IpcMethods}. */\nexport interface IpcClient {\n /** The host's answer to the `ipc.hello` handshake. */\n hello: HelloResult\n /**\n * Calls a host method and resolves with its result. Rejects with a `JsonRpcRemoteError` when the host answers with an error, or with `JsonRpcConnectionClosedError` when the connection closes first.\n * A request not answered within its deadline (30 s, longer for slow methods) rejects and drops the connection. `machine.call` has no deadline.\n */\n request: <M extends IpcMethodName>(method: M, params: IpcParams<M>) => Promise<IpcResult<M>>\n /**\n * Fire-and-forget notification — same method table as `request`, but no\n * id, no response, no pending-promise bookkeeping. The hot path for\n * high-frequency writes (per-keystroke draft sync). Daemons that don't\n * know the method ignore it silently.\n */\n notify: <M extends IpcMethodName>(method: M, params: IpcParams<M>) => void\n /** Host signals, with their number in their session when they have one (`SignalMeta`). */\n onSignal: (cb: SignalListener) => () => void\n /** Closes the connection. Pending requests reject. */\n close: () => void\n}\n\nexport type SignalListener = (signal: HostSignal, meta?: SignalMeta) => void\n\n/** Thrown when the host refuses the handshake because its protocol, build or code version does not match the client's. */\nexport class IpcVersionMismatchError extends Error {\n /**\n * @param message - Description of the mismatch.\n * @param daemon - The host's handshake data, when it sent any.\n */\n constructor(message: string, readonly daemon?: HelloResult) {\n super(message)\n this.name = 'IpcVersionMismatchError'\n }\n}\n\n/** A connected JSON-RPC link to a host, as a transport hands it over. */\nexport interface ClientChannel {\n rpc: Pick<AcpPeer, 'sendRequest' | 'sendNotification'>\n /** The caller's `close()`: quiet (no `onClose`), fails what is in flight. Idempotent. */\n close: () => void\n /** Drop a wedged connection: like the host going away, so the transport's `onClose` fires. */\n abort: () => void\n /** Where the host is, for error messages (a socket path, a URL). */\n where: string\n}\n\nexport interface ClientCoreOptions {\n appVersion?: string\n /**\n * When the host refuses the handshake on a version / build mismatch:\n * `always` asks it to drain so a matching one can be spawned (session\n * controllers on the local daemon); `stale-only` only when the host is\n * provably older (passive local clients); `never` leaves it running (a\n * remote server's lifetime is not the client's to end).\n */\n drain: 'always' | 'stale-only' | 'never'\n /**\n * Send this build's fingerprint, so a host from an older build of the same\n * protocol refuses us. Off sends the `'unknown'` sentinel: clients that\n * can't act on a build mismatch (no drain, no respawn) opt out of that fence.\n */\n buildFence: boolean\n /**\n * Ping the host (`$/ping`) this often once attached; two pings in a row\n * left unanswered at the next tick abort the connection (the transport's\n * `onClose` fires, its owner reconnects). Off when absent or `0`: a\n * local socket learns of a dead host from the kernel.\n */\n heartbeatMs?: number\n}\n\n/** Missed heartbeats after which the host is considered gone. */\nexport const HEARTBEAT_MISSES = 2\n\n/** Starts the client heartbeat on `channel`; returns its stop. Stops itself once the channel closes. */\nfunction startHeartbeat(channel: ClientChannel, intervalMs: number): () => void {\n let awaiting = false\n let missed = 0\n const timer = setInterval(() => {\n if (awaiting && ++missed >= HEARTBEAT_MISSES) {\n clearInterval(timer)\n channel.abort()\n return\n }\n awaiting = true\n channel.rpc.sendRequest(PING_METHOD, {}).then(\n () => {\n awaiting = false\n missed = 0\n },\n (err: unknown) => {\n if (err instanceof JsonRpcConnectionClosedError) {\n clearInterval(timer)\n return\n }\n // Any answer, an error included (a host predating `$/ping`), proves it alive.\n awaiting = false\n missed = 0\n },\n )\n }, intervalMs)\n timer.unref?.()\n return () => clearInterval(timer)\n}\n\n/** The signal listeners of one client, and the JSON-RPC notification handler that feeds them. */\nexport function createSignalListeners() {\n const listeners = new Set<SignalListener>()\n return {\n listeners,\n onNotification(message: JsonRpcMessage & { method: string }): void {\n if (message.method !== 'ipc.signal')\n return\n const params = (message as { params?: HostSignal & { [SIGNAL_SEQ_FIELD]?: unknown } }).params\n if (!params)\n return\n const { [SIGNAL_SEQ_FIELD]: seq, ...signal } = params\n const meta = typeof seq === 'number' ? { seq } : undefined\n for (const cb of listeners)\n cb(signal as HostSignal, meta)\n },\n }\n}\n\nfunction hostRelease(hello: HelloResult): string | undefined {\n for (const value of [hello.codeVersion, hello.appVersion]) {\n if (typeof value !== 'string')\n continue\n const match = /^(?:zidane-gui\\/)?(v?\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)$/.exec(value.trim())\n if (match)\n return match[1]\n }\n return undefined\n}\n\nconst HELLO_TIMEOUT_MS = 5000\nconst LONG_REQUEST_TIMEOUT_MS = 10 * 60 * 1000\n/** RPCs that legitimately run long — full compaction, index-backed cross-session search. */\nconst LONG_REQUEST_METHODS = new Set<string>(['sessions.compact', 'history.searchAll', 'history.searchSession'])\n\n/**\n * A refused handshake. Asks the host to drain when `drain` allows it, closes\n * the channel, and returns the error to throw.\n */\nasync function refusedHello(channel: ClientChannel, err: JsonRpcRemoteError, drain: ClientCoreOptions['drain']): Promise<IpcVersionMismatchError> {\n /*\n * Ask the stale daemon to drain so the caller can respawn a matching\n * one. AWAIT the reply (bounded) before tearing the connection down —\n * closing discards unflushed writes, so a fire-and-forget\n * `daemon.shutdown` could be dropped, leaving the stale daemon alive\n * and defeating `connectOrSpawnDaemon`'s retry.\n */\n const daemon = (err.data as { daemon?: HelloResult } | undefined)?.daemon\n // A network server's lifetime is never a client's to end, whatever `drain` says.\n if (daemon?.hostKind === 'server') {\n channel.close()\n return new IpcVersionMismatchError(serverMismatchMessage(daemon, channel.where) ?? err.message, daemon)\n }\n /*\n * Passive clients (`stale-only`) never retire a daemon on a protocol /\n * build mismatch — it may be NEWER than them. A daemon that is provably\n * OLDER — an older release, or the same release speaking an older\n * protocol (a dev build from before a protocol bump) — is the case where\n * draining is always safe: nothing newer can be running on it, and\n * whoever attaches next spawns a current one. Without this a passive\n * client (the GUI) would sit in-process forever behind a stale daemon\n * nobody restarts: every retry is refused.\n */\n const daemonIsOlderRelease\n = typeof daemon?.codeVersion === 'string' && compareVersions(daemon.codeVersion, ZIDANE_VERSION) < 0\n const release = daemon && hostRelease(daemon)\n const daemonIsNewerRelease = release !== undefined && compareVersions(release, ZIDANE_VERSION) > 0\n const daemonSpeaksOlderProtocol = typeof daemon?.protocolVersion === 'number' && daemon.protocolVersion < IPC_PROTOCOL_VERSION\n const daemonIsStale = daemonIsOlderRelease || daemonSpeaksOlderProtocol\n if (drain !== 'never' && !daemonIsNewerRelease && (drain === 'always' || daemonIsStale)) {\n // `whenIdle`: a daemon with live runs defers its exit instead of\n // aborting them (older daemons ignore the flag and drain now).\n await Promise.race([\n channel.rpc.sendRequest('daemon.shutdown', { whenIdle: true }).catch(() => undefined),\n new Promise(resolve => setTimeout(resolve, 1500)),\n ])\n }\n channel.close()\n return new IpcVersionMismatchError(err.message, daemon)\n}\n\n/** A server's protocol refusal, from this client's side: both versions and which side to upgrade. */\nfunction serverMismatchMessage(server: HelloResult, where: string): string | null {\n if (typeof server.protocolVersion !== 'number' || server.protocolVersion === IPC_PROTOCOL_VERSION)\n return null\n const upgrade = server.protocolVersion < IPC_PROTOCOL_VERSION ? 'server' : 'client'\n return `Protocol version mismatch with ${where}: the server speaks v${server.protocolVersion}, this client v${IPC_PROTOCOL_VERSION}. Upgrade the ${upgrade}.`\n}\n\nasync function handshake(channel: ClientChannel, options: ClientCoreOptions): Promise<HelloResult> {\n /*\n * Bound the handshake: a wedged daemon (accepting connections but never\n * reading — e.g. SIGSTOP'd) would otherwise hang this promise forever.\n * On timeout the channel is closed and the connect rejects.\n * `Promise.race` keeps the losing request promise handled, so the\n * connection-closed rejection that follows `close()` can't surface as an\n * unhandled rejection.\n */\n let helloTimer: ReturnType<typeof setTimeout> | undefined\n try {\n return (await Promise.race([\n channel.rpc.sendRequest('ipc.hello', {\n protocolVersion: IPC_PROTOCOL_VERSION,\n appVersion: options.appVersion ?? 'dev',\n pid: process.pid,\n // Build fingerprint: a daemon started from an OLDER build of this\n // same protocol version rejects us, gets drained, and respawns —\n // dev daemons cannot run stale code silently. Clients that\n // cannot act on a build mismatch send the 'unknown' sentinel\n // instead (see `buildFence`): fencing them would just lock them\n // out until someone else retires the host. They stay fenced by\n // protocol version and degrade via `optional()` like any client of\n // an older daemon.\n buildId: options.buildFence ? IPC_BUILD_ID : 'unknown',\n // Always sent, passive clients included: a stale-RELEASE daemon is\n // safe for anyone to retire (see `refusedHello`).\n codeVersion: ZIDANE_VERSION,\n }),\n new Promise<never>((_, reject) => {\n helloTimer = setTimeout(\n () => reject(new Error(`Daemon handshake timed out after ${HELLO_TIMEOUT_MS}ms on ${channel.where}.`)),\n HELLO_TIMEOUT_MS,\n )\n }),\n ])) as HelloResult\n }\n catch (err) {\n if (err instanceof JsonRpcRemoteError && err.code === IpcErrorCode.VersionMismatch)\n throw await refusedHello(channel, err, options.drain)\n channel.close()\n throw err\n }\n finally {\n clearTimeout(helloTimer)\n }\n}\n\n/** Handshake over `channel`, then the typed client around it. On failure the channel is closed. */\nexport async function openIpcClient(\n channel: ClientChannel,\n listeners: Set<SignalListener>,\n options: ClientCoreOptions,\n): Promise<IpcClient> {\n const hello = await handshake(channel, options)\n\n const release = hostRelease(hello)\n // A `retiring` daemon is fence-aware and already exits once idle; staying\n // attached until then keeps its live runs reachable from this window. A\n // network server is never retired for a client: an older one is attached\n // to, and this client degrades (`optional()`) like an older client does.\n if (!hello.retiring && hello.hostKind !== 'server' && release !== undefined && compareVersions(release, ZIDANE_VERSION) < 0) {\n // Pre-fence daemons accept hello without checking codeVersion. Disconnect\n // only: shutting down a successfully attached daemon can abort active runs.\n channel.close()\n throw new IpcVersionMismatchError(\n `Daemon release ${release} is older than client ${ZIDANE_VERSION}. Restart the daemon when active runs have finished.`,\n hello,\n )\n }\n\n // Env override is a test seam (process-level e2e exercises the real\n // deadline without waiting 30 s); undocumented, not a user setting.\n const requestTimeoutMs = Number(process.env.ZIDANE_IPC_REQUEST_TIMEOUT_MS) || 30_000\n\n const stopHeartbeat = options.heartbeatMs ? startHeartbeat(channel, options.heartbeatMs) : () => undefined\n\n return {\n hello,\n notify: (method, params) => {\n try {\n channel.rpc.sendNotification(method, params)\n }\n catch {\n // fire-and-forget by contract — a dying connection surfaces via onClose\n }\n },\n request: async (method, params) => {\n /*\n * Per-request deadline: close rejection covers a DEAD host, but a\n * wedged one (alive, not reading — the same class the server-side\n * backpressure bound handles for peers) would hang callers forever.\n * On timeout the connection is aborted (NOT `close()` — the `onClose`\n * path must fire so reconnect logic takes over) and every pending\n * request rejects via the close sweep. Known long-running RPCs get a\n * generous budget; tearing the connection down mid-`sessions.compact`\n * would abort real work, so those bounds are deliberately loose.\n */\n // A machine call lasts as long as its command (`docs/MACHINES.md`); a dead host is the heartbeat's to catch.\n if (method === 'machine.call')\n return channel.rpc.sendRequest(method, params) as Promise<IpcResult<typeof method>>\n const timeoutMs = LONG_REQUEST_METHODS.has(method) ? LONG_REQUEST_TIMEOUT_MS : requestTimeoutMs\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n return (await Promise.race([\n channel.rpc.sendRequest(method, params),\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(new Error(`Daemon request ${method} timed out after ${timeoutMs}ms, closing the connection.`))\n channel.abort()\n }, timeoutMs)\n timer.unref?.()\n }),\n ])) as IpcResult<typeof method>\n }\n finally {\n clearTimeout(timer)\n }\n },\n onSignal: (cb) => {\n listeners.add(cb)\n return () => listeners.delete(cb)\n },\n close: () => {\n stopHeartbeat()\n channel.close()\n },\n }\n}\n","/** Stable hashes of sync values: equal values (whatever their key order) hash the same, on both sides. */\nimport { createHash } from 'node:crypto'\n\nexport const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** JSON with sorted keys (undefined members dropped). */\nexport function stableStringify(value: unknown): string {\n if (Array.isArray(value))\n return `[${value.map(stableStringify).join(',')}]`\n if (isRecord(value))\n return `{${Object.keys(value).sort().filter(k => value[k] !== undefined).map(k => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`\n return JSON.stringify(value) ?? 'undefined'\n}\n\nexport function hashValue(value: unknown): string {\n return createHash('sha256').update(stableStringify(value)).digest('hex')\n}\n","/** Which settings the `settings` sync category carries (`./types`). */\nimport type { Settings } from '../types'\nimport { HOST_ENABLED_LISTS, isWindowOnlySetting } from '../host-scoped-settings'\n\n/**\n * Host-scoped settings that still don't sync: they describe this computer\n * (its environments, its daemon, remote hosts on it) or would turn off what\n * the server has that this computer doesn't (the enabled allowlists, its OS\n * sandbox). Every `sandbox*` key, present and future, is the server's own:\n * a client turning it off there would be a sandbox escape.\n */\nconst NOT_SYNCED_SETTINGS: ReadonlySet<string> = new Set<string>(['environments', 'useDaemon', 'remoteHosts', ...HOST_ENABLED_LISTS])\n\n// The same legacy `safeMode` mapping as the live settings wire (`settings.get` / `settings.set`).\nexport { settingsFromWire, settingsToWire } from '../host-scoped-settings'\n\n/** A setting the `settings` category carries: host-scoped (never window-only) and not machine-specific. */\nexport function isSyncableSetting(key: keyof Settings | string): boolean {\n return !isWindowOnlySetting(key) && !NOT_SYNCED_SETTINGS.has(key) && !key.startsWith('sandbox')\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"settings-keys-CnrJgT1T.js","names":[],"sources":["../src/ipc/daemon/lifecycle.ts","../src/ipc/transports/client-core.ts","../src/chat/sync/hash.ts","../src/chat/sync/settings-keys.ts"],"sourcesContent":["/**\n * Daemon lifecycle: socket-path resolution, liveness probing, stale-socket\n * cleanup, and spawn-on-demand.\n *\n * The socket lives in a dedicated daemon subdir of the resolved state dir\n * (`~/.zidane/daemon/daemon.sock` by default, honoring the same\n * `ZIDANE_STORAGE_DIR` / `ZIDANE_PREFIX` env knobs every other surface\n * uses), overridable via `ZIDANE_DAEMON_SOCKET` for parallel installs and\n * tests.\n */\nimport { spawn } from 'node:child_process'\nimport { existsSync, readdirSync, statSync } from 'node:fs'\nimport { connect } from 'node:net'\nimport { basename, dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { resolveStorageDirs } from '../../chat/xdg'\n\n/**\n * Build fingerprint for the handshake. Running from source it is the newest\n * mtime under the `src/` tree at import time. Bundled, it is the mtime of this\n * module's own file. Client and daemon load the same files from disk, so a\n * daemon spawned before a rebuild (or a source edit in dev) reports a\n * different id than a freshly-started client, and the client drains +\n * respawns it. This closes the \"same protocol version, different code\"\n * gap that a hand-bumped version constant can't: during development the\n * daemon process is frozen at spawn while the tree keeps moving.\n *\n * Compiled binaries (`/$bunfs/…` virtual paths) can't be stat'ed and\n * report `'unknown'`; the check is skipped when EITHER side is unknown,\n * so released builds rely on `IPC_PROTOCOL_VERSION` alone — correct,\n * since their code only changes when the version does.\n */\nexport const IPC_BUILD_ID: string = (() => {\n try {\n const selfPath = fileURLToPath(import.meta.url)\n /*\n * Dev (running from source): fingerprint the whole source tree, not\n * just this file — a change to server.ts / engine.ts / runtime code\n * with no edit to lifecycle.ts must still retire an old daemon. Walk\n * `src/` and take the newest mtime. When the tree isn't present\n * (bundled dist, compiled binary) we fall back to this module's own\n * file mtime; in a bundle every ipc source collapses into one chunk, so\n * that single mtime already moves on any ipc change.\n */\n /*\n * Root validation is load-bearing: from a built bundle this module\n * lives flat in `dist/`, so the two-levels-up guess would be the\n * PACKAGE root — walking it would synchronously stat the whole repo\n * (node_modules, .git) at import time and fingerprint unrelated\n * churn. Only walk when the candidate directory demonstrably IS the\n * ipc source tree; a bundle falls back to this chunk's own mtime,\n * which already moves on every rebuild.\n */\n const ipcRoot = dirname(dirname(selfPath))\n if (!existsSync(join(ipcRoot, 'protocol.ts')))\n return String(Math.floor(statSync(selfPath).mtimeMs))\n\n /*\n * Dev scope: walk the whole `src/` tree, not just `src/ipc` — daemon\n * behavior also lives in agent.ts, loop.ts, and chat/** (store,\n * replay, discovery), and an edit there must retire a running dev\n * daemon just as surely as an ipc edit (the spawn-marker incident\n * lived in chat/store.ts). A few hundred stats — milliseconds.\n */\n const srcRoot = dirname(ipcRoot)\n let newest = 0\n const walk = (dir: string): void => {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const full = join(dir, entry.name)\n if (entry.isDirectory())\n walk(full)\n else\n newest = Math.max(newest, statSync(full).mtimeMs)\n }\n }\n try {\n walk(srcRoot)\n }\n catch {\n newest = statSync(selfPath).mtimeMs\n }\n return String(Math.floor(newest))\n }\n catch {\n return 'unknown'\n }\n})()\n\n/**\n * Socket file name. Dev builds (`ZIDANE_DEV=1`, set by every `dev` script\n * in the repo) get their OWN socket so a checkout's TUI, GUI and `zidane\n * daemon` pair up among themselves and never attach to — or get refused by\n * — the installed daemon. Without this, a protocol bump in the working\n * tree turned the dev GUI's dot red against the stable daemon, and a dev\n * daemon could drain the one the installed TUI relies on.\n * @param env - Environment to read `ZIDANE_DEV` from.\n * @returns `dev.sock` for a dev build, else `daemon.sock`.\n */\nexport function daemonSocketName(env: NodeJS.ProcessEnv = process.env): 'daemon.sock' | 'dev.sock' {\n return env.ZIDANE_DEV && env.ZIDANE_DEV !== '0' ? 'dev.sock' : 'daemon.sock'\n}\n\n/**\n * Path of the daemon socket this process uses.\n * The `ZIDANE_DAEMON_SOCKET` environment variable wins. Otherwise it is `<stateDir>/daemon/<socket name>`, see {@link daemonSocketName}.\n * @returns The socket path.\n */\nexport function defaultSocketPath(): string {\n const explicit = process.env.ZIDANE_DAEMON_SOCKET\n if (explicit)\n return explicit\n /*\n * The socket is transient runtime state, so it lands under the resolved\n * state dir — XDG-aware on XDG installs, `<storageDir>/<prefix>` in\n * legacy mode. Uses the same resolver every other surface reads through,\n * honoring `ZIDANE_STORAGE_DIR` / `ZIDANE_PREFIX`.\n *\n * DEDICATED `daemon/` subdir: the state dir is shared with\n * `state.json` / logs, so the daemon can't chmod it 0700 without\n * affecting unrelated files. The server hardens this subdir instead;\n * client and server must agree on this path.\n */\n const dirs = resolveStorageDirs({\n ...(process.env.ZIDANE_PREFIX !== undefined ? { prefix: process.env.ZIDANE_PREFIX } : {}),\n })\n return join(dirs.stateDir, 'daemon', daemonSocketName())\n}\n\n/**\n * Legacy socket location (`<stateDir>/daemon.sock`) for the given socket path,\n * or null when the path isn't the default `<stateDir>/daemon/daemon.sock`\n * layout (explicit `ZIDANE_DAEMON_SOCKET` users never had a migration to run).\n * A daemon still serving the old path would be ORPHANED by the move — new clients\n * never probe it, and a service-style `--idle-exit 0` daemon would hold\n * agents and run-guard markers against the same sessions.db forever —\n * so daemon startup probes this path and drains any survivor.\n */\nexport function legacySocketPath(socketPath: string): string | null {\n const dir = dirname(socketPath)\n if (basename(socketPath) !== 'daemon.sock' || basename(dir) !== 'daemon')\n return null\n return join(dirname(dir), 'daemon.sock')\n}\n\n/**\n * Spawn command that re-enters the CURRENT zidane program as `… daemon`.\n *\n * Three launch shapes:\n * - compiled Bun binary — `Bun.main` is the virtual `/$bunfs/…` entry the\n * binary self-inserts as argv[1], so the spawn must NOT repeat it\n * (verified: repeating it makes the child parse the junk path as its\n * subcommand and boot the full TUI instead of the daemon).\n * - dev (`bun run cli.ts`) — the script path must ride along.\n * - Node hosts (no `Bun` global) — fall back to the published bin.\n */\nexport function selfDaemonSpawnCommand(): string[] {\n const bunMain = (globalThis as { Bun?: { main?: string } }).Bun?.main\n // Compiled binary: the virtual entry parses its own `daemon` subcommand.\n if (bunMain?.startsWith('/$bunfs'))\n return [process.execPath, 'daemon']\n // Dev-from-source: only re-execute `Bun.main` when it's actually a\n // zidane CLI entry (`cli.ts`/`cli.js`). For any other embedder (library\n // consumer, script) re-running their entry with a `daemon` argv would\n // launch an arbitrary program — fall back to the published bin instead.\n if (bunMain && /(?:^|[/\\\\])cli\\.[cm]?[jt]s$/.test(bunMain))\n return [process.execPath, bunMain, 'daemon']\n return ['zidane-daemon']\n}\n\n/**\n * True when something accepts connections on `socketPath`. Distinguishes a\n * live daemon from a stale socket file left by a crashed one.\n * @param socketPath - Path of the socket to try.\n * @param timeoutMs - How long to wait for the connection, in milliseconds.\n */\nexport function probeSocket(socketPath: string, timeoutMs = 1000): Promise<boolean> {\n return new Promise((resolve) => {\n /*\n * Skip the connect entirely unless the path is an actual socket file.\n * A stale REGULAR file (crashed daemon) or a missing path both mean\n * \"nothing listening\" — and connecting to them can surface a noisy\n * async ENOENT/ENOTSOCK that some runtimes treat as uncaught.\n */\n try {\n if (!existsSync(socketPath) || !statSync(socketPath).isSocket()) {\n resolve(false)\n return\n }\n }\n catch {\n resolve(false)\n return\n }\n\n let socket: ReturnType<typeof connect>\n try {\n socket = connect(socketPath)\n }\n catch {\n resolve(false)\n return\n }\n const done = (alive: boolean): void => {\n socket.destroy()\n resolve(alive)\n }\n socket.once('connect', () => done(true))\n socket.once('error', () => done(false))\n socket.setTimeout(timeoutMs, () => done(false))\n })\n}\n\n/** Options for {@link ensureDaemonRunning}. */\nexport interface EnsureDaemonOptions {\n /**\n * Socket the daemon listens on.\n * @default defaultSocketPath()\n */\n socketPath?: string\n /**\n * Command used to spawn the daemon when nothing is listening. Defaults to\n * `['zidane-daemon']` (the published bin). The child is detached and\n * unref'd — it outlives the spawning client by design.\n */\n spawnCommand?: string[]\n /** Total time to wait for the daemon to accept connections. Default 8000 ms. */\n timeoutMs?: number\n}\n\n/** Daemon starts in flight, by socket path, so concurrent callers share one. */\nconst pendingStarts = new Map<string, Promise<string>>()\n\n/**\n * Makes sure a daemon is listening on the socket, spawning one when nothing is. Concurrent calls for the same socket share one start.\n * Rejects when the spawn fails or the daemon does not accept connections within `timeoutMs`.\n * @param options - Socket, spawn command and timeout.\n * @returns The socket path once a connection succeeds.\n */\nexport function ensureDaemonRunning(options: EnsureDaemonOptions = {}): Promise<string> {\n const socketPath = options.socketPath ?? defaultSocketPath()\n const pending = pendingStarts.get(socketPath)\n if (pending)\n return pending\n const start = startDaemon({ ...options, socketPath }).finally(() => pendingStarts.delete(socketPath))\n pendingStarts.set(socketPath, start)\n return start\n}\n\nasync function startDaemon(options: EnsureDaemonOptions & { socketPath: string }): Promise<string> {\n const socketPath = options.socketPath\n if (await probeSocket(socketPath))\n return socketPath\n\n const [cmd, ...args] = options.spawnCommand ?? ['zidane-daemon']\n const child = spawn(cmd!, args, {\n detached: true,\n stdio: 'ignore',\n env: { ...process.env, ZIDANE_DAEMON_SOCKET: socketPath },\n })\n /*\n * A missing binary (e.g. `zidane-daemon` not on PATH) emits an async\n * `error` event; without a listener Node rethrows it as an uncaught\n * exception and crashes the caller. Capture it and surface a clean\n * rejection through the poll loop below.\n */\n let spawnError: Error | undefined\n child.once('error', (err: Error) => {\n spawnError = err\n })\n child.unref()\n\n const deadline = Date.now() + (options.timeoutMs ?? 8000)\n for (;;) {\n if (spawnError !== undefined)\n throw new Error(`Failed to spawn zidane daemon (${cmd}): ${spawnError.message}`)\n if (await probeSocket(socketPath))\n return socketPath\n if (Date.now() > deadline)\n throw new Error(`zidane daemon did not come up on ${socketPath} within ${options.timeoutMs ?? 8000}ms.`)\n await new Promise(r => setTimeout(r, 150))\n }\n}\n","/**\n * The client side every wire transport shares (the unix socket in\n * `../client.ts`, the WebSocket in `./ws-client.ts`): the `ipc.hello`\n * handshake with its version fences, typed requests with deadlines,\n * notifications, and signal fan-out. A transport supplies a connected\n * JSON-RPC {@link ClientChannel}; this turns it into an {@link IpcClient}.\n */\nimport type { AcpPeer, JsonRpcMessage } from '../../acp/types'\nimport type { HelloResult, HostSignal, IpcMethodName, IpcParams, IpcResult, SignalMeta } from '../protocol'\nimport { JsonRpcConnectionClosedError, JsonRpcRemoteError } from '../../acp/json-rpc'\nimport { compareVersions, ZIDANE_VERSION } from '../../version'\nimport { IPC_BUILD_ID } from '../daemon/lifecycle'\nimport { IPC_PROTOCOL_VERSION, IpcErrorCode, PING_METHOD, SIGNAL_SEQ_FIELD } from '../protocol'\n\n/** A connected host client, shared by every transport. Requests are checked against {@link IpcMethods}. */\nexport interface IpcClient {\n /** The host's answer to the `ipc.hello` handshake. */\n hello: HelloResult\n /**\n * Calls a host method and resolves with its result. Rejects with a `JsonRpcRemoteError` when the host answers with an error, or with `JsonRpcConnectionClosedError` when the connection closes first.\n * A request not answered within its deadline (30 s, longer for slow methods) rejects and drops the connection. `machine.call` has no deadline.\n */\n request: <M extends IpcMethodName>(method: M, params: IpcParams<M>) => Promise<IpcResult<M>>\n /**\n * Fire-and-forget notification — same method table as `request`, but no\n * id, no response, no pending-promise bookkeeping. The hot path for\n * high-frequency writes (per-keystroke draft sync). Daemons that don't\n * know the method ignore it silently.\n */\n notify: <M extends IpcMethodName>(method: M, params: IpcParams<M>) => void\n /** Host signals, with their number in their session when they have one (`SignalMeta`). */\n onSignal: (cb: SignalListener) => () => void\n /** Closes the connection. Pending requests reject. */\n close: () => void\n}\n\nexport type SignalListener = (signal: HostSignal, meta?: SignalMeta) => void\n\n/** Thrown when the host refuses the handshake because its protocol, build or code version does not match the client's. */\nexport class IpcVersionMismatchError extends Error {\n /**\n * @param message - Description of the mismatch.\n * @param daemon - The host's handshake data, when it sent any.\n */\n constructor(message: string, readonly daemon?: HelloResult) {\n super(message)\n this.name = 'IpcVersionMismatchError'\n }\n}\n\n/** A connected JSON-RPC link to a host, as a transport hands it over. */\nexport interface ClientChannel {\n rpc: Pick<AcpPeer, 'sendRequest' | 'sendNotification'>\n /** The caller's `close()`: quiet (no `onClose`), fails what is in flight. Idempotent. */\n close: () => void\n /** Drop a wedged connection: like the host going away, so the transport's `onClose` fires. */\n abort: () => void\n /** Where the host is, for error messages (a socket path, a URL). */\n where: string\n}\n\nexport interface ClientCoreOptions {\n appVersion?: string\n /**\n * When the host refuses the handshake on a version / build mismatch:\n * `always` asks it to drain so a matching one can be spawned (session\n * controllers on the local daemon); `stale-only` only when the host is\n * provably older (passive local clients); `never` leaves it running (a\n * remote server's lifetime is not the client's to end).\n */\n drain: 'always' | 'stale-only' | 'never'\n /**\n * Send this build's fingerprint, so a host from an older build of the same\n * protocol refuses us. Off sends the `'unknown'` sentinel: clients that\n * can't act on a build mismatch (no drain, no respawn) opt out of that fence.\n */\n buildFence: boolean\n /**\n * Ping the host (`$/ping`) this often once attached; two pings in a row\n * left unanswered at the next tick abort the connection (the transport's\n * `onClose` fires, its owner reconnects). Off when absent or `0`: a\n * local socket learns of a dead host from the kernel.\n */\n heartbeatMs?: number\n}\n\n/** Missed heartbeats after which the host is considered gone. */\nexport const HEARTBEAT_MISSES = 2\n\n/** Starts the client heartbeat on `channel`; returns its stop. Stops itself once the channel closes. */\nfunction startHeartbeat(channel: ClientChannel, intervalMs: number): () => void {\n let awaiting = false\n let missed = 0\n const timer = setInterval(() => {\n if (awaiting && ++missed >= HEARTBEAT_MISSES) {\n clearInterval(timer)\n channel.abort()\n return\n }\n awaiting = true\n channel.rpc.sendRequest(PING_METHOD, {}).then(\n () => {\n awaiting = false\n missed = 0\n },\n (err: unknown) => {\n if (err instanceof JsonRpcConnectionClosedError) {\n clearInterval(timer)\n return\n }\n // Any answer, an error included (a host predating `$/ping`), proves it alive.\n awaiting = false\n missed = 0\n },\n )\n }, intervalMs)\n timer.unref?.()\n return () => clearInterval(timer)\n}\n\n/** The signal listeners of one client, and the JSON-RPC notification handler that feeds them. */\nexport function createSignalListeners() {\n const listeners = new Set<SignalListener>()\n return {\n listeners,\n onNotification(message: JsonRpcMessage & { method: string }): void {\n if (message.method !== 'ipc.signal')\n return\n const params = (message as { params?: HostSignal & { [SIGNAL_SEQ_FIELD]?: unknown } }).params\n if (!params)\n return\n const { [SIGNAL_SEQ_FIELD]: seq, ...signal } = params\n const meta = typeof seq === 'number' ? { seq } : undefined\n for (const cb of listeners)\n cb(signal as HostSignal, meta)\n },\n }\n}\n\nfunction hostRelease(hello: HelloResult): string | undefined {\n for (const value of [hello.codeVersion, hello.appVersion]) {\n if (typeof value !== 'string')\n continue\n const match = /^(?:zidane-gui\\/)?(v?\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)$/.exec(value.trim())\n if (match)\n return match[1]\n }\n return undefined\n}\n\nconst HELLO_TIMEOUT_MS = 5000\nconst LONG_REQUEST_TIMEOUT_MS = 10 * 60 * 1000\n/** RPCs that legitimately run long — full compaction, index-backed cross-session search. */\nconst LONG_REQUEST_METHODS = new Set<string>(['sessions.compact', 'history.searchAll', 'history.searchSession'])\n\n/**\n * A refused handshake. Asks the host to drain when `drain` allows it, closes\n * the channel, and returns the error to throw.\n */\nasync function refusedHello(channel: ClientChannel, err: JsonRpcRemoteError, drain: ClientCoreOptions['drain']): Promise<IpcVersionMismatchError> {\n /*\n * Ask the stale daemon to drain so the caller can respawn a matching\n * one. AWAIT the reply (bounded) before tearing the connection down —\n * closing discards unflushed writes, so a fire-and-forget\n * `daemon.shutdown` could be dropped, leaving the stale daemon alive\n * and defeating `connectOrSpawnDaemon`'s retry.\n */\n const daemon = (err.data as { daemon?: HelloResult } | undefined)?.daemon\n // A network server's lifetime is never a client's to end, whatever `drain` says.\n if (daemon?.hostKind === 'server') {\n channel.close()\n return new IpcVersionMismatchError(serverMismatchMessage(daemon, channel.where) ?? err.message, daemon)\n }\n /*\n * Passive clients (`stale-only`) never retire a daemon on a protocol /\n * build mismatch — it may be NEWER than them. A daemon that is provably\n * OLDER — an older release, or the same release speaking an older\n * protocol (a dev build from before a protocol bump) — is the case where\n * draining is always safe: nothing newer can be running on it, and\n * whoever attaches next spawns a current one. Without this a passive\n * client (the GUI) would sit in-process forever behind a stale daemon\n * nobody restarts: every retry is refused.\n */\n const daemonIsOlderRelease\n = typeof daemon?.codeVersion === 'string' && compareVersions(daemon.codeVersion, ZIDANE_VERSION) < 0\n const release = daemon && hostRelease(daemon)\n const daemonIsNewerRelease = release !== undefined && compareVersions(release, ZIDANE_VERSION) > 0\n const daemonSpeaksOlderProtocol = typeof daemon?.protocolVersion === 'number' && daemon.protocolVersion < IPC_PROTOCOL_VERSION\n const daemonIsStale = daemonIsOlderRelease || daemonSpeaksOlderProtocol\n if (drain !== 'never' && !daemonIsNewerRelease && (drain === 'always' || daemonIsStale)) {\n // `whenIdle`: a daemon with live runs defers its exit instead of\n // aborting them (older daemons ignore the flag and drain now).\n await Promise.race([\n channel.rpc.sendRequest('daemon.shutdown', { whenIdle: true }).catch(() => undefined),\n new Promise(resolve => setTimeout(resolve, 1500)),\n ])\n }\n channel.close()\n return new IpcVersionMismatchError(err.message, daemon)\n}\n\n/** A server's protocol refusal, from this client's side: both versions and which side to upgrade. */\nfunction serverMismatchMessage(server: HelloResult, where: string): string | null {\n if (typeof server.protocolVersion !== 'number' || server.protocolVersion === IPC_PROTOCOL_VERSION)\n return null\n const upgrade = server.protocolVersion < IPC_PROTOCOL_VERSION ? 'server' : 'client'\n return `Protocol version mismatch with ${where}: the server speaks v${server.protocolVersion}, this client v${IPC_PROTOCOL_VERSION}. Upgrade the ${upgrade}.`\n}\n\nasync function handshake(channel: ClientChannel, options: ClientCoreOptions): Promise<HelloResult> {\n /*\n * Bound the handshake: a wedged daemon (accepting connections but never\n * reading — e.g. SIGSTOP'd) would otherwise hang this promise forever.\n * On timeout the channel is closed and the connect rejects.\n * `Promise.race` keeps the losing request promise handled, so the\n * connection-closed rejection that follows `close()` can't surface as an\n * unhandled rejection.\n */\n let helloTimer: ReturnType<typeof setTimeout> | undefined\n try {\n return (await Promise.race([\n channel.rpc.sendRequest('ipc.hello', {\n protocolVersion: IPC_PROTOCOL_VERSION,\n appVersion: options.appVersion ?? 'dev',\n pid: process.pid,\n // Build fingerprint: a daemon started from an OLDER build of this\n // same protocol version rejects us, gets drained, and respawns —\n // dev daemons cannot run stale code silently. Clients that\n // cannot act on a build mismatch send the 'unknown' sentinel\n // instead (see `buildFence`): fencing them would just lock them\n // out until someone else retires the host. They stay fenced by\n // protocol version and degrade via `optional()` like any client of\n // an older daemon.\n buildId: options.buildFence ? IPC_BUILD_ID : 'unknown',\n // Always sent, passive clients included: a stale-RELEASE daemon is\n // safe for anyone to retire (see `refusedHello`).\n codeVersion: ZIDANE_VERSION,\n }),\n new Promise<never>((_, reject) => {\n helloTimer = setTimeout(\n () => reject(new Error(`Daemon handshake timed out after ${HELLO_TIMEOUT_MS}ms on ${channel.where}.`)),\n HELLO_TIMEOUT_MS,\n )\n }),\n ])) as HelloResult\n }\n catch (err) {\n if (err instanceof JsonRpcRemoteError && err.code === IpcErrorCode.VersionMismatch)\n throw await refusedHello(channel, err, options.drain)\n channel.close()\n throw err\n }\n finally {\n clearTimeout(helloTimer)\n }\n}\n\n/** Handshake over `channel`, then the typed client around it. On failure the channel is closed. */\nexport async function openIpcClient(\n channel: ClientChannel,\n listeners: Set<SignalListener>,\n options: ClientCoreOptions,\n): Promise<IpcClient> {\n const hello = await handshake(channel, options)\n\n const release = hostRelease(hello)\n // A `retiring` daemon is fence-aware and already exits once idle; staying\n // attached until then keeps its live runs reachable from this window. A\n // network server is never retired for a client: an older one is attached\n // to, and this client degrades (`optional()`) like an older client does.\n if (!hello.retiring && hello.hostKind !== 'server' && release !== undefined && compareVersions(release, ZIDANE_VERSION) < 0) {\n // Pre-fence daemons accept hello without checking codeVersion. Disconnect\n // only: shutting down a successfully attached daemon can abort active runs.\n channel.close()\n throw new IpcVersionMismatchError(\n `Daemon release ${release} is older than client ${ZIDANE_VERSION}. Restart the daemon when active runs have finished.`,\n hello,\n )\n }\n\n // Env override is a test seam (process-level e2e exercises the real\n // deadline without waiting 30 s); undocumented, not a user setting.\n const requestTimeoutMs = Number(process.env.ZIDANE_IPC_REQUEST_TIMEOUT_MS) || 30_000\n\n const stopHeartbeat = options.heartbeatMs ? startHeartbeat(channel, options.heartbeatMs) : () => undefined\n\n return {\n hello,\n notify: (method, params) => {\n try {\n channel.rpc.sendNotification(method, params)\n }\n catch {\n // fire-and-forget by contract — a dying connection surfaces via onClose\n }\n },\n request: async (method, params) => {\n /*\n * Per-request deadline: close rejection covers a DEAD host, but a\n * wedged one (alive, not reading — the same class the server-side\n * backpressure bound handles for peers) would hang callers forever.\n * On timeout the connection is aborted (NOT `close()` — the `onClose`\n * path must fire so reconnect logic takes over) and every pending\n * request rejects via the close sweep. Known long-running RPCs get a\n * generous budget; tearing the connection down mid-`sessions.compact`\n * would abort real work, so those bounds are deliberately loose.\n */\n // A machine call lasts as long as its command (`docs/MACHINES.md`); a dead host is the heartbeat's to catch.\n if (method === 'machine.call')\n return channel.rpc.sendRequest(method, params) as Promise<IpcResult<typeof method>>\n const timeoutMs = LONG_REQUEST_METHODS.has(method) ? LONG_REQUEST_TIMEOUT_MS : requestTimeoutMs\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n return (await Promise.race([\n channel.rpc.sendRequest(method, params),\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(new Error(`Daemon request ${method} timed out after ${timeoutMs}ms, closing the connection.`))\n channel.abort()\n }, timeoutMs)\n timer.unref?.()\n }),\n ])) as IpcResult<typeof method>\n }\n finally {\n clearTimeout(timer)\n }\n },\n onSignal: (cb) => {\n listeners.add(cb)\n return () => listeners.delete(cb)\n },\n close: () => {\n stopHeartbeat()\n channel.close()\n },\n }\n}\n","/** Stable hashes of sync values: equal values (whatever their key order) hash the same, on both sides. */\nimport { createHash } from 'node:crypto'\n\nexport const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** JSON with sorted keys (undefined members dropped). */\nexport function stableStringify(value: unknown): string {\n if (Array.isArray(value))\n return `[${value.map(stableStringify).join(',')}]`\n if (isRecord(value))\n return `{${Object.keys(value).sort().filter(k => value[k] !== undefined).map(k => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`\n return JSON.stringify(value) ?? 'undefined'\n}\n\nexport function hashValue(value: unknown): string {\n return createHash('sha256').update(stableStringify(value)).digest('hex')\n}\n","/** Which settings the `settings` sync category carries (`./types`). */\nimport type { Settings } from '../types'\nimport { HOST_ENABLED_LISTS, isWindowOnlySetting } from '../host-scoped-settings'\n\n/**\n * Host-scoped settings that still don't sync: they describe this computer\n * (its environments, its daemon, remote hosts on it) or would turn off what\n * the server has that this computer doesn't (the enabled allowlists, its OS\n * sandbox). Every `sandbox*` key, present and future, is the server's own:\n * a client turning it off there would be a sandbox escape.\n */\nconst NOT_SYNCED_SETTINGS: ReadonlySet<string> = new Set<string>(['environments', 'useDaemon', 'remoteHosts', ...HOST_ENABLED_LISTS])\n\n// The same legacy `safeMode` mapping as the live settings wire (`settings.get` / `settings.set`).\nexport { settingsFromWire, settingsToWire } from '../host-scoped-settings'\n\n/** A setting the `settings` category carries: host-scoped (never window-only) and not machine-specific. */\nexport function isSyncableSetting(key: keyof Settings | string): boolean {\n return !isWindowOnlySetting(key) && !NOT_SYNCED_SETTINGS.has(key) && !key.startsWith('sandbox')\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { a as errorResponse, o as successResponse, r as createJsonRpcConnection } from "./json-rpc-G0rBfEar.js";
2
- import { d as legacySocketPath, f as probeSocket } from "./settings-keys-QoGJ5n3e.js";
2
+ import { d as legacySocketPath, f as probeSocket } from "./settings-keys-CnrJgT1T.js";
3
3
  import { l as SIGNAL_SEQ_FIELD, n as IpcErrorCode } from "./protocol-Do_i6Ras.js";
4
- import { g as createPacedWriter, m as connectIpcClient, t as createHostDispatcher } from "./dispatcher-Dclv6HXn.js";
4
+ import { g as createPacedWriter, m as connectIpcClient, t as createHostDispatcher } from "./dispatcher-D_9_QFO3.js";
5
5
  import { Buffer } from "node:buffer";
6
6
  import { chmodSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
7
7
  import { basename, delimiter, dirname } from "node:path";
@@ -452,4 +452,4 @@ function fixPath() {
452
452
  //#endregion
453
453
  export { startIpcDaemon as a, shellPathAsync as i, mergePath as n, shellPath as r, fixPath as t };
454
454
 
455
- //# sourceMappingURL=shell-path-BXXtRmEA.js.map
455
+ //# sourceMappingURL=shell-path-rJDa0OMv.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shell-path-BXXtRmEA.js","names":[],"sources":["../src/ipc/transports/peer-rpc.ts","../src/ipc/transports/send-budget.ts","../src/ipc/daemon/server.ts","../src/ipc/daemon/shell-path.ts"],"sourcesContent":["/**\n * The host side of one JSON-RPC connection, for every wire transport (the\n * unix socket in `../daemon/server.ts`, the WebSocket in `./ws-server.ts`):\n * a dispatcher {@link HostPeer} whose signals go out as `ipc.signal`\n * notifications, and the request / notification handlers that hand incoming\n * messages to the dispatcher. Framing and the write-backpressure bound\n * (`./send-budget`) stay with the transport.\n *\n * A signal is serialized once per fan-out, not once per peer: the first peer\n * builds the notification text from the signal's JSON and every other peer\n * of the fan-out sends the same string ({@link signalFrame}).\n */\nimport type { JsonRpcConnectionOptions } from '../../acp/json-rpc'\nimport type { AcpPeer, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse } from '../../acp/types'\nimport type { HostDispatcher, HostPeer } from '../daemon/dispatcher'\nimport type { HostSignal } from '../protocol'\nimport { errorResponse, successResponse } from '../../acp/json-rpc'\nimport { SIGNAL_SEQ_FIELD } from '../protocol'\n\n/**\n * Per-peer write backpressure bound. A wedged peer (e.g. a SIGSTOP'd\n * terminal) stops reading, so fan-out writes pile up in the transport's\n * send buffer without bound and can OOM the host. A peer buffering more\n * than this is dropped: the protocol supports re-attach + backfill, so\n * killing a wedged peer is safe and self-healing.\n */\nexport const PEER_WRITE_HIGH_WATERMARK = 8 * 1024 * 1024\n\nexport interface RpcPeerOptions {\n /** The connection's JSON-RPC side; signals are dropped until it exists. */\n rpc: () => (Pick<AcpPeer, 'sendNotification'> & { sendSerialized?: (json: string, message: JsonRpcMessage) => void }) | undefined\n}\n\n/** What the send budget sees of a pre-serialized signal: a notification, never a reply. */\nconst SIGNAL_MESSAGE: JsonRpcMessage = { jsonrpc: '2.0', method: 'ipc.signal' }\n\n/**\n * The `ipc.signal` notification text for a signal whose JSON is `json`,\n * byte for byte what serializing `{ jsonrpc, method, params: { ...signal,\n * [SIGNAL_SEQ_FIELD]: seq } }` gives; `undefined` when splicing could not\n * promise that (an empty or non-object JSON, a signal that has the field).\n */\nexport function signalFrame(signal: HostSignal, json: string, seq: number | undefined): string | undefined {\n if (seq === undefined)\n return `{\"jsonrpc\":\"2.0\",\"method\":\"ipc.signal\",\"params\":${json}}`\n if (json.length <= 2 || json[0] !== '{' || !json.endsWith('}') || Object.hasOwn(signal, SIGNAL_SEQ_FIELD))\n return undefined\n return `{\"jsonrpc\":\"2.0\",\"method\":\"ipc.signal\",\"params\":${json.slice(0, -1)},\"${SIGNAL_SEQ_FIELD}\":${seq}}}`\n}\n\nexport function createRpcPeer(options: RpcPeerOptions): HostPeer {\n return {\n ready: false,\n watching: null,\n send(signal, seq, encoded) {\n try {\n const rpc = options.rpc()\n if (!rpc)\n return\n if (encoded && rpc.sendSerialized) {\n const json = encoded.json()\n if (json === undefined)\n return // Not JSON-able: no peer can receive it.\n encoded.frame ??= signalFrame(signal, json, seq)\n if (encoded.frame !== undefined) {\n rpc.sendSerialized(encoded.frame, SIGNAL_MESSAGE)\n return\n }\n }\n rpc.sendNotification('ipc.signal', seq === undefined ? signal : { ...signal, [SIGNAL_SEQ_FIELD]: seq })\n }\n catch {\n // A dying connection (or a signal JSON can't encode) must never take the host down with it.\n }\n },\n }\n}\n\n/** The JSON-RPC handlers that route one peer's requests and notifications to the dispatcher. */\nexport function dispatcherHandlers(dispatcher: HostDispatcher, peer: HostPeer): Pick<JsonRpcConnectionOptions, 'onRequest' | 'onNotification'> {\n return {\n onRequest: async (message: JsonRpcRequest): Promise<JsonRpcResponse> => {\n const outcome = await dispatcher.handleRequest(message.method, message.params, peer)\n return outcome.ok\n ? successResponse(message.id, outcome.result)\n : errorResponse(message.id, outcome.code, outcome.message, outcome.data)\n },\n onNotification: message => dispatcher.handleNotification(message.method, message.params, peer),\n }\n}\n","/**\n * One connection's outgoing budget, for every wire transport (the unix\n * socket in `../daemon/server.ts`, the WebSocket in `./ws-server.ts`).\n *\n * The slow-consumer bound is about pushed signals piling up for a peer that\n * stopped reading, not about one big reply it asked for: reply bytes still\n * queued are set aside (`replyReserve`) when the backlog is measured. The\n * buffer drains in order, so the reserve can only shrink to what is still\n * buffered; a peer that never reads keeps it whole and is dropped once the\n * signals behind it pass the bound. A reply itself is bounded by the message\n * size (over it, an `IpcErrorCode.ResponseTooLarge` error goes instead and\n * the connection stays), and the whole buffer by a hard cap for memory.\n *\n * The reserve is an approximation: it assumes the buffer drains oldest\n * first and only clamps it to what is still buffered, so after a partial\n * drain it may still count reply bytes that already left, and the signals\n * backlog reads low by at most that much. The memory bound, precisely: a\n * reply leaves the whole buffer at most `H = maxBuffered + 2 × maxMessage`\n * (checked after its write), and every signal re-clamps the reserve to what\n * is buffered, so the reserve never exceeds what the buffer held at the\n * first signal after the last reply (at most `H` plus that signal). A\n * signal leaves the measured backlog at most `maxBuffered`, so between\n * sends a peer holds at most `maxBuffered + H = 2 × maxBuffered + 2 ×\n * maxMessage` bytes (plus one signal). The write that crosses a bound is\n * the last: the peer is marked stuck and nothing more goes out. That is the\n * only backlog check: the buffer grows through `send`, so a backlog\n * over the bound is always seen by the write that made it.\n */\nimport type { JsonRpcMessage } from '../../acp/types'\nimport { Buffer } from 'node:buffer'\nimport { errorResponse } from '../../acp/json-rpc'\nimport { IpcErrorCode } from '../protocol'\n\n/** What the budget writes to: the transport's buffer and its framing. */\nexport interface SendTarget {\n /** Bytes written but not yet handed to the OS. */\n buffered: () => number\n /** Write one serialized message (the target frames it); `false`: the transport dropped it. */\n write: (json: string) => boolean\n}\n\nexport interface SendLimits {\n /** Signals backlog past which the peer is stuck (replies set aside). */\n maxBuffered: number\n /** Largest reply; a bigger one is answered with `ResponseTooLarge`. */\n maxMessage: number\n /** The peer cannot keep up: tear it down (called once). */\n stuck: (why: string) => void\n log: (line: string) => void\n /** Log prefix, e.g. `[ipc-ws]`. */\n label: string\n}\n\nexport interface PeerSend {\n /** Bytes buffered that are not reply bytes: what the slow-consumer bound measures. */\n backlog: () => number\n /** Send one message (the JSON-RPC endpoint's `send`): replies and the rest apart. */\n send: (json: string, message: JsonRpcMessage) => void\n /** Give up on the peer (once): {@link SendLimits.stuck} runs and nothing more goes out. */\n markStuck: (why: string) => void\n}\n\nfunction isReply(message: JsonRpcMessage): boolean {\n return 'id' in message && message.id !== undefined && message.id !== null && ('result' in message || 'error' in message)\n}\n\nexport function createPeerSend(target: SendTarget, limits: SendLimits): PeerSend {\n const { maxBuffered, maxMessage, log, label } = limits\n let replyReserve = 0\n let stuck = false\n const markStuck = (why: string): void => {\n if (stuck)\n return\n stuck = true\n limits.stuck(why)\n }\n const backlog = (): number => {\n const buffered = target.buffered()\n replyReserve = Math.min(replyReserve, buffered)\n return buffered - replyReserve\n }\n const send = (json: string, message: JsonRpcMessage): void => {\n if (stuck)\n return\n if (!isReply(message)) {\n if (!target.write(json) || backlog() > maxBuffered)\n markStuck(`more than ${maxBuffered} bytes of signals unsent`)\n return\n }\n let text = json\n let bytes = Buffer.byteLength(text)\n if (bytes > maxMessage) {\n const id = (message as { id: string | number }).id\n log(`${label} reply to ${String(id)} is ${bytes} bytes (> ${maxMessage}): answering with an error instead`)\n text = JSON.stringify(errorResponse(id, IpcErrorCode.ResponseTooLarge, `The reply is over ${maxMessage} bytes: ask for less (a transcript: a smaller page).`, { bytes, maxBytes: maxMessage }))\n bytes = Buffer.byteLength(text)\n }\n if (!target.write(text)) {\n markStuck('reply dropped by the transport')\n return\n }\n replyReserve += bytes\n if (target.buffered() > maxBuffered + 2 * maxMessage)\n markStuck(`more than ${maxBuffered + 2 * maxMessage} bytes unsent`)\n }\n return { backlog, send, markStuck }\n}\n","/**\n * Unix-socket JSON-RPC server around a `ChatHost`.\n *\n * One `net.Server`, one JSON-RPC connection per peer (NDJSON framing: a\n * `net.Socket` is both the Readable and the Writable). The method table,\n * per-peer state, signal fan-out and the `ipc.hello` handshake live in the\n * transport-free dispatcher (`./dispatcher`); this module owns only what is\n * specific to the socket and the process: framing, the socket file and its\n * permissions, idle exit, retiring, and draining. Write backpressure (the\n * slow-consumer bound, the reply size) is shared with the WebSocket server\n * (`../transports/send-budget`).\n *\n * Handshake: a peer's first request MUST be `ipc.hello` carrying its\n * protocol version. Mismatched peers get a structured error (code\n * `IpcErrorCode.VersionMismatch`) and can ask the daemon to drain via `daemon.shutdown`\n * before respawning a fresh one.\n */\nimport type { Server, Socket } from 'node:net'\nimport type { JsonRpcConnection } from '../../acp/json-rpc'\nimport type { ChatHost } from '../../chat/host/engine'\nimport type { HostSignal } from '../protocol'\nimport type { HostDispatcher } from './dispatcher'\nimport type { DaemonEnvironment } from './environment'\nimport { chmodSync, existsSync, mkdirSync, unlinkSync } from 'node:fs'\nimport { createServer } from 'node:net'\nimport { basename, dirname } from 'node:path'\nimport { createJsonRpcConnection } from '../../acp/json-rpc'\nimport { connectIpcClient } from '../client'\nimport { createPacedWriter } from '../transports/paced-writer'\nimport { createRpcPeer, dispatcherHandlers, PEER_WRITE_HIGH_WATERMARK } from '../transports/peer-rpc'\nimport { createPeerSend } from '../transports/send-budget'\nimport { createHostDispatcher } from './dispatcher'\nimport { legacySocketPath, probeSocket } from './lifecycle'\n\n// Canonical home is the protocol module (clients import the codes without\n// pulling the server's module graph); re-exported here for compatibility.\nexport { IpcErrorCode } from '../protocol'\n\n/** Options for {@link startIpcDaemon}. */\nexport interface IpcDaemonOptions {\n /** Path of the unix socket to listen on. Its directory is created with mode 0700 and the socket gets mode 0600. */\n socketPath: string\n /**\n * Environment factory — the production daemon uses the disk-backed\n * `createDaemonEnvironment`; tests inject an in-memory one. Receives the\n * broadcast sink wired to peer fan-out.\n */\n createEnvironment?: (broadcast: (signal: HostSignal) => void) => DaemonEnvironment | Promise<DaemonEnvironment>\n /**\n * zidane version reported in the handshake.\n * @default 'dev'\n */\n appVersion?: string\n /**\n * Milliseconds the daemon may have zero peers and no running session before\n * it calls `onIdle`. Unset or 0: never. Ignored with a shared `dispatcher`.\n */\n idleShutdownMs?: number\n /** Called when the daemon has had no peers for `idleShutdownMs` and no session is active, or when it retires after a newer client attached mid-work. The CLI exits the process; embedded servers ignore it. */\n onIdle?: () => void\n /** Receives one diagnostic line per event, such as peers connecting. */\n onLog?: (line: string) => void\n /**\n * Serve this already-built host instead of creating one: another front-end\n * (zidane-server's WebSocket) serves it too. Its owner drains it and owns\n * its lifecycle, so `createEnvironment`, `appVersion` and the idle options\n * are ignored, and `close()` only closes this socket's connections.\n */\n dispatcher?: HostDispatcher\n /**\n * A peer with more than this of signals queued for sending (it stopped\n * reading) is dropped; it re-attaches and resumes. Replies are bounded by\n * `maxMessageBytes` instead (`../transports/send-budget`). Default\n * `PEER_WRITE_HIGH_WATERMARK` (8 MiB).\n */\n maxBufferedBytes?: number\n /** A bigger reply goes out as an `IpcErrorCode.ResponseTooLarge` error. Default {@link SOCKET_MAX_REPLY_BYTES}. */\n maxMessageBytes?: number\n}\n\n/**\n * Largest reply on the socket by default. Generous: the peer is this user's\n * own window on this machine, and whole-session reads (`sessions.get`) of a\n * long local session with images must keep fitting. The bound is there so\n * one reply cannot grow the write buffer without limit.\n */\nconst SOCKET_MAX_REPLY_BYTES = 256 * 1024 * 1024\n\n/** A running unix-socket daemon. */\nexport interface IpcDaemon {\n /** Path of the socket it listens on. */\n socketPath: string\n /** The chat host it serves. */\n host: ChatHost\n /** Number of connected peers. */\n peerCount: () => number\n /** Drains the host, disconnects every peer and removes the socket file. Calling it again returns the same promise. With a shared `dispatcher`, the host is left to its owner. */\n close: () => Promise<void>\n}\n\n/** One open socket connection; `close()` ends them all. */\ninterface SocketConnection {\n socket: Socket\n connection: JsonRpcConnection\n}\n\n/**\n * Starts a unix-socket daemon around a chat host. Each peer must open with `ipc.hello`.\n * A stale socket file left by a dead daemon is replaced. Throws when a live daemon already listens on `socketPath`.\n * @param options - Socket path, environment and lifecycle settings.\n * @returns The running daemon.\n * @example\n * ```ts\n * import { startIpcDaemon } from 'zidane/ipc'\n *\n * const daemon = await startIpcDaemon({\n * socketPath: '/tmp/zidane-demo/daemon/ipc.sock',\n * onLog: line => console.log(line),\n * })\n * console.log(daemon.peerCount())\n * await daemon.close()\n * ```\n */\nexport async function startIpcDaemon(options: IpcDaemonOptions): Promise<IpcDaemon> {\n const { socketPath } = options\n const log = options.onLog ?? (() => undefined)\n const startedAt = Date.now()\n const appVersion = options.appVersion ?? 'dev'\n const maxBuffered = options.maxBufferedBytes ?? PEER_WRITE_HIGH_WATERMARK\n const maxMessage = options.maxMessageBytes ?? SOCKET_MAX_REPLY_BYTES\n\n const connections = new Set<SocketConnection>()\n let idleTimer: ReturnType<typeof setTimeout> | undefined\n // See `retireWhenIdle`.\n let retiring = false\n let retireTimer: ReturnType<typeof setInterval> | undefined\n\n const shared = options.dispatcher\n const dispatcher = shared ?? await createHostDispatcher({\n ...(options.createEnvironment ? { createEnvironment: options.createEnvironment } : {}),\n appVersion,\n startedAt,\n identity: { kind: 'daemon' },\n lifecycle: {\n shutdown: () => {\n void close().then(() => options.onIdle?.())\n },\n retireWhenIdle,\n isRetiring: () => retiring,\n },\n })\n const host = dispatcher.host\n\n /* ----------------------------------------------------------------------\n * Socket plumbing\n * -------------------------------------------------------------------- */\n\n /*\n * Retire-when-idle: a NEWER client found this daemon mid-work. Draining\n * now would abort every window's runs (a dev edit under `src/ipc` or an\n * upgrade mid-`/review` did exactly that), so keep serving and exit at\n * the first idle point. Two consecutive idle polls: a queue drain is\n * scheduled just after a dispatch releases, and one poll could land in\n * that gap.\n */\n function retireWhenIdle(): void {\n if (retiring)\n return\n retiring = true\n log('[ipc-daemon] a newer client attached mid-work; retiring once idle')\n let idlePolls = 0\n retireTimer = setInterval(() => {\n if (host.hasActiveWork()) {\n idlePolls = 0\n return\n }\n idlePolls += 1\n if (idlePolls < 2)\n return\n clearInterval(retireTimer)\n void close().then(() => options.onIdle?.())\n }, 1000)\n }\n\n function armIdleTimer(): void {\n if (shared || !options.idleShutdownMs || !options.onIdle)\n return\n clearTimeout(idleTimer)\n idleTimer = setTimeout(() => {\n if (dispatcher.peerCount() > 0)\n return // Peers reconnected — the next last-disconnect re-arms.\n /*\n * Never abort a live run just because every window detached — `/exit`\n * is explicitly \"the run keeps going\". Re-arm and check again later.\n */\n if (host.hasActiveWork()) {\n armIdleTimer()\n return\n }\n options.onIdle?.()\n }, options.idleShutdownMs)\n }\n\n function handleConnection(socket: Socket): void {\n // Assigned right below; a signal can only reach this peer after `ipc.hello`, i.e. once it is set.\n let connection: JsonRpcConnection | undefined\n const writer = createPacedWriter(socket)\n const budget = createPeerSend(\n {\n buffered: writer.buffered,\n // NDJSON: `JSON.stringify` escapes newlines, so a message is one line.\n write: (json) => {\n writer.write(`${json}\\n`)\n return true\n },\n },\n {\n maxBuffered,\n maxMessage,\n log,\n label: '[ipc-daemon]',\n stuck: (why) => {\n log(`[ipc-daemon] dropping peer: ${why}`)\n socket.destroy()\n },\n },\n )\n const peer = createRpcPeer({ rpc: () => connection })\n\n const rpc = createJsonRpcConnection({\n input: socket,\n output: socket,\n send: budget.send,\n ...dispatcherHandlers(dispatcher, peer),\n onError: err => log(`[ipc-daemon] peer error: ${err.message}`),\n })\n\n connection = rpc\n const entry: SocketConnection = { socket, connection: rpc }\n connections.add(entry)\n dispatcher.addPeer(peer)\n rpc.start()\n log(`[ipc-daemon] peer connected (${dispatcher.peerCount()} total)`)\n\n const drop = (): void => {\n if (!connections.delete(entry))\n return\n rpc.close()\n dispatcher.removePeer(peer)\n log(`[ipc-daemon] peer disconnected (${dispatcher.peerCount()} left)`)\n if (dispatcher.peerCount() === 0)\n armIdleTimer()\n }\n\n socket.on('close', drop)\n socket.on('error', drop)\n }\n\n /*\n * Stale-socket recovery: a previous daemon that died without cleanup\n * leaves the socket file behind and `listen` would fail EADDRINUSE.\n * Probe before unlinking so a LIVE daemon is never yanked out from\n * under its peers.\n */\n if (existsSync(socketPath)) {\n if (await probeSocket(socketPath))\n throw new Error(`A zidane daemon is already listening on ${socketPath}.`)\n /*\n * Narrow the concurrent-spawn race: another daemon may have bound the\n * socket between the probe above and the unlink below (each probe is a\n * full connect attempt). Re-probe immediately before unlinking and\n * abort startup if something now answers — never yank a live daemon's\n * socket out from under its peers.\n */\n if (await probeSocket(socketPath))\n throw new Error(`A zidane daemon is already listening on ${socketPath}.`)\n unlinkSync(socketPath)\n }\n /*\n * The daemon speaks an unauthenticated RPC surface that can execute tools\n * as the daemon user — restrict access to the owner. The containing dir\n * is created 0700 (belt) and the socket is chmod'd to 0600 right after\n * `listen` (suspenders) so connectability never depends on ambient umask.\n *\n * `mkdirSync`'s mode only applies on CREATION — a pre-existing dir keeps\n * its old mode. When the dir is the DEDICATED daemon dir (the `daemon/`\n * subdir `defaultSocketPath` uses, or one we created fresh for an\n * explicit socket path) chmod it 0700 explicitly. A shared, pre-existing\n * dir (tests pointing the socket at a tmpdir) is left alone — never\n * tighten a directory that also holds unrelated state.\n */\n const socketDir = dirname(socketPath)\n const dedicatedSocketDir = basename(socketDir) === 'daemon' || !existsSync(socketDir)\n mkdirSync(socketDir, { recursive: true, mode: 0o700 })\n if (dedicatedSocketDir) {\n try {\n chmodSync(socketDir, 0o700)\n }\n catch {\n // Best-effort — mirrors the socket chmod below.\n }\n }\n\n const server: Server = createServer(handleConnection)\n await new Promise<void>((resolvePromise, reject) => {\n server.once('error', reject)\n server.listen(socketPath, () => {\n server.off('error', reject)\n resolvePromise()\n })\n })\n try {\n chmodSync(socketPath, 0o600)\n }\n catch {\n // Best-effort on platforms where socket chmod is a no-op (e.g. some\n // network filesystems); the 0700 dir still constrains access.\n }\n log(`[ipc-daemon] listening on ${socketPath}`)\n // We bound the socket, so we own the file — close() unlinks it exactly\n // once. Cleared after the unlink so a file later re-created by a NEW\n // daemon on the same path is never yanked by a stale close.\n let ownsSocketFile = true\n armIdleTimer()\n\n /*\n * Drain any daemon still serving the legacy socket path\n * (`<stateDir>/daemon.sock`). New clients never probe it,\n * so a `--idle-exit 0` service daemon there would run forever alongside\n * this one — two sets of live agents, run guards, and heartbeat writers\n * against the same sessions.db. Fire-and-forget: version mismatch is\n * the EXPECTED case (a legacy daemon speaks an older protocol) and\n * `drainOnMismatch: true` performs the shutdown for us; a same-version\n * survivor (explicit env overrides) gets an explicit daemon.shutdown.\n */\n const legacyPath = shared ? null : legacySocketPath(socketPath)\n if (legacyPath) {\n void (async () => {\n if (!(await probeSocket(legacyPath)))\n return\n log(`[ipc-daemon] draining legacy daemon on ${legacyPath}`)\n try {\n const survivor = await connectIpcClient({ socketPath: legacyPath, appVersion: `${appVersion}/legacy-drain`, drainOnMismatch: true })\n await survivor.request('daemon.shutdown', {}).catch(() => undefined)\n survivor.close()\n }\n catch {\n // IpcVersionMismatchError = drained by the handshake path; anything\n // else (raced exit, wedged socket) is best-effort — leave it.\n }\n })()\n }\n\n async function doClose(): Promise<void> {\n clearTimeout(idleTimer)\n clearInterval(retireTimer)\n // A shared host is its owner's to drain and detach (see `dispatcher`).\n if (!shared)\n await host.drain()\n // Stop accepting before closing what is open: a connection landing in\n // between would otherwise keep `server.close` waiting forever.\n const stopped = new Promise<void>((resolvePromise) => {\n server.close(() => resolvePromise())\n })\n for (const { connection, socket } of connections) {\n connection.close()\n socket.destroy()\n }\n connections.clear()\n if (!shared)\n dispatcher.detachAll()\n await stopped\n if (ownsSocketFile) {\n ownsSocketFile = false\n try {\n unlinkSync(socketPath)\n }\n catch {\n // already gone\n }\n }\n }\n\n /*\n * Idempotent close: `daemon.shutdown` runs close() and then onIdle, whose\n * CLI implementation calls daemon.close() again — both callers share one\n * drain instead of double-unlinking the socket. Memoized rather than\n * flag-guarded so the second caller awaits actual completion.\n */\n let closePromise: Promise<void> | null = null\n function close(): Promise<void> {\n closePromise ??= doClose()\n return closePromise\n }\n\n return {\n socketPath,\n host,\n peerCount: () => dispatcher.peerCount(),\n close,\n }\n}\n","/**\n * Login-shell PATH for processes not started from a terminal.\n *\n * A Dock/Finder-launched GUI, or a daemon it spawns, inherits launchd's PATH\n * (`/usr/bin:/bin:/usr/sbin:/sbin`), so `node` / `bun` / `yarn` are invisible\n * to agent shell commands. Ask the user's login shell for its PATH and merge\n * it into `process.env.PATH`.\n *\n * Merge, don't replace: a terminal launch may have prepended entries the rc\n * files know nothing about (`nvm use`, direnv, `PATH=... zidane`), and those\n * must survive. Idempotent, so every surface (GUI, daemon) can call it and a\n * daemon spawned by an already-fixed GUI ends up with the same PATH.\n */\nimport { spawn, spawnSync } from 'node:child_process'\nimport { userInfo } from 'node:os'\nimport { delimiter } from 'node:path'\nimport process from 'node:process'\n\nconst DELIMITER = '_ZIDANE_SHELL_ENV_'\n\n/** A slow rc file costs startup time; a prompting one must never hang the process. */\nconst DEFAULT_TIMEOUT_MS = 3_000\n\nfunction defaultShell(env: NodeJS.ProcessEnv): string {\n try {\n const { shell } = userInfo()\n if (shell)\n return shell\n }\n catch {}\n return env.SHELL || (process.platform === 'darwin' ? '/bin/zsh' : '/bin/sh')\n}\n\nfunction parsePath(stdout: string): string | undefined {\n const body = stdout.split(DELIMITER)[1]\n if (body === undefined)\n return undefined\n // eslint-disable-next-line no-control-regex\n const clean = body.replace(/\\u001B\\[[0-9;]*[A-Z]/gi, '')\n for (const line of clean.split('\\n')) {\n if (line.startsWith('PATH='))\n return line.slice('PATH='.length) || undefined\n }\n return undefined\n}\n\n/**\n * Options for {@link shellPath} and {@link shellPathAsync}. Without `shell`,\n * the user's shell is tried first, then POSIX fallbacks for non-POSIX defaults\n * (nushell, xonsh) whose `-ilc` differs. An explicit `shell` is tried alone.\n * `timeoutMs` bounds each shell. Default 3000.\n */\nexport interface ShellPathOptions { env?: NodeJS.ProcessEnv, timeoutMs?: number, shell?: string }\n\n// `command env` sidesteps aliases/functions named `env`.\nconst SHELL_ARGS = ['-ilc', `echo -n \"${DELIMITER}\"; command env; echo -n \"${DELIMITER}\"; exit`]\n\nfunction shellCandidates(options: ShellPathOptions): { shells: string[], env: NodeJS.ProcessEnv, timeout: number } {\n const env = options.env ?? process.env\n return {\n shells: options.shell ? [options.shell] : [...new Set([defaultShell(env), '/bin/zsh', '/bin/bash'])],\n timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n env: {\n ...env,\n // Keep oh-my-zsh from blocking on its update prompt / tmux autostart.\n DISABLE_AUTO_UPDATE: 'true',\n ZSH_TMUX_AUTOSTARTED: 'true',\n ZSH_TMUX_AUTOSTART: 'false',\n },\n }\n}\n\n/**\n * Reads the PATH of the user's interactive login shell, blocking until it answers.\n * Tries the user's shell, then `/bin/zsh` and `/bin/bash`. Returns `undefined` on Windows, or when no shell answers in time.\n * @param options - Shell, environment and timeout overrides.\n * @returns The PATH string, or `undefined`.\n */\nexport function shellPath(options: ShellPathOptions = {}): string | undefined {\n if (process.platform === 'win32')\n return undefined\n const { shells, env, timeout } = shellCandidates(options)\n for (const shell of shells) {\n const result = spawnSync(shell, SHELL_ARGS, {\n encoding: 'utf8',\n timeout,\n stdio: ['ignore', 'pipe', 'ignore'],\n env,\n })\n if (result.error || result.status !== 0)\n continue\n const path = parsePath(result.stdout)\n if (path)\n return path\n }\n return undefined\n}\n\n/**\n * {@link shellPath} without blocking the event loop: same shells, args,\n * timeout and parsing, run with `spawn`. For hosts (the GUI main\n * process) that can start on a cached PATH and refresh in the background.\n * @param options - Shell, environment and timeout overrides.\n * @returns The PATH string, or `undefined`.\n */\nexport async function shellPathAsync(options: ShellPathOptions = {}): Promise<string | undefined> {\n if (process.platform === 'win32')\n return undefined\n const { shells, env, timeout } = shellCandidates(options)\n for (const shell of shells) {\n const stdout = await new Promise<string | undefined>((resolve) => {\n // Same stdio as the sync variant: no stdin, stderr discarded.\n const child = spawn(shell, SHELL_ARGS, { stdio: ['ignore', 'pipe', 'ignore'], env })\n let out = ''\n let settled = false\n let timer: ReturnType<typeof setTimeout> | undefined\n const finish = (value: string | undefined): void => {\n if (settled)\n return\n settled = true\n clearTimeout(timer)\n resolve(value)\n }\n timer = setTimeout(() => {\n child.kill('SIGTERM')\n finish(undefined)\n }, timeout)\n child.stdout?.setEncoding('utf8')\n child.stdout?.on('data', (chunk: string) => {\n out += chunk\n })\n child.on('error', () => finish(undefined))\n child.on('close', code => finish(code === 0 ? out : undefined))\n })\n if (stdout === undefined)\n continue\n const path = parsePath(stdout)\n if (path)\n return path\n }\n return undefined\n}\n\n/**\n * Entries of `current` the shell doesn't know about (ad-hoc prepends from the\n * launching terminal) stay in front; then the shell's PATH in its own order.\n * From the Dock every launchd entry is already in the shell PATH, so the\n * result is exactly the shell PATH, with the shell's precedence intact.\n * @param current - The PATH of this process.\n * @param shell - The PATH reported by the login shell.\n * @returns The merged PATH string.\n */\nexport function mergePath(current: string | undefined, shell: string | undefined): string {\n const split = (path: string | undefined): string[] => (path ?? '').split(delimiter).filter(Boolean)\n const fromShell = split(shell)\n const known = new Set(fromShell)\n return [...new Set([...split(current).filter(entry => !known.has(entry)), ...fromShell])].join(delimiter)\n}\n\n/** Merge the login shell's PATH into `process.env.PATH`. No-op on Windows or when no shell answers. */\nexport function fixPath(): void {\n const extra = shellPath()\n if (extra)\n process.env.PATH = mergePath(process.env.PATH, extra)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shell-path-rJDa0OMv.js","names":[],"sources":["../src/ipc/transports/peer-rpc.ts","../src/ipc/transports/send-budget.ts","../src/ipc/daemon/server.ts","../src/ipc/daemon/shell-path.ts"],"sourcesContent":["/**\n * The host side of one JSON-RPC connection, for every wire transport (the\n * unix socket in `../daemon/server.ts`, the WebSocket in `./ws-server.ts`):\n * a dispatcher {@link HostPeer} whose signals go out as `ipc.signal`\n * notifications, and the request / notification handlers that hand incoming\n * messages to the dispatcher. Framing and the write-backpressure bound\n * (`./send-budget`) stay with the transport.\n *\n * A signal is serialized once per fan-out, not once per peer: the first peer\n * builds the notification text from the signal's JSON and every other peer\n * of the fan-out sends the same string ({@link signalFrame}).\n */\nimport type { JsonRpcConnectionOptions } from '../../acp/json-rpc'\nimport type { AcpPeer, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse } from '../../acp/types'\nimport type { HostDispatcher, HostPeer } from '../daemon/dispatcher'\nimport type { HostSignal } from '../protocol'\nimport { errorResponse, successResponse } from '../../acp/json-rpc'\nimport { SIGNAL_SEQ_FIELD } from '../protocol'\n\n/**\n * Per-peer write backpressure bound. A wedged peer (e.g. a SIGSTOP'd\n * terminal) stops reading, so fan-out writes pile up in the transport's\n * send buffer without bound and can OOM the host. A peer buffering more\n * than this is dropped: the protocol supports re-attach + backfill, so\n * killing a wedged peer is safe and self-healing.\n */\nexport const PEER_WRITE_HIGH_WATERMARK = 8 * 1024 * 1024\n\nexport interface RpcPeerOptions {\n /** The connection's JSON-RPC side; signals are dropped until it exists. */\n rpc: () => (Pick<AcpPeer, 'sendNotification'> & { sendSerialized?: (json: string, message: JsonRpcMessage) => void }) | undefined\n}\n\n/** What the send budget sees of a pre-serialized signal: a notification, never a reply. */\nconst SIGNAL_MESSAGE: JsonRpcMessage = { jsonrpc: '2.0', method: 'ipc.signal' }\n\n/**\n * The `ipc.signal` notification text for a signal whose JSON is `json`,\n * byte for byte what serializing `{ jsonrpc, method, params: { ...signal,\n * [SIGNAL_SEQ_FIELD]: seq } }` gives; `undefined` when splicing could not\n * promise that (an empty or non-object JSON, a signal that has the field).\n */\nexport function signalFrame(signal: HostSignal, json: string, seq: number | undefined): string | undefined {\n if (seq === undefined)\n return `{\"jsonrpc\":\"2.0\",\"method\":\"ipc.signal\",\"params\":${json}}`\n if (json.length <= 2 || json[0] !== '{' || !json.endsWith('}') || Object.hasOwn(signal, SIGNAL_SEQ_FIELD))\n return undefined\n return `{\"jsonrpc\":\"2.0\",\"method\":\"ipc.signal\",\"params\":${json.slice(0, -1)},\"${SIGNAL_SEQ_FIELD}\":${seq}}}`\n}\n\nexport function createRpcPeer(options: RpcPeerOptions): HostPeer {\n return {\n ready: false,\n watching: null,\n send(signal, seq, encoded) {\n try {\n const rpc = options.rpc()\n if (!rpc)\n return\n if (encoded && rpc.sendSerialized) {\n const json = encoded.json()\n if (json === undefined)\n return // Not JSON-able: no peer can receive it.\n encoded.frame ??= signalFrame(signal, json, seq)\n if (encoded.frame !== undefined) {\n rpc.sendSerialized(encoded.frame, SIGNAL_MESSAGE)\n return\n }\n }\n rpc.sendNotification('ipc.signal', seq === undefined ? signal : { ...signal, [SIGNAL_SEQ_FIELD]: seq })\n }\n catch {\n // A dying connection (or a signal JSON can't encode) must never take the host down with it.\n }\n },\n }\n}\n\n/** The JSON-RPC handlers that route one peer's requests and notifications to the dispatcher. */\nexport function dispatcherHandlers(dispatcher: HostDispatcher, peer: HostPeer): Pick<JsonRpcConnectionOptions, 'onRequest' | 'onNotification'> {\n return {\n onRequest: async (message: JsonRpcRequest): Promise<JsonRpcResponse> => {\n const outcome = await dispatcher.handleRequest(message.method, message.params, peer)\n return outcome.ok\n ? successResponse(message.id, outcome.result)\n : errorResponse(message.id, outcome.code, outcome.message, outcome.data)\n },\n onNotification: message => dispatcher.handleNotification(message.method, message.params, peer),\n }\n}\n","/**\n * One connection's outgoing budget, for every wire transport (the unix\n * socket in `../daemon/server.ts`, the WebSocket in `./ws-server.ts`).\n *\n * The slow-consumer bound is about pushed signals piling up for a peer that\n * stopped reading, not about one big reply it asked for: reply bytes still\n * queued are set aside (`replyReserve`) when the backlog is measured. The\n * buffer drains in order, so the reserve can only shrink to what is still\n * buffered; a peer that never reads keeps it whole and is dropped once the\n * signals behind it pass the bound. A reply itself is bounded by the message\n * size (over it, an `IpcErrorCode.ResponseTooLarge` error goes instead and\n * the connection stays), and the whole buffer by a hard cap for memory.\n *\n * The reserve is an approximation: it assumes the buffer drains oldest\n * first and only clamps it to what is still buffered, so after a partial\n * drain it may still count reply bytes that already left, and the signals\n * backlog reads low by at most that much. The memory bound, precisely: a\n * reply leaves the whole buffer at most `H = maxBuffered + 2 × maxMessage`\n * (checked after its write), and every signal re-clamps the reserve to what\n * is buffered, so the reserve never exceeds what the buffer held at the\n * first signal after the last reply (at most `H` plus that signal). A\n * signal leaves the measured backlog at most `maxBuffered`, so between\n * sends a peer holds at most `maxBuffered + H = 2 × maxBuffered + 2 ×\n * maxMessage` bytes (plus one signal). The write that crosses a bound is\n * the last: the peer is marked stuck and nothing more goes out. That is the\n * only backlog check: the buffer grows through `send`, so a backlog\n * over the bound is always seen by the write that made it.\n */\nimport type { JsonRpcMessage } from '../../acp/types'\nimport { Buffer } from 'node:buffer'\nimport { errorResponse } from '../../acp/json-rpc'\nimport { IpcErrorCode } from '../protocol'\n\n/** What the budget writes to: the transport's buffer and its framing. */\nexport interface SendTarget {\n /** Bytes written but not yet handed to the OS. */\n buffered: () => number\n /** Write one serialized message (the target frames it); `false`: the transport dropped it. */\n write: (json: string) => boolean\n}\n\nexport interface SendLimits {\n /** Signals backlog past which the peer is stuck (replies set aside). */\n maxBuffered: number\n /** Largest reply; a bigger one is answered with `ResponseTooLarge`. */\n maxMessage: number\n /** The peer cannot keep up: tear it down (called once). */\n stuck: (why: string) => void\n log: (line: string) => void\n /** Log prefix, e.g. `[ipc-ws]`. */\n label: string\n}\n\nexport interface PeerSend {\n /** Bytes buffered that are not reply bytes: what the slow-consumer bound measures. */\n backlog: () => number\n /** Send one message (the JSON-RPC endpoint's `send`): replies and the rest apart. */\n send: (json: string, message: JsonRpcMessage) => void\n /** Give up on the peer (once): {@link SendLimits.stuck} runs and nothing more goes out. */\n markStuck: (why: string) => void\n}\n\nfunction isReply(message: JsonRpcMessage): boolean {\n return 'id' in message && message.id !== undefined && message.id !== null && ('result' in message || 'error' in message)\n}\n\nexport function createPeerSend(target: SendTarget, limits: SendLimits): PeerSend {\n const { maxBuffered, maxMessage, log, label } = limits\n let replyReserve = 0\n let stuck = false\n const markStuck = (why: string): void => {\n if (stuck)\n return\n stuck = true\n limits.stuck(why)\n }\n const backlog = (): number => {\n const buffered = target.buffered()\n replyReserve = Math.min(replyReserve, buffered)\n return buffered - replyReserve\n }\n const send = (json: string, message: JsonRpcMessage): void => {\n if (stuck)\n return\n if (!isReply(message)) {\n if (!target.write(json) || backlog() > maxBuffered)\n markStuck(`more than ${maxBuffered} bytes of signals unsent`)\n return\n }\n let text = json\n let bytes = Buffer.byteLength(text)\n if (bytes > maxMessage) {\n const id = (message as { id: string | number }).id\n log(`${label} reply to ${String(id)} is ${bytes} bytes (> ${maxMessage}): answering with an error instead`)\n text = JSON.stringify(errorResponse(id, IpcErrorCode.ResponseTooLarge, `The reply is over ${maxMessage} bytes: ask for less (a transcript: a smaller page).`, { bytes, maxBytes: maxMessage }))\n bytes = Buffer.byteLength(text)\n }\n if (!target.write(text)) {\n markStuck('reply dropped by the transport')\n return\n }\n replyReserve += bytes\n if (target.buffered() > maxBuffered + 2 * maxMessage)\n markStuck(`more than ${maxBuffered + 2 * maxMessage} bytes unsent`)\n }\n return { backlog, send, markStuck }\n}\n","/**\n * Unix-socket JSON-RPC server around a `ChatHost`.\n *\n * One `net.Server`, one JSON-RPC connection per peer (NDJSON framing: a\n * `net.Socket` is both the Readable and the Writable). The method table,\n * per-peer state, signal fan-out and the `ipc.hello` handshake live in the\n * transport-free dispatcher (`./dispatcher`); this module owns only what is\n * specific to the socket and the process: framing, the socket file and its\n * permissions, idle exit, retiring, and draining. Write backpressure (the\n * slow-consumer bound, the reply size) is shared with the WebSocket server\n * (`../transports/send-budget`).\n *\n * Handshake: a peer's first request MUST be `ipc.hello` carrying its\n * protocol version. Mismatched peers get a structured error (code\n * `IpcErrorCode.VersionMismatch`) and can ask the daemon to drain via `daemon.shutdown`\n * before respawning a fresh one.\n */\nimport type { Server, Socket } from 'node:net'\nimport type { JsonRpcConnection } from '../../acp/json-rpc'\nimport type { ChatHost } from '../../chat/host/engine'\nimport type { HostSignal } from '../protocol'\nimport type { HostDispatcher } from './dispatcher'\nimport type { DaemonEnvironment } from './environment'\nimport { chmodSync, existsSync, mkdirSync, unlinkSync } from 'node:fs'\nimport { createServer } from 'node:net'\nimport { basename, dirname } from 'node:path'\nimport { createJsonRpcConnection } from '../../acp/json-rpc'\nimport { connectIpcClient } from '../client'\nimport { createPacedWriter } from '../transports/paced-writer'\nimport { createRpcPeer, dispatcherHandlers, PEER_WRITE_HIGH_WATERMARK } from '../transports/peer-rpc'\nimport { createPeerSend } from '../transports/send-budget'\nimport { createHostDispatcher } from './dispatcher'\nimport { legacySocketPath, probeSocket } from './lifecycle'\n\n// Canonical home is the protocol module (clients import the codes without\n// pulling the server's module graph); re-exported here for compatibility.\nexport { IpcErrorCode } from '../protocol'\n\n/** Options for {@link startIpcDaemon}. */\nexport interface IpcDaemonOptions {\n /** Path of the unix socket to listen on. Its directory is created with mode 0700 and the socket gets mode 0600. */\n socketPath: string\n /**\n * Environment factory — the production daemon uses the disk-backed\n * `createDaemonEnvironment`; tests inject an in-memory one. Receives the\n * broadcast sink wired to peer fan-out.\n */\n createEnvironment?: (broadcast: (signal: HostSignal) => void) => DaemonEnvironment | Promise<DaemonEnvironment>\n /**\n * zidane version reported in the handshake.\n * @default 'dev'\n */\n appVersion?: string\n /**\n * Milliseconds the daemon may have zero peers and no running session before\n * it calls `onIdle`. Unset or 0: never. Ignored with a shared `dispatcher`.\n */\n idleShutdownMs?: number\n /** Called when the daemon has had no peers for `idleShutdownMs` and no session is active, or when it retires after a newer client attached mid-work. The CLI exits the process; embedded servers ignore it. */\n onIdle?: () => void\n /** Receives one diagnostic line per event, such as peers connecting. */\n onLog?: (line: string) => void\n /**\n * Serve this already-built host instead of creating one: another front-end\n * (zidane-server's WebSocket) serves it too. Its owner drains it and owns\n * its lifecycle, so `createEnvironment`, `appVersion` and the idle options\n * are ignored, and `close()` only closes this socket's connections.\n */\n dispatcher?: HostDispatcher\n /**\n * A peer with more than this of signals queued for sending (it stopped\n * reading) is dropped; it re-attaches and resumes. Replies are bounded by\n * `maxMessageBytes` instead (`../transports/send-budget`). Default\n * `PEER_WRITE_HIGH_WATERMARK` (8 MiB).\n */\n maxBufferedBytes?: number\n /** A bigger reply goes out as an `IpcErrorCode.ResponseTooLarge` error. Default {@link SOCKET_MAX_REPLY_BYTES}. */\n maxMessageBytes?: number\n}\n\n/**\n * Largest reply on the socket by default. Generous: the peer is this user's\n * own window on this machine, and whole-session reads (`sessions.get`) of a\n * long local session with images must keep fitting. The bound is there so\n * one reply cannot grow the write buffer without limit.\n */\nconst SOCKET_MAX_REPLY_BYTES = 256 * 1024 * 1024\n\n/** A running unix-socket daemon. */\nexport interface IpcDaemon {\n /** Path of the socket it listens on. */\n socketPath: string\n /** The chat host it serves. */\n host: ChatHost\n /** Number of connected peers. */\n peerCount: () => number\n /** Drains the host, disconnects every peer and removes the socket file. Calling it again returns the same promise. With a shared `dispatcher`, the host is left to its owner. */\n close: () => Promise<void>\n}\n\n/** One open socket connection; `close()` ends them all. */\ninterface SocketConnection {\n socket: Socket\n connection: JsonRpcConnection\n}\n\n/**\n * Starts a unix-socket daemon around a chat host. Each peer must open with `ipc.hello`.\n * A stale socket file left by a dead daemon is replaced. Throws when a live daemon already listens on `socketPath`.\n * @param options - Socket path, environment and lifecycle settings.\n * @returns The running daemon.\n * @example\n * ```ts\n * import { startIpcDaemon } from 'zidane/ipc'\n *\n * const daemon = await startIpcDaemon({\n * socketPath: '/tmp/zidane-demo/daemon/ipc.sock',\n * onLog: line => console.log(line),\n * })\n * console.log(daemon.peerCount())\n * await daemon.close()\n * ```\n */\nexport async function startIpcDaemon(options: IpcDaemonOptions): Promise<IpcDaemon> {\n const { socketPath } = options\n const log = options.onLog ?? (() => undefined)\n const startedAt = Date.now()\n const appVersion = options.appVersion ?? 'dev'\n const maxBuffered = options.maxBufferedBytes ?? PEER_WRITE_HIGH_WATERMARK\n const maxMessage = options.maxMessageBytes ?? SOCKET_MAX_REPLY_BYTES\n\n const connections = new Set<SocketConnection>()\n let idleTimer: ReturnType<typeof setTimeout> | undefined\n // See `retireWhenIdle`.\n let retiring = false\n let retireTimer: ReturnType<typeof setInterval> | undefined\n\n const shared = options.dispatcher\n const dispatcher = shared ?? await createHostDispatcher({\n ...(options.createEnvironment ? { createEnvironment: options.createEnvironment } : {}),\n appVersion,\n startedAt,\n identity: { kind: 'daemon' },\n lifecycle: {\n shutdown: () => {\n void close().then(() => options.onIdle?.())\n },\n retireWhenIdle,\n isRetiring: () => retiring,\n },\n })\n const host = dispatcher.host\n\n /* ----------------------------------------------------------------------\n * Socket plumbing\n * -------------------------------------------------------------------- */\n\n /*\n * Retire-when-idle: a NEWER client found this daemon mid-work. Draining\n * now would abort every window's runs (a dev edit under `src/ipc` or an\n * upgrade mid-`/review` did exactly that), so keep serving and exit at\n * the first idle point. Two consecutive idle polls: a queue drain is\n * scheduled just after a dispatch releases, and one poll could land in\n * that gap.\n */\n function retireWhenIdle(): void {\n if (retiring)\n return\n retiring = true\n log('[ipc-daemon] a newer client attached mid-work; retiring once idle')\n let idlePolls = 0\n retireTimer = setInterval(() => {\n if (host.hasActiveWork()) {\n idlePolls = 0\n return\n }\n idlePolls += 1\n if (idlePolls < 2)\n return\n clearInterval(retireTimer)\n void close().then(() => options.onIdle?.())\n }, 1000)\n }\n\n function armIdleTimer(): void {\n if (shared || !options.idleShutdownMs || !options.onIdle)\n return\n clearTimeout(idleTimer)\n idleTimer = setTimeout(() => {\n if (dispatcher.peerCount() > 0)\n return // Peers reconnected — the next last-disconnect re-arms.\n /*\n * Never abort a live run just because every window detached — `/exit`\n * is explicitly \"the run keeps going\". Re-arm and check again later.\n */\n if (host.hasActiveWork()) {\n armIdleTimer()\n return\n }\n options.onIdle?.()\n }, options.idleShutdownMs)\n }\n\n function handleConnection(socket: Socket): void {\n // Assigned right below; a signal can only reach this peer after `ipc.hello`, i.e. once it is set.\n let connection: JsonRpcConnection | undefined\n const writer = createPacedWriter(socket)\n const budget = createPeerSend(\n {\n buffered: writer.buffered,\n // NDJSON: `JSON.stringify` escapes newlines, so a message is one line.\n write: (json) => {\n writer.write(`${json}\\n`)\n return true\n },\n },\n {\n maxBuffered,\n maxMessage,\n log,\n label: '[ipc-daemon]',\n stuck: (why) => {\n log(`[ipc-daemon] dropping peer: ${why}`)\n socket.destroy()\n },\n },\n )\n const peer = createRpcPeer({ rpc: () => connection })\n\n const rpc = createJsonRpcConnection({\n input: socket,\n output: socket,\n send: budget.send,\n ...dispatcherHandlers(dispatcher, peer),\n onError: err => log(`[ipc-daemon] peer error: ${err.message}`),\n })\n\n connection = rpc\n const entry: SocketConnection = { socket, connection: rpc }\n connections.add(entry)\n dispatcher.addPeer(peer)\n rpc.start()\n log(`[ipc-daemon] peer connected (${dispatcher.peerCount()} total)`)\n\n const drop = (): void => {\n if (!connections.delete(entry))\n return\n rpc.close()\n dispatcher.removePeer(peer)\n log(`[ipc-daemon] peer disconnected (${dispatcher.peerCount()} left)`)\n if (dispatcher.peerCount() === 0)\n armIdleTimer()\n }\n\n socket.on('close', drop)\n socket.on('error', drop)\n }\n\n /*\n * Stale-socket recovery: a previous daemon that died without cleanup\n * leaves the socket file behind and `listen` would fail EADDRINUSE.\n * Probe before unlinking so a LIVE daemon is never yanked out from\n * under its peers.\n */\n if (existsSync(socketPath)) {\n if (await probeSocket(socketPath))\n throw new Error(`A zidane daemon is already listening on ${socketPath}.`)\n /*\n * Narrow the concurrent-spawn race: another daemon may have bound the\n * socket between the probe above and the unlink below (each probe is a\n * full connect attempt). Re-probe immediately before unlinking and\n * abort startup if something now answers — never yank a live daemon's\n * socket out from under its peers.\n */\n if (await probeSocket(socketPath))\n throw new Error(`A zidane daemon is already listening on ${socketPath}.`)\n unlinkSync(socketPath)\n }\n /*\n * The daemon speaks an unauthenticated RPC surface that can execute tools\n * as the daemon user — restrict access to the owner. The containing dir\n * is created 0700 (belt) and the socket is chmod'd to 0600 right after\n * `listen` (suspenders) so connectability never depends on ambient umask.\n *\n * `mkdirSync`'s mode only applies on CREATION — a pre-existing dir keeps\n * its old mode. When the dir is the DEDICATED daemon dir (the `daemon/`\n * subdir `defaultSocketPath` uses, or one we created fresh for an\n * explicit socket path) chmod it 0700 explicitly. A shared, pre-existing\n * dir (tests pointing the socket at a tmpdir) is left alone — never\n * tighten a directory that also holds unrelated state.\n */\n const socketDir = dirname(socketPath)\n const dedicatedSocketDir = basename(socketDir) === 'daemon' || !existsSync(socketDir)\n mkdirSync(socketDir, { recursive: true, mode: 0o700 })\n if (dedicatedSocketDir) {\n try {\n chmodSync(socketDir, 0o700)\n }\n catch {\n // Best-effort — mirrors the socket chmod below.\n }\n }\n\n const server: Server = createServer(handleConnection)\n await new Promise<void>((resolvePromise, reject) => {\n server.once('error', reject)\n server.listen(socketPath, () => {\n server.off('error', reject)\n resolvePromise()\n })\n })\n try {\n chmodSync(socketPath, 0o600)\n }\n catch {\n // Best-effort on platforms where socket chmod is a no-op (e.g. some\n // network filesystems); the 0700 dir still constrains access.\n }\n log(`[ipc-daemon] listening on ${socketPath}`)\n // We bound the socket, so we own the file — close() unlinks it exactly\n // once. Cleared after the unlink so a file later re-created by a NEW\n // daemon on the same path is never yanked by a stale close.\n let ownsSocketFile = true\n armIdleTimer()\n\n /*\n * Drain any daemon still serving the legacy socket path\n * (`<stateDir>/daemon.sock`). New clients never probe it,\n * so a `--idle-exit 0` service daemon there would run forever alongside\n * this one — two sets of live agents, run guards, and heartbeat writers\n * against the same sessions.db. Fire-and-forget: version mismatch is\n * the EXPECTED case (a legacy daemon speaks an older protocol) and\n * `drainOnMismatch: true` performs the shutdown for us; a same-version\n * survivor (explicit env overrides) gets an explicit daemon.shutdown.\n */\n const legacyPath = shared ? null : legacySocketPath(socketPath)\n if (legacyPath) {\n void (async () => {\n if (!(await probeSocket(legacyPath)))\n return\n log(`[ipc-daemon] draining legacy daemon on ${legacyPath}`)\n try {\n const survivor = await connectIpcClient({ socketPath: legacyPath, appVersion: `${appVersion}/legacy-drain`, drainOnMismatch: true })\n await survivor.request('daemon.shutdown', {}).catch(() => undefined)\n survivor.close()\n }\n catch {\n // IpcVersionMismatchError = drained by the handshake path; anything\n // else (raced exit, wedged socket) is best-effort — leave it.\n }\n })()\n }\n\n async function doClose(): Promise<void> {\n clearTimeout(idleTimer)\n clearInterval(retireTimer)\n // A shared host is its owner's to drain and detach (see `dispatcher`).\n if (!shared)\n await host.drain()\n // Stop accepting before closing what is open: a connection landing in\n // between would otherwise keep `server.close` waiting forever.\n const stopped = new Promise<void>((resolvePromise) => {\n server.close(() => resolvePromise())\n })\n for (const { connection, socket } of connections) {\n connection.close()\n socket.destroy()\n }\n connections.clear()\n if (!shared)\n dispatcher.detachAll()\n await stopped\n if (ownsSocketFile) {\n ownsSocketFile = false\n try {\n unlinkSync(socketPath)\n }\n catch {\n // already gone\n }\n }\n }\n\n /*\n * Idempotent close: `daemon.shutdown` runs close() and then onIdle, whose\n * CLI implementation calls daemon.close() again — both callers share one\n * drain instead of double-unlinking the socket. Memoized rather than\n * flag-guarded so the second caller awaits actual completion.\n */\n let closePromise: Promise<void> | null = null\n function close(): Promise<void> {\n closePromise ??= doClose()\n return closePromise\n }\n\n return {\n socketPath,\n host,\n peerCount: () => dispatcher.peerCount(),\n close,\n }\n}\n","/**\n * Login-shell PATH for processes not started from a terminal.\n *\n * A Dock/Finder-launched GUI, or a daemon it spawns, inherits launchd's PATH\n * (`/usr/bin:/bin:/usr/sbin:/sbin`), so `node` / `bun` / `yarn` are invisible\n * to agent shell commands. Ask the user's login shell for its PATH and merge\n * it into `process.env.PATH`.\n *\n * Merge, don't replace: a terminal launch may have prepended entries the rc\n * files know nothing about (`nvm use`, direnv, `PATH=... zidane`), and those\n * must survive. Idempotent, so every surface (GUI, daemon) can call it and a\n * daemon spawned by an already-fixed GUI ends up with the same PATH.\n */\nimport { spawn, spawnSync } from 'node:child_process'\nimport { userInfo } from 'node:os'\nimport { delimiter } from 'node:path'\nimport process from 'node:process'\n\nconst DELIMITER = '_ZIDANE_SHELL_ENV_'\n\n/** A slow rc file costs startup time; a prompting one must never hang the process. */\nconst DEFAULT_TIMEOUT_MS = 3_000\n\nfunction defaultShell(env: NodeJS.ProcessEnv): string {\n try {\n const { shell } = userInfo()\n if (shell)\n return shell\n }\n catch {}\n return env.SHELL || (process.platform === 'darwin' ? '/bin/zsh' : '/bin/sh')\n}\n\nfunction parsePath(stdout: string): string | undefined {\n const body = stdout.split(DELIMITER)[1]\n if (body === undefined)\n return undefined\n // eslint-disable-next-line no-control-regex\n const clean = body.replace(/\\u001B\\[[0-9;]*[A-Z]/gi, '')\n for (const line of clean.split('\\n')) {\n if (line.startsWith('PATH='))\n return line.slice('PATH='.length) || undefined\n }\n return undefined\n}\n\n/**\n * Options for {@link shellPath} and {@link shellPathAsync}. Without `shell`,\n * the user's shell is tried first, then POSIX fallbacks for non-POSIX defaults\n * (nushell, xonsh) whose `-ilc` differs. An explicit `shell` is tried alone.\n * `timeoutMs` bounds each shell. Default 3000.\n */\nexport interface ShellPathOptions { env?: NodeJS.ProcessEnv, timeoutMs?: number, shell?: string }\n\n// `command env` sidesteps aliases/functions named `env`.\nconst SHELL_ARGS = ['-ilc', `echo -n \"${DELIMITER}\"; command env; echo -n \"${DELIMITER}\"; exit`]\n\nfunction shellCandidates(options: ShellPathOptions): { shells: string[], env: NodeJS.ProcessEnv, timeout: number } {\n const env = options.env ?? process.env\n return {\n shells: options.shell ? [options.shell] : [...new Set([defaultShell(env), '/bin/zsh', '/bin/bash'])],\n timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n env: {\n ...env,\n // Keep oh-my-zsh from blocking on its update prompt / tmux autostart.\n DISABLE_AUTO_UPDATE: 'true',\n ZSH_TMUX_AUTOSTARTED: 'true',\n ZSH_TMUX_AUTOSTART: 'false',\n },\n }\n}\n\n/**\n * Reads the PATH of the user's interactive login shell, blocking until it answers.\n * Tries the user's shell, then `/bin/zsh` and `/bin/bash`. Returns `undefined` on Windows, or when no shell answers in time.\n * @param options - Shell, environment and timeout overrides.\n * @returns The PATH string, or `undefined`.\n */\nexport function shellPath(options: ShellPathOptions = {}): string | undefined {\n if (process.platform === 'win32')\n return undefined\n const { shells, env, timeout } = shellCandidates(options)\n for (const shell of shells) {\n const result = spawnSync(shell, SHELL_ARGS, {\n encoding: 'utf8',\n timeout,\n stdio: ['ignore', 'pipe', 'ignore'],\n env,\n })\n if (result.error || result.status !== 0)\n continue\n const path = parsePath(result.stdout)\n if (path)\n return path\n }\n return undefined\n}\n\n/**\n * {@link shellPath} without blocking the event loop: same shells, args,\n * timeout and parsing, run with `spawn`. For hosts (the GUI main\n * process) that can start on a cached PATH and refresh in the background.\n * @param options - Shell, environment and timeout overrides.\n * @returns The PATH string, or `undefined`.\n */\nexport async function shellPathAsync(options: ShellPathOptions = {}): Promise<string | undefined> {\n if (process.platform === 'win32')\n return undefined\n const { shells, env, timeout } = shellCandidates(options)\n for (const shell of shells) {\n const stdout = await new Promise<string | undefined>((resolve) => {\n // Same stdio as the sync variant: no stdin, stderr discarded.\n const child = spawn(shell, SHELL_ARGS, { stdio: ['ignore', 'pipe', 'ignore'], env })\n let out = ''\n let settled = false\n let timer: ReturnType<typeof setTimeout> | undefined\n const finish = (value: string | undefined): void => {\n if (settled)\n return\n settled = true\n clearTimeout(timer)\n resolve(value)\n }\n timer = setTimeout(() => {\n child.kill('SIGTERM')\n finish(undefined)\n }, timeout)\n child.stdout?.setEncoding('utf8')\n child.stdout?.on('data', (chunk: string) => {\n out += chunk\n })\n child.on('error', () => finish(undefined))\n child.on('close', code => finish(code === 0 ? out : undefined))\n })\n if (stdout === undefined)\n continue\n const path = parsePath(stdout)\n if (path)\n return path\n }\n return undefined\n}\n\n/**\n * Entries of `current` the shell doesn't know about (ad-hoc prepends from the\n * launching terminal) stay in front; then the shell's PATH in its own order.\n * From the Dock every launchd entry is already in the shell PATH, so the\n * result is exactly the shell PATH, with the shell's precedence intact.\n * @param current - The PATH of this process.\n * @param shell - The PATH reported by the login shell.\n * @returns The merged PATH string.\n */\nexport function mergePath(current: string | undefined, shell: string | undefined): string {\n const split = (path: string | undefined): string[] => (path ?? '').split(delimiter).filter(Boolean)\n const fromShell = split(shell)\n const known = new Set(fromShell)\n return [...new Set([...split(current).filter(entry => !known.has(entry)), ...fromShell])].join(delimiter)\n}\n\n/** Merge the login shell's PATH into `process.env.PATH`. No-op on Windows or when no shell answers. */\nexport function fixPath(): void {\n const extra = shellPath()\n if (extra)\n process.env.PATH = mergePath(process.env.PATH, extra)\n}\n"],"mappings":"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"}
@@ -8,14 +8,14 @@ import { a as listMarkdownDefFiles, o as markdownDefRoots } from "./headless-Bs9
8
8
  import { t as projectUserPaths } from "./project-user-paths-DWqZyUgb.js";
9
9
  import { r as artifactKindLabel, u as renderArtifactRow } from "./builtin-artifact-kinds-BmorPkgz.js";
10
10
  import { B as normalizeGithubHost, L as githubClient, M as clockSkewMs, z as githubTokenSource } from "./row-fields-F0tyvIgg.js";
11
- import { O as formatBindingForDisplay } from "./extensions-DIbpUj5R.js";
11
+ import { O as formatBindingForDisplay } from "./extensions-BJof-2mi.js";
12
12
  import { t as resolveStorageDirs } from "./xdg-CJS70jqe.js";
13
13
  import { p as oauthAccountLabel } from "./cred-keys-C0KaSRp7.js";
14
14
  import { l as resolveOAuthApiKey } from "./oauth-DRrs-65k.js";
15
15
  import { A as stepTranscriptCap, Ln as providerBrand, M as takeChars, O as smoothCharsForTick, g as appendThinkingLines, h as appendMarkdownDelta, kn as NATIVE_EXTENSIONS, vn as reduceMcpAuth } from "./transcript-reconcile-D_SWw6dX.js";
16
16
  import { P as settingsFromStored, f as enforceSettingsInvariants, v as DEFAULT_THEME } from "./secret-shapes-HvaYVzwc.js";
17
17
  import { C as foreignSkillScanPaths, S as foreignMcpConfigFiles, _ as parseMcpsFile, g as parseJsonTolerant, m as defaultMcpsConfigPaths, w as parseClaudeState } from "./host-scoped-settings-DxxI_yRo.js";
18
- import { c as mcpsWritePath, i as supportsOAuth, u as upsertMcpServer } from "./oauth-Ck3wQX4Q.js";
18
+ import { c as mcpsWritePath, i as supportsOAuth, u as upsertMcpServer } from "./oauth-BwPiX0hL.js";
19
19
  import { i as deriveSessionTitle } from "./store-BpkWIaSH.js";
20
20
  import { S as credKeyOf, g as storedCredentialsOf, n as credentialsPath, o as readCredentials } from "./credentials-DxOQmset.js";
21
21
  import { r as detectAuth } from "./cli-shared-GOehTeqk.js";
@@ -5546,4 +5546,4 @@ function computeTurnAnchors(items) {
5546
5546
  //#endregion
5547
5547
  export { truncateTrailing as $, groupArtifacts as $t, buildToolToggle as A, buildRestartHint as At, useInteractionsActions as B, writeUpdateAppliedMarker as Bt, providerUsageSupported as C, claudeImportHostNote as Ct, fetchOAuthRedirect as D, previewClaudeImport as Dt, formatPathForCwd as E, importFromClaudeCode as Et, parentServerName as F, probeInstalledVersion as Ft, onboardingInstallSignals as G, detectLibc as Gt, connectorGithubToken as H, checkForUpdate as Ht, McpAuthProvider as I, readUpdateAppliedMarker as It, onboardingProviders as J, parseSemver as Jt, probeLocalModelServers as K, detectPackageManager as Kt, useMcpAuthDispatch as L, updateAppliedMarkerPath as Lt, useMcpToolToggleSet as M, useUpdateCheck as Mt, buildVisibleMcpRows as N, UPDATE_APPLIED_MARKER as Nt, normalizeRedirectPaste as O, bootProfileEnabled as Ot, indexOfServerRow as P, hasUpdateAppliedMarker as Pt, hintsLength as Q, resolvePlatformPackage as Qt, useMcpAuthState as R, useRestartRequired as Rt, fetchProviderUsageAccounts as S, CLAUDE_IMPORT_NOTES as St, usageSourceFor as T, hasClaudeCodeSetup as Tt, detectOnboardingEnvironment as U, checkTarballAvailable as Ut, useInteractionsQueue as V, findInstalledCompanion as Vt, detectOtherAgents as W, compareSemver as Wt, clipHintsToWidth as X, performSelfUpdate as Xt, EMPTY_HINTS as Y, performInPlaceSelfUpdate as Yt, hintLayout as Z, reinstallHint as Zt, withRunningCallStarts as _, useConfig as _t, useSurfaces as a, useEnabledToggleSet as at, fetchAllProviderUsage as b, scoreArtifacts as bt, useStreamBuffer as c, useSettings as ct, resolveSessionExportTarget as d, createDiscoverySlot as dt, summarizeArtifactCounts as en, fetchGithubUsage as et, writeSessionExport as f, DiscoveryProvider as ft, hasUntimedOpenCalls as g, ConfigProvider as gt, useSafeModeQueue as h, ConfigContext as ht, useSelectStyle as i, listProjectFiles as it, useMcpToolToggleMap as j, buildUpdateHint as jt, redirectUriFromAuthUrl as k, bootTick as kt, aggregateUsage as l, parseEditorCommand as lt, useSafeModeActions as m, useDiscoveryOptional as mt, ThemeProvider as n, shortChord as nt, useSyntaxStyles as o, SettingsOverride as ot, SafeModeProvider as p, useDiscovery as pt, FEATURED_PROVIDERS as q, overrideManager as qt, useColors as r, fileEntriesFromPaths as rt, useTheme as s, SettingsProvider as st, computeTurnAnchors as t, buildHints as tt, renderSession as u, quoteForShell as ut, USAGE_CACHE_TTL_MS as v, ARTIFACTS_COMPLETION_PROVIDER_ID as vt, usageChecksOptedOut as w, countClaudeSessionFiles as wt, fetchProviderUsage as x, useCompletion as xt, clearProviderUsageCache as y, createArtifactsCompletionProvider as yt, InteractionsProvider as z, watchInstalledVersion as zt };
5548
5548
 
5549
- //# sourceMappingURL=transcript-anchors-D8jWLpe5.js.map
5549
+ //# sourceMappingURL=transcript-anchors-BcFLh3ZA.js.map