@guuey/agent-client 0.2.2 → 0.3.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.
@@ -1,164 +0,0 @@
1
- import { isJsonObject } from "./block-ui";
2
- /** The `_meta` key the ggui render bootstrap rides on (MCP-Apps slice convention). */
3
- export const GGUI_RENDER_META_KEY = "ai.ggui/render";
4
- /** The `ui://` scheme prefix every ggui render resource uri carries. */
5
- const UI_SCHEME = "ui://";
6
- /** A non-empty JSON string field. */
7
- function isNonEmptyString(value) {
8
- return typeof value === "string" && value.length > 0;
9
- }
10
- /**
11
- * Does this slice carry at least one MOUNT MODE discriminator?
12
- *
13
- * Mirrors `@ggui-ai/iframe-runtime`'s `validateMeta` (see
14
- * `node_modules/@ggui-ai/iframe-runtime/dist/meta-parse.d.ts`) and the
15
- * `McpAppAiGguiRenderMeta` doc comment it implements
16
- * (`@ggui-ai/protocol/integrations/mcp-apps`): the runtime needs `runtimeUrl`
17
- * PLUS one of live mode (`wsUrl` + `wsToken` together), `codeUrl`, or `kind` —
18
- * without one of those three the iframe has nothing to mount.
19
- */
20
- function hasModeDiscriminator(slice) {
21
- if (isNonEmptyString(slice.wsUrl) && isNonEmptyString(slice.wsToken))
22
- return true;
23
- if (isNonEmptyString(slice.codeUrl))
24
- return true;
25
- if (isNonEmptyString(slice.kind))
26
- return true;
27
- return false;
28
- }
29
- /**
30
- * A `_meta` container → the ggui render bootstrap, or `undefined`.
31
- *
32
- * Two hard requirements, both the runtime's own `validateMeta` enforces
33
- * (`MALFORMED_BOOTSTRAP`): a non-empty `runtimeUrl`, AND at least one mode
34
- * discriminator (see {@link hasModeDiscriminator}). A slice with `runtimeUrl`
35
- * alone has a bundle to load but nothing for it to mount — the runtime would
36
- * boot into a blank shell rather than a card, so this guard treats that shape
37
- * as unmountable too and returns `undefined`.
38
- */
39
- export function asGguiRenderBootstrap(meta) {
40
- if (!isJsonObject(meta))
41
- return undefined;
42
- const slice = meta[GGUI_RENDER_META_KEY];
43
- if (!isJsonObject(slice))
44
- return undefined;
45
- const runtimeUrl = slice.runtimeUrl;
46
- if (typeof runtimeUrl !== "string" || runtimeUrl.length === 0)
47
- return undefined;
48
- if (!hasModeDiscriminator(slice))
49
- return undefined;
50
- return { runtimeUrl, slice };
51
- }
52
- /**
53
- * A tool result's `uiData` (+ its `_meta`, when carried) → a ggui render
54
- * descriptor, or `undefined` for anything that is not one.
55
- *
56
- * The `ui://` scheme gate is deliberate: `uiData` is a general-purpose channel
57
- * (every `structuredContent` of a `_meta.ui`-stamped tool lands there), so a
58
- * bare `resourceUri` string is not on its own a claim of generative UI.
59
- */
60
- export function asGguiRender(uiData, meta) {
61
- if (!isJsonObject(uiData))
62
- return undefined;
63
- const resourceUri = uiData.resourceUri;
64
- if (typeof resourceUri !== "string" || !resourceUri.startsWith(UI_SCHEME))
65
- return undefined;
66
- const bootstrap = asGguiRenderBootstrap(meta);
67
- return {
68
- resourceUri,
69
- ...(typeof uiData.sessionId === "string" ? { sessionId: uiData.sessionId } : {}),
70
- ...(bootstrap ? { bootstrap } : {}),
71
- };
72
- }
73
- /** A live `tool-result` AgBlock → its ggui render descriptor, if it is one. */
74
- export function toolResultGguiRender(block) {
75
- return asGguiRender(block.uiData, block._meta);
76
- }
77
- /** An untyped (persisted-snapshot) block → its ggui render descriptor, if it is one. */
78
- export function blockGguiRender(block) {
79
- if (!isJsonObject(block))
80
- return undefined;
81
- if (block.type !== "tool-result")
82
- return undefined;
83
- return asGguiRender(block.uiData, block._meta);
84
- }
85
- /**
86
- * Embed a JSON value inside an inline `<script>` safely.
87
- *
88
- * `</script` inside a string literal terminates the element in the HTML
89
- * parser regardless of JS quoting, and U+2028/U+2029 are line terminators in
90
- * JS source but not in JSON — both are escaped at the `<`/codepoint level so
91
- * the emitted text is still exactly the same JSON value.
92
- */
93
- function inlineJson(value) {
94
- return JSON.stringify(value)
95
- .replace(/</g, "\\u003c")
96
- .replace(/\u2028/g, "\\u2028")
97
- .replace(/\u2029/g, "\\u2029");
98
- }
99
- /** Escape a string for use inside a double-quoted HTML attribute. */
100
- function attr(value) {
101
- return value
102
- .replace(/&/g, "&amp;")
103
- .replace(/"/g, "&quot;")
104
- .replace(/</g, "&lt;")
105
- .replace(/>/g, "&gt;");
106
- }
107
- /**
108
- * The ggui **self-contained shell** for a render bootstrap — see this module's
109
- * header for the contract it implements.
110
- *
111
- * Ordering is guaranteed twice over: the classic `<script>` runs during parse,
112
- * and the runtime's `<script type="module">` is deferred by definition, so the
113
- * global is always populated before the bundle evaluates.
114
- */
115
- export function gguiShellHtml(bootstrap) {
116
- const envelope = inlineJson({ [GGUI_RENDER_META_KEY]: bootstrap.slice });
117
- return [
118
- "<!doctype html>",
119
- '<html lang="en">',
120
- "<head>",
121
- '<meta charset="utf-8">',
122
- '<meta name="viewport" content="width=device-width, initial-scale=1">',
123
- '<meta name="color-scheme" content="light dark">',
124
- "<title>ggui card</title>",
125
- "<style>html,body{margin:0;height:100%;background:transparent}</style>",
126
- `<script>globalThis.__GGUI_META__=${envelope};</script>`,
127
- `<script type="module" src="${attr(bootstrap.runtimeUrl)}"></script>`,
128
- "</head>",
129
- "<body></body>",
130
- "</html>",
131
- "",
132
- ].join("\n");
133
- }
134
- /**
135
- * A ggui render descriptor → the mountable resource the host's existing
136
- * mcp-ui path already knows how to mount, or `undefined` when the descriptor
137
- * carries no bootstrap (history cards, and any fold that dropped `_meta`).
138
- *
139
- * The `uri` is the render's REAL `resourceUri` — the shell is the payload, not
140
- * a renaming of the resource.
141
- *
142
- * **On `_meta` being required to MOUNT (but never to RECOGNISE).** Recognition
143
- * — "this tool result is a ggui card" — is keyed on `uiData.resourceUri` alone
144
- * and never waits for anything (see {@link asGguiRender}); nothing in this
145
- * package is blocked on an upstream change. Mounting is different, and the
146
- * requirement is ggui's, not ours: its runtime rejects a slice without
147
- * `runtimeUrl` AND without at least one mode discriminator (`wsUrl`+`wsToken`,
148
- * `codeUrl`, or `kind`) as `MALFORMED_BOOTSTRAP` and renders nothing. `uiData`
149
- * carries none of those fields — it has `sessionId`, `resourceUri`, `action`,
150
- * `contractHash`, `blueprintId`, `variantKey`, `cache`, `nextStep`, and that is
151
- * all. So a bootstrap-less descriptor could only ever produce a blank frame;
152
- * returning `undefined` and letting the host show its own placeholder is the
153
- * honest answer, not a deferral. `@silverprotocol/core`'s `Reducer` is what
154
- * puts `_meta` on the block for a live turn, in-repo, today.
155
- */
156
- export function gguiRenderResource(render) {
157
- if (!render.bootstrap)
158
- return undefined;
159
- return {
160
- uri: render.resourceUri,
161
- mimeType: "text/html",
162
- text: gguiShellHtml(render.bootstrap),
163
- };
164
- }
package/src/block-ui.ts DELETED
@@ -1,212 +0,0 @@
1
- /**
2
- * Pure block-walk / resource-narrowing helpers for a block-preserving agent
3
- * transcript — no React, no DOM, so the narrowing logic stays unit-testable in
4
- * isolation (this package's vitest runs a `node` environment) and can be shared
5
- * by every host renderer (Studio's `AgentBlocks`, Portal-web's agent chat).
6
- *
7
- * The pod's AgJSON wire carries generative-UI payloads on `tool.done` events,
8
- * which the reducer folds onto `tool-result` blocks. Two channels reach us:
9
- *
10
- * 1. **`uiData`** — the MCP-Apps *surface* channel. The pod's Claude facet
11
- * routes a tool result's `structuredContent` here when the server stamped
12
- * `_meta.ui`. Any resource here is intended as UI.
13
- * 2. **`provider-raw` content blocks** — an MCP embedded `resource` content
14
- * part does NOT survive as a first-class `resource` AgBlock in the Claude
15
- * facet; it degrades to `{ type:'provider-raw', vendor, raw:<part> }`. So a
16
- * `ui://` resource can be hiding inside `provider-raw.raw` and must be
17
- * scanned for defensively.
18
- *
19
- * The resource-narrowing (opaque `JsonValue` → typed payload) mirrors the
20
- * proven `create-agentic-app` web template — structural validation, never a cast.
21
- */
22
- import type { AgBlock, AgMessage, JsonValue } from "@silverprotocol/core";
23
- import type { HistoryCard } from "./types";
24
-
25
- /** A narrowed MCP embedded UI resource (the `_meta.ui.resource` shape). */
26
- export interface McpUiResourcePayload {
27
- uri: string;
28
- mimeType?: string;
29
- text?: string;
30
- blob?: string;
31
- }
32
-
33
- /** Narrow an opaque `JsonValue` to a plain (non-array) JSON object. */
34
- export function isJsonObject(v: JsonValue | undefined): v is { [key: string]: JsonValue } {
35
- return typeof v === "object" && v !== null && !Array.isArray(v);
36
- }
37
-
38
- /**
39
- * A JSON object → an MCP UI resource, if it has a `uri` plus renderable
40
- * payload (`text` or base64 `blob`). Returns `undefined` for anything else.
41
- */
42
- export function asResourcePayload(v: JsonValue | undefined): McpUiResourcePayload | undefined {
43
- if (!isJsonObject(v)) return undefined;
44
- if (typeof v.uri !== "string") return undefined;
45
- if (typeof v.text !== "string" && typeof v.blob !== "string") return undefined;
46
- return {
47
- uri: v.uri,
48
- ...(typeof v.mimeType === "string" ? { mimeType: v.mimeType } : {}),
49
- ...(typeof v.text === "string" ? { text: v.text } : {}),
50
- ...(typeof v.blob === "string" ? { blob: v.blob } : {}),
51
- };
52
- }
53
-
54
- /**
55
- * Does a `tool-result` block's `uiData` carry an MCP embedded UI resource?
56
- * Accepts the resource inlined directly, or wrapped as `{ resource: {...} }`
57
- * (the shape an MCP `resource` content part carries). No `ui://` scheme gate
58
- * here on purpose: `uiData` is the explicit *surface* channel (the server
59
- * stamped `_meta.ui`), so any resource on it is meant to render.
60
- */
61
- export function asUiResource(uiData: JsonValue | undefined): McpUiResourcePayload | undefined {
62
- if (!isJsonObject(uiData)) return undefined;
63
- const direct = asResourcePayload(uiData);
64
- if (direct) return direct;
65
- return asResourcePayload(uiData.resource);
66
- }
67
-
68
- /**
69
- * Scan a `provider-raw` block's `raw` (the vendor tool_result content part)
70
- * for a *generative-UI* resource. Unlike {@link asUiResource}, this path IS
71
- * gated on the `ui://` scheme: `provider-raw` degradation is a lossy catch-all,
72
- * so a plain file/text resource riding it is NOT a UI to mount — only the
73
- * mcp-ui `ui://` convention is.
74
- */
75
- export function scanProviderRawForUiResource(
76
- raw: JsonValue | undefined,
77
- ): McpUiResourcePayload | undefined {
78
- if (!isJsonObject(raw)) return undefined;
79
- const candidate =
80
- raw.resource !== undefined ? asResourcePayload(raw.resource) : asResourcePayload(raw);
81
- if (!candidate) return undefined;
82
- return candidate.uri.startsWith("ui://") ? candidate : undefined;
83
- }
84
-
85
- /**
86
- * Extract a mountable UI resource from an opaque AgBlock-shaped `JsonValue`
87
- * (used for persisted card snapshot parts, which arrive untyped). Dispatches
88
- * by `block.type`:
89
- * - `tool-result` → its `uiData` surface channel, then a `ui://` resource
90
- * degraded into a `provider-raw` content part — the SAME two channels
91
- * the live path ({@link toolResultUiResource}) mounts. The write side
92
- * (`nocode-runtime`'s `uiCardArtifactsFromMessages`, guuey#86) persists
93
- * card rows for both, so the snapshot arm must mount both or
94
- * provider-raw-only cards rehydrate as placeholders.
95
- * - `provider-raw` → a `ui://` resource hiding in `raw`
96
- * - `resource` → a first-class embedded resource (gated on `ui://`)
97
- * Everything else → `undefined`.
98
- */
99
- export function blockUiResource(block: JsonValue): McpUiResourcePayload | undefined {
100
- if (!isJsonObject(block)) return undefined;
101
- switch (block.type) {
102
- case "tool-result": {
103
- const fromUiData = asUiResource(block.uiData);
104
- if (fromUiData) return fromUiData;
105
- if (Array.isArray(block.content)) {
106
- for (const part of block.content) {
107
- if (isJsonObject(part) && part.type === "provider-raw") {
108
- const found = scanProviderRawForUiResource(part.raw);
109
- if (found) return found;
110
- }
111
- }
112
- }
113
- return undefined;
114
- }
115
- case "provider-raw":
116
- return scanProviderRawForUiResource(block.raw);
117
- case "resource": {
118
- const r = asResourcePayload(block.resource);
119
- return r && r.uri.startsWith("ui://") ? r : undefined;
120
- }
121
- default:
122
- return undefined;
123
- }
124
- }
125
-
126
- /**
127
- * A live `tool-result` AgBlock → its mountable UI resource, checking BOTH
128
- * channels the Claude facet uses:
129
- * 1. the `uiData` surface channel (server stamped `_meta.ui`), and
130
- * 2. an embedded `ui://` resource that degraded into a `provider-raw`
131
- * content part inside the tool result (MCP `resource` parts do NOT survive
132
- * as first-class `resource` AgBlocks here).
133
- * First-class `resource` content parts are intentionally not scanned in this
134
- * typed live path (the Claude facet never emits them); the untyped card path
135
- * ({@link blockUiResource}) covers them for other facets' persisted snapshots.
136
- */
137
- export function toolResultUiResource(
138
- block: Extract<AgBlock, { type: "tool-result" }>,
139
- ): McpUiResourcePayload | undefined {
140
- const fromUiData = asUiResource(block.uiData);
141
- if (fromUiData) return fromUiData;
142
- for (const part of block.content) {
143
- if (part.type === "provider-raw") {
144
- const found = scanProviderRawForUiResource(part.raw);
145
- if (found) return found;
146
- }
147
- }
148
- return undefined;
149
- }
150
-
151
- /**
152
- * A persisted `HistoryCard`'s `cardSnapshot` → a mountable UI resource. The
153
- * snapshot is the verbatim `AgArtifact` the pod stored (`{ parts: AgBlock[] }`),
154
- * so walk its `parts` for the first block that yields a resource; fall back to
155
- * treating the snapshot root itself as a block.
156
- *
157
- * NOTE (`no-ggui-tools`): a ggui-rendered card carries NO inline HTML resource —
158
- * its UI rides `_meta.ggui.bootstrap` and mounts via `@ggui-ai/react`'s
159
- * `McpAppIframe`. That branch is OUT OF SCOPE for v1 (deferred-pending-capture).
160
- * So a real ggui card resolves to `undefined` here and renders as the host's
161
- * coherent placeholder, not a broken mount.
162
- */
163
- export function cardUiResource(cardSnapshot: JsonValue): McpUiResourcePayload | undefined {
164
- if (!isJsonObject(cardSnapshot)) return undefined;
165
- const parts = cardSnapshot.parts;
166
- if (Array.isArray(parts)) {
167
- for (const part of parts) {
168
- const found = blockUiResource(part);
169
- if (found) return found;
170
- }
171
- }
172
- return blockUiResource(cardSnapshot);
173
- }
174
-
175
- /**
176
- * The resource's HTML: inline `text` wins; else base64-decode `blob`. `atob`
177
- * alone yields a Latin-1 string (mojibake on multibyte UTF-8), so decode via
178
- * bytes + `TextDecoder`. Invalid base64 → `undefined` (no renderable payload).
179
- */
180
- export function resourceHtml(resource: McpUiResourcePayload): string | undefined {
181
- if (resource.text !== undefined) return resource.text;
182
- if (resource.blob !== undefined) {
183
- try {
184
- return new TextDecoder().decode(Uint8Array.from(atob(resource.blob), (c) => c.charCodeAt(0)));
185
- } catch {
186
- return undefined;
187
- }
188
- }
189
- return undefined;
190
- }
191
-
192
- /**
193
- * The tool name for a `tool-result` block, read off its paired `tool-call`
194
- * block in the same message (the reducer keeps both in one message's content).
195
- * Falls back to `"tool"` when the pair is missing.
196
- */
197
- export function toolNameFor(message: AgMessage, toolCallId: string): string {
198
- for (const b of message.content) {
199
- if (b.type === "tool-call" && b.toolCallId === toolCallId) return b.name;
200
- }
201
- return "tool";
202
- }
203
-
204
- /**
205
- * Persisted cards, ascending by transcript `seq` (stable; input untouched).
206
- * These are PRIOR-turn cards — they always precede the live fold, so a
207
- * block-preserving renderer surfaces them first (e.g. under an "Earlier in
208
- * this conversation" divider).
209
- */
210
- export function sortHistoryCards(cards: readonly HistoryCard[]): HistoryCard[] {
211
- return [...cards].sort((a, b) => a.seq - b.seq);
212
- }
package/src/card-mount.ts DELETED
@@ -1,101 +0,0 @@
1
- /**
2
- * The card-mount dispatcher: ONE narrowing that answers "what, if anything,
3
- * does this block mount?" across BOTH generative-UI channels a guuey pod
4
- * emits.
5
- *
6
- * 1. **inline mcp-ui resource** — `{uri, text|blob}` on `uiData`, or a
7
- * `ui://` resource degraded into a `provider-raw` content part. Handled
8
- * verbatim by `block-ui.ts`; this module does not touch that path, it
9
- * only tries it FIRST.
10
- * 2. **ggui render** — `uiData.resourceUri` + the `_meta["ai.ggui/render"]`
11
- * bootstrap, mounted through ggui's self-contained shell. See
12
- * `ggui-render.ts`.
13
- *
14
- * Both channels land on the SAME `McpUiResourcePayload`, which is the whole
15
- * point: a host that already mounts inline resources through
16
- * `@mcp-ui/client`'s `AppRenderer` in a second-origin sandbox gains ggui cards
17
- * without a second mount mechanism, a second iframe contract, or a second
18
- * security posture to review.
19
- *
20
- * Precedence is inline-first and deliberate: an inline resource is the
21
- * server's explicit, self-sufficient HTML. A ggui render only ever wins when
22
- * there is no inline resource to prefer, so this dispatcher can never change
23
- * what an existing inline card renders.
24
- *
25
- * ## Why the CHANNEL is returned alongside the resource
26
- *
27
- * The payload alone cannot say where it came from — a ggui shell is a string
28
- * of HTML like any other. But a host has one decision that genuinely depends
29
- * on the origin of that HTML: WHICH sandbox host page to mount it in. A ggui
30
- * shell must load ggui's runtime bundle and open its WSS, so it needs a page
31
- * whose CSP names the ggui origins; an inline card is arbitrary tenant HTML
32
- * and must keep the self-only page it has always had. Handing back the channel
33
- * keeps that one narrowing in one place — the alternative was for every host
34
- * to re-run `toolResultGguiRender` beside this call and ask again.
35
- */
36
- import { cardUiResource, toolResultUiResource, type McpUiResourcePayload } from "./block-ui";
37
- import { blockGguiRender, gguiRenderResource, toolResultGguiRender } from "./ggui-render";
38
- import type { AgBlock, JsonValue } from "@silverprotocol/core";
39
-
40
- /** Which generative-UI channel produced a mount. See this module's header. */
41
- export type CardMountChannel = "inline" | "ggui";
42
-
43
- /** A mountable card: the resource to hand the host, and where it came from. */
44
- export interface CardMount {
45
- /** The payload an mcp-ui host mounts, identical in shape for both channels. */
46
- resource: McpUiResourcePayload;
47
- /**
48
- * `"inline"` — the server's own HTML, untrusted tenant content.
49
- * `"ggui"` — a shell that boots the ggui runtime from a platform-pinned
50
- * origin, and therefore needs a host page whose CSP allows that origin.
51
- */
52
- channel: CardMountChannel;
53
- }
54
-
55
- /**
56
- * A live `tool-result` block → the card to mount, across both channels.
57
- * `undefined` when the block carries no generative UI at all (or carries a
58
- * ggui render whose bootstrap did not reach us — see `ggui-render.ts`).
59
- */
60
- export function toolResultCardMount(
61
- block: Extract<AgBlock, { type: "tool-result" }>,
62
- ): CardMount | undefined {
63
- const inline = toolResultUiResource(block);
64
- if (inline) return { resource: inline, channel: "inline" };
65
- const ggui = toolResultGguiRender(block);
66
- const resource = ggui ? gguiRenderResource(ggui) : undefined;
67
- return resource ? { resource, channel: "ggui" } : undefined;
68
- }
69
-
70
- /**
71
- * A persisted `HistoryCard`'s `cardSnapshot` → the card to mount, across both
72
- * channels.
73
- *
74
- * The ggui arm is reached only for a snapshot that stored the render's
75
- * `_meta`; a bootstrap old enough to have been persisted has an expired
76
- * `wsToken` anyway, so in practice a ggui history card resolves to `undefined`
77
- * and the host renders its placeholder — honest, and NOT a broken mount.
78
- */
79
- export function cardCardMount(cardSnapshot: JsonValue): CardMount | undefined {
80
- const inline = cardUiResource(cardSnapshot);
81
- if (inline) return { resource: inline, channel: "inline" };
82
- for (const block of snapshotBlocks(cardSnapshot)) {
83
- const ggui = blockGguiRender(block);
84
- const resource = ggui ? gguiRenderResource(ggui) : undefined;
85
- if (resource) return { resource, channel: "ggui" };
86
- }
87
- return undefined;
88
- }
89
-
90
- /**
91
- * The blocks to scan inside a card snapshot: the stored `AgArtifact`'s `parts`
92
- * when present, then the snapshot root itself — exactly `cardUiResource`'s own
93
- * walk order, so both channels see the same candidates in the same order.
94
- */
95
- function snapshotBlocks(cardSnapshot: JsonValue): JsonValue[] {
96
- if (typeof cardSnapshot !== "object" || cardSnapshot === null || Array.isArray(cardSnapshot)) {
97
- return [];
98
- }
99
- const parts = cardSnapshot.parts;
100
- return Array.isArray(parts) ? [...parts, cardSnapshot] : [cardSnapshot];
101
- }