@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.
- package/ABIDE.md +8 -5
- package/CHANGELOG.md +58 -0
- package/bin/abide.ts +36 -65
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +24 -14
- package/docs/abide.md +999 -459
- package/package.json +1 -1
- package/src/compiler/ARRAY_MUTATORS.ts +20 -0
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +18 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowHost.ts +670 -0
- package/src/compiler/ShadowMap.ts +1 -1
- package/src/compiler/TypeSession.ts +76 -1743
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +2 -2
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +102 -71
- package/src/compiler/declaredExports.ts +46 -21
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +7 -8
- package/src/compiler/handlersUnder.ts +32 -63
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +181 -82
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +46 -74
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/splitImports.ts +2 -2
- package/src/compiler/templateWarnings.ts +1 -2
- package/src/compiler/trackingWarnings.ts +1 -1
- package/src/server/CLIENT_BUILD.ts +4 -32
- package/src/server/DesktopSession.ts +213 -0
- package/src/server/Followers.ts +399 -0
- package/src/server/MODULE_QUERIES.ts +6 -2
- package/src/server/PageRender.ts +201 -0
- package/src/server/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +16 -369
- package/src/server/answerRpc.ts +48 -302
- package/src/server/answerSocket.ts +41 -490
- package/src/server/appBuild.ts +377 -0
- package/src/server/appKit.ts +1 -1
- package/src/server/appPlugins.ts +15 -17
- package/src/server/appTree.ts +6 -5
- package/src/server/argsContract.ts +209 -0
- package/src/server/bridgeRelay.ts +153 -65
- package/src/server/claudeCode.ts +30 -127
- package/src/server/config.ts +6 -7
- package/src/server/createApp.ts +38 -66
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/devWorker.ts +11 -15
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +3 -3
- package/src/server/installServer.ts +5 -9
- package/src/server/invokeHandler.ts +230 -0
- package/src/server/mountHandlers.ts +184 -0
- package/src/server/openDesktopWindow.ts +308 -0
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +21 -2
- package/src/server/principal.ts +5 -4
- package/src/server/remote.ts +2 -2
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +20 -448
- package/src/server/seedBuffer.ts +12 -12
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/shell.ts +1 -1
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +3 -2
- package/src/server/versionOf.ts +7 -16
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/index.ts +1 -1
- package/src/shared/{note.ts → issueReport.ts} +22 -0
- package/src/shared/loadCall.ts +66 -0
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +24 -16
- package/src/shared/reactive/Reactive.ts +37 -991
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/byOf.ts +2 -5
- package/src/shared/reactive/callLoad.ts +8 -14
- package/src/shared/reactive/cellShape.ts +734 -0
- package/src/shared/reactive/index.ts +12 -23
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +20 -24
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- package/src/ui/ArmRegion.ts +10 -4
- package/src/ui/Level.ts +11 -20
- package/src/ui/LevelChain.ts +272 -0
- package/src/ui/Router.ts +106 -313
- package/src/ui/Scope.ts +17 -3
- package/src/ui/TryRegion.ts +22 -23
- package/src/ui/bindSpread.ts +2 -4
- package/src/ui/listenForRoutes.ts +3 -10
- package/src/ui/outboxQueue.ts +23 -11
- package/src/ui/outcomeOf.ts +120 -0
- package/src/ui/reconcile.ts +4 -6
- package/src/ui/routing.ts +2 -1
- package/src/ui/rows.ts +2 -2
- package/src/ui/setAttribute.ts +12 -3
- package/src/ui/socketWire.ts +28 -13
- package/src/ui/wire.ts +9 -10
- package/src/compiler/Finding.ts +0 -2
- package/src/compiler/Refusals.ts +0 -65
- package/src/compiler/formatDiagnosis.ts +0 -20
- package/src/server/appDirectories.ts +0 -9
- package/src/server/argsFor.ts +0 -39
- package/src/server/buildDirectory.ts +0 -10
- package/src/server/coerceArgs.ts +0 -109
- package/src/server/derivedArgs.ts +0 -26
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- package/src/shared/LOAD_OFFLINE.ts +0 -15
- package/src/shared/LOAD_SIGNAL.ts +0 -10
- package/src/shared/issuesOf.ts +0 -18
- package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
- package/src/shared/reactive/FOLLOWED.ts +0 -8
- package/src/shared/reactive/PATCHES.ts +0 -11
- package/src/shared/reactive/PATCH_SINKS.ts +0 -26
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
*
|
|
106
|
-
*
|
|
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
|
|
177
|
+
export async function* pairingStatus(
|
|
109
178
|
caller: string,
|
|
110
179
|
signedIn: string,
|
|
111
|
-
|
|
112
|
-
):
|
|
113
|
-
|
|
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
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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: {
|
package/src/server/claudeCode.ts
CHANGED
|
@@ -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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 = {}):
|
|
169
|
-
|
|
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
|
|
132
|
+
/** The command and the link that pair a bridge by `code`, granting `tools`. */
|
|
173
133
|
function commandFor(
|
|
174
|
-
|
|
175
|
-
signedIn: string,
|
|
134
|
+
code: string,
|
|
176
135
|
{ tools }: PairingOptions,
|
|
177
|
-
):
|
|
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
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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. */
|
package/src/server/config.ts
CHANGED
|
@@ -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`
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
/**
|