@abide/abide 0.30.0 → 0.31.1
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/AGENTS.md +4 -3
- package/CHANGELOG.md +26 -0
- package/package.json +2 -1
- package/src/lib/bundle/disconnected.abide +82 -82
- package/src/lib/cli/dispatchCommand.ts +3 -2
- package/src/lib/cli/resolveCliTarget.ts +2 -3
- package/src/lib/cli/runCli.ts +2 -3
- package/src/lib/cli/runSession.ts +2 -3
- package/src/lib/mcp/dispatchMcpRequest.ts +3 -2
- package/src/lib/mcp/mcpSurface.ts +2 -1
- package/src/lib/mcp/toolResultFromResponse.ts +2 -1
- package/src/lib/server/rpc/parseArgs.ts +1 -3
- package/src/lib/server/runtime/streamFromIterator.ts +3 -1
- package/src/lib/server/runtime/warnUnguardedMcp.ts +4 -3
- package/src/lib/server/sockets/createSocketDispatcher.ts +5 -7
- package/src/lib/shared/cacheEntryFromSnapshot.ts +2 -1
- package/src/lib/shared/contentTypeOf.ts +6 -0
- package/src/lib/shared/decodeResponse.ts +2 -1
- package/src/lib/shared/isCompileTarget.ts +7 -1
- package/src/lib/shared/isModuleNotFound.ts +3 -1
- package/src/lib/shared/isStreamingResponse.ts +2 -1
- package/src/lib/shared/messageFromError.ts +6 -0
- package/src/lib/shared/streamResponse.ts +2 -1
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +7 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/VOID_TAGS.ts +4 -3
- package/src/lib/ui/compile/compileComponent.ts +12 -2
- package/src/lib/ui/compile/compileModule.ts +7 -4
- package/src/lib/ui/compile/compileSSR.ts +11 -2
- package/src/lib/ui/compile/compileShadow.ts +146 -49
- package/src/lib/ui/compile/createShadowLanguageService.ts +2 -1
- package/src/lib/ui/compile/createShadowProgram.ts +2 -1
- package/src/lib/ui/compile/desugarSignals.ts +41 -14
- package/src/lib/ui/compile/parseTemplate.ts +21 -26
- package/src/lib/ui/compile/prepareNestedScript.ts +4 -2
- package/src/lib/ui/derived.ts +25 -4
- package/src/lib/ui/dom/awaitBlock.ts +1 -24
- package/src/lib/ui/dom/discardBoundary.ts +27 -0
- package/src/lib/ui/dom/tryBlock.ts +7 -26
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/linked.ts +34 -0
- package/src/lib/ui/router.ts +1 -1
- package/src/lib/ui/state.ts +9 -2
- package/template/src/ui/pages/page.abide +1 -1
package/AGENTS.md
CHANGED
|
@@ -160,8 +160,9 @@ Same selector grammar as `cache.invalidate`; also accept a `Subscribable`. See C
|
|
|
160
160
|
## UI surface — `abide/ui/*` (client-only)
|
|
161
161
|
|
|
162
162
|
### Reactive primitives — `@readme plumbing` (in scope inside `.abide`, no import)
|
|
163
|
-
- `abide/ui/state(initial)` → writable `State<T>` (`.value` getter/setter).
|
|
164
|
-
- `abide/ui/
|
|
163
|
+
- `abide/ui/state(initial, transform?)` → writable `State<T>` (`.value` getter/setter). Local truth. `transform(next, prev) => T` is a write-coercion gate: each `.value=` stores what it returns (`return prev` rejects via the `Object.is` no-op); construction `initial` is verbatim. Plain `state(x)` is a serializable doc slot; `state(x, transform)` is a non-serializing `.value` cell.
|
|
164
|
+
- `abide/ui/linked(seed, transform?)` → writable `State<T>` seeded reactively from upstream (Angular's `linkedSignal`): owns a local value, reseeds when the `seed` thunk's deps change, edits stay local. `transform` gates reseeds and writes alike. Thunk seed is required (it is the reactivity); seed captured by reference (clone in the thunk for isolation). Non-serializing — reseeds on resume.
|
|
165
|
+
- `abide/ui/derived(compute)` → lazy read-only computed (`.value`); re-derived on resume, never serialized. `derived(compute, set)` → writable lens: `.value` derives from upstream, assigning runs imperative `set(next)` to write *through* to the sources (no local store).
|
|
165
166
|
- `abide/ui/effect(fn)` → run now, re-run on dependency change; `fn` may return teardown / be async; returns dispose.
|
|
166
167
|
- `abide/ui/doc(initial?)` → reactive document: immutable tree addressed by path, every change a patch (the substrate under all reactivity / resumability / sync).
|
|
167
168
|
|
|
@@ -180,7 +181,7 @@ Same selector grammar as `cache.invalidate`; also accept a `Subscribable`. See C
|
|
|
180
181
|
Valid HTML with `<script>` + native `<template>` control flow + scoped `<style>`.
|
|
181
182
|
- **Bindings:** `{expr}` text, `name={expr}` attr, `onclick={fn}`, `bind:value={…}` / `bind:checked` / `bind:group`, `attach={fn}` (node-lifetime attachment — the dual of `on`; the `use:`-action / `{@attach}` equivalent, lowered to `ui/dom/attach`).
|
|
182
183
|
- **Control flow (native `<template>`):** `if`/`else`, `each={list} as="x" key="x.id"`, `await={p}`/`then`/`catch`, `switch`/`case`/`default`.
|
|
183
|
-
- **Components:** capitalised tags (`<Layout title=…>`); children fill `<slot/>`; props are reactive (passed as thunks). `prop('name')` reads a typed page
|
|
184
|
+
- **Components:** capitalised tags (`<Layout title=…>`); children fill `<slot/>`; props are reactive (passed as thunks). A component has no directives — every attribute is a prop under its written name (so `onclick=`/`bind:open=`/`attach=` pass through as props, e.g. callbacks, not the DOM-element directives those are on a lowercase tag) and is type-checked against the child's declared props. `prop('name')` reads a typed component prop (the parent-supplied thunk, reactive + read-only); route params come from the `page` proxy (`page.params.name`), not `prop()`.
|
|
184
185
|
- **Snippets / named slots:** `<template name="x" args={…}>` declares a reusable named builder (the `snippet()` form), rendered like a function — covers named slots / `{@render}`.
|
|
185
186
|
- **Reactivity:** write plain assignment (`count += 1`, `items.push(x)`); the compiler lowers it to patches. Deep-field edits wake only that field.
|
|
186
187
|
- **SSR:** byte-identical HTML string; `renderToStream` ships the shell then streams `<template await>` fragments out of order; `hydrate` adopts static structure in place (control-flow blocks + child components fall back to `mount`/re-render — known gap).
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
+
## 0.31.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`a9f7b3b`](https://github.com/briancray/abide/commit/a9f7b3b09f1db15fa784844a2672b1058dde2b25) - stop the type-check shadow merging a semicolon-less call into the next component ([`7d863d7`](https://github.com/briancray/abide/commit/7d863d7b1f2d4d973a5eafd94cf5002fdeb519c4))
|
|
8
|
+
|
|
9
|
+
## 0.31.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [`ea69bad`](https://github.com/briancray/abide/commit/ea69bad96d516dc6884d8a8c014b47c4864ebbe6) - linked() reactive cell, writable derived, state write-transform ([`82301ea`](https://github.com/briancray/abide/commit/82301eabcda0866eec471f95048f74079ea93ab7))
|
|
14
|
+
|
|
15
|
+
- [`ea69bad`](https://github.com/briancray/abide/commit/ea69bad96d516dc6884d8a8c014b47c4864ebbe6) - sharper template type-checking in the shadow ([`f280619`](https://github.com/briancray/abide/commit/f280619778c23200a0f11cf8e6344edf2ea39884))
|
|
16
|
+
|
|
17
|
+
- [`ea69bad`](https://github.com/briancray/abide/commit/ea69bad96d516dc6884d8a8c014b47c4864ebbe6) - pass component on\*/bind/attach attributes through as props ([`f9a9202`](https://github.com/briancray/abide/commit/f9a9202d8555d7246fcc4338e833551f54ee8968))
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [`ea69bad`](https://github.com/briancray/abide/commit/ea69bad96d516dc6884d8a8c014b47c4864ebbe6) - extract messageFromError, contentTypeOf, discardBoundary helpers ([`1bfb721`](https://github.com/briancray/abide/commit/1bfb721a8a8d3c8907bc9d4d0c6d97ddc265ed1e))
|
|
22
|
+
|
|
23
|
+
- [`ea69bad`](https://github.com/briancray/abide/commit/ea69bad96d516dc6884d8a8c014b47c4864ebbe6) - resolve the chained route on the page proxy ([`ed518c1`](https://github.com/briancray/abide/commit/ed518c1e8232c04496fe3fd04783f750164bc378))
|
|
24
|
+
|
|
25
|
+
- [`ea69bad`](https://github.com/briancray/abide/commit/ea69bad96d516dc6884d8a8c014b47c4864ebbe6) - share one component analysis across client and SSR back-ends ([`f330664`](https://github.com/briancray/abide/commit/f33066441625c08077b1b413603204ec2afe59eb))
|
|
26
|
+
|
|
27
|
+
- [`ea69bad`](https://github.com/briancray/abide/commit/ea69bad96d516dc6884d8a8c014b47c4864ebbe6) - format .abide <script> bodies via Biome ([`f4fc64e`](https://github.com/briancray/abide/commit/f4fc64ecb838a449dfb669af5fcaf40bcf7fa8ca))
|
|
28
|
+
|
|
3
29
|
## 0.30.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abide/abide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"./shared/url": "./src/lib/shared/url.ts",
|
|
74
74
|
"./shared/createSubscriber": "./src/lib/shared/createSubscriber.ts",
|
|
75
75
|
"./ui/state": "./src/lib/ui/state.ts",
|
|
76
|
+
"./ui/linked": "./src/lib/ui/linked.ts",
|
|
76
77
|
"./ui/derived": "./src/lib/ui/derived.ts",
|
|
77
78
|
"./ui/effect": "./src/lib/ui/effect.ts",
|
|
78
79
|
"./ui/doc": "./src/lib/ui/doc.ts",
|
|
@@ -1,149 +1,149 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
2
|
+
/*
|
|
3
3
|
Default bundle connect screen (abide-ui). The launcher serves this (logo baked in
|
|
4
4
|
at build time, app title injected at runtime) instead of a blank window; override
|
|
5
5
|
it by dropping a `src/bundle/disconnected.abide`. It connects to a remote server
|
|
6
6
|
by URL and boots the embedded server, talking to the launcher's in-process control
|
|
7
7
|
server: POST /connect and POST /start each reply with a `{ redirect }` to follow.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
const LAST_URL_KEY = 'abide:last-server-url'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const heading = globalThis.__ABIDE_TITLE__ ?? 'abide app'
|
|
12
|
+
const logo = globalThis.__ABIDE_LOGO__
|
|
13
|
+
const placeholder = 'https://example.com'
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
let url = state(localStorage.getItem(LAST_URL_KEY) ?? '')
|
|
16
|
+
let error = state('')
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const launchAction = new URLSearchParams(location.search).get('action')
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/* Two phases so the screen never flashes before a redirect. */
|
|
21
|
+
let phase = state(launchAction === 'start' ? 'splash' : 'connect')
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
let configFields = state([])
|
|
24
|
+
let configValues = state({})
|
|
25
|
+
let showConfig = state(false)
|
|
26
|
+
let savingConfig = state(false)
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
/* Interpret the boot intent once on load. */
|
|
29
|
+
effect(() => {
|
|
30
30
|
if (launchAction === 'start') {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
void start()
|
|
32
|
+
return
|
|
33
33
|
}
|
|
34
34
|
if (launchAction === 'lost') {
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
error = 'The server stopped responding.'
|
|
36
|
+
return
|
|
37
37
|
}
|
|
38
38
|
if (launchAction === 'disconnect') {
|
|
39
|
-
|
|
39
|
+
void fetch('/__abide/disconnect').catch(() => {})
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
})
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
async function connect() {
|
|
44
44
|
const cleaned = url.trim()
|
|
45
45
|
if (!cleaned) {
|
|
46
|
-
|
|
46
|
+
return
|
|
47
47
|
}
|
|
48
48
|
await postAndFollow('/connect', { url: cleaned }, 'Could not connect', () => {
|
|
49
|
-
|
|
49
|
+
localStorage.setItem(LAST_URL_KEY, cleaned)
|
|
50
50
|
})
|
|
51
|
-
|
|
51
|
+
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
async function postAndFollow(path, body, failure, onSuccess) {
|
|
54
54
|
error = ''
|
|
55
55
|
phase = 'splash'
|
|
56
56
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
const response = await fetch(path, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
...(body === undefined
|
|
60
|
+
? {}
|
|
61
|
+
: { headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }),
|
|
62
|
+
})
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const reply = await response.json()
|
|
65
|
+
throw new Error(reply.error ?? `${path.slice(1)} failed (${response.status})`)
|
|
66
|
+
}
|
|
67
|
+
const { redirect } = await response.json()
|
|
68
|
+
onSuccess?.()
|
|
69
|
+
location.href = redirect
|
|
70
70
|
} catch (cause) {
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
error = `${failure}: ${String(cause)}`
|
|
72
|
+
phase = 'connect'
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
async function start() {
|
|
77
77
|
error = ''
|
|
78
78
|
const config = await loadConfig().catch(() => undefined)
|
|
79
79
|
if (config) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
configFields = config.fields
|
|
81
|
+
configValues = { ...config.values }
|
|
82
|
+
phase = 'connect'
|
|
83
|
+
showConfig = true
|
|
84
|
+
return
|
|
85
85
|
}
|
|
86
86
|
await boot()
|
|
87
|
-
|
|
87
|
+
}
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
async function boot() {
|
|
90
90
|
await postAndFollow('/start', undefined, 'Could not start the server')
|
|
91
|
-
|
|
91
|
+
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
async function loadConfig() {
|
|
94
94
|
const response = await fetch('/__abide/config')
|
|
95
95
|
const { schema, values } = await response.json()
|
|
96
96
|
if (!schema) {
|
|
97
|
-
|
|
97
|
+
return undefined
|
|
98
98
|
}
|
|
99
99
|
return { fields: fieldsFromSchema(schema), values: values ?? {} }
|
|
100
|
-
|
|
100
|
+
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
function fieldsFromSchema(schema) {
|
|
103
103
|
const properties = schema.properties ?? {}
|
|
104
104
|
const required = new Set(schema.required ?? [])
|
|
105
105
|
return Object.entries(properties).map(([key, property]) => ({
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
key,
|
|
107
|
+
label: property.title ?? key,
|
|
108
|
+
description: property.description,
|
|
109
|
+
inputType: inputType(property),
|
|
110
|
+
required: required.has(key),
|
|
111
111
|
}))
|
|
112
|
-
|
|
112
|
+
}
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
function inputType(property) {
|
|
115
115
|
if (property.type === 'boolean') {
|
|
116
|
-
|
|
116
|
+
return 'checkbox'
|
|
117
117
|
}
|
|
118
118
|
if (property.type === 'number' || property.type === 'integer') {
|
|
119
|
-
|
|
119
|
+
return 'number'
|
|
120
120
|
}
|
|
121
121
|
if (property.format === 'password') {
|
|
122
|
-
|
|
122
|
+
return 'password'
|
|
123
123
|
}
|
|
124
124
|
return 'text'
|
|
125
|
-
|
|
125
|
+
}
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
async function saveConfig() {
|
|
128
128
|
error = ''
|
|
129
129
|
savingConfig = true
|
|
130
130
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
131
|
+
const response = await fetch('/__abide/config', {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'content-type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({ values: configValues }),
|
|
135
|
+
})
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
throw new Error(`save failed (${response.status})`)
|
|
138
|
+
}
|
|
139
|
+
showConfig = false
|
|
140
|
+
savingConfig = false
|
|
141
|
+
await boot()
|
|
142
142
|
} catch (cause) {
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
error = `Could not save settings: ${String(cause)}`
|
|
144
|
+
savingConfig = false
|
|
145
145
|
}
|
|
146
|
-
|
|
146
|
+
}
|
|
147
147
|
</script>
|
|
148
148
|
|
|
149
149
|
<template if={phase === 'splash'}>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { decodeResponse } from '../shared/decodeResponse.ts'
|
|
2
2
|
import { isStreamingResponse } from '../shared/isStreamingResponse.ts'
|
|
3
|
+
import { messageFromError } from '../shared/messageFromError.ts'
|
|
3
4
|
import { responseErrorText } from '../shared/responseErrorText.ts'
|
|
4
5
|
import { streamResponse } from '../shared/streamResponse.ts'
|
|
5
6
|
import { createClient } from './createClient.ts'
|
|
@@ -41,7 +42,7 @@ export async function dispatchCommand({
|
|
|
41
42
|
try {
|
|
42
43
|
args = await parseArgvForRpc(argvTail, entry.jsonSchema)
|
|
43
44
|
} catch (error) {
|
|
44
|
-
console.error(`${programName}: ${
|
|
45
|
+
console.error(`${programName}: ${messageFromError(error)}`)
|
|
45
46
|
return 1
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -65,7 +66,7 @@ export async function dispatchCommand({
|
|
|
65
66
|
printValue(await decodeResponse(response), true)
|
|
66
67
|
return 0
|
|
67
68
|
} catch (error) {
|
|
68
|
-
console.error(`${programName}: ${
|
|
69
|
+
console.error(`${programName}: ${messageFromError(error)}`)
|
|
69
70
|
return 1
|
|
70
71
|
}
|
|
71
72
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { probeAbideServer } from '../bundle/probeAbideServer.ts'
|
|
2
2
|
import { spawnEmbeddedServer } from '../bundle/spawnEmbeddedServer.ts'
|
|
3
3
|
import { abideLog } from '../shared/abideLog.ts'
|
|
4
|
+
import { messageFromError } from '../shared/messageFromError.ts'
|
|
4
5
|
import { readLastConnection } from '../shared/readLastConnection.ts'
|
|
5
6
|
import type { CliTarget } from './types/CliTarget.ts'
|
|
6
7
|
|
|
@@ -28,9 +29,7 @@ export async function resolveCliTarget(programName: string): Promise<CliTarget |
|
|
|
28
29
|
})
|
|
29
30
|
return { url, child }
|
|
30
31
|
} catch (error) {
|
|
31
|
-
abideLog.warn(
|
|
32
|
-
`could not start local instance: ${error instanceof Error ? error.message : String(error)}`,
|
|
33
|
-
)
|
|
32
|
+
abideLog.warn(`could not start local instance: ${messageFromError(error)}`)
|
|
34
33
|
return undefined
|
|
35
34
|
}
|
|
36
35
|
}
|
package/src/lib/cli/runCli.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { clearLastConnection } from '../shared/clearLastConnection.ts'
|
|
2
2
|
import { loadEnvFromDataDir } from '../shared/loadEnvFromDataDir.ts'
|
|
3
|
+
import { messageFromError } from '../shared/messageFromError.ts'
|
|
3
4
|
import { connectToServer } from './connectToServer.ts'
|
|
4
5
|
import { dispatchCommand } from './dispatchCommand.ts'
|
|
5
6
|
import { loadEnvFromBinaryDir } from './loadEnvFromBinaryDir.ts'
|
|
@@ -146,9 +147,7 @@ export async function runCli({
|
|
|
146
147
|
try {
|
|
147
148
|
target = await startLocalInstance(programName)
|
|
148
149
|
} catch (error) {
|
|
149
|
-
console.error(
|
|
150
|
-
`${programName}: ${error instanceof Error ? error.message : String(error)}`,
|
|
151
|
-
)
|
|
150
|
+
console.error(`${programName}: ${messageFromError(error)}`)
|
|
152
151
|
return 1
|
|
153
152
|
}
|
|
154
153
|
return runSession({ programName, manifest, footer, target })
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { clearLastConnection } from '../shared/clearLastConnection.ts'
|
|
2
|
+
import { messageFromError } from '../shared/messageFromError.ts'
|
|
2
3
|
import { connectToServer } from './connectToServer.ts'
|
|
3
4
|
import { dispatchCommand } from './dispatchCommand.ts'
|
|
4
5
|
import { printSessionHelp } from './printSessionHelp.ts'
|
|
@@ -77,9 +78,7 @@ export async function runSession({
|
|
|
77
78
|
try {
|
|
78
79
|
await swap(await startLocalInstance(programName))
|
|
79
80
|
} catch (error) {
|
|
80
|
-
console.error(
|
|
81
|
-
`could not start local instance: ${error instanceof Error ? error.message : String(error)}`,
|
|
82
|
-
)
|
|
81
|
+
console.error(`could not start local instance: ${messageFromError(error)}`)
|
|
83
82
|
}
|
|
84
83
|
} else if (head === '/disconnect') {
|
|
85
84
|
await clearLastConnection(programName)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ensureRegistriesLoaded } from '../server/runtime/registryManifests.ts'
|
|
2
|
+
import { messageFromError } from '../shared/messageFromError.ts'
|
|
2
3
|
import { getMcpResourceServer } from './mcpResourceServerSlot.ts'
|
|
3
4
|
import { buildPrompts, buildTools, callTool, renderPrompt } from './mcpSurface.ts'
|
|
4
5
|
import type { JsonRpcRequest } from './types/JsonRpcRequest.ts'
|
|
@@ -68,7 +69,7 @@ export async function dispatchMcpRequest(
|
|
|
68
69
|
try {
|
|
69
70
|
await opts.authorize(request)
|
|
70
71
|
} catch (error) {
|
|
71
|
-
const message =
|
|
72
|
+
const message = messageFromError(error)
|
|
72
73
|
return jsonRpcError(id, -32001, message)
|
|
73
74
|
}
|
|
74
75
|
}
|
|
@@ -132,7 +133,7 @@ export async function dispatchMcpRequest(
|
|
|
132
133
|
return jsonRpcError(id, -32601, `Method not found: ${envelope.method}`)
|
|
133
134
|
}
|
|
134
135
|
} catch (error) {
|
|
135
|
-
const message =
|
|
136
|
+
const message = messageFromError(error)
|
|
136
137
|
return jsonRpcError(id, -32603, message)
|
|
137
138
|
}
|
|
138
139
|
}
|
|
@@ -8,6 +8,7 @@ import { abideLog } from '../shared/abideLog.ts'
|
|
|
8
8
|
import { commandNameForUrl } from '../shared/commandNameForUrl.ts'
|
|
9
9
|
import { forwardHeaders } from '../shared/forwardHeaders.ts'
|
|
10
10
|
import { jsonSchemaForSchema } from '../shared/jsonSchemaForSchema.ts'
|
|
11
|
+
import { messageFromError } from '../shared/messageFromError.ts'
|
|
11
12
|
import { annotationsForMethod } from './annotationsForMethod.ts'
|
|
12
13
|
import { getMcpResourceServer } from './mcpResourceServerSlot.ts'
|
|
13
14
|
import { toolResultFromResponse } from './toolResultFromResponse.ts'
|
|
@@ -193,7 +194,7 @@ function callSocketTool(
|
|
|
193
194
|
// publish() validates the payload against the socket schema and throws on failure.
|
|
194
195
|
entry.socket.publish(args)
|
|
195
196
|
} catch (error) {
|
|
196
|
-
return textResult(
|
|
197
|
+
return textResult(messageFromError(error), true)
|
|
197
198
|
}
|
|
198
199
|
return textResult('ok')
|
|
199
200
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { decodeResponse } from '../shared/decodeResponse.ts'
|
|
2
2
|
import { isStreamingResponse } from '../shared/isStreamingResponse.ts'
|
|
3
|
+
import { messageFromError } from '../shared/messageFromError.ts'
|
|
3
4
|
import { responseErrorText } from '../shared/responseErrorText.ts'
|
|
4
5
|
import { streamResponse } from '../shared/streamResponse.ts'
|
|
5
6
|
|
|
@@ -42,7 +43,7 @@ export async function toolResultFromResponse(response: Response): Promise<Record
|
|
|
42
43
|
{ type: 'text', text: frames.map(asText).join('\n') },
|
|
43
44
|
{
|
|
44
45
|
type: 'text',
|
|
45
|
-
text: `stream error: ${
|
|
46
|
+
text: `stream error: ${messageFromError(error)}`,
|
|
46
47
|
},
|
|
47
48
|
],
|
|
48
49
|
structuredContent: { frames },
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
Shared body builder for the streaming respond helpers (`jsonl`, `sse`).
|
|
3
5
|
Both flow the same shape — pull from an AsyncIterator, encode each frame
|
|
@@ -62,7 +64,7 @@ export function streamFromIterator<T>(
|
|
|
62
64
|
}
|
|
63
65
|
controller.enqueue(textEncoder.encode(encoder.encodeFrame(next.value)))
|
|
64
66
|
} catch (error) {
|
|
65
|
-
const message =
|
|
67
|
+
const message = messageFromError(error)
|
|
66
68
|
controller.enqueue(textEncoder.encode(encoder.encodeError(message)))
|
|
67
69
|
stopKeepalive()
|
|
68
70
|
controller.close()
|
|
@@ -18,9 +18,10 @@ export async function warnUnguardedMcp(): Promise<void> {
|
|
|
18
18
|
} catch {
|
|
19
19
|
return
|
|
20
20
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const isMcpExposed = (entry: { clients: { mcp: boolean } }): boolean => entry.clients.mcp
|
|
22
|
+
const exposed = [...verbRegistry.values(), ...socketRegistry.values()].filter(
|
|
23
|
+
isMcpExposed,
|
|
24
|
+
).length
|
|
24
25
|
if (exposed === 0) {
|
|
25
26
|
return
|
|
26
27
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ServerWebSocket } from 'bun'
|
|
2
2
|
import { abideLog } from '../../shared/abideLog.ts'
|
|
3
3
|
import { memoizeByKey } from '../../shared/memoizeByKey.ts'
|
|
4
|
+
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
4
5
|
import { error } from '../error.ts'
|
|
5
6
|
import { json } from '../json.ts'
|
|
6
7
|
import { sse } from '../sse.ts'
|
|
@@ -86,7 +87,7 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
86
87
|
abideLog.error(loadError)
|
|
87
88
|
return {
|
|
88
89
|
failure: 'load-failed',
|
|
89
|
-
message:
|
|
90
|
+
message: messageFromError(loadError),
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
const entry = lookupSocket(name)
|
|
@@ -214,8 +215,8 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
214
215
|
*/
|
|
215
216
|
try {
|
|
216
217
|
entry.socket.publish(frame.message)
|
|
217
|
-
} catch (
|
|
218
|
-
abideLog.error(
|
|
218
|
+
} catch (publishError) {
|
|
219
|
+
abideLog.error(publishError)
|
|
219
220
|
}
|
|
220
221
|
/*
|
|
221
222
|
ws parameter retained for future per-ws auth context (cookies on
|
|
@@ -276,10 +277,7 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
276
277
|
// publish() validates against the socket schema and throws on a bad payload.
|
|
277
278
|
entry.socket.publish(message)
|
|
278
279
|
} catch (publishError) {
|
|
279
|
-
return error(
|
|
280
|
-
422,
|
|
281
|
-
publishError instanceof Error ? publishError.message : String(publishError),
|
|
282
|
-
)
|
|
280
|
+
return error(422, messageFromError(publishError))
|
|
283
281
|
}
|
|
284
282
|
return json({ ok: true })
|
|
285
283
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentTypeOf } from './contentTypeOf.ts'
|
|
1
2
|
import type { CacheEntry } from './types/CacheEntry.ts'
|
|
2
3
|
import type { CacheSnapshotEntry } from './types/CacheSnapshotEntry.ts'
|
|
3
4
|
|
|
@@ -39,7 +40,7 @@ function warmValueFromSnapshot(status: number, headers: Headers, body: string):
|
|
|
39
40
|
if (status === 204 || status < 200 || status >= 300) {
|
|
40
41
|
return undefined
|
|
41
42
|
}
|
|
42
|
-
const contentType = (headers
|
|
43
|
+
const contentType = contentTypeOf(headers)
|
|
43
44
|
if (contentType.includes('json')) {
|
|
44
45
|
/*
|
|
45
46
|
`.includes('json')` also matches streaming/non-JSON types like
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* The `content-type` header, lowercased, or '' when absent — the canonical shape
|
|
2
|
+
every content-type comparison in the codebase reads (case-insensitive match,
|
|
3
|
+
no undefined to guard). */
|
|
4
|
+
export function contentTypeOf(headers: Headers): string {
|
|
5
|
+
return (headers.get('content-type') ?? '').toLowerCase()
|
|
6
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentTypeOf } from './contentTypeOf.ts'
|
|
1
2
|
import { HttpError } from './HttpError.ts'
|
|
2
3
|
import { isStreamingResponse } from './isStreamingResponse.ts'
|
|
3
4
|
|
|
@@ -31,7 +32,7 @@ export async function decodeResponse(response: Response): Promise<unknown> {
|
|
|
31
32
|
if (response.status === 204) {
|
|
32
33
|
return undefined
|
|
33
34
|
}
|
|
34
|
-
const contentType = (response.headers
|
|
35
|
+
const contentType = contentTypeOf(response.headers)
|
|
35
36
|
if (isStreamingResponse(response)) {
|
|
36
37
|
throw new Error(
|
|
37
38
|
`[abide] response at ${response.url} is a stream (${contentType}) — use tail(fn.stream(args)) for a reactive view, or fn.stream(args) for per-call iteration, instead of awaiting the bare call or cache()`,
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { CompileTarget } from './types/CompileTarget.ts'
|
|
2
2
|
|
|
3
|
-
/* The canonical cross-compile targets, as a runtime set for validation.
|
|
3
|
+
/* The canonical cross-compile targets, as a runtime set for validation. Kept
|
|
4
|
+
separate from `detectTarget`'s `HOST_TO_TARGET` on purpose, though the two lists
|
|
5
|
+
coincide today: this is "every target `--target` accepts" (all cross-targets),
|
|
6
|
+
while that map is "targets auto-detectable from a host". A future Bun target that
|
|
7
|
+
no host maps to (a new arch/libc we cross-compile to but don't run on) belongs
|
|
8
|
+
here and not there — deriving one from the other would couple distinct concerns.
|
|
9
|
+
Both stay aligned to `CompileTarget`, the single source of truth. */
|
|
4
10
|
const COMPILE_TARGETS: readonly CompileTarget[] = [
|
|
5
11
|
'bun-darwin-arm64',
|
|
6
12
|
'bun-darwin-x64',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { messageFromError } from './messageFromError.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
True when an error from a dynamic `import(...)` is a module-resolution
|
|
3
5
|
failure (the package isn't installed) rather than an error thrown while
|
|
@@ -11,6 +13,6 @@ export function isModuleNotFound(error: unknown): boolean {
|
|
|
11
13
|
if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') {
|
|
12
14
|
return true
|
|
13
15
|
}
|
|
14
|
-
const message =
|
|
16
|
+
const message = messageFromError(error)
|
|
15
17
|
return /cannot find (module|package)|failed to resolve/i.test(message)
|
|
16
18
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentTypeOf } from './contentTypeOf.ts'
|
|
1
2
|
import { STREAMING_CONTENT_TYPES } from './STREAMING_CONTENT_TYPES.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
@@ -6,6 +7,6 @@ Content-Type, so callers drain it frame-by-frame instead of buffering.
|
|
|
6
7
|
Shared by the CLI print path and the MCP tool dispatcher.
|
|
7
8
|
*/
|
|
8
9
|
export function isStreamingResponse(response: Response): boolean {
|
|
9
|
-
const contentType = (response.headers
|
|
10
|
+
const contentType = contentTypeOf(response.headers)
|
|
10
11
|
return STREAMING_CONTENT_TYPES.some((type) => contentType.startsWith(type))
|
|
11
12
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* A thrown value's human message: an Error's `.message`, otherwise the value
|
|
2
|
+
coerced to a string. The one place the "how a non-Error surfaces to users"
|
|
3
|
+
rule lives, instead of inlining `x instanceof Error ? x.message : String(x)`. */
|
|
4
|
+
export function messageFromError(error: unknown): string {
|
|
5
|
+
return error instanceof Error ? error.message : String(error)
|
|
6
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentTypeOf } from './contentTypeOf.ts'
|
|
1
2
|
import { decodeResponse } from './decodeResponse.ts'
|
|
2
3
|
import { HttpError } from './HttpError.ts'
|
|
3
4
|
import { jsonlErrorFrame } from './jsonlErrorFrame.ts'
|
|
@@ -29,7 +30,7 @@ export function streamResponse<T>(response: Response): AsyncIterable<T> {
|
|
|
29
30
|
if (!response.ok) {
|
|
30
31
|
return errorIterable<T>(new HttpError(response))
|
|
31
32
|
}
|
|
32
|
-
const contentType = (response.headers
|
|
33
|
+
const contentType = contentTypeOf(response.headers)
|
|
33
34
|
if (contentType.startsWith('text/event-stream')) {
|
|
34
35
|
return parseSse<T>(response)
|
|
35
36
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* The callee names the `.abide` compiler recognises as reactive declarations
|
|
2
|
+
(`let x = state(...)`, `linked(...)`, `derived(...)`, `prop(...)`): the shared
|
|
3
|
+
"is this a reactive binding" allowlist read by the desugarer, the nested-script
|
|
4
|
+
scoper, and the type-checking shadow. How each lowers — a serializable doc slot
|
|
5
|
+
vs a `.value` cell — is decided per-site; this is only the membership set, so a
|
|
6
|
+
new primitive is a single edit here. */
|
|
7
|
+
export const REACTIVE_CALLEES: ReadonlySet<string> = new Set(['state', 'linked', 'derived', 'prop'])
|