@mercury-fw/channel-http 0.9.0 → 0.10.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/CHANGELOG.md +14 -0
- package/README.md +4 -3
- package/dist/http-server.d.ts +6 -2
- package/http-server.ts +25 -9
- package/openapi.yaml +42 -8
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @mercury-fw/channel-http
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b2f8cfb: - A confirmation can offer several choices (for example "only this time" / "always for this host"): a plugin tool stages them with the new `stageChoices` of `SessionToolContext`, and gets one token per choice. The person takes one; the others die with it, and they all expire together.
|
|
8
|
+
- The paper-trail note of a group of choices is one, under its first token, and says which choice was taken.
|
|
9
|
+
- `detectPendingConfirmation` returns the `choices` a staging offers; a plain staging is one choice labelled `Conferma`.
|
|
10
|
+
- A confirmed action can hand its result back to the model (`resume` on a choice): instead of showing the person the raw result, the channel resumes the turn and the model answers on it, the result reaching it as the tool result of the call that staged it, never stored in the history.
|
|
11
|
+
- A resumed turn answers the person who confirmed even in a shared space, and the line standing for the confirmation isn't archived as something they said. A result nobody resumes within five minutes (the life of a confirmation token) is forgotten, and a restart loses it.
|
|
12
|
+
- `InboundTurn.resumeAfterConfirm` and `RESUME_AFTER_CONFIRM` (channel contract) carry the resumed turn; `ConfirmOutcome` has a `resume` status.
|
|
13
|
+
- Terminal: every choice is printed on its own line with its token; a confirmation that resumes runs the turn again.
|
|
14
|
+
- Google Chat: the confirmation card has one button per choice; a typed token or a clicked choice that resumes runs the turn again, in the session's queue. The channel now needs channel-types 0.51.0 or later.
|
|
15
|
+
- HTTP: the `pending` event carries `choices`; a token sent to `POST /turn` that resumes streams the model's answer on the same request; `POST /confirm` answers `resume: true`, and `POST /turn { resume: true, conversationId }` runs that turn. The channel now needs channel-types 0.51.0 or later.
|
|
16
|
+
|
|
3
17
|
## 0.9.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -45,8 +45,9 @@ Runs one conversational turn; the reply streams back as Server-Sent Events.
|
|
|
45
45
|
|
|
46
46
|
| field | type | required | description |
|
|
47
47
|
|---|---|---|---|
|
|
48
|
-
| `text` | string | yes, unless `opening` | The user message. A bare confirmation token here confirms a staged action (see the `pending` event) without invoking the model. |
|
|
48
|
+
| `text` | string | yes, unless `opening` or `resume` | The user message. A bare confirmation token here confirms a staged action (see the `pending` event) without invoking the model; when the action hands its result back to the model, the model's answer streams on this same request. |
|
|
49
49
|
| `opening` | `true` | no | The assistant speaks first, before the person writes anything: what a UI asks right after someone signs in, when `GET /me` says the assistant doesn't know their name. It introduces itself and asks how to call them, and saves the answer once they give it. For someone it already knows, the stream is a single `final` with an empty text. Needs `conversationId`; `text` is ignored. |
|
|
50
|
+
| `resume` | `true` | no | The turn a `POST /confirm` answered `resume: true` for: the model answers on what the confirmed action handed back. Needs `conversationId`; `text` is ignored. With nothing to resume, the stream is a single `final` with an empty text. |
|
|
50
51
|
| `conversationId` | string | no | Opaque, client-owned id that continues one of your conversations: letters, digits, `-` and `_`, up to 128 characters (a UUID fits) once surrounding whitespace is trimmed, `400` otherwise. Sent again, however long after, it continues that conversation, rebuilt from the archive if Mercury had freed it (see `HTTP_SURFACE_RESUME_MAX_CHARS`). Omitted ⇒ a fresh one-off session that can't be continued. |
|
|
51
52
|
|
|
52
53
|
**Responses**: `200 text/event-stream` (the events below); `400` if `text` is missing, an opening has no `conversationId`, the body isn't JSON, or `conversationId` has other characters than the ones above.
|
|
@@ -62,7 +63,7 @@ Reasoning and answer text arrive as **incremental deltas** — never one finishe
|
|
|
62
63
|
| `tool` | `{ label, detail, toolCallId, name }` | a tool call starts; `name` is the tool the model called, absent when Mercury is saving something to memory |
|
|
63
64
|
| `tool_finish` | `{ toolCallId, outcome }` | a tool call settles (`outcome`: `success` \| `failed` \| `pending`) |
|
|
64
65
|
| `text` | `{ chunk }` | an answer-text delta |
|
|
65
|
-
| `pending` | `{ command, token }` | a confirm-required action was staged; send
|
|
66
|
+
| `pending` | `{ command, token, choices }` | a confirm-required action was staged; send a token back to confirm it — as a later `/turn` `text`, or via `POST /confirm`. `choices` is every way to answer (`{ token, label }`, in order; one, labelled `Conferma`, for a plain staging): the others die with the one sent |
|
|
66
67
|
| `login` | `{ service, url }` | the caller isn't logged in to `service` yet, which a plugin acts on as them: show them `url` to open (the model never gets it) |
|
|
67
68
|
| `final` | `{ text }` | the complete answer (also emitted for a token confirmation, with no model turn) |
|
|
68
69
|
| `error` | `{ message }` | the turn failed mid-stream |
|
|
@@ -86,7 +87,7 @@ A code for linking another of the caller's accounts to them, for the UI to show:
|
|
|
86
87
|
|
|
87
88
|
## `POST /confirm`
|
|
88
89
|
|
|
89
|
-
Explicit alternative to re-sending a token as `/turn` `text`. Body `{ token, conversationId }`; returns `{ ok: true, resolved: true, text }` when the token was a pending confirmation, `{ ok: true, resolved: false }` otherwise. `400` if `token` or `conversationId` is missing, or the id has other characters than `/turn` accepts. Never invokes the model.
|
|
90
|
+
Explicit alternative to re-sending a token as `/turn` `text`. Body `{ token, conversationId }`; returns `{ ok: true, resolved: true, text }` when the token was a pending confirmation, `{ ok: true, resolved: false }` otherwise. When the confirmed action hands its result back to the model, it returns `{ ok: true, resolved: true, resume: true }` instead: open `POST /turn { resume: true, conversationId }` to hear the model's answer. `400` if `token` or `conversationId` is missing, or the id has other characters than `/turn` accepts. Never invokes the model.
|
|
90
91
|
|
|
91
92
|
## Read-only introspection
|
|
92
93
|
|
package/dist/http-server.d.ts
CHANGED
|
@@ -64,7 +64,9 @@ export declare function openApiResponse(corsOrigin?: string): Response;
|
|
|
64
64
|
* `{ opening: true, conversationId }`, with no `text`, asks the assistant to
|
|
65
65
|
* speak first in that conversation (right after someone signs in): the core
|
|
66
66
|
* decides whether it has anything to say, and a `final` with an empty text
|
|
67
|
-
* means it hasn't.
|
|
67
|
+
* means it hasn't. `{ resume: true, conversationId }` runs the turn a
|
|
68
|
+
* `/confirm` answered `resume: true` for; a bare token whose confirmation
|
|
69
|
+
* resumes runs that turn on the same stream.
|
|
68
70
|
*/
|
|
69
71
|
export declare function handleTurnRequest(req: Request, deps: TurnRequestDeps): Promise<Response>;
|
|
70
72
|
export type ConfirmRequestDeps = {
|
|
@@ -83,7 +85,9 @@ export type ConfirmRequestDeps = {
|
|
|
83
85
|
* pending confirmation and its staged action was consumed and run; the `text`
|
|
84
86
|
* then reports whether that execution succeeded. `resolved: false` means the
|
|
85
87
|
* token was not a pending confirmation (unknown, expired, already used, or not
|
|
86
|
-
* even token-shaped).
|
|
88
|
+
* even token-shaped). `resume: true` (with no `text`) means the action handed
|
|
89
|
+
* its result back to the model: the client hears its answer by opening
|
|
90
|
+
* `/turn { resume: true, conversationId }`. Never touches the model.
|
|
87
91
|
*/
|
|
88
92
|
export declare function handleConfirmRequest(req: Request, deps: ConfirmRequestDeps): Promise<Response>;
|
|
89
93
|
export type LinkRequestDeps = {
|
package/http-server.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
detectLoginRequired,
|
|
27
27
|
detectPendingConfirmation,
|
|
28
28
|
PENDING_CONFIRMATION_NOTE,
|
|
29
|
+
RESUME_AFTER_CONFIRM,
|
|
29
30
|
type HandleTurn,
|
|
30
31
|
type TurnSink,
|
|
31
32
|
type Authenticate,
|
|
@@ -151,14 +152,16 @@ export function openApiResponse(corsOrigin = "*"): Response {
|
|
|
151
152
|
* `{ opening: true, conversationId }`, with no `text`, asks the assistant to
|
|
152
153
|
* speak first in that conversation (right after someone signs in): the core
|
|
153
154
|
* decides whether it has anything to say, and a `final` with an empty text
|
|
154
|
-
* means it hasn't.
|
|
155
|
+
* means it hasn't. `{ resume: true, conversationId }` runs the turn a
|
|
156
|
+
* `/confirm` answered `resume: true` for; a bare token whose confirmation
|
|
157
|
+
* resumes runs that turn on the same stream.
|
|
155
158
|
*/
|
|
156
159
|
export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Promise<Response> {
|
|
157
160
|
const origin = deps.corsOrigin ?? "*";
|
|
158
161
|
const cors = corsHeaders(origin);
|
|
159
162
|
const principal = await caller(req, deps, origin);
|
|
160
163
|
if (principal instanceof Response) return principal;
|
|
161
|
-
let body: { text?: unknown; conversationId?: unknown; opening?: unknown };
|
|
164
|
+
let body: { text?: unknown; conversationId?: unknown; opening?: unknown; resume?: unknown };
|
|
162
165
|
try {
|
|
163
166
|
body = (await req.json()) as typeof body;
|
|
164
167
|
} catch {
|
|
@@ -167,13 +170,18 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
167
170
|
const opening = body.opening === true;
|
|
168
171
|
// A conversation the client names is one it reopens; an ephemeral one never comes back.
|
|
169
172
|
const named = typeof body.conversationId === "string" && body.conversationId.trim().length > 0;
|
|
173
|
+
// The turn a `/confirm` answered `resume: true` for.
|
|
174
|
+
const resume = body.resume === true;
|
|
170
175
|
if (opening && !named) {
|
|
171
176
|
return Response.json({ ok: false, error: "an opening needs a conversationId" }, { status: 400, headers: cors });
|
|
172
177
|
}
|
|
173
|
-
if (
|
|
178
|
+
if (resume && !named) {
|
|
179
|
+
return Response.json({ ok: false, error: "a resumed turn needs a conversationId" }, { status: 400, headers: cors });
|
|
180
|
+
}
|
|
181
|
+
if (!opening && !resume && (typeof body.text !== "string" || body.text.trim().length === 0)) {
|
|
174
182
|
return Response.json({ ok: false, error: "missing text" }, { status: 400, headers: cors });
|
|
175
183
|
}
|
|
176
|
-
const text = opening ? "" : (body.text as string);
|
|
184
|
+
const text = opening || resume ? "" : (body.text as string);
|
|
177
185
|
const conversationId = named ? (body.conversationId as string).trim() : (deps.newSessionKey ?? (() => crypto.randomUUID()))();
|
|
178
186
|
if (!CONVERSATION_ID.test(conversationId)) {
|
|
179
187
|
return Response.json({ ok: false, error: "invalid conversationId" }, { status: 400, headers: cors });
|
|
@@ -207,12 +215,14 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
207
215
|
|
|
208
216
|
// Same deterministic interception as every other channel — a
|
|
209
217
|
// previously-approved mutation must never depend on the model.
|
|
210
|
-
const confirmReply = opening ? null : await deps.confirm(text, sessionKey, principal);
|
|
211
|
-
if (confirmReply !== null) {
|
|
218
|
+
const confirmReply = opening || resume ? null : await deps.confirm(text, sessionKey, principal);
|
|
219
|
+
if (confirmReply !== null && confirmReply !== RESUME_AFTER_CONFIRM) {
|
|
212
220
|
send("final", { text: confirmReply });
|
|
213
221
|
close();
|
|
214
222
|
return;
|
|
215
223
|
}
|
|
224
|
+
// The confirmed action's result goes back to the model: the turn runs again, on this stream.
|
|
225
|
+
const resuming = resume || confirmReply === RESUME_AFTER_CONFIRM;
|
|
216
226
|
|
|
217
227
|
const sink: TurnSink = {
|
|
218
228
|
onToolStart: (label, detail, toolCallId, toolName) => send("tool", { label, detail, toolCallId, name: toolName }),
|
|
@@ -224,7 +234,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
224
234
|
onReasoningEnd: (id, failed) => send("reasoning_end", { id, failed }),
|
|
225
235
|
onStep: (step) => {
|
|
226
236
|
const pending = detectPendingConfirmation(step);
|
|
227
|
-
if (pending) send("pending", { command: pending.summary, token: pending.token });
|
|
237
|
+
if (pending) send("pending", { command: pending.summary, token: pending.token, choices: pending.choices });
|
|
228
238
|
const login = detectLoginRequired(step);
|
|
229
239
|
if (login) send("login", { service: login.service, url: login.url });
|
|
230
240
|
},
|
|
@@ -238,7 +248,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
238
248
|
{
|
|
239
249
|
channel: "http",
|
|
240
250
|
multiUser: false,
|
|
241
|
-
text,
|
|
251
|
+
text: resuming ? "" : text,
|
|
242
252
|
sessionKey,
|
|
243
253
|
principal,
|
|
244
254
|
// The caller's id comes from the auth provider, unchecked here.
|
|
@@ -246,6 +256,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
246
256
|
abortSignal: abort.signal,
|
|
247
257
|
...(named ? { resume: deps.resumeMaxChars === undefined ? {} : { maxChars: deps.resumeMaxChars } } : {}),
|
|
248
258
|
...(opening ? { opening: true as const } : {}),
|
|
259
|
+
...(resuming ? { resumeAfterConfirm: true as const } : {}),
|
|
249
260
|
},
|
|
250
261
|
sink,
|
|
251
262
|
);
|
|
@@ -285,7 +296,9 @@ export type ConfirmRequestDeps = {
|
|
|
285
296
|
* pending confirmation and its staged action was consumed and run; the `text`
|
|
286
297
|
* then reports whether that execution succeeded. `resolved: false` means the
|
|
287
298
|
* token was not a pending confirmation (unknown, expired, already used, or not
|
|
288
|
-
* even token-shaped).
|
|
299
|
+
* even token-shaped). `resume: true` (with no `text`) means the action handed
|
|
300
|
+
* its result back to the model: the client hears its answer by opening
|
|
301
|
+
* `/turn { resume: true, conversationId }`. Never touches the model.
|
|
289
302
|
*/
|
|
290
303
|
export async function handleConfirmRequest(req: Request, deps: ConfirmRequestDeps): Promise<Response> {
|
|
291
304
|
const origin = deps.corsOrigin ?? "*";
|
|
@@ -315,6 +328,9 @@ export async function handleConfirmRequest(req: Request, deps: ConfirmRequestDep
|
|
|
315
328
|
return Response.json({ ok: true, resolved: true, text: `Confermato ed eseguito: ${JSON.stringify(outcome.data)}` }, { headers: cors });
|
|
316
329
|
case "failed":
|
|
317
330
|
return Response.json({ ok: true, resolved: true, text: `Confermato, ma l'esecuzione è fallita: ${outcome.error}` }, { headers: cors });
|
|
331
|
+
case "resume":
|
|
332
|
+
// The result went back to the model: the client opens `/turn { resume: true }` to hear its answer.
|
|
333
|
+
return Response.json({ ok: true, resolved: true, resume: true }, { headers: cors });
|
|
318
334
|
}
|
|
319
335
|
}
|
|
320
336
|
|
package/openapi.yaml
CHANGED
|
@@ -71,7 +71,16 @@ info:
|
|
|
71
71
|
An irreversible action is never executed on the model's say-so: the turn
|
|
72
72
|
emits a `pending` event carrying a one-time `token`. The client confirms by
|
|
73
73
|
sending that token back — either as the `text` of a follow-up `POST /turn`,
|
|
74
|
-
or via `POST /confirm`.
|
|
74
|
+
or via `POST /confirm`. An action can offer several `choices` (e.g. "only
|
|
75
|
+
this time" / "always"), each with its own token: the caller sends back one,
|
|
76
|
+
and the others die with it.
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
Some actions hand their result back to the model instead of to the caller
|
|
80
|
+
(a page read after the caller allowed it, for example). Confirming one by
|
|
81
|
+
`POST /turn` streams the model's answer on that same stream; `POST /confirm`
|
|
82
|
+
answers `resume: true`, and the client then opens
|
|
83
|
+
`POST /turn { resume: true, conversationId }` to hear it.
|
|
75
84
|
security:
|
|
76
85
|
- bearerAuth: []
|
|
77
86
|
servers:
|
|
@@ -85,7 +94,10 @@ paths:
|
|
|
85
94
|
Runs one model turn and streams the result as `text/event-stream`. If
|
|
86
95
|
`text` is a pending confirmation token it is resolved deterministically
|
|
87
96
|
without ever consulting the model. Close the connection to cancel an
|
|
88
|
-
in-flight turn.
|
|
97
|
+
in-flight turn. When the confirmation hands its result back to the
|
|
98
|
+
model, the model's answer to it streams instead. With `resume: true`
|
|
99
|
+
and no `text`, the turn is the one a `/confirm` answered `resume: true`
|
|
100
|
+
for. With `opening: true` and no `text`, the assistant
|
|
89
101
|
speaks first in the conversation named (right after someone signs
|
|
90
102
|
in): it does only for someone it doesn't know the name of, asking how
|
|
91
103
|
to call them, and otherwise the stream carries a single `final` with
|
|
@@ -108,7 +120,8 @@ paths:
|
|
|
108
120
|
An SSE stream. Each event is one of the types in `TurnEvent`:
|
|
109
121
|
`reasoning`/`reasoning_end`, `text` (incremental deltas), `tool`/
|
|
110
122
|
`tool_finish` (status of a tool call), `pending` (a staged
|
|
111
|
-
confirm-required action, with its command
|
|
123
|
+
confirm-required action, with its command, its token and the
|
|
124
|
+
choices it offers), `login` (a
|
|
112
125
|
service the caller has to log in to first, with the link to open),
|
|
113
126
|
`final` (the
|
|
114
127
|
authoritative complete answer), and `error`.
|
|
@@ -117,7 +130,7 @@ paths:
|
|
|
117
130
|
schema:
|
|
118
131
|
$ref: "#/components/schemas/TurnEvent"
|
|
119
132
|
"400":
|
|
120
|
-
description: Missing/invalid body, an opening without a conversationId, or a conversationId with characters outside the allowed ones.
|
|
133
|
+
description: Missing/invalid body, an opening or a resumed turn without a conversationId, or a conversationId with characters outside the allowed ones.
|
|
121
134
|
content:
|
|
122
135
|
application/json:
|
|
123
136
|
schema:
|
|
@@ -146,7 +159,9 @@ paths:
|
|
|
146
159
|
description: >
|
|
147
160
|
`resolved: true` (with the reply `text`) when the token was a pending
|
|
148
161
|
confirmation, `resolved: false` when it was not (unknown, expired, or
|
|
149
|
-
already used).
|
|
162
|
+
already used). `resolved: true` with `resume: true` and no `text`
|
|
163
|
+
when the action handed its result back to the model: the answer
|
|
164
|
+
comes from `POST /turn { resume: true, conversationId }`.
|
|
150
165
|
content:
|
|
151
166
|
application/json:
|
|
152
167
|
schema:
|
|
@@ -552,11 +567,15 @@ components:
|
|
|
552
567
|
properties:
|
|
553
568
|
text:
|
|
554
569
|
type: string
|
|
555
|
-
description: The user's message, or a bare confirmation token. Required, unless `opening` is true.
|
|
570
|
+
description: The user's message, or a bare confirmation token. Required, unless `opening` or `resume` is true.
|
|
556
571
|
opening:
|
|
557
572
|
type: boolean
|
|
558
573
|
enum: [true]
|
|
559
574
|
description: The assistant speaks first in `conversationId`, which is then required; `text` is ignored.
|
|
575
|
+
resume:
|
|
576
|
+
type: boolean
|
|
577
|
+
enum: [true]
|
|
578
|
+
description: The turn a `/confirm` answered `resume: true` for, in `conversationId`, which is then required; `text` is ignored.
|
|
560
579
|
conversationId:
|
|
561
580
|
type: string
|
|
562
581
|
pattern: "^[A-Za-z0-9_-]{1,128}$"
|
|
@@ -594,7 +613,18 @@ components:
|
|
|
594
613
|
description: On `pending` — the staged action's summary.
|
|
595
614
|
token:
|
|
596
615
|
type: string
|
|
597
|
-
description: On `pending` — the one-time confirmation token.
|
|
616
|
+
description: On `pending` — the one-time confirmation token (the first choice's).
|
|
617
|
+
choices:
|
|
618
|
+
type: array
|
|
619
|
+
description: On `pending` — every way to answer, in order; a plain staging has one, labelled `Conferma`.
|
|
620
|
+
items:
|
|
621
|
+
type: object
|
|
622
|
+
required: [token, label]
|
|
623
|
+
properties:
|
|
624
|
+
token:
|
|
625
|
+
type: string
|
|
626
|
+
label:
|
|
627
|
+
type: string
|
|
598
628
|
service:
|
|
599
629
|
type: string
|
|
600
630
|
description: On `login` — the service the caller has to log in to.
|
|
@@ -626,7 +656,11 @@ components:
|
|
|
626
656
|
type: boolean
|
|
627
657
|
text:
|
|
628
658
|
type: string
|
|
629
|
-
description: The confirmation reply, present when `resolved` is true.
|
|
659
|
+
description: The confirmation reply, present when `resolved` is true and `resume` isn't.
|
|
660
|
+
resume:
|
|
661
|
+
type: boolean
|
|
662
|
+
enum: [true]
|
|
663
|
+
description: The action handed its result back to the model; its answer comes from `POST /turn { resume: true, conversationId }`.
|
|
630
664
|
Message:
|
|
631
665
|
type: object
|
|
632
666
|
required: [role, content, timestamp]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/channel-http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@mercury-fw/channel-types": ">=0.
|
|
21
|
+
"@mercury-fw/channel-types": ">=0.51.0 <1.0.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@mercury-fw/channel-types": "0.
|
|
25
|
-
"@mercury-fw/plugin-types": "0.
|
|
24
|
+
"@mercury-fw/channel-types": "0.51.0",
|
|
25
|
+
"@mercury-fw/plugin-types": "0.51.0",
|
|
26
26
|
"@mercury-fw/typescript-config": "*",
|
|
27
27
|
"@types/bun": "^1.4.2",
|
|
28
28
|
"typescript": "^6.0.3"
|