@nimbus-sh/worker 0.5.0 → 0.7.0

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 (164) hide show
  1. package/README.md +2 -2
  2. package/dist/_shared/preview-host.d.ts +26 -8
  3. package/dist/_shared/preview-host.d.ts.map +1 -1
  4. package/dist/_shared/preview-host.js +51 -11
  5. package/dist/cirrus-npm-cjs.generated.d.ts +1 -1
  6. package/dist/cirrus-npm-cjs.generated.js +1 -1
  7. package/dist/facets/cirrus-real.d.ts.map +1 -1
  8. package/dist/facets/cirrus-real.js +6 -5
  9. package/dist/facets/compose.d.ts +148 -0
  10. package/dist/facets/compose.d.ts.map +1 -0
  11. package/dist/facets/compose.js +146 -0
  12. package/dist/facets/durable-images.d.ts +18 -5
  13. package/dist/facets/durable-images.d.ts.map +1 -1
  14. package/dist/facets/durable-images.js +36 -7
  15. package/dist/facets/durable-slots.d.ts +1 -1
  16. package/dist/facets/durable-slots.d.ts.map +1 -1
  17. package/dist/facets/durable-slots.js +2 -1
  18. package/dist/facets/esbuild-bundle-pool.d.ts +49 -0
  19. package/dist/facets/esbuild-bundle-pool.d.ts.map +1 -0
  20. package/dist/facets/esbuild-bundle-pool.js +122 -0
  21. package/dist/facets/exec-telemetry.d.ts +8 -0
  22. package/dist/facets/exec-telemetry.d.ts.map +1 -1
  23. package/dist/facets/manager.d.ts +337 -123
  24. package/dist/facets/manager.d.ts.map +1 -1
  25. package/dist/facets/manager.js +1100 -449
  26. package/dist/facets/process.d.ts +20 -16
  27. package/dist/facets/process.d.ts.map +1 -1
  28. package/dist/facets/process.js +59 -28
  29. package/dist/facets/real-vite-fs-shim.js +2 -2
  30. package/dist/facets/resident-identity.d.ts +23 -0
  31. package/dist/facets/resident-identity.d.ts.map +1 -0
  32. package/dist/facets/resident-identity.js +31 -0
  33. package/dist/facets/vite-dev-server.d.ts +25 -47
  34. package/dist/facets/vite-dev-server.d.ts.map +1 -1
  35. package/dist/facets/vite-dev-server.js +185 -219
  36. package/dist/facets/wasm-image-digest.d.ts +25 -0
  37. package/dist/facets/wasm-image-digest.d.ts.map +1 -0
  38. package/dist/facets/wasm-image-digest.js +25 -0
  39. package/dist/facets/wasm-swap-registry.d.ts +51 -92
  40. package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
  41. package/dist/facets/wasm-swap-registry.js +68 -227
  42. package/dist/git/commands.d.ts +0 -1
  43. package/dist/git/commands.d.ts.map +1 -1
  44. package/dist/git/commands.js +0 -3
  45. package/dist/git/network-facet.d.ts.map +1 -1
  46. package/dist/git/network-facet.js +6 -5
  47. package/dist/loaders/child-process/spawn-pool.d.ts +3 -2
  48. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  49. package/dist/loaders/child-process/spawn-pool.js +3 -2
  50. package/dist/loaders/generated-workers.d.ts +2 -2
  51. package/dist/loaders/generated-workers.d.ts.map +1 -1
  52. package/dist/loaders/generated-workers.js +3 -3
  53. package/dist/loaders/npm-resolve-preamble.d.ts +3 -6
  54. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
  55. package/dist/loaders/npm-resolve-preamble.js +34 -108
  56. package/dist/node-shims-artifact.generated.js +3 -3
  57. package/dist/npm/install-batch-facet.d.ts +3 -2
  58. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  59. package/dist/npm/install-batch-facet.js +45 -15
  60. package/dist/npm/installer.d.ts +22 -17
  61. package/dist/npm/installer.d.ts.map +1 -1
  62. package/dist/npm/installer.js +209 -193
  63. package/dist/npm/pre-bundle-facet.d.ts +1 -1
  64. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  65. package/dist/npm/pre-bundle-facet.js +1 -1
  66. package/dist/npm/resolve-facet.d.ts +6 -0
  67. package/dist/npm/resolve-facet.d.ts.map +1 -1
  68. package/dist/npm/resolve-one-facet.d.ts +21 -20
  69. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  70. package/dist/npm/resolve-one-facet.js +85 -84
  71. package/dist/npm/semver.d.ts +59 -0
  72. package/dist/npm/semver.d.ts.map +1 -0
  73. package/dist/npm/semver.js +226 -0
  74. package/dist/router/index.d.ts.map +1 -1
  75. package/dist/router/index.js +47 -10
  76. package/dist/router/public-directory.d.ts +5 -1
  77. package/dist/router/public-directory.d.ts.map +1 -1
  78. package/dist/router/public-directory.js +9 -2
  79. package/dist/router/remote-api.d.ts +18 -0
  80. package/dist/router/remote-api.d.ts.map +1 -1
  81. package/dist/router/remote-api.js +64 -5
  82. package/dist/runtime/cpython-resident.d.ts.map +1 -1
  83. package/dist/runtime/cpython-resident.js +3 -1
  84. package/dist/runtime/html-entrypoint.d.ts +7 -0
  85. package/dist/runtime/html-entrypoint.d.ts.map +1 -1
  86. package/dist/runtime/html-entrypoint.js +18 -2
  87. package/dist/runtime/node-runner.d.ts +3 -0
  88. package/dist/runtime/node-runner.d.ts.map +1 -1
  89. package/dist/runtime/node-runner.js +7 -0
  90. package/dist/runtime/node-shims.d.ts.map +1 -1
  91. package/dist/runtime/node-shims.js +305 -29
  92. package/dist/runtime/opencode-facet-runner.js +2 -2
  93. package/dist/runtime/package-manager.d.ts +44 -1
  94. package/dist/runtime/package-manager.d.ts.map +1 -1
  95. package/dist/runtime/package-manager.js +168 -3
  96. package/dist/runtime/ruby-resident.d.ts.map +1 -1
  97. package/dist/runtime/ruby-resident.js +2 -0
  98. package/dist/session/agent.d.ts.map +1 -1
  99. package/dist/session/agent.js +28 -1
  100. package/dist/session/helpers.d.ts +38 -0
  101. package/dist/session/helpers.d.ts.map +1 -1
  102. package/dist/session/helpers.js +76 -0
  103. package/dist/session/init-phases.d.ts +1 -1
  104. package/dist/session/init-phases.d.ts.map +1 -1
  105. package/dist/session/init-phases.js +2 -12
  106. package/dist/session/init.d.ts +15 -1
  107. package/dist/session/init.d.ts.map +1 -1
  108. package/dist/session/init.js +91 -603
  109. package/dist/session/keys.d.ts +4 -0
  110. package/dist/session/keys.d.ts.map +1 -1
  111. package/dist/session/keys.js +4 -0
  112. package/dist/session/nimbus-session.d.ts +90 -23
  113. package/dist/session/nimbus-session.d.ts.map +1 -1
  114. package/dist/session/nimbus-session.js +200 -110
  115. package/dist/session/npm-install-port.d.ts +16 -0
  116. package/dist/session/npm-install-port.d.ts.map +1 -0
  117. package/dist/session/npm-install-port.js +37 -0
  118. package/dist/session/port-capability.d.ts +90 -0
  119. package/dist/session/port-capability.d.ts.map +1 -1
  120. package/dist/session/port-capability.js +219 -9
  121. package/dist/session/programmatic.d.ts +125 -5
  122. package/dist/session/programmatic.d.ts.map +1 -1
  123. package/dist/session/programmatic.js +439 -89
  124. package/dist/session/routes.d.ts +39 -17
  125. package/dist/session/routes.d.ts.map +1 -1
  126. package/dist/session/routes.js +67 -131
  127. package/dist/session/rpc.d.ts +39 -16
  128. package/dist/session/rpc.d.ts.map +1 -1
  129. package/dist/session/rpc.js +137 -130
  130. package/dist/session/serving-port.d.ts +27 -0
  131. package/dist/session/serving-port.d.ts.map +1 -0
  132. package/dist/session/serving-port.js +27 -0
  133. package/dist/session/start-real-vite.d.ts +12 -0
  134. package/dist/session/start-real-vite.d.ts.map +1 -1
  135. package/dist/session/start-real-vite.js +4 -4
  136. package/dist/session/supervisor-op.d.ts +41 -4
  137. package/dist/session/supervisor-op.d.ts.map +1 -1
  138. package/dist/session/supervisor-op.js +101 -73
  139. package/dist/session/supervisor-rpc.d.ts +7 -7
  140. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  141. package/dist/session/supervisor-rpc.js +6 -3
  142. package/dist/session/vite-command.d.ts +18 -0
  143. package/dist/session/vite-command.d.ts.map +1 -0
  144. package/dist/session/vite-command.js +609 -0
  145. package/dist/session/ws.d.ts +61 -0
  146. package/dist/session/ws.d.ts.map +1 -1
  147. package/dist/session/ws.js +122 -12
  148. package/dist/shell/npm-bin-entrypoints.d.ts +0 -35
  149. package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
  150. package/dist/shell/npm-bin-entrypoints.js +12 -85
  151. package/package.json +24 -4
  152. package/public/_assets/runtime/{node-shims-9e1380d9ee430fcc.js → node-shims-8f8c64edd2a1f0f5.js} +305 -29
  153. package/public/index.html +1 -1
  154. package/scripts/bundle-facet-workers.mjs +1 -1
  155. package/scripts/bundle-npm-cjs.mjs +2 -2
  156. package/dist/facets/on-demand-bundle-gate.d.ts +0 -65
  157. package/dist/facets/on-demand-bundle-gate.d.ts.map +0 -1
  158. package/dist/facets/on-demand-bundle-gate.js +0 -125
  159. package/dist/npm/install-args.d.ts +0 -21
  160. package/dist/npm/install-args.d.ts.map +0 -1
  161. package/dist/npm/install-args.js +0 -70
  162. package/dist/npm/npm-log.d.ts +0 -31
  163. package/dist/npm/npm-log.d.ts.map +0 -1
  164. package/dist/npm/npm-log.js +0 -51
@@ -37,6 +37,7 @@ import type { SessionProcessSupervisor } from '@nimbus-sh/core/runtime/session-p
37
37
  import type { SqliteVFS } from '@nimbus-sh/core/vfs/sqlite-vfs.js';
38
38
  import type { CirrusReal } from '../facets/cirrus-real.js';
39
39
  import type { WebSocketTerminal } from '../facets/ws-terminal.js';
40
+ import type { InitSessionOptions } from './init.js';
40
41
  import type { Kernel, Shell } from '@nimbus-sh/core/substrate/lifo/index.js';
41
42
  /**
42
43
  * Minimal host shape for WS lifecycle. Per plan §IX.1 b': fields here
@@ -67,9 +68,69 @@ export interface WsHost {
67
68
  _w5LastPersistRingSize: number;
68
69
  /** [B'.4] live phase indicator — see nimbus-session-internal.d.ts */
69
70
  _b4Phase: import('@nimbus-sh/platform/oom-discriminator.js').SessionState | null;
71
+ /** The session rebuild a woken instance has in flight; see bindShellSocket. */
72
+ _wakeRebuild: Promise<void> | null;
73
+ initSession(ws: WebSocket, options?: InitSessionOptions): Promise<void>;
70
74
  _w5PersistRing(): Promise<void> | null;
71
75
  _w9FlushOnClose(): void;
72
76
  }
77
+ /**
78
+ * Give the shell socket a frame arrived on a live session to land in.
79
+ *
80
+ * The runtime evicts a quiet object from memory while its accepted sockets
81
+ * stay open (the WebSocket Hibernation API), and the next frame from the
82
+ * peer wakes a fresh instance whose `shell`, `terminal` and `kernel` are
83
+ * null: the socket outlived the sleep, the session in memory did not.
84
+ * Measured 2026-09-14 on a deployed Worker: eight quiet seconds and the
85
+ * object is still there, ten and it is gone; the frame that wakes it is
86
+ * delivered and handled without error, and the peer gets nothing back —
87
+ * no output, no close, no error — because the terminal lookup that follows
88
+ * found nothing to hand it to. Only a new /ws upgrade rebuilt the session,
89
+ * and a peer whose socket still reads OPEN has no reason to make one. A
90
+ * browser tab hides this by probing every few seconds; a driver that
91
+ * simply waits between commands does not.
92
+ *
93
+ * So the rebuild runs here, on the socket that spoke, before the frame is
94
+ * handled: the cold path the upgrade takes after an eviction (cwd, env,
95
+ * mounts back from SQLite) minus the scrollback replay the peer already has
96
+ * on screen. Frames that land while the build runs await the same build —
97
+ * the input gate does not serialise handlers across a non-storage await, so
98
+ * without the shared promise two quick keystrokes would build two shells.
99
+ *
100
+ * A session that exists but answers to another socket — one an SDK call
101
+ * built on its headless terminal after the same wake — is handed to this
102
+ * socket the way the warm rejoin hands it to a new upgrade. The upgrade
103
+ * refuses a second shell socket while one still has a peer, so the socket
104
+ * that speaks is the one the peer is on.
105
+ *
106
+ * A build that fails closes the socket with a reason and answers false, so
107
+ * the caller drops the frame. The peer must learn that its shell is gone;
108
+ * the silence this replaces is the whole defect.
109
+ *
110
+ * A socket this side has already closed gets no session either: destroy
111
+ * closes every accepted socket and nulls the terminal before it wipes
112
+ * storage, and a frame still in flight on one of them must not rebuild
113
+ * the session it is tearing down.
114
+ */
115
+ export declare function bindShellSocket(self: WsHost, ws: WebSocket): Promise<boolean>;
116
+ /**
117
+ * What every output frame of the shell terminal feeds besides the socket:
118
+ * the persisted scrollback, and the shell-state snapshot.
119
+ *
120
+ * The snapshot runs here and not only after each inbound frame because
121
+ * commands run asynchronously: the `cd` a line asked for takes effect
122
+ * after the inbound handler has already snapshotted, and the next frame —
123
+ * which used to catch the row up — never arrives when the object
124
+ * hibernates first. Measured 2026-09-14: `cd /tmp && echo one`, ten quiet
125
+ * seconds, and the woken shell answered `pwd` from /home/user. The prompt
126
+ * that ends every command is an output frame, so by the time it has
127
+ * flushed, the state it reflects is what the row records.
128
+ *
129
+ * Every socket a shell terminal is built on or handed to goes through
130
+ * this — initSession, the warm rejoin, and the wake rebuild — so the
131
+ * three cannot drift.
132
+ */
133
+ export declare function shellTerminalTee(self: WsHost): (frame: string) => void;
73
134
  /**
74
135
  * Classify a closing/erroring WebSocket by its serialized attachment.
75
136
  * Shell sockets carry `{kind:'shell'}` (set at the /ws upgrade site);
@@ -1 +1 @@
1
- {"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../../src/session/ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAOH,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uDAAuD,CAAC;AACtG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,yCAAyC,CAAC;AAG7E;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,SAAS,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACnC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,mBAAmB,EAAE,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IACnD;;;;8DAI0D;IAC1D,YAAY,CAAC,EAAE,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;IAC5C,SAAS,EAAE,wBAAwB,CAAC;IACpC,wBAAwB,EAAE,OAAO,CAAC;IAClC,eAAe,EAAE,OAAO,CAAC;IACzB,aAAa,EAAE,GAAG,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,GAAG,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,EAAE,OAAO,0CAA0C,EAAE,YAAY,GAAG,IAAI,CAAC;IACjF,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvC,eAAe,IAAI,IAAI,CAAC;CACzB;AAgDD;;;;;;;GAOG;AACH,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AA0CD,wBAAgB,MAAM,CAAC,EAAE,EAAE,SAAS,GAAG,YAAY,CAElD;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA4EzG;AA+CD,wBAAsB,OAAO,CAC3B,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,SAAS,EACb,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,OAAO,GAClB,OAAO,CAAC,IAAI,CAAC,CAiGf;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CA4EtF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAUlD"}
1
+ {"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../../src/session/ws.ts"],"names":[],"mappings":"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"}
@@ -33,30 +33,97 @@
33
33
  * touches ctx.waitUntil; uses `(host.ctx as any)` cast).
34
34
  */
35
35
  import { dec } from '@nimbus-sh/core/_shared/bytes.js';
36
+ import { errorText } from '@nimbus-sh/core/_shared/error-text.js';
36
37
  import { recordFailure, getLastRpcFrame, getLastFacetId, recordRecoveryEvent } from '@nimbus-sh/platform/oom-discriminator.js';
37
38
  import { generation } from '@nimbus-sh/fabric/generation.js';
38
- import { persistShellState } from './state-store.js';
39
+ import { appendScrollback, persistShellState } from './state-store.js';
39
40
  import { handleFsWatchSubscribe, handleFsWatchUnsubscribe, cleanupFsWatchOnClose, } from './fs-watch.js';
40
41
  import { parseProcessLogClientFrame } from '@nimbus-sh/core/runtime/process-io-protocol.js';
41
42
  import { applyProcessClientFrame } from '@nimbus-sh/core/runtime/process-input-routing.js';
42
43
  import { z } from 'zod/v4';
43
44
  import { clearShellSocketStamp, noteShellSocketActivity } from './shell-socket.js';
44
45
  import { PRIOR_GENERATION_EXIT_REASON } from './rpc.js';
46
+ /**
47
+ * Give the shell socket a frame arrived on a live session to land in.
48
+ *
49
+ * The runtime evicts a quiet object from memory while its accepted sockets
50
+ * stay open (the WebSocket Hibernation API), and the next frame from the
51
+ * peer wakes a fresh instance whose `shell`, `terminal` and `kernel` are
52
+ * null: the socket outlived the sleep, the session in memory did not.
53
+ * Measured 2026-09-14 on a deployed Worker: eight quiet seconds and the
54
+ * object is still there, ten and it is gone; the frame that wakes it is
55
+ * delivered and handled without error, and the peer gets nothing back —
56
+ * no output, no close, no error — because the terminal lookup that follows
57
+ * found nothing to hand it to. Only a new /ws upgrade rebuilt the session,
58
+ * and a peer whose socket still reads OPEN has no reason to make one. A
59
+ * browser tab hides this by probing every few seconds; a driver that
60
+ * simply waits between commands does not.
61
+ *
62
+ * So the rebuild runs here, on the socket that spoke, before the frame is
63
+ * handled: the cold path the upgrade takes after an eviction (cwd, env,
64
+ * mounts back from SQLite) minus the scrollback replay the peer already has
65
+ * on screen. Frames that land while the build runs await the same build —
66
+ * the input gate does not serialise handlers across a non-storage await, so
67
+ * without the shared promise two quick keystrokes would build two shells.
68
+ *
69
+ * A session that exists but answers to another socket — one an SDK call
70
+ * built on its headless terminal after the same wake — is handed to this
71
+ * socket the way the warm rejoin hands it to a new upgrade. The upgrade
72
+ * refuses a second shell socket while one still has a peer, so the socket
73
+ * that speaks is the one the peer is on.
74
+ *
75
+ * A build that fails closes the socket with a reason and answers false, so
76
+ * the caller drops the frame. The peer must learn that its shell is gone;
77
+ * the silence this replaces is the whole defect.
78
+ *
79
+ * A socket this side has already closed gets no session either: destroy
80
+ * closes every accepted socket and nulls the terminal before it wipes
81
+ * storage, and a frame still in flight on one of them must not rebuild
82
+ * the session it is tearing down.
83
+ */
84
+ export async function bindShellSocket(self, ws) {
85
+ if (ws.readyState !== WS_READY_STATE_OPEN)
86
+ return false;
87
+ if (self.shell == null || self.terminal == null || self.kernel == null) {
88
+ if (!self._wakeRebuild) {
89
+ self._wakeRebuild = self.initSession(ws, { resume: 'wake' })
90
+ .finally(() => { self._wakeRebuild = null; });
91
+ }
92
+ try {
93
+ await self._wakeRebuild;
94
+ }
95
+ catch (e) {
96
+ const message = errorText(e);
97
+ console.error('[nimbus] session rebuild on wake failed:', message);
98
+ // A close reason is capped at 123 bytes by the protocol.
99
+ try {
100
+ ws.close(1011, `session rebuild after wake failed: ${message}`.slice(0, 123));
101
+ }
102
+ catch { /* closing */ }
103
+ return false;
104
+ }
105
+ return true;
106
+ }
107
+ if (self.terminal.ws !== ws)
108
+ self.terminal.attach(ws, shellTerminalTee(self));
109
+ return true;
110
+ }
111
+ /** The shell state each instance last wrote, so an unchanged state costs no SQL. */
112
+ const lastShellSnapshot = new WeakMap();
45
113
  /**
46
114
  * Snapshot the live Shell state and write it through to DO SQLite
47
115
  * [Phase 3 B'.1].
48
116
  *
49
- * Called from wsMessage (post-process, every inbound keystroke) and
50
- * once more in the wsClose / wsError shell-kind branch as a final
51
- * safety net before the in-memory Shell is torn down.
117
+ * Called after every inbound frame, from every output flush of the shell
118
+ * terminal (see shellTerminalTee), and once more in the wsClose / wsError
119
+ * shell-kind branch as a final safety net before the in-memory Shell is
120
+ * torn down.
52
121
  *
53
122
  * Read-only and synchronous (DO storage SQL is sync inside a request
54
- * context). Cheap: one read of `shell.getCwd()` + `shell.getEnv()`,
55
- * a JSON.stringify of env, and an INSERT-OR-REPLACE into the small
56
- * nimbus_session_kv table. Skips the SQL write entirely when nothing
57
- * has changed since the previous snapshot — the comparison is
58
- * pointer-equality on cwd plus env reference, since Shell.getEnv()
59
- * returns a live Record and `cd` mutates `this.cwd` in place.
123
+ * context). Cheap: one read of `shell.getCwd()` + `shell.getEnv()` and a
124
+ * JSON.stringify of env; the INSERT-OR-REPLACE into nimbus_session_kv
125
+ * only runs when either differs from what this instance last wrote, so
126
+ * the per-flush call during a chatty command costs a string compare.
60
127
  *
61
128
  * Failure model: persistShellState throws ONLY on env-too-large
62
129
  * (the SESSION_ENV_MAX_BYTES gate). We surface that via console.warn
@@ -88,13 +155,49 @@ function snapshotShellState(self) {
88
155
  catch { /* best-effort */ }
89
156
  if (!cwd && !env)
90
157
  return;
158
+ const envJson = env ? JSON.stringify(env) : null;
159
+ const last = lastShellSnapshot.get(self);
160
+ if (last && last.cwd === cwd && last.envJson === envJson)
161
+ return;
91
162
  try {
92
163
  persistShellState(ctx, { cwd, env });
164
+ lastShellSnapshot.set(self, { cwd, envJson });
93
165
  }
94
166
  catch (e) {
95
167
  console.warn('[nimbus/B\'.1] persistShellState failed:', e?.message || e);
96
168
  }
97
169
  }
170
+ /**
171
+ * What every output frame of the shell terminal feeds besides the socket:
172
+ * the persisted scrollback, and the shell-state snapshot.
173
+ *
174
+ * The snapshot runs here and not only after each inbound frame because
175
+ * commands run asynchronously: the `cd` a line asked for takes effect
176
+ * after the inbound handler has already snapshotted, and the next frame —
177
+ * which used to catch the row up — never arrives when the object
178
+ * hibernates first. Measured 2026-09-14: `cd /tmp && echo one`, ten quiet
179
+ * seconds, and the woken shell answered `pwd` from /home/user. The prompt
180
+ * that ends every command is an output frame, so by the time it has
181
+ * flushed, the state it reflects is what the row records.
182
+ *
183
+ * Every socket a shell terminal is built on or handed to goes through
184
+ * this — initSession, the warm rejoin, and the wake rebuild — so the
185
+ * three cannot drift.
186
+ */
187
+ export function shellTerminalTee(self) {
188
+ // `ctx` is `protected` on the DO base class and so cannot sit on the
189
+ // host interface (DEFECT-D1); scrollback is the one thing here needing it.
190
+ const ctx = 'ctx' in self ? self.ctx : undefined;
191
+ return (frame) => {
192
+ try {
193
+ appendScrollback(ctx, frame, Date.now());
194
+ }
195
+ catch (e) {
196
+ console.warn("[B'.3] appendScrollback failed:", errorText(e));
197
+ }
198
+ snapshotShellState(self);
199
+ };
200
+ }
98
201
  const WsAttachmentSchema = z.object({
99
202
  kind: z.string(),
100
203
  clientId: z.string().optional(),
@@ -112,6 +215,8 @@ const FsWatchClientFrameSchema = z.discriminatedUnion('type', [
112
215
  subId: z.string().optional(),
113
216
  }).passthrough(),
114
217
  ]);
218
+ /** `WebSocket.readyState` for an open socket; the constant name differs between workerd and Node. */
219
+ const WS_READY_STATE_OPEN = 1;
115
220
  const TerminalMessageSchema = z.object({
116
221
  type: z.string(),
117
222
  data: z.string().optional(),
@@ -156,8 +261,13 @@ export async function wsMessage(self, ws, message) {
156
261
  // The stamp goes in the attachment because isolate memory does not
157
262
  // survive hibernation and the /ws upgrade has to read it afterwards.
158
263
  // Rate-limited inside; see src/session/shell-socket.ts.
159
- if (attach?.kind === 'shell')
264
+ if (attach?.kind === 'shell') {
160
265
  noteShellSocketActivity(ws);
266
+ // Before the frame is parsed: a woken instance has no session to
267
+ // hand ANY shell-socket frame to, the liveness probe included.
268
+ if (!(await bindShellSocket(self, ws)))
269
+ return;
270
+ }
161
271
  const data = typeof message === 'string' ? message : dec.decode(message);
162
272
  const value = JSON.parse(data);
163
273
  // file-tree-watch (2026-05-15): handle fs-watch-* on this WS BEFORE
@@ -212,7 +322,7 @@ export async function wsMessage(self, ws, message) {
212
322
  // Persist Shell state to DO SQLite after the terminal has handled
213
323
  // the user's keystroke. The Shell builtin `cd` mutates this.cwd
214
324
  // synchronously inside executeLine, so by the time we reach this
215
- // line a `cd app\r` has already taken effect and we capture the
325
+ // line a `cd example-app\r` has already taken effect and we capture the
216
326
  // new cwd. Cheap when nothing has changed; SESSION_ENV_MAX_BYTES
217
327
  // is the only failure mode and is logged, not thrown.
218
328
  snapshotShellState(self);
@@ -4,41 +4,6 @@ import type { FacetManager } from '../facets/manager.js';
4
4
  type Output = {
5
5
  write(data: string): void;
6
6
  };
7
- /**
8
- * How long a bin invocation may take before the shell stops waiting for it.
9
- *
10
- * The program itself is already bounded: FACET_TIMEOUT_MS kills a one-shot
11
- * facet and the session reports exit 124 with a reason. Nothing bounds the
12
- * supervisor-side work AROUND that run — the prefetch-bundle walk, the ESM
13
- * transform, staging a bundled artifact, the loader hop — and nothing bounds
14
- * the staged-artifact dispatch at all. A dispatch that never settles leaves
15
- * `running` stuck true on this connection's shell: every later keystroke is
16
- * swallowed, no prompt returns, and nothing says why. A terminal that goes
17
- * silent forever is worse than a command that fails.
18
- *
19
- * So the invocation gets the program's lifetime twice over: the program keeps
20
- * its full FACET_TIMEOUT_MS and the supervisor-side work gets the same again.
21
- * Derived rather than chosen, so it cannot drift from the bound it exists to
22
- * sit outside. Measured against a deployed Worker, the heaviest bins we run
23
- * sit far inside it: `pi --version` (a 17.4 MiB module map, the largest
24
- * observed) returns in 16s, and every staged-opencode one-shot in 2-4s.
25
- */
26
- export declare const BIN_DISPATCH_TIMEOUT_MS: number;
27
- export type BinDispatchOutcome<T> = {
28
- expired: false;
29
- value: T;
30
- } | {
31
- expired: true;
32
- };
33
- /**
34
- * Await a bin dispatch under a bound. Reports expiry instead of hanging.
35
- *
36
- * The abandoned work keeps running — there is nothing to cancel it with, and a
37
- * facet that eventually finishes still lands its own exit and its write-back.
38
- * It just no longer holds the shell open, and it can never surface as an
39
- * unhandled rejection once we have stopped listening.
40
- */
41
- export declare function awaitBinDispatch<T>(work: Promise<T>, budgetMs: number): Promise<BinDispatchOutcome<T>>;
42
7
  type RegistryLike = {
43
8
  resolve(name: string): Promise<unknown> | unknown;
44
9
  };
@@ -1 +1 @@
1
- {"version":3,"file":"npm-bin-entrypoints.d.ts","sourceRoot":"","sources":["../../src/shell/npm-bin-entrypoints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uDAAuD,CAAC;AACtG,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,sBAAsB,CAAC;AAWnF,KAAK,MAAM,GAAG;IAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE5C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,uBAAuB,QAAuB,CAAC;AAI5D,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,CAAC;AAErF;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAchC;AA4BD,KAAK,YAAY,GAAG;IAClB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACnD,CAAC;AAEF,KAAK,kBAAkB,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC;AAezD,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE;IACJ,GAAG,EAAE,eAAe,CAAC;IACrB,MAAM,IAAI,MAAM,CAAC;IACjB,SAAS,EAAE,wBAAwB,CAAC;IACpC,eAAe,IAAI,YAAY,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mBAAmB,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxJ,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC9D,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7F,GACA,IAAI,CA4HN;AA0FD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEnF,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EAAE,GACb,yBAAyB,CAS3B;AA+FD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAS/E"}
1
+ {"version":3,"file":"npm-bin-entrypoints.d.ts","sourceRoot":"","sources":["../../src/shell/npm-bin-entrypoints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uDAAuD,CAAC;AACtG,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,sBAAsB,CAAC;AAWnF,KAAK,MAAM,GAAG;IAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAiB5C,KAAK,YAAY,GAAG;IAClB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACnD,CAAC;AAEF,KAAK,kBAAkB,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC;AAezD,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE;IACJ,GAAG,EAAE,eAAe,CAAC;IACrB,MAAM,IAAI,MAAM,CAAC;IACjB,SAAS,EAAE,wBAAwB,CAAC;IACpC,eAAe,IAAI,YAAY,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mBAAmB,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxJ,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC9D,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7F,GACA,IAAI,CAqHN;AAqFD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEnF,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EAAE,GACb,yBAAyB,CAS3B;AA+FD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAS/E"}
@@ -2,67 +2,8 @@ import { resolveNpmBin, resolveNpmBinFromPath, isStagedArtifactTarget, stagedArt
2
2
  import { bundleProfileForNpmBin } from '@nimbus-sh/core/runtime/bundle-profile.js';
3
3
  import { OPENCODE_TREE_SITTER_DIAG_ARG } from '../runtime/opencode-facet-runner.js';
4
4
  import { normalizeVfsPath } from '@nimbus-sh/core/vfs/path.js';
5
- import { DEFAULT_PATH, FACET_TIMEOUT_MS } from '@nimbus-sh/core/constants.js';
5
+ import { DEFAULT_PATH } from '@nimbus-sh/core/constants.js';
6
6
  import { z } from 'zod/v4';
7
- /**
8
- * How long a bin invocation may take before the shell stops waiting for it.
9
- *
10
- * The program itself is already bounded: FACET_TIMEOUT_MS kills a one-shot
11
- * facet and the session reports exit 124 with a reason. Nothing bounds the
12
- * supervisor-side work AROUND that run — the prefetch-bundle walk, the ESM
13
- * transform, staging a bundled artifact, the loader hop — and nothing bounds
14
- * the staged-artifact dispatch at all. A dispatch that never settles leaves
15
- * `running` stuck true on this connection's shell: every later keystroke is
16
- * swallowed, no prompt returns, and nothing says why. A terminal that goes
17
- * silent forever is worse than a command that fails.
18
- *
19
- * So the invocation gets the program's lifetime twice over: the program keeps
20
- * its full FACET_TIMEOUT_MS and the supervisor-side work gets the same again.
21
- * Derived rather than chosen, so it cannot drift from the bound it exists to
22
- * sit outside. Measured against a deployed Worker, the heaviest bins we run
23
- * sit far inside it: `pi --version` (a 17.4 MiB module map, the largest
24
- * observed) returns in 16s, and every staged-opencode one-shot in 2-4s.
25
- */
26
- export const BIN_DISPATCH_TIMEOUT_MS = 2 * FACET_TIMEOUT_MS;
27
- const DISPATCH_EXPIRED = Symbol('nimbus.bin-dispatch-expired');
28
- /**
29
- * Await a bin dispatch under a bound. Reports expiry instead of hanging.
30
- *
31
- * The abandoned work keeps running — there is nothing to cancel it with, and a
32
- * facet that eventually finishes still lands its own exit and its write-back.
33
- * It just no longer holds the shell open, and it can never surface as an
34
- * unhandled rejection once we have stopped listening.
35
- */
36
- export async function awaitBinDispatch(work, budgetMs) {
37
- let timer;
38
- const deadline = new Promise((_resolve, reject) => {
39
- timer = setTimeout(() => reject(DISPATCH_EXPIRED), budgetMs);
40
- });
41
- try {
42
- return { expired: false, value: await Promise.race([work, deadline]) };
43
- }
44
- catch (e) {
45
- if (e !== DISPATCH_EXPIRED)
46
- throw e;
47
- work.catch(() => { });
48
- return { expired: true };
49
- }
50
- finally {
51
- if (timer !== undefined)
52
- clearTimeout(timer);
53
- }
54
- }
55
- /**
56
- * The reason a bin invocation is being abandoned. Names the limit and both
57
- * numbers it is derived from so the text cannot drift from the code, and says
58
- * where anything the program still produces will go.
59
- */
60
- function dispatchExpiredReason(shellLine, pid) {
61
- return `${shellLine}: did not come back after ${BIN_DISPATCH_TIMEOUT_MS / 1000}s — the ` +
62
- `${FACET_TIMEOUT_MS / 1000}s a program is given to run, and the same again for the work ` +
63
- 'around it. The shell is no longer waiting on it' +
64
- (pid === null ? '.' : `; anything it still produces goes to the log for pid ${pid}.`);
65
- }
66
7
  const NpmBinPackageMetadataSchema = z.object({
67
8
  name: z.string().optional(),
68
9
  keywords: z.array(z.string()).optional(),
@@ -135,18 +76,14 @@ export function installNpmBinFallbackResolver(registry, deps) {
135
76
  const label = longRunning ? 'started (long-running)' : 'started';
136
77
  deps.terminal?.write(`\x1b[2m[bin ${label}: pid=${pid} cmd="${shellLine}"]\x1b[0m\r\n`);
137
78
  deps.notifyTerminalEvent({ type: 'spawn', pid, command: shellLine, longRunning, attachedTty });
138
- // Once the invocation is abandoned the terminal has moved on to the next
139
- // prompt, so late output must not be interleaved into it. The pid's log
140
- // still takes everything — that is where the rest of the run shows up.
141
- let abandoned = false;
79
+ // Output goes to the pid's log ring and the caller's streams; nothing
80
+ // abandons this invocation — it runs until the program exits.
142
81
  const writeThrough = (stream, target) => (data) => {
143
82
  const text = String(data);
144
83
  try {
145
84
  deps.processes.appendOutput(pid, stream, text);
146
85
  }
147
86
  catch { }
148
- if (abandoned)
149
- return;
150
87
  try {
151
88
  target.write(text);
152
89
  }
@@ -154,7 +91,10 @@ export function installNpmBinFallbackResolver(registry, deps) {
154
91
  };
155
92
  let exitCode = 1;
156
93
  try {
157
- const dispatched = await awaitBinDispatch(Promise.resolve(runRuntime({
94
+ // A user-invoked bin is a foreground program: the shell waits for its
95
+ // real exit — no dispatch timeout. Ctrl-C ends it through the
96
+ // terminator the exec path registers on the pid.
97
+ exitCode = await runRuntime({
158
98
  ...ctx,
159
99
  args: ['/' + bin.targetPath, ...argv],
160
100
  stdout: { write: writeThrough('stdout', ctx.stdout) },
@@ -167,15 +107,7 @@ export function installNpmBinFallbackResolver(registry, deps) {
167
107
  attachedTty,
168
108
  },
169
109
  __nimbusBundleProfile: bundleProfile,
170
- })), BIN_DISPATCH_TIMEOUT_MS);
171
- if (dispatched.expired) {
172
- writeThrough('stderr', ctx.stderr)(`${dispatchExpiredReason(shellLine, pid)}\n`);
173
- abandoned = true;
174
- exitCode = 124;
175
- }
176
- else {
177
- exitCode = dispatched.value;
178
- }
110
+ });
179
111
  }
180
112
  catch (e) {
181
113
  writeThrough('stderr', ctx.stderr)(`bin error: ${formatError(e)}\n`);
@@ -212,21 +144,16 @@ async function runStagedArtifact(deps, name, artifact, argv, cwd, ctx, dispositi
212
144
  // Piped stdin is not yet wired for staged artifacts; the interactive TUI reads
213
145
  // keystrokes from the live ProcessInputStore via the attached-TTY stdin pump.
214
146
  const base = { argv, env: ctx.env ?? {}, cwd, command: shellLine };
215
- let dispatched;
147
+ let result;
216
148
  try {
217
- dispatched = await awaitBinDispatch(stagedArtifactWork(fm, artifact, base, disposition), BIN_DISPATCH_TIMEOUT_MS);
149
+ // Same contract as the node-bin path: a user-invoked program runs to
150
+ // its own exit; the shell does not bound the wait.
151
+ result = await stagedArtifactWork(fm, artifact, base, disposition);
218
152
  }
219
153
  catch (e) {
220
154
  ctx.stderr.write(`${name}: ${formatError(e)}\n`);
221
155
  return 1;
222
156
  }
223
- // A staged artifact owns its own process-table entry and only tells us the
224
- // pid on the way out, so an abandoned dispatch has no pid to point at.
225
- if (dispatched.expired) {
226
- ctx.stderr.write(`${dispatchExpiredReason(shellLine, null)}\n`);
227
- return 124;
228
- }
229
- const result = dispatched.value;
230
157
  // Resident dispositions (dual / server / attached): the facet(s) are now
231
158
  // resident, streaming live and reporting their own exit through the
232
159
  // supervisor. Surface the long-running spawn and hand off — the same
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbus-sh/worker",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Nimbus runtime package for Cloudflare Workers, re-exported publicly through @nimbus-sh/sdk/worker.",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -53,6 +53,26 @@
53
53
  "types": "./dist/_shared/preview-host.d.ts",
54
54
  "import": "./dist/_shared/preview-host.js"
55
55
  },
56
+ "./workspace-host": {
57
+ "workspace": "./src/facets/compose.ts",
58
+ "types": "./dist/facets/compose.d.ts",
59
+ "import": "./dist/facets/compose.js"
60
+ },
61
+ "./port-capability": {
62
+ "workspace": "./src/session/port-capability.ts",
63
+ "types": "./dist/session/port-capability.d.ts",
64
+ "import": "./dist/session/port-capability.js"
65
+ },
66
+ "./durable-slots": {
67
+ "workspace": "./src/facets/durable-slots.ts",
68
+ "types": "./dist/facets/durable-slots.d.ts",
69
+ "import": "./dist/facets/durable-slots.js"
70
+ },
71
+ "./git": {
72
+ "workspace": "./src/git/commands.ts",
73
+ "types": "./dist/git/commands.d.ts",
74
+ "import": "./dist/git/commands.js"
75
+ },
56
76
  "./runtime-sync-helper": "./scripts/bundle-runtime.mjs",
57
77
  "./public/*": "./public/*",
58
78
  "./package.json": "./package.json"
@@ -84,9 +104,9 @@
84
104
  },
85
105
  "dependencies": {
86
106
  "@ai-sdk/openai-compatible": "^2.0.41",
87
- "@nimbus-sh/core": "^0.7.0",
88
- "@nimbus-sh/fabric": "^0.3.0",
89
- "@nimbus-sh/platform": "^0.2.0",
107
+ "@nimbus-sh/core": "^0.9.0",
108
+ "@nimbus-sh/fabric": "^0.5.0",
109
+ "@nimbus-sh/platform": "^0.4.0",
90
110
  "@renovatebot/pep440": "^5.0.0",
91
111
  "ai": "^6.0.182",
92
112
  "clean-git-ref": "^2.0.1",