@abide/abide 0.124.1 → 0.125.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 +4 -0
- package/CHANGELOG.md +36 -0
- package/bin/abide.ts +3 -2
- package/docs/REGISTRY.md +66 -40
- package/docs/RULEBOOK.md +202 -18
- package/docs/abide.md +391 -67
- package/package.json +1 -1
- package/src/compiler/appMap.ts +14 -1
- package/src/compiler/declaredExports.ts +41 -0
- package/src/compiler/handlersUnder.ts +1 -0
- package/src/compiler/hookCalls.ts +1 -0
- package/src/compiler/projectShadow.ts +52 -2
- package/src/compiler/readsIn.ts +39 -34
- package/src/server/PageRender.ts +7 -8
- package/src/server/abideModules.ts +6 -0
- package/src/server/answerRequest.ts +115 -52
- package/src/server/answerSocket.ts +6 -5
- package/src/server/answerWorker.ts +34 -23
- package/src/server/assistant.ts +141 -35
- package/src/server/banner.ts +9 -5
- package/src/server/bridgeRelay.ts +3 -3
- package/src/server/callerOf.ts +5 -0
- package/src/server/claudeCode.ts +20 -11
- package/src/server/commands.ts +14 -7
- package/src/server/createApp.ts +65 -12
- package/src/server/devWorker.ts +18 -16
- package/src/server/generatedSurfaces.ts +6 -5
- package/src/server/handlerFileMount.ts +2 -0
- package/src/server/hooks.ts +54 -8
- package/src/server/index.ts +18 -4
- package/src/server/invokeHandler.ts +1 -1
- package/src/server/mountHandlers.ts +6 -0
- package/src/server/principal.ts +3 -2
- package/src/server/render.ts +11 -2
- package/src/server/requestScope.ts +61 -1
- package/src/server/responseRelay.ts +9 -3
- package/src/server/serveDev.ts +18 -3
- package/src/server/sqliteStore.ts +65 -0
- package/src/server/testMount.ts +1 -0
- package/src/shared/AssistantEvent.ts +40 -0
- package/src/shared/BrowserHooks.ts +73 -0
- package/src/shared/CURRENT_ROUTER.ts +3 -1
- package/src/shared/Issues.ts +11 -3
- package/src/shared/Middleware.ts +9 -9
- package/src/shared/UNEXPECTED.ts +1 -1
- package/src/shared/answerOf.ts +9 -4
- package/src/shared/imagePiece.ts +85 -0
- package/src/shared/index.ts +17 -0
- package/src/shared/isServer.ts +15 -0
- package/src/shared/navigate.ts +10 -3
- package/src/shared/onError.ts +51 -0
- package/src/shared/onRefusal.ts +47 -0
- package/src/shared/online.ts +5 -1
- package/src/shared/reactive/Keyed.ts +12 -1
- package/src/shared/reactive/Reactive.ts +53 -25
- package/src/shared/reactive/cellShape.ts +4 -3
- package/src/shared/reactive/poll.ts +49 -0
- package/src/shared/reactive/scoped.ts +10 -1
- package/src/shared/reactive/scoping.ts +14 -0
- package/src/shared/reactive/watch.ts +19 -8
- package/src/shared/resultsOf.ts +21 -0
- package/src/shared/route.ts +18 -4
- package/src/shared/rpc.ts +112 -26
- package/src/shared/seededDocument.ts +5 -6
- package/src/shared/storedValue.ts +32 -0
- package/src/shared/webStore.ts +54 -0
- package/src/ui/Router.ts +42 -9
- package/src/ui/outboxQueue.ts +1 -1
- package/src/ui/wire.ts +4 -4
- package/src/shared/KEEP_OPENED.ts +0 -7
- /package/src/{server → shared}/IMAGE_TYPES.ts +0 -0
|
@@ -26,6 +26,7 @@ import type { FollowedHandler } from './mountHandlers.ts'
|
|
|
26
26
|
import { sendOrClose } from './muxUpgrade.ts'
|
|
27
27
|
import {
|
|
28
28
|
inOpenScope,
|
|
29
|
+
RECEIVED_FROM,
|
|
29
30
|
releaseScope,
|
|
30
31
|
type RequestScope,
|
|
31
32
|
scopeFor,
|
|
@@ -139,11 +140,11 @@ export class MuxConnection {
|
|
|
139
140
|
upgrade.url,
|
|
140
141
|
)
|
|
141
142
|
// NO `received`: an upgraded connection has no http idle timeout for 16.23 to set.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
143
|
+
const request = new Request(upgrade, { signal: this.hangUp.signal })
|
|
144
|
+
// 22.12 — every frame came from where the upgrade did.
|
|
145
|
+
if (RECEIVED_FROM.has(upgrade))
|
|
146
|
+
RECEIVED_FROM.set(request, RECEIVED_FROM.get(upgrade) ?? null)
|
|
147
|
+
this.scope = scopeFor(request, undefined, mux.app)
|
|
147
148
|
// 22.10 — the headers went out with the upgrade, so nothing a frame writes is carried.
|
|
148
149
|
this.scope.applied = true
|
|
149
150
|
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
9
9
|
import { KEEP_HEADER } from '#shared/KEEP_HEADER.ts'
|
|
10
|
-
import { KEEP_OPENED } from '#shared/KEEP_OPENED.ts'
|
|
11
10
|
import { KEPT_HEADER } from '#shared/KEPT_HEADER.ts'
|
|
12
11
|
import { MOUNT, mounted } from '#shared/mount.ts'
|
|
13
12
|
import { NAVIGATION_HEADER } from '#shared/NAVIGATION_HEADER.ts'
|
|
@@ -42,7 +41,7 @@ async function dropOurs(keeping) {
|
|
|
42
41
|
}
|
|
43
42
|
`
|
|
44
43
|
|
|
45
|
-
// 45.3 through 45.8, 45.13, 45.17 through 45.
|
|
44
|
+
// 45.3 through 45.8, 45.13, and 45.17 through 45.26.
|
|
46
45
|
const SAVING = `
|
|
47
46
|
self.addEventListener('install', (event) => {
|
|
48
47
|
event.waitUntil(
|
|
@@ -149,31 +148,33 @@ function marked(saved) {
|
|
|
149
148
|
})
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
// A page as the worker asks for it
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
function pageRequest(url, headers, keep) {
|
|
151
|
+
// A page no navigation is asking for, as the worker asks for it: past the browser's cache, as a page
|
|
152
|
+
// kept, and with its redirects left unfollowed — one followed would be saved under the address it was
|
|
153
|
+
// asked at, holding where it led (RULEBOOK 45.4, 45.21, 45.23).
|
|
154
|
+
function pageRequest(url, headers) {
|
|
157
155
|
const asking = new Headers(headers)
|
|
158
|
-
asking.set(KEEP,
|
|
156
|
+
asking.set(KEEP, '1')
|
|
159
157
|
return new Request(url, { headers: asking, redirect: 'manual', cache: 'no-cache' })
|
|
160
158
|
}
|
|
161
159
|
|
|
162
|
-
// 45.
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
160
|
+
// 45.25 — THE NAVIGATION ASKED AS ITSELF: any init rebuilds a navigation's request as a script's
|
|
161
|
+
// fetch, and a proxy in front of the app tells the two apart — one sending a page load to sign-in
|
|
162
|
+
// answers a fetch 401. As itself its mode is navigate, the page that hydrates (RULEBOOK 41.29), its redirects
|
|
163
|
+
// left to the browser. Anything else is asked past the browser's own cache (RULEBOOK 45.21), which
|
|
164
|
+
// holds the same answers under the same headers and would hand a revalidation the copy it is replacing
|
|
165
|
+
// — and a page as one kept, which a script's fetch is not otherwise told from (RULEBOOK 45.23, 41.30).
|
|
167
166
|
async function fromNetwork(event, page, opened) {
|
|
168
167
|
const request = event.request
|
|
169
|
-
const asked =
|
|
170
|
-
?
|
|
171
|
-
:
|
|
168
|
+
const asked = opened
|
|
169
|
+
? request
|
|
170
|
+
: page
|
|
171
|
+
? pageRequest(request.url, request.headers)
|
|
172
|
+
: new Request(request, { cache: 'no-cache' })
|
|
172
173
|
const since = drops
|
|
173
174
|
const response = await fetch(asked)
|
|
174
175
|
// A copy nothing reads is a tee holding every chunk, so one is cloned only to be saved.
|
|
175
176
|
const copy = kept(response) ? response.clone() : undefined
|
|
176
|
-
event.waitUntil(keep(request, copy, since
|
|
177
|
+
event.waitUntil(keep(request, response, copy, since))
|
|
177
178
|
return response
|
|
178
179
|
}
|
|
179
180
|
|
|
@@ -181,11 +182,11 @@ async function fromNetwork(event, page, opened) {
|
|
|
181
182
|
// window's as the worker activates, and an address the router reached.
|
|
182
183
|
function keepPage(url) {
|
|
183
184
|
const since = drops
|
|
184
|
-
return fetch(pageRequest(url
|
|
185
|
+
return fetch(pageRequest(url)).then((response) => {
|
|
185
186
|
// Nothing reads this answer, so one 45.3 or 45.4 turns away is let go here.
|
|
186
187
|
const copy = kept(response) ? response : undefined
|
|
187
188
|
if (copy === undefined) response.body?.cancel()
|
|
188
|
-
return keep(new Request(url), copy, since
|
|
189
|
+
return keep(new Request(url), response, copy, since)
|
|
189
190
|
}, () => {})
|
|
190
191
|
}
|
|
191
192
|
|
|
@@ -273,12 +274,23 @@ function dropSaved() {
|
|
|
273
274
|
// Saves a copy 45.3 and 45.4 let through where no drop has come since its request was asked — after
|
|
274
275
|
// the drop its own answer makes where that answer rotated the principal — and cancels one where one has.
|
|
275
276
|
// One any caller may be answered is saved whenever it was asked, since no caller's cookies made it.
|
|
276
|
-
async function keep(request, copy, since
|
|
277
|
-
if (
|
|
277
|
+
async function keep(request, response, copy, since) {
|
|
278
|
+
if (response.headers.has(PRINCIPAL)) {
|
|
278
279
|
await dropSaved()
|
|
279
280
|
since = drops
|
|
280
281
|
}
|
|
281
|
-
|
|
282
|
+
// 45.26 — AN ANSWER NOT SAVED STILL REPLACES THE COPY: a redirect or a 401 is what the address
|
|
283
|
+
// answers now, and a copy left standing would answer every load in its stale-while-revalidate
|
|
284
|
+
// window in its place — a session lapsed behind a sign-in proxy never reaching the sign-in. A
|
|
285
|
+
// server error is no answer to replace a copy with.
|
|
286
|
+
if (copy === undefined) {
|
|
287
|
+
if (response.status >= 500) return
|
|
288
|
+
await Promise.all([
|
|
289
|
+
caches.open(SAVED).then((cache) => cache.delete(request)),
|
|
290
|
+
caches.open(SHARED).then((cache) => cache.delete(request)),
|
|
291
|
+
])
|
|
292
|
+
return
|
|
293
|
+
}
|
|
282
294
|
const shared = directivesOf(copy).has('public')
|
|
283
295
|
if (!shared && since !== drops) return copy.body?.cancel()
|
|
284
296
|
// An address whose answer went public or private since leaves no copy behind for savedCopy to
|
|
@@ -338,7 +350,6 @@ export function answerWorker(manifest: Manifest | undefined): Response {
|
|
|
338
350
|
script = `${CACHES}const PRINCIPAL = ${JSON.stringify(PRINCIPAL_HEADER)}
|
|
339
351
|
const KEPT = ${JSON.stringify(KEPT_HEADER)}
|
|
340
352
|
const KEEP = ${JSON.stringify(KEEP_HEADER)}
|
|
341
|
-
const OPENED = ${JSON.stringify(KEEP_OPENED)}
|
|
342
353
|
const NAVIGATION = ${JSON.stringify(NAVIGATION_HEADER)}
|
|
343
354
|
const REACHED = ${JSON.stringify(REACHED_MESSAGE)}
|
|
344
355
|
// 45.4 — the types a stream 16.32 frames, which has no end to save.
|
package/src/server/assistant.ts
CHANGED
|
@@ -10,35 +10,28 @@
|
|
|
10
10
|
// another request's scope — `claudeCode` answers one from the bridge's — and the call is still this
|
|
11
11
|
// asker's.
|
|
12
12
|
|
|
13
|
+
import type {
|
|
14
|
+
AssistantContent,
|
|
15
|
+
AssistantEvent,
|
|
16
|
+
AssistantMessage,
|
|
17
|
+
} from '#shared/AssistantEvent.ts'
|
|
18
|
+
import { IMAGE_TYPES } from '#shared/IMAGE_TYPES.ts'
|
|
13
19
|
import { namedLogger } from '#shared/log.ts'
|
|
14
|
-
import {
|
|
20
|
+
import { mounted } from '#shared/mount.ts'
|
|
21
|
+
import { aborted, refuse } from '#shared/refuse.ts'
|
|
15
22
|
|
|
16
23
|
import { type McpReply, McpSurface } from './generatedSurfaces.ts'
|
|
17
|
-
import { IMAGE_TYPES } from './IMAGE_TYPES.ts'
|
|
18
24
|
import { request, within } from './requestScope.ts'
|
|
25
|
+
import type { Listed } from './surfaceOf.ts'
|
|
19
26
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export type AssistantMessage = {
|
|
27
|
-
role: 'user' | 'assistant'
|
|
28
|
-
content: string | AssistantContent[]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** What a conversation yields, the same from every provider. */
|
|
32
|
-
export type AssistantEvent =
|
|
33
|
-
| { type: 'text'; text: string }
|
|
27
|
+
/**
|
|
28
|
+
* An `AssistantEvent` as a `Provider` yields it: a tool call named, and its result in words, with what
|
|
29
|
+
* `assistant` reads off the app's own surface for both — the handler, and the value — still to come.
|
|
30
|
+
*/
|
|
31
|
+
export type ProviderEvent =
|
|
32
|
+
| Exclude<AssistantEvent, { type: 'tool' | 'result' }>
|
|
34
33
|
| { type: 'tool'; id: string; name: string }
|
|
35
|
-
| { type: 'input'; id: string; input: unknown }
|
|
36
34
|
| { type: 'result'; id: string; text: string; failed: boolean }
|
|
37
|
-
| {
|
|
38
|
-
type: 'done'
|
|
39
|
-
stop: string
|
|
40
|
-
usage: { input: number; output: number }
|
|
41
|
-
}
|
|
42
35
|
|
|
43
36
|
/** A tool as the app's MCP surface lists it. */
|
|
44
37
|
export type ToolDefinition = {
|
|
@@ -68,11 +61,15 @@ export type Provider = {
|
|
|
68
61
|
messages: (AssistantMessage & { content: AssistantContent[] })[],
|
|
69
62
|
tools: Toolbox,
|
|
70
63
|
limits: { maxTurns: number; signal: AbortSignal },
|
|
71
|
-
): AsyncGenerator<
|
|
64
|
+
): AsyncGenerator<ProviderEvent>
|
|
72
65
|
}
|
|
73
66
|
|
|
74
67
|
/** What `assistant` takes beside a provider, the same for every one. */
|
|
75
|
-
export type AssistantOptions = {
|
|
68
|
+
export type AssistantOptions = {
|
|
69
|
+
mcp?: boolean
|
|
70
|
+
maxTurns?: number
|
|
71
|
+
signal?: AbortSignal
|
|
72
|
+
}
|
|
76
73
|
|
|
77
74
|
const MAX_TURNS = 10
|
|
78
75
|
const ASSISTANT_LOG = namedLogger('abide:assistant')
|
|
@@ -118,19 +115,128 @@ export async function* assistant(
|
|
|
118
115
|
)
|
|
119
116
|
}
|
|
120
117
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
const surface = options.mcp === true ? appSurface() : NO_SURFACE
|
|
119
|
+
const tools = toolboxOver(surface, askerOf(request()))
|
|
120
|
+
// 44.57 — the caller's signal and the request's are one: either ends the conversation.
|
|
121
|
+
const signal =
|
|
122
|
+
options.signal === undefined
|
|
123
|
+
? request().signal
|
|
124
|
+
: AbortSignal.any([request().signal, options.signal])
|
|
125
|
+
const events = endedBy(
|
|
126
|
+
provider.run(turns, tools, {
|
|
127
|
+
maxTurns: options.maxTurns ?? MAX_TURNS,
|
|
128
|
+
signal,
|
|
129
|
+
}),
|
|
128
130
|
signal,
|
|
129
|
-
|
|
130
|
-
// 28.7 — a closed namespace
|
|
131
|
+
)
|
|
132
|
+
// 28.7 — a closed namespace pays for no line.
|
|
131
133
|
yield* ASSISTANT_LOG.enabled()
|
|
132
|
-
? traced(events, messages.length, signal)
|
|
133
|
-
: events
|
|
134
|
+
? traced(read(events, surface), messages.length, signal)
|
|
135
|
+
: read(events, surface)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `events` until `signal` aborts, then `aborted`. 44.57.
|
|
140
|
+
*
|
|
141
|
+
* THE ABORT DOES NOT WAIT FOR THE PROVIDER: one stuck in an await it was handed no signal for — a
|
|
142
|
+
* bridge that never answers — is asked to return and left to, and the conversation ends now. An
|
|
143
|
+
* abort read ahead of a throw, as `traced` reads it: a provider that throws as it unwinds was ended
|
|
144
|
+
* by the abort.
|
|
145
|
+
*/
|
|
146
|
+
async function* endedBy(
|
|
147
|
+
events: AsyncGenerator<ProviderEvent>,
|
|
148
|
+
signal: AbortSignal,
|
|
149
|
+
): AsyncGenerator<ProviderEvent> {
|
|
150
|
+
// A STEP RACES ITS OWN ABORT: one promise pending for the whole conversation would keep a
|
|
151
|
+
// reaction per event, none freed until it ends.
|
|
152
|
+
let rejectStep: (reason: unknown) => void = () => {}
|
|
153
|
+
const stop = (): void => rejectStep(aborted())
|
|
154
|
+
signal.addEventListener('abort', stop, { once: true })
|
|
155
|
+
try {
|
|
156
|
+
for (;;) {
|
|
157
|
+
if (signal.aborted) throw aborted()
|
|
158
|
+
const step = await Promise.race([
|
|
159
|
+
events.next(),
|
|
160
|
+
new Promise<never>((_, reject) => {
|
|
161
|
+
rejectStep = reject
|
|
162
|
+
}),
|
|
163
|
+
])
|
|
164
|
+
if (step.done === true) return
|
|
165
|
+
yield step.value
|
|
166
|
+
}
|
|
167
|
+
} catch (thrown) {
|
|
168
|
+
throw signal.aborted ? aborted() : thrown
|
|
169
|
+
} finally {
|
|
170
|
+
signal.removeEventListener('abort', stop)
|
|
171
|
+
// A provider still awaiting returns once that await settles, and nobody waits for it.
|
|
172
|
+
events.return(undefined).catch(() => {})
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A called tool's handler, and whether its answer is a value read as JSON (19.21). */
|
|
177
|
+
type Called = {
|
|
178
|
+
handler: { method: Listed['method']; address: string } | undefined
|
|
179
|
+
valued: boolean
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const NOT_THE_APPS: Called = { handler: undefined, valued: false }
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* `events` with each call's handler, and each answer's value, read off `surface`. 44.48, 44.49.
|
|
186
|
+
*
|
|
187
|
+
* READ HERE RATHER THAN BY EACH PROVIDER, so a provider the app wrote yields the same events abide's
|
|
188
|
+
* do (44.2): a provider names the tool as the `Toolbox` lists it (44.50), and the rest is the surface's.
|
|
189
|
+
*/
|
|
190
|
+
async function* read(
|
|
191
|
+
events: AsyncGenerator<ProviderEvent>,
|
|
192
|
+
surface: McpSurface,
|
|
193
|
+
): AsyncGenerator<AssistantEvent> {
|
|
194
|
+
const calls = new Map<string, Called>()
|
|
195
|
+
for await (const event of events) {
|
|
196
|
+
if (event.type === 'tool') {
|
|
197
|
+
const tool = surface.tool(event.name)
|
|
198
|
+
const called: Called =
|
|
199
|
+
tool === undefined
|
|
200
|
+
? NOT_THE_APPS
|
|
201
|
+
: {
|
|
202
|
+
// 16.10 — the address an `Rpc` carries, under the mount.
|
|
203
|
+
handler: {
|
|
204
|
+
method: tool.method,
|
|
205
|
+
address: mounted(tool.address),
|
|
206
|
+
},
|
|
207
|
+
valued: tool.result !== undefined,
|
|
208
|
+
}
|
|
209
|
+
calls.set(event.id, called)
|
|
210
|
+
yield {
|
|
211
|
+
type: 'tool',
|
|
212
|
+
id: event.id,
|
|
213
|
+
name: event.name,
|
|
214
|
+
handler: called.handler,
|
|
215
|
+
}
|
|
216
|
+
} else if (event.type === 'result') {
|
|
217
|
+
const called = calls.get(event.id) ?? NOT_THE_APPS
|
|
218
|
+
yield {
|
|
219
|
+
type: 'result',
|
|
220
|
+
id: event.id,
|
|
221
|
+
text: event.text,
|
|
222
|
+
failed: event.failed,
|
|
223
|
+
handler: called.handler,
|
|
224
|
+
value:
|
|
225
|
+
called.valued && !event.failed
|
|
226
|
+
? valueOf(event.text)
|
|
227
|
+
: undefined,
|
|
228
|
+
}
|
|
229
|
+
} else yield event
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** An answer's words as the JSON value they spell, or `undefined` where they spell none. */
|
|
234
|
+
function valueOf(text: string): unknown {
|
|
235
|
+
try {
|
|
236
|
+
return JSON.parse(text)
|
|
237
|
+
} catch {
|
|
238
|
+
return undefined
|
|
239
|
+
}
|
|
134
240
|
}
|
|
135
241
|
|
|
136
242
|
/**
|
package/src/server/banner.ts
CHANGED
|
@@ -9,15 +9,16 @@ import type { AppPackage } from './config.ts'
|
|
|
9
9
|
* What `start` and `dev` print once the app is listening: the app, and every address it answers at.
|
|
10
10
|
*
|
|
11
11
|
* THE LOCAL ADDRESS IS THE FIRST URL PRINTED, because a harness reading the output takes the first
|
|
12
|
-
* `http://` it finds (`addressOf` in the tests).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* `http://` it finds (`addressOf` in the tests). 38.58, 38.136 — the network rows follow it where the
|
|
13
|
+
* listener binds every interface, which `Bun.serve` given no `hostname` does whatever `url` says, and
|
|
14
|
+
* are left out where `hostname` names one interface.
|
|
15
15
|
*/
|
|
16
16
|
export function banner(
|
|
17
17
|
host: string,
|
|
18
18
|
command: string,
|
|
19
19
|
identity: AppPackage,
|
|
20
20
|
server: Bun.Server<unknown>,
|
|
21
|
+
hostname: string | undefined,
|
|
21
22
|
): string {
|
|
22
23
|
const color = useColor()
|
|
23
24
|
const paint = (code: string, text: string): string =>
|
|
@@ -29,8 +30,11 @@ export function banner(
|
|
|
29
30
|
? ''
|
|
30
31
|
: ` ${paint('2', `${identity.name}${identity.version === undefined ? '' : ` ${identity.version}`}`)}`
|
|
31
32
|
let text = `\n ${title}${named}\n\n ${paint('32', '➜')} ${paint('1', 'Local:')} ${paint('36', local)}\n`
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const everyInterface =
|
|
34
|
+
hostname === undefined || hostname === '0.0.0.0' || hostname === '::'
|
|
35
|
+
if (everyInterface)
|
|
36
|
+
for (const address of networkAddresses())
|
|
37
|
+
text += ` ${paint('32', '➜')} ${paint('1', 'Network:')} ${paint('36', `${server.url.protocol}//${address}:${server.port}/`)}\n`
|
|
34
38
|
return text
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -22,7 +22,7 @@ import { refuse } from '#shared/refuse.ts'
|
|
|
22
22
|
import { answered } from './answerFailure.ts'
|
|
23
23
|
import { bodyWithin } from './answerRpc.ts'
|
|
24
24
|
import { framed } from './answerValue.ts'
|
|
25
|
-
import type {
|
|
25
|
+
import type { ProviderEvent } from './assistant.ts'
|
|
26
26
|
import { Inbox } from './Inbox.ts'
|
|
27
27
|
import { randomToken } from './randomToken.ts'
|
|
28
28
|
import { RESUME_MILLISECONDS } from './RESUME_MILLISECONDS.ts'
|
|
@@ -126,7 +126,7 @@ export async function* runTurn({
|
|
|
126
126
|
/** Never rejects: its failure is a reply too. */
|
|
127
127
|
answer: (method: string, params: unknown) => Promise<unknown>
|
|
128
128
|
signal: AbortSignal
|
|
129
|
-
}): AsyncGenerator<
|
|
129
|
+
}): AsyncGenerator<ProviderEvent> {
|
|
130
130
|
const bridge = BY_KEY.get(pairingKey(caller, signedIn))
|
|
131
131
|
// 44.13, 44.33 — nothing paired by this sign-in is the page's cue to show the command.
|
|
132
132
|
if (bridge === undefined)
|
|
@@ -146,7 +146,7 @@ export async function* runTurn({
|
|
|
146
146
|
signal.addEventListener('abort', cancel)
|
|
147
147
|
try {
|
|
148
148
|
for await (const item of inbox) {
|
|
149
|
-
if (item.kind === 'event') yield item.event as
|
|
149
|
+
if (item.kind === 'event') yield item.event as ProviderEvent
|
|
150
150
|
else if (item.kind === 'mcp')
|
|
151
151
|
void answer(item.method, item.params).then((reply) => {
|
|
152
152
|
bridge.frames.push({
|
package/src/server/callerOf.ts
CHANGED
|
@@ -46,6 +46,11 @@ export function callerOrMint(): string {
|
|
|
46
46
|
return callerOf() ?? mintCaller(scope)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** This request's caller, given rather than read off its cookie, with no cookie written. 42.21. */
|
|
50
|
+
export function presumeCaller(caller: string): void {
|
|
51
|
+
within('App caller').caller = caller
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
/**
|
|
50
55
|
* Mints a fresh caller for this request, sets its cookie, and returns it.
|
|
51
56
|
*
|
package/src/server/claudeCode.ts
CHANGED
|
@@ -8,17 +8,15 @@
|
|
|
8
8
|
// directory, settings — is not an option here at all, and its built-in `tools` are a request the visitor
|
|
9
9
|
// grants or does not: the bridge decides (44.15, 44.16, 44.29).
|
|
10
10
|
|
|
11
|
+
import type {
|
|
12
|
+
AssistantContent,
|
|
13
|
+
AssistantMessage,
|
|
14
|
+
} from '#shared/AssistantEvent.ts'
|
|
11
15
|
import { BRIDGE_SCHEME } from '#shared/BRIDGE_SCHEME.ts'
|
|
12
16
|
import { mounted } from '#shared/mount.ts'
|
|
13
17
|
import { refuse } from '#shared/refuse.ts'
|
|
14
18
|
|
|
15
|
-
import {
|
|
16
|
-
type AssistantContent,
|
|
17
|
-
type AssistantMessage,
|
|
18
|
-
blocksOf,
|
|
19
|
-
forwardOf,
|
|
20
|
-
type Provider,
|
|
21
|
-
} from './assistant.ts'
|
|
19
|
+
import { blocksOf, forwardOf, type Provider } from './assistant.ts'
|
|
22
20
|
import { BRIDGE_VERSION } from './BRIDGE_VERSION.ts'
|
|
23
21
|
import { mintCode, pairingStatus, runTurn } from './bridgeRelay.ts'
|
|
24
22
|
import { callerOf } from './callerOf.ts'
|
|
@@ -45,12 +43,13 @@ type PairingOptions = Pick<ClaudeCodeConfig, 'tools'>
|
|
|
45
43
|
|
|
46
44
|
/**
|
|
47
45
|
* Whether this browser's bridge is connected, and the command and the link that pair one where it is
|
|
48
|
-
* not.
|
|
46
|
+
* not, with the command that installs the app the link opens.
|
|
49
47
|
*/
|
|
50
48
|
export type BridgeStatus = {
|
|
51
49
|
paired: boolean
|
|
52
50
|
command: string | undefined
|
|
53
51
|
link: string | undefined
|
|
52
|
+
install: string | undefined
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
/** The visitor's own Claude Code, reached through the bridge paired to their browser. */
|
|
@@ -123,17 +122,21 @@ function askingCaller(): string {
|
|
|
123
122
|
function bridgeCommand(options: PairingOptions = {}): {
|
|
124
123
|
command: string
|
|
125
124
|
link: string
|
|
125
|
+
install: string
|
|
126
126
|
expires: number
|
|
127
127
|
} {
|
|
128
128
|
const { code, expires } = mintCode(askingCaller(), signedInAs())
|
|
129
129
|
return { ...commandFor(code, options), expires }
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* The command and the link that pair a bridge by `code`, granting `tools`, and the command that
|
|
134
|
+
* installs the app the link opens — of the release the command runs, 44.56.
|
|
135
|
+
*/
|
|
133
136
|
function commandFor(
|
|
134
137
|
code: string,
|
|
135
138
|
{ tools }: PairingOptions,
|
|
136
|
-
): { command: string; link: string } {
|
|
139
|
+
): { command: string; link: string; install: string } {
|
|
137
140
|
const site = `${appOrigin() ?? new URL(request().url).origin}${mounted('')}`
|
|
138
141
|
const granted = tools?.length ? tools.join(',') : undefined
|
|
139
142
|
const query = new URLSearchParams({ site, code, version: BRIDGE_VERSION })
|
|
@@ -142,6 +145,7 @@ function commandFor(
|
|
|
142
145
|
return {
|
|
143
146
|
command: `bunx @abide/bridge@${BRIDGE_VERSION} ${site} ${code}${grant}`,
|
|
144
147
|
link: `${BRIDGE_SCHEME}://pair?${query}`,
|
|
148
|
+
install: `bunx @abide/bridge@${BRIDGE_VERSION} install`,
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
|
|
@@ -159,7 +163,12 @@ async function* bridge(
|
|
|
159
163
|
request().signal,
|
|
160
164
|
))
|
|
161
165
|
yield code === undefined
|
|
162
|
-
? {
|
|
166
|
+
? {
|
|
167
|
+
paired: true,
|
|
168
|
+
command: undefined,
|
|
169
|
+
link: undefined,
|
|
170
|
+
install: undefined,
|
|
171
|
+
}
|
|
163
172
|
: { paired: false, ...commandFor(code, options) }
|
|
164
173
|
}
|
|
165
174
|
|
package/src/server/commands.ts
CHANGED
|
@@ -245,10 +245,11 @@ function usage(host: Host, commands: Row[]): string {
|
|
|
245
245
|
text += ` ${command.padEnd(width)} ${meaning}\n`
|
|
246
246
|
if (!sourced)
|
|
247
247
|
return `${text}
|
|
248
|
-
--port
|
|
249
|
-
--
|
|
250
|
-
--
|
|
251
|
-
--
|
|
248
|
+
--port the listen port the app is served on
|
|
249
|
+
--hostname the interface it listens on — every one where none is named
|
|
250
|
+
--url the running app the shell, call, watch, openapi and mcp talk to
|
|
251
|
+
--method the method call sends, where the handler answers more than one
|
|
252
|
+
--version the version of this executable's app
|
|
252
253
|
`
|
|
253
254
|
text += `
|
|
254
255
|
--root the app's source directory, where its pages sit —
|
|
@@ -256,6 +257,7 @@ function usage(host: Host, commands: Row[]): string {
|
|
|
256
257
|
--out where the build goes, beside the app by default; the file compile and
|
|
257
258
|
bundle write — named after the app, where you stand
|
|
258
259
|
--port the listen port for dev, start and the shell's app
|
|
260
|
+
--hostname the interface they listen on — every one where none is named
|
|
259
261
|
--url the running app the shell, call, watch, openapi and mcp talk to
|
|
260
262
|
--target the platform compile builds for — linux-x64, darwin-arm64, windows-x64…
|
|
261
263
|
--platforms several of them, comma-separated, one executable each
|
|
@@ -291,6 +293,8 @@ function parseFlags(argv: string[]) {
|
|
|
291
293
|
// `PORT` and then falls back to 3000 — `abide dev` scanning on from there, 38.63 — and
|
|
292
294
|
// a default written here would take that away from every host that sets it.
|
|
293
295
|
port: { type: 'string' },
|
|
296
|
+
// NO DEFAULT EITHER: `Bun.serve` given none binds every interface (38.136).
|
|
297
|
+
hostname: { type: 'string' },
|
|
294
298
|
url: { type: 'string' },
|
|
295
299
|
method: { type: 'string' },
|
|
296
300
|
target: { type: 'string' },
|
|
@@ -321,14 +325,17 @@ async function served(
|
|
|
321
325
|
process.env.NODE_ENV ??= 'production'
|
|
322
326
|
const booted = await booting(host, app, given)
|
|
323
327
|
if (typeof booted === 'number') return booted
|
|
324
|
-
const port = given.values
|
|
325
|
-
await booted.listen(
|
|
328
|
+
const { port, hostname } = given.values
|
|
329
|
+
await booted.listen({
|
|
330
|
+
...(port === undefined ? {} : { port: Number(port) }),
|
|
331
|
+
...(hostname === undefined ? {} : { hostname }),
|
|
332
|
+
})
|
|
326
333
|
stopOnSignalOrFault(booted)
|
|
327
334
|
const server = LISTENING.server as Bun.Server<unknown>
|
|
328
335
|
return {
|
|
329
336
|
app: booted,
|
|
330
337
|
url: withoutTrailingSlash(server.url.href),
|
|
331
|
-
heading: banner(host.name, 'start', app.package, server),
|
|
338
|
+
heading: banner(host.name, 'start', app.package, server, hostname),
|
|
332
339
|
}
|
|
333
340
|
}
|
|
334
341
|
|