@abide/abide 0.123.2 → 0.124.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 (150) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +58 -0
  3. package/bin/abide.ts +36 -65
  4. package/docs/REGISTRY.md +45 -12
  5. package/docs/RULEBOOK.md +24 -14
  6. package/docs/abide.md +999 -459
  7. package/package.json +1 -1
  8. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  9. package/src/compiler/Diagnostics.ts +198 -0
  10. package/src/compiler/KeptChecker.ts +97 -0
  11. package/src/compiler/LoweredFile.ts +609 -0
  12. package/src/compiler/Lowering.ts +18 -1
  13. package/src/compiler/PositionedRefusal.ts +1 -1
  14. package/src/compiler/SHARED_NAMES.ts +2 -0
  15. package/src/compiler/ShadowHost.ts +670 -0
  16. package/src/compiler/ShadowMap.ts +1 -1
  17. package/src/compiler/TypeSession.ts +76 -1743
  18. package/src/compiler/appLayout.ts +28 -20
  19. package/src/compiler/appLints.ts +109 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -1
  21. package/src/compiler/checkApp.ts +64 -68
  22. package/src/compiler/checkedShadow.ts +2 -2
  23. package/src/compiler/classifyRead.ts +16 -1
  24. package/src/compiler/compileApp.ts +102 -71
  25. package/src/compiler/declaredExports.ts +46 -21
  26. package/src/compiler/diagnoseSources.ts +232 -0
  27. package/src/compiler/emitClient.ts +1 -1
  28. package/src/compiler/formatAbide.ts +7 -8
  29. package/src/compiler/handlersUnder.ts +32 -63
  30. package/src/compiler/lineOf.ts +0 -19
  31. package/src/compiler/matchersOf.ts +3 -3
  32. package/src/compiler/mountsOf.ts +3 -3
  33. package/src/compiler/reactiveReads.ts +235 -0
  34. package/src/compiler/reactiveTypes.ts +13 -0
  35. package/src/compiler/readSources.ts +16 -18
  36. package/src/compiler/readsIn.ts +181 -82
  37. package/src/compiler/routesDeclaration.ts +46 -0
  38. package/src/compiler/scaffold.ts +7 -2
  39. package/src/compiler/serveLanguage.ts +46 -74
  40. package/src/compiler/sharedModuleWrites.ts +1 -1
  41. package/src/compiler/splitImports.ts +2 -2
  42. package/src/compiler/templateWarnings.ts +1 -2
  43. package/src/compiler/trackingWarnings.ts +1 -1
  44. package/src/server/CLIENT_BUILD.ts +4 -32
  45. package/src/server/DesktopSession.ts +213 -0
  46. package/src/server/Followers.ts +399 -0
  47. package/src/server/MODULE_QUERIES.ts +6 -2
  48. package/src/server/PageRender.ts +201 -0
  49. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  50. package/src/server/abideModules.ts +7 -7
  51. package/src/server/answerFailure.ts +3 -2
  52. package/src/server/answerRequest.ts +16 -369
  53. package/src/server/answerRpc.ts +48 -302
  54. package/src/server/answerSocket.ts +41 -490
  55. package/src/server/appBuild.ts +377 -0
  56. package/src/server/appKit.ts +1 -1
  57. package/src/server/appPlugins.ts +15 -17
  58. package/src/server/appTree.ts +6 -5
  59. package/src/server/argsContract.ts +209 -0
  60. package/src/server/bridgeRelay.ts +153 -65
  61. package/src/server/claudeCode.ts +30 -127
  62. package/src/server/config.ts +6 -7
  63. package/src/server/createApp.ts +38 -66
  64. package/src/server/csp.ts +1 -1
  65. package/src/server/deferredBuilds.ts +3 -16
  66. package/src/server/devWorker.ts +11 -15
  67. package/src/server/faceOf.ts +1 -1
  68. package/src/server/generatedSurfaces.ts +2 -1
  69. package/src/server/handlerFileMount.ts +40 -0
  70. package/src/server/index.ts +3 -3
  71. package/src/server/installServer.ts +5 -9
  72. package/src/server/invokeHandler.ts +230 -0
  73. package/src/server/mountHandlers.ts +184 -0
  74. package/src/server/openDesktopWindow.ts +308 -0
  75. package/src/server/packageDirectory.ts +14 -0
  76. package/src/server/preload.ts +21 -2
  77. package/src/server/principal.ts +5 -4
  78. package/src/server/remote.ts +2 -2
  79. package/src/server/render.ts +31 -59
  80. package/src/server/requestScope.ts +5 -36
  81. package/src/server/runCompiled.ts +1 -1
  82. package/src/server/runDesktop.ts +20 -448
  83. package/src/server/seedBuffer.ts +12 -12
  84. package/src/server/serveDev.ts +24 -58
  85. package/src/server/servedApp.ts +390 -0
  86. package/src/server/shell.ts +1 -1
  87. package/src/server/signInChanged.ts +20 -0
  88. package/src/server/surfaceOf.ts +3 -2
  89. package/src/server/versionOf.ts +7 -16
  90. package/src/shared/RPC_SERVER.ts +27 -31
  91. package/src/shared/Schema.ts +1 -2
  92. package/src/shared/applyPatch.ts +1 -1
  93. package/src/shared/index.ts +1 -1
  94. package/src/shared/{note.ts → issueReport.ts} +22 -0
  95. package/src/shared/loadCall.ts +66 -0
  96. package/src/shared/muxFrames.ts +1 -1
  97. package/src/shared/reactive/Keyed.ts +24 -16
  98. package/src/shared/reactive/Reactive.ts +37 -991
  99. package/src/shared/reactive/argsContract.ts +131 -4
  100. package/src/shared/reactive/byOf.ts +2 -5
  101. package/src/shared/reactive/callLoad.ts +8 -14
  102. package/src/shared/reactive/cellShape.ts +734 -0
  103. package/src/shared/reactive/index.ts +12 -23
  104. package/src/shared/reactive/pairReactive.ts +5 -3
  105. package/src/shared/reactive/patchLog.ts +79 -0
  106. package/src/shared/reactive/patchMarks.ts +34 -0
  107. package/src/shared/reactive/producer.ts +121 -0
  108. package/src/shared/reactive/recordWrites.ts +11 -14
  109. package/src/shared/reactive/scoped.ts +3 -2
  110. package/src/shared/reactive/selections.ts +209 -0
  111. package/src/shared/reactive/share.ts +4 -3
  112. package/src/shared/reactive/watch.ts +3 -4
  113. package/src/shared/rpc.ts +20 -24
  114. package/src/shared/validateJson.ts +1 -2
  115. package/src/shared/writePath.ts +29 -0
  116. package/src/ui/ArmRegion.ts +10 -4
  117. package/src/ui/Level.ts +11 -20
  118. package/src/ui/LevelChain.ts +272 -0
  119. package/src/ui/Router.ts +106 -313
  120. package/src/ui/Scope.ts +17 -3
  121. package/src/ui/TryRegion.ts +22 -23
  122. package/src/ui/bindSpread.ts +2 -4
  123. package/src/ui/listenForRoutes.ts +3 -10
  124. package/src/ui/outboxQueue.ts +23 -11
  125. package/src/ui/outcomeOf.ts +120 -0
  126. package/src/ui/reconcile.ts +4 -6
  127. package/src/ui/routing.ts +2 -1
  128. package/src/ui/rows.ts +2 -2
  129. package/src/ui/setAttribute.ts +12 -3
  130. package/src/ui/socketWire.ts +28 -13
  131. package/src/ui/wire.ts +9 -10
  132. package/src/compiler/Finding.ts +0 -2
  133. package/src/compiler/Refusals.ts +0 -65
  134. package/src/compiler/formatDiagnosis.ts +0 -20
  135. package/src/server/appDirectories.ts +0 -9
  136. package/src/server/argsFor.ts +0 -39
  137. package/src/server/buildDirectory.ts +0 -10
  138. package/src/server/coerceArgs.ts +0 -109
  139. package/src/server/derivedArgs.ts +0 -26
  140. package/src/server/runBuild.ts +0 -24
  141. package/src/server/serveBuild.ts +0 -144
  142. package/src/server/testBuild.ts +0 -220
  143. package/src/shared/LOAD_OFFLINE.ts +0 -15
  144. package/src/shared/LOAD_SIGNAL.ts +0 -10
  145. package/src/shared/issuesOf.ts +0 -18
  146. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
  147. package/src/shared/reactive/FOLLOWED.ts +0 -8
  148. package/src/shared/reactive/PATCHES.ts +0 -11
  149. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  150. package/src/shared/reactive/canonicalKey.ts +0 -95
@@ -12,6 +12,9 @@
12
12
  // in there when its code was minted (44.33), for as long as the bridge's stream is open, or
13
13
  // resumable. HELD IN THIS PROCESS: an app served by several needs a visitor's requests to reach the
14
14
  // one its bridge dialed.
15
+ //
16
+ // Every frame on that stream is written here; what the rest of the server sees is a turn run, a
17
+ // pairing's status as it changes, and a pairing code.
15
18
 
16
19
  import { isFailed } from '#shared/Failed.ts'
17
20
  import { refuse } from '#shared/refuse.ts'
@@ -19,14 +22,17 @@ import { refuse } from '#shared/refuse.ts'
19
22
  import { answered } from './answerFailure.ts'
20
23
  import { bodyWithin } from './answerRpc.ts'
21
24
  import { framed } from './answerValue.ts'
25
+ import type { AssistantEvent } from './assistant.ts'
22
26
  import { Inbox } from './Inbox.ts'
23
27
  import { randomToken } from './randomToken.ts'
28
+ import { RESUME_MILLISECONDS } from './RESUME_MILLISECONDS.ts'
29
+ import { onSignInChanged } from './signInChanged.ts'
24
30
 
25
31
  /**
26
32
  * What a turn hears from the bridge, in the order the bridge posted it: an event, an MCP request, the
27
33
  * end, or a failure. ONE SHAPE for all four, every field set, since one is built per streamed chunk.
28
34
  */
29
- export type TurnItem = {
35
+ type TurnItem = {
30
36
  kind: 'event' | 'mcp' | 'end' | 'fail'
31
37
  event: unknown
32
38
  id: number
@@ -36,7 +42,7 @@ export type TurnItem = {
36
42
  }
37
43
 
38
44
  /** One paired bridge: what it is sent, and the turns it is answering. */
39
- export type Bridge = {
45
+ type Bridge = {
40
46
  /** The browser and the sign-in it is paired to (`pairingKey`). 44.33. */
41
47
  key: string
42
48
  token: string
@@ -53,8 +59,6 @@ export type Bridge = {
53
59
  const CODE_LIFETIME = 10 * 60_000
54
60
  // How long a bridge outlives the last page watching it, so a reload does not end it.
55
61
  const IDLE_GRACE = 30_000
56
- /** How long a pairing whose stream dropped waits to be resumed. 44.26. */
57
- export const RESUME_MILLISECONDS = 60_000
58
62
 
59
63
  const CODES = new Map<string, { key: string; expires: number }>()
60
64
  const BY_KEY = new Map<string, Bridge>()
@@ -65,6 +69,13 @@ const FOLLOWERS = new Map<
65
69
  Set<(caller: string, signedIn: string) => void>
66
70
  >()
67
71
 
72
+ // 44.34 — A STATUS STREAM OPENED BEFORE A SIGN-IN carries the cookies it was opened with, so it hears
73
+ // of the new one, or of the caller a sign-out rotated it to (26.9), here rather than off its own
74
+ // request.
75
+ onSignInChanged((caller, now, signedIn) => {
76
+ for (const follow of FOLLOWERS.get(caller) ?? []) follow(now, signedIn)
77
+ })
78
+
68
79
  /**
69
80
  * What a pairing is filed under: the browser, and who was signed in there (`signedInAs`).
70
81
  *
@@ -75,7 +86,7 @@ function pairingKey(caller: string, signedIn: string): string {
75
86
  return `${caller}\n${signedIn}`
76
87
  }
77
88
 
78
- // 44.19 — the relay answers once the app has handed out a pairing command, and never before.
89
+ // 44.19 — the relay answers once the app has handed out a pairing code, and never before.
79
90
  let minted = false
80
91
 
81
92
  /** A single-use pairing code for `caller` signed in as `signedIn`, and when it stops standing. 44.14. */
@@ -93,24 +104,135 @@ export function mintCode(
93
104
  return { code, expires }
94
105
  }
95
106
 
96
- /** The bridge paired to `caller` signed in as `signedIn`, if its stream is open. 44.33. */
97
- export function pairedBridge(
98
- caller: string,
99
- signedIn: string,
100
- ): Bridge | undefined {
101
- return BY_KEY.get(pairingKey(caller, signedIn))
107
+ /**
108
+ * One turn on the bridge paired to `caller` signed in as `signedIn`: sent `prompt` under `config`,
109
+ * yielding the events its Claude Code reports, each MCP request it makes handed to `answer` and the
110
+ * reply sent back. 44.13, 44.17, 44.21.
111
+ */
112
+ export async function* runTurn({
113
+ caller,
114
+ signedIn,
115
+ prompt,
116
+ config,
117
+ maxTurns,
118
+ answer,
119
+ signal,
120
+ }: {
121
+ caller: string
122
+ signedIn: string
123
+ prompt: unknown[]
124
+ config: unknown
125
+ maxTurns: number
126
+ /** Never rejects: its failure is a reply too. */
127
+ answer: (method: string, params: unknown) => Promise<unknown>
128
+ signal: AbortSignal
129
+ }): AsyncGenerator<AssistantEvent> {
130
+ const bridge = BY_KEY.get(pairingKey(caller, signedIn))
131
+ // 44.13, 44.33 — nothing paired by this sign-in is the page's cue to show the command.
132
+ if (bridge === undefined)
133
+ throw refuse(
134
+ 409,
135
+ 'No Claude Code is paired with this browser as the one signed in. claudeCode.bridgeCommand() is the command that pairs one.',
136
+ )
137
+ const turn = randomToken(9, 'base64url')
138
+ const inbox = new Inbox<TurnItem>()
139
+ bridge.turns.set(turn, inbox)
140
+ // `bridge.frames` is read at each push: a resumed stream (44.26) replaces it.
141
+ bridge.frames.push({ type: 'turn', id: turn, prompt, config, maxTurns })
142
+ // 44.18 — a caller hanging up ends the run on the bridge.
143
+ const cancel = (): void => {
144
+ bridge.frames.push({ type: 'cancel', turn })
145
+ }
146
+ signal.addEventListener('abort', cancel)
147
+ try {
148
+ for await (const item of inbox) {
149
+ if (item.kind === 'event') yield item.event as AssistantEvent
150
+ else if (item.kind === 'mcp')
151
+ void answer(item.method, item.params).then((reply) => {
152
+ bridge.frames.push({
153
+ type: 'reply',
154
+ turn,
155
+ id: item.id,
156
+ reply,
157
+ })
158
+ })
159
+ else if (item.kind === 'end') return
160
+ else throw refuse(502, `Claude Code failed: ${item.message}`)
161
+ }
162
+ } finally {
163
+ signal.removeEventListener('abort', cancel)
164
+ bridge.turns.delete(turn)
165
+ if (!WATCHERS.has(bridge.key)) armIdle(bridge.key)
166
+ }
102
167
  }
103
168
 
104
169
  /**
105
- * Registers `wake` to run whenever the pairing of `caller` signed in as `signedIn` changes; the
106
- * returned function undoes it.
170
+ * The pairing of `caller` signed in as `signedIn` as it changes, until `signal` aborts: the code that
171
+ * pairs it while unpaired, then `undefined` the moment a bridge connects, and a code again if it goes. A
172
+ * code that expires while nothing is paired is replaced with a fresh one. 44.20.
173
+ *
174
+ * 44.34 — IT FOLLOWS WHOEVER SIGNS IN AT THE BROWSER: a sign-in or a sign-out made since is the
175
+ * pairing it watches from then on, and the code it offers is that sign-in's.
107
176
  */
108
- export function watchPairing(
177
+ export async function* pairingStatus(
109
178
  caller: string,
110
179
  signedIn: string,
111
- wake: () => void,
112
- ): () => void {
113
- const key = pairingKey(caller, signedIn)
180
+ signal: AbortSignal,
181
+ ): AsyncGenerator<string | undefined> {
182
+ // A change landing while the loop was not waiting — paused at its `yield` — stays queued, so it
183
+ // looks again; a look that finds nothing new yields nothing.
184
+ const changes = new Inbox<undefined>()
185
+ const waiting = changes[Symbol.asyncIterator]()
186
+ const rouse = (): void => changes.push(undefined)
187
+ const follow = (now: string, next: string): void => {
188
+ caller = now
189
+ signedIn = next
190
+ rouse()
191
+ }
192
+ let watchingKey = ''
193
+ let unwatch = (): void => {}
194
+ let unfollow = (): void => {}
195
+ let pairing: { code: string; expires: number } | undefined
196
+ // `null` until the first is said; `undefined` says paired.
197
+ let said: string | undefined | null = null
198
+ signal.addEventListener('abort', rouse)
199
+ try {
200
+ while (!signal.aborted) {
201
+ const key = pairingKey(caller, signedIn)
202
+ if (key !== watchingKey) {
203
+ unwatch()
204
+ unfollow()
205
+ unwatch = watchPairing(key, rouse)
206
+ unfollow = followSignIn(caller, follow)
207
+ watchingKey = key
208
+ pairing = undefined
209
+ }
210
+ const paired = BY_KEY.has(key)
211
+ if (
212
+ !paired &&
213
+ (pairing === undefined || pairing.expires <= Date.now())
214
+ )
215
+ pairing = mintCode(caller, signedIn)
216
+ const code = paired ? undefined : pairing?.code
217
+ if (code !== said) {
218
+ said = code
219
+ yield code
220
+ }
221
+ const renew = paired
222
+ ? undefined
223
+ : setTimeout(rouse, (pairing?.expires ?? 0) - Date.now())
224
+ await waiting.next()
225
+ clearTimeout(renew)
226
+ }
227
+ } finally {
228
+ signal.removeEventListener('abort', rouse)
229
+ unwatch()
230
+ unfollow()
231
+ }
232
+ }
233
+
234
+ /** Registers `wake` to run whenever the pairing filed under `key` changes; the returned function undoes it. */
235
+ function watchPairing(key: string, wake: () => void): () => void {
114
236
  let watchers = WATCHERS.get(key)
115
237
  if (watchers === undefined) {
116
238
  watchers = new Set()
@@ -131,22 +253,20 @@ export function watchPairing(
131
253
  }
132
254
  }
133
255
 
134
- /** A new turn on `bridge`, the inbox its events arrive in, and the undo that closes it. */
135
- export function openTurn(bridge: Bridge): {
136
- id: string
137
- inbox: Inbox<TurnItem>
138
- close: () => void
139
- } {
140
- const id = randomToken(9, 'base64url')
141
- const inbox = new Inbox<TurnItem>()
142
- bridge.turns.set(id, inbox)
143
- return {
144
- id,
145
- inbox,
146
- close: () => {
147
- bridge.turns.delete(id)
148
- if (!WATCHERS.has(bridge.key)) armIdle(bridge.key)
149
- },
256
+ /** Registers `follow` to hear who signs in at `caller` from here on; the returned function undoes it. */
257
+ function followSignIn(
258
+ caller: string,
259
+ follow: (caller: string, signedIn: string) => void,
260
+ ): () => void {
261
+ let followers = FOLLOWERS.get(caller)
262
+ if (followers === undefined) {
263
+ followers = new Set()
264
+ FOLLOWERS.set(caller, followers)
265
+ }
266
+ followers.add(follow)
267
+ return () => {
268
+ followers.delete(follow)
269
+ if (followers.size === 0) FOLLOWERS.delete(caller)
150
270
  }
151
271
  }
152
272
 
@@ -171,38 +291,6 @@ function wakeWatchers(key: string): void {
171
291
  for (const wake of WATCHERS.get(key) ?? []) wake()
172
292
  }
173
293
 
174
- /**
175
- * Registers `follow` to hear who signs in at `caller` from here on, and the caller a sign-out rotates
176
- * it to (26.9); the returned function undoes it.
177
- *
178
- * 44.34 — A STATUS STREAM OPENED BEFORE A SIGN-IN carries the cookies it was opened with, so it hears
179
- * of the new one here rather than off its own request.
180
- */
181
- export function followSignIn(
182
- caller: string,
183
- follow: (caller: string, signedIn: string) => void,
184
- ): () => void {
185
- let followers = FOLLOWERS.get(caller)
186
- if (followers === undefined) {
187
- followers = new Set()
188
- FOLLOWERS.set(caller, followers)
189
- }
190
- followers.add(follow)
191
- return () => {
192
- followers.delete(follow)
193
- if (followers.size === 0) FOLLOWERS.delete(caller)
194
- }
195
- }
196
-
197
- /** Tells every status stream at `caller` that the browser is `now`, signed in as `signedIn`. 44.34. */
198
- export function signedInAt(
199
- caller: string,
200
- now: string,
201
- signedIn: string,
202
- ): void {
203
- for (const follow of FOLLOWERS.get(caller) ?? []) follow(now, signedIn)
204
- }
205
-
206
294
  function item(
207
295
  kind: TurnItem['kind'],
208
296
  fields: {
@@ -14,24 +14,16 @@ import { refuse } from '#shared/refuse.ts'
14
14
 
15
15
  import {
16
16
  type AssistantContent,
17
- type AssistantEvent,
18
17
  type AssistantMessage,
19
18
  blocksOf,
20
19
  forwardOf,
21
20
  type Provider,
22
21
  } from './assistant.ts'
23
22
  import { BRIDGE_VERSION } from './BRIDGE_VERSION.ts'
24
- import {
25
- followSignIn,
26
- mintCode,
27
- openTurn,
28
- pairedBridge,
29
- watchPairing,
30
- } from './bridgeRelay.ts'
23
+ import { mintCode, pairingStatus, runTurn } from './bridgeRelay.ts'
31
24
  import { callerOf } from './callerOf.ts'
32
25
  import { appOrigin } from './config.ts'
33
26
  import { mcpFailure } from './generatedSurfaces.ts'
34
- import { Inbox } from './Inbox.ts'
35
27
  import { signedInAs } from './principal.ts'
36
28
  import { request } from './requestScope.ts'
37
29
 
@@ -65,53 +57,19 @@ export type BridgeStatus = {
65
57
  function claudeCodeProvider(config: ClaudeCodeConfig = {}): Provider {
66
58
  return {
67
59
  async *run(messages, tools, { maxTurns, signal }) {
68
- const bridge = pairedBridge(askingCaller(), signedInAs())
69
- // 44.13, 44.33 — nothing paired by this sign-in is the page's cue to show the command.
70
- if (bridge === undefined)
71
- throw refuse(
72
- 409,
73
- 'No Claude Code is paired with this browser as the one signed in. claudeCode.bridgeCommand() is the command that pairs one.',
74
- )
75
- const turn = openTurn(bridge)
76
- bridge.frames.push({
77
- type: 'turn',
78
- id: turn.id,
60
+ yield* runTurn({
61
+ caller: askingCaller(),
62
+ signedIn: signedInAs(),
79
63
  prompt: foldedContent(messages),
80
64
  config,
81
65
  maxTurns,
66
+ // 44.17 — answered by the app's own MCP surface, through the turn's `Toolbox`.
67
+ answer: (method, params) =>
68
+ forwardOf(tools)(method, params).catch((thrown: unknown) =>
69
+ mcpFailure(-32603, String(thrown)),
70
+ ),
71
+ signal,
82
72
  })
83
- // 44.18 — a caller hanging up ends the run on the bridge.
84
- const cancel = (): void => {
85
- bridge.frames.push({ type: 'cancel', turn: turn.id })
86
- }
87
- signal.addEventListener('abort', cancel)
88
- try {
89
- for await (const item of turn.inbox) {
90
- if (item.kind === 'event')
91
- yield item.event as AssistantEvent
92
- else if (item.kind === 'mcp')
93
- // 44.17 — answered by the app's own MCP surface, through the turn's `Toolbox`.
94
- void forwardOf(tools)(item.method, item.params)
95
- .catch((thrown: unknown) =>
96
- mcpFailure(-32603, String(thrown)),
97
- )
98
- .then((reply) => {
99
- bridge.frames.push({
100
- type: 'reply',
101
- turn: turn.id,
102
- id: item.id,
103
- reply,
104
- })
105
- })
106
- else if (item.kind === 'end') return
107
- // 44.21.
108
- else
109
- throw refuse(502, `Claude Code failed: ${item.message}`)
110
- }
111
- } finally {
112
- signal.removeEventListener('abort', cancel)
113
- turn.close()
114
- }
115
73
  },
116
74
  }
117
75
  }
@@ -157,25 +115,25 @@ function askingCaller(): string {
157
115
  return caller
158
116
  }
159
117
 
160
- /** One pairing code, as the command a visitor runs and the link they open, and when it stops standing. */
161
- type Pairing = { command: string; link: string; expires: number }
162
-
163
118
  /**
164
119
  * The command and the link that pair a bridge with this browser, and when their code stops standing.
165
120
  * 44.14. The `tools` it is given are written after the command's `--` and in the link, so the visitor
166
121
  * running either grants them (44.29, 44.32, 44.40).
167
122
  */
168
- function bridgeCommand(options: PairingOptions = {}): Pairing {
169
- return commandFor(askingCaller(), signedInAs(), options)
123
+ function bridgeCommand(options: PairingOptions = {}): {
124
+ command: string
125
+ link: string
126
+ expires: number
127
+ } {
128
+ const { code, expires } = mintCode(askingCaller(), signedInAs())
129
+ return { ...commandFor(code, options), expires }
170
130
  }
171
131
 
172
- /** The command and the link pairing a bridge with `caller` signed in as `signedIn` (44.33). */
132
+ /** The command and the link that pair a bridge by `code`, granting `tools`. */
173
133
  function commandFor(
174
- caller: string,
175
- signedIn: string,
134
+ code: string,
176
135
  { tools }: PairingOptions,
177
- ): Pairing {
178
- const { code, expires } = mintCode(caller, signedIn)
136
+ ): { command: string; link: string } {
179
137
  const site = `${appOrigin() ?? new URL(request().url).origin}${mounted('')}`
180
138
  const granted = tools?.length ? tools.join(',') : undefined
181
139
  const query = new URLSearchParams({ site, code, version: BRIDGE_VERSION })
@@ -184,80 +142,25 @@ function commandFor(
184
142
  return {
185
143
  command: `bunx @abide/bridge@${BRIDGE_VERSION} ${site} ${code}${grant}`,
186
144
  link: `${BRIDGE_SCHEME}://pair?${query}`,
187
- expires,
188
145
  }
189
146
  }
190
147
 
191
148
  /**
192
149
  * This browser's pairing as it changes: unpaired with the command to run and the link to open, then
193
- * paired the moment a bridge connects, and unpaired again if it goes. 44.20.
194
- *
195
- * 44.34 — IT FOLLOWS WHOEVER SIGNS IN AT THE BROWSER, though its own request carries the cookies it
196
- * opened with: a sign-in or a sign-out made since is the pairing it watches from then on, and the
197
- * command it offers is that sign-in's.
150
+ * paired the moment a bridge connects, and unpaired again if it goes — following whoever signs in at
151
+ * the browser since. 44.20, 44.34.
198
152
  */
199
153
  async function* bridge(
200
154
  options: PairingOptions = {},
201
155
  ): AsyncGenerator<BridgeStatus> {
202
- let caller = askingCaller()
203
- let signedIn = signedInAs()
204
- const signal = request().signal
205
- // A change landing while the loop was not waiting — paused at its `yield` — stays queued, so it
206
- // looks again; a look that finds nothing new yields nothing.
207
- const changes = new Inbox<undefined>()
208
- const waiting = changes[Symbol.asyncIterator]()
209
- const rouse = (): void => changes.push(undefined)
210
- let watchingAs = ''
211
- let unwatch = (): void => {}
212
- let unfollow = (): void => {}
213
- const follow = (now: string, next: string): void => {
214
- caller = now
215
- signedIn = next
216
- rouse()
217
- }
218
- signal.addEventListener('abort', rouse)
219
- let pairing: Pairing | undefined
220
- let said = ''
221
- try {
222
- while (!signal.aborted) {
223
- const watching = `${caller} ${signedIn}`
224
- if (watching !== watchingAs) {
225
- unwatch()
226
- unfollow()
227
- unwatch = watchPairing(caller, signedIn, rouse)
228
- unfollow = followSignIn(caller, follow)
229
- watchingAs = watching
230
- pairing = undefined
231
- }
232
- const paired = pairedBridge(caller, signedIn) !== undefined
233
- if (
234
- !paired &&
235
- (pairing === undefined || pairing.expires <= Date.now())
236
- )
237
- pairing = commandFor(caller, signedIn, options)
238
- const status: BridgeStatus = {
239
- paired,
240
- command: paired ? undefined : pairing?.command,
241
- link: paired ? undefined : pairing?.link,
242
- }
243
- // The link is minted with the command, so the command alone tells a new pairing.
244
- const saying = `${status.paired} ${status.command}`
245
- if (saying !== said) {
246
- said = saying
247
- yield status
248
- }
249
- // A code that expires while nothing is paired is replaced with a fresh one.
250
- const renew = paired
251
- ? undefined
252
- : setTimeout(rouse, (pairing?.expires ?? 0) - Date.now())
253
- await waiting.next()
254
- clearTimeout(renew)
255
- }
256
- } finally {
257
- signal.removeEventListener('abort', rouse)
258
- unwatch()
259
- unfollow()
260
- }
156
+ for await (const code of pairingStatus(
157
+ askingCaller(),
158
+ signedInAs(),
159
+ request().signal,
160
+ ))
161
+ yield code === undefined
162
+ ? { paired: true, command: undefined, link: undefined }
163
+ : { paired: false, ...commandFor(code, options) }
261
164
  }
262
165
 
263
166
  /** The visitor's own Claude Code as a provider, with the pairing that reaches it. */
@@ -12,8 +12,8 @@ import { reactive } from '#shared/reactive/index.ts'
12
12
  import { validationError } from '#shared/refuse.ts'
13
13
  import { runSchema } from '#shared/Schema.ts'
14
14
 
15
- import { appDirectories } from './appDirectories.ts'
16
15
  import { type Env, HOOKS, inOrder } from './hooks.ts'
16
+ import { packageDirectory } from './packageDirectory.ts'
17
17
  import { withoutTrailingSlash } from './withoutTrailingSlash.ts'
18
18
 
19
19
  /** The fields of an app's `package.json` 27.3 derives from. */
@@ -27,14 +27,13 @@ export type AppPackage = { name?: string; version?: string }
27
27
  */
28
28
  export const APP_PACKAGE: { current: AppPackage } = { current: {} }
29
29
 
30
- /** The `package.json` beside an app on disk, in the first of `appDirectories` that has one. */
30
+ /** The `package.json` of an app on disk (`packageDirectory`), or nothing where it has none. */
31
31
  export function packageUnder(root: string): AppPackage {
32
- for (const directory of appDirectories(root)) {
33
- try {
34
- return require(`${directory}/package.json`) as AppPackage
35
- } catch {}
32
+ try {
33
+ return require(`${packageDirectory(root)}/package.json`) as AppPackage
34
+ } catch {
35
+ return {}
36
36
  }
37
- return {}
38
37
  }
39
38
 
40
39
  /**