@guuey/create-agentic-app 0.1.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/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/chunk-JD4RAKEG.js +203 -0
- package/dist/chunk-U7JDNY6B.js +149 -0
- package/dist/cli.cjs +293 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/index.cjs +233 -0
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +10 -0
- package/dist/templates/claude-agent-sdk/.env.example +4 -0
- package/dist/templates/claude-agent-sdk/.mcp.json +1 -0
- package/dist/templates/claude-agent-sdk/README.md +107 -0
- package/dist/templates/claude-agent-sdk/ggui/blueprints/.gitkeep +0 -0
- package/dist/templates/claude-agent-sdk/ggui/ggui.json +15 -0
- package/dist/templates/claude-agent-sdk/ggui/themes/.gitkeep +0 -0
- package/dist/templates/claude-agent-sdk/guuey.json +13 -0
- package/dist/templates/claude-agent-sdk/mcps/todo/Dockerfile +46 -0
- package/dist/templates/claude-agent-sdk/mcps/todo/package.json +22 -0
- package/dist/templates/claude-agent-sdk/mcps/todo/src/server.ts +173 -0
- package/dist/templates/claude-agent-sdk/mcps/todo/tsconfig.json +15 -0
- package/dist/templates/claude-agent-sdk/package.json +22 -0
- package/dist/templates/claude-agent-sdk/pnpm-workspace.yaml +24 -0
- package/dist/templates/claude-agent-sdk/prompts/system.md +4 -0
- package/dist/templates/claude-agent-sdk/scripts/dev.mjs +44 -0
- package/dist/templates/claude-agent-sdk/src/agent-config.ts +70 -0
- package/dist/templates/claude-agent-sdk/src/worker.ts +89 -0
- package/dist/templates/claude-agent-sdk/tsconfig.json +15 -0
- package/dist/templates/claude-agent-sdk/tsup.config.ts +9 -0
- package/dist/templates/claude-agent-sdk/web/.env.example +17 -0
- package/dist/templates/claude-agent-sdk/web/index.html +13 -0
- package/dist/templates/claude-agent-sdk/web/package.json +25 -0
- package/dist/templates/claude-agent-sdk/web/sandbox-proxy.ts +239 -0
- package/dist/templates/claude-agent-sdk/web/src/App.tsx +272 -0
- package/dist/templates/claude-agent-sdk/web/src/main.tsx +12 -0
- package/dist/templates/claude-agent-sdk/web/src/useAgentChat.ts +122 -0
- package/dist/templates/claude-agent-sdk/web/src/vite-env.d.ts +27 -0
- package/dist/templates/claude-agent-sdk/web/tsconfig.json +20 -0
- package/dist/templates/claude-agent-sdk/web/vite.config.ts +12 -0
- package/dist/templates/mcp-base/Dockerfile +46 -0
- package/dist/templates/mcp-base/package.json +22 -0
- package/dist/templates/mcp-base/src/server.ts +113 -0
- package/dist/templates/mcp-base/tsconfig.json +15 -0
- package/dist/templates/openai-agents-sdk/.env.example +4 -0
- package/dist/templates/openai-agents-sdk/.mcp.json +1 -0
- package/dist/templates/openai-agents-sdk/README.md +107 -0
- package/dist/templates/openai-agents-sdk/ggui/blueprints/.gitkeep +0 -0
- package/dist/templates/openai-agents-sdk/ggui/ggui.json +15 -0
- package/dist/templates/openai-agents-sdk/ggui/themes/.gitkeep +0 -0
- package/dist/templates/openai-agents-sdk/guuey.json +13 -0
- package/dist/templates/openai-agents-sdk/mcps/todo/Dockerfile +46 -0
- package/dist/templates/openai-agents-sdk/mcps/todo/package.json +22 -0
- package/dist/templates/openai-agents-sdk/mcps/todo/src/server.ts +173 -0
- package/dist/templates/openai-agents-sdk/mcps/todo/tsconfig.json +15 -0
- package/dist/templates/openai-agents-sdk/package.json +22 -0
- package/dist/templates/openai-agents-sdk/pnpm-workspace.yaml +24 -0
- package/dist/templates/openai-agents-sdk/prompts/system.md +4 -0
- package/dist/templates/openai-agents-sdk/scripts/dev.mjs +44 -0
- package/dist/templates/openai-agents-sdk/src/agent-config.ts +70 -0
- package/dist/templates/openai-agents-sdk/src/worker.ts +75 -0
- package/dist/templates/openai-agents-sdk/tsconfig.json +15 -0
- package/dist/templates/openai-agents-sdk/tsup.config.ts +9 -0
- package/dist/templates/openai-agents-sdk/web/.env.example +17 -0
- package/dist/templates/openai-agents-sdk/web/index.html +13 -0
- package/dist/templates/openai-agents-sdk/web/package.json +25 -0
- package/dist/templates/openai-agents-sdk/web/sandbox-proxy.ts +239 -0
- package/dist/templates/openai-agents-sdk/web/src/App.tsx +272 -0
- package/dist/templates/openai-agents-sdk/web/src/main.tsx +12 -0
- package/dist/templates/openai-agents-sdk/web/src/useAgentChat.ts +122 -0
- package/dist/templates/openai-agents-sdk/web/src/vite-env.d.ts +27 -0
- package/dist/templates/openai-agents-sdk/web/tsconfig.json +20 -0
- package/dist/templates/openai-agents-sdk/web/vite.config.ts +12 -0
- package/package.json +45 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal-but-real local dev chat client: message list folded from the
|
|
3
|
+
* reducer's `AgReduceResult.messages` (via `useAgentChat`) + an input box.
|
|
4
|
+
* Tool-result blocks that carry an inlined MCP-Apps UI resource (the
|
|
5
|
+
* `_meta.ui.resource` convention landing on `AgBlock.tool-result.uiData` —
|
|
6
|
+
* opaque JSON on the wire, so it's structurally validated below rather than
|
|
7
|
+
* cast) render through `@mcp-ui/client`'s `AppRenderer`, the spec-canonical
|
|
8
|
+
* MCP Apps host.
|
|
9
|
+
*
|
|
10
|
+
* `onCallTool`/`onReadResource` are typed via `ComponentProps<typeof
|
|
11
|
+
* AppRenderer>` instead of importing `@modelcontextprotocol/sdk`'s request
|
|
12
|
+
* types directly — that keeps this file honest against whatever the
|
|
13
|
+
* installed `@mcp-ui/client` version actually expects, with zero guessing.
|
|
14
|
+
*
|
|
15
|
+
* Sandbox origin (MCP Apps spec, double-iframe architecture): the untrusted
|
|
16
|
+
* app HTML must be mounted from a DIFFERENT origin than this page. In dev,
|
|
17
|
+
* `vite.config.ts`'s `sandboxProxyPlugin` serves the spec-canonical
|
|
18
|
+
* `sandbox.html` on :6891 (a different localhost port = a different origin);
|
|
19
|
+
* deployed builds must set `VITE_SANDBOX_URL` to a hosted copy (see
|
|
20
|
+
* `../sandbox-proxy.ts` + `.env.example`). Without a resolvable sandbox URL,
|
|
21
|
+
* UI resources fall back to a visible notice — NEVER a same-origin mount.
|
|
22
|
+
*/
|
|
23
|
+
import {
|
|
24
|
+
Fragment,
|
|
25
|
+
useCallback,
|
|
26
|
+
useState,
|
|
27
|
+
type ComponentProps,
|
|
28
|
+
type FormEvent,
|
|
29
|
+
type ReactNode,
|
|
30
|
+
} from "react";
|
|
31
|
+
import { AppRenderer } from "@mcp-ui/client";
|
|
32
|
+
import type { AgBlock, AgMessage, JsonValue } from "@silverprotocol/core";
|
|
33
|
+
import { AGENT_URL, envUrl, useAgentChat } from "./useAgentChat";
|
|
34
|
+
|
|
35
|
+
type AppRendererProps = ComponentProps<typeof AppRenderer>;
|
|
36
|
+
type CallToolHandler = NonNullable<AppRendererProps["onCallTool"]>;
|
|
37
|
+
type ReadResourceHandler = NonNullable<AppRendererProps["onReadResource"]>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The dev sandbox proxy served by `vite.config.ts`'s `sandboxProxyPlugin`.
|
|
41
|
+
* The `6891` literal mirrors `SANDBOX_PROXY_PORT` in `../sandbox-proxy.ts`
|
|
42
|
+
* (not imported — that module pulls in `node:http`, which must never enter
|
|
43
|
+
* the browser bundle). Keep the two in sync.
|
|
44
|
+
*/
|
|
45
|
+
const DEV_SANDBOX_URL = "http://127.0.0.1:6891/sandbox.html";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The sandbox origin UI resources mount from. `VITE_SANDBOX_URL` (a deployed
|
|
49
|
+
* copy of the sandbox page on its own origin) wins; in dev the local proxy
|
|
50
|
+
* is the fallback. `undefined` (a production build without the env) shows
|
|
51
|
+
* the plain-text fallback below instead — the untrusted HTML is never
|
|
52
|
+
* mounted same-origin. `envUrl` treats an empty `VITE_SANDBOX_URL=`
|
|
53
|
+
* declaration as unset (it loads as `""`, which would defeat `??` and make
|
|
54
|
+
* `new URL("")` throw mid-render).
|
|
55
|
+
*/
|
|
56
|
+
const SANDBOX_URL: string | undefined =
|
|
57
|
+
envUrl(import.meta.env.VITE_SANDBOX_URL) ?? (import.meta.env.DEV ? DEV_SANDBOX_URL : undefined);
|
|
58
|
+
|
|
59
|
+
// ── MCP-embedded-resource narrowing (opaque JsonValue → typed payload) ──────
|
|
60
|
+
|
|
61
|
+
interface McpUiResourcePayload {
|
|
62
|
+
uri: string;
|
|
63
|
+
mimeType?: string;
|
|
64
|
+
text?: string;
|
|
65
|
+
blob?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isJsonObject(v: JsonValue): v is { [key: string]: JsonValue } {
|
|
69
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function asResourcePayload(v: JsonValue): McpUiResourcePayload | undefined {
|
|
73
|
+
if (!isJsonObject(v)) return undefined;
|
|
74
|
+
if (typeof v.uri !== "string") return undefined;
|
|
75
|
+
if (typeof v.text !== "string" && typeof v.blob !== "string") return undefined;
|
|
76
|
+
return {
|
|
77
|
+
uri: v.uri,
|
|
78
|
+
...(typeof v.mimeType === "string" ? { mimeType: v.mimeType } : {}),
|
|
79
|
+
...(typeof v.text === "string" ? { text: v.text } : {}),
|
|
80
|
+
...(typeof v.blob === "string" ? { blob: v.blob } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Does a tool-result's `uiData` carry an MCP embedded UI resource? Accepts
|
|
86
|
+
* either the resource inlined directly, or wrapped as `{ resource: {...} }`
|
|
87
|
+
* (the shape an MCP `resource` content block carries).
|
|
88
|
+
*/
|
|
89
|
+
function asUiResource(uiData: JsonValue | undefined): McpUiResourcePayload | undefined {
|
|
90
|
+
if (uiData === undefined || !isJsonObject(uiData)) return undefined;
|
|
91
|
+
const direct = asResourcePayload(uiData);
|
|
92
|
+
if (direct) return direct;
|
|
93
|
+
const nested = uiData.resource;
|
|
94
|
+
return nested !== undefined ? asResourcePayload(nested) : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The resource's HTML: inline `text` wins; else base64-decode `blob`.
|
|
99
|
+
* `atob` alone yields a Latin-1 string (mojibake on multibyte UTF-8), so
|
|
100
|
+
* decode via bytes + `TextDecoder`.
|
|
101
|
+
*/
|
|
102
|
+
function resourceHtml(resource: McpUiResourcePayload): string | undefined {
|
|
103
|
+
if (resource.text !== undefined) return resource.text;
|
|
104
|
+
if (resource.blob !== undefined) {
|
|
105
|
+
try {
|
|
106
|
+
return new TextDecoder().decode(Uint8Array.from(atob(resource.blob), (c) => c.charCodeAt(0)));
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined; // not valid base64 — treat as no renderable payload
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The tool name for a tool-result block, read off its paired tool-call block in the same message. */
|
|
115
|
+
function toolNameFor(message: AgMessage, toolCallId: string): string {
|
|
116
|
+
const call = message.content.find(
|
|
117
|
+
(b): b is Extract<AgBlock, { type: "tool-call" }> =>
|
|
118
|
+
b.type === "tool-call" && b.toolCallId === toolCallId
|
|
119
|
+
);
|
|
120
|
+
return call?.name ?? "tool";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderBlock(
|
|
124
|
+
message: AgMessage,
|
|
125
|
+
block: AgBlock,
|
|
126
|
+
handlers: { onCallTool: CallToolHandler; onReadResource: ReadResourceHandler }
|
|
127
|
+
): ReactNode {
|
|
128
|
+
switch (block.type) {
|
|
129
|
+
case "text":
|
|
130
|
+
return block.text ? <p className="block block-text">{block.text}</p> : null;
|
|
131
|
+
case "reasoning":
|
|
132
|
+
return block.text ? <p className="block block-reasoning">{block.text}</p> : null;
|
|
133
|
+
case "tool-call":
|
|
134
|
+
return (
|
|
135
|
+
<p className="block block-tool-call">
|
|
136
|
+
→ calling <code>{block.name}</code>…
|
|
137
|
+
</p>
|
|
138
|
+
);
|
|
139
|
+
case "tool-result": {
|
|
140
|
+
const resource = asUiResource(block.uiData);
|
|
141
|
+
const html = resource !== undefined ? resourceHtml(resource) : undefined;
|
|
142
|
+
if (html !== undefined) {
|
|
143
|
+
if (SANDBOX_URL === undefined) {
|
|
144
|
+
// No second-origin sandbox available (production build without
|
|
145
|
+
// VITE_SANDBOX_URL). Never mount the untrusted HTML same-origin —
|
|
146
|
+
// degrade to a visible notice instead.
|
|
147
|
+
return (
|
|
148
|
+
<p className="block block-tool-result">
|
|
149
|
+
← UI resource received — set <code>VITE_SANDBOX_URL</code> to render it (see{" "}
|
|
150
|
+
<code>.env.example</code>).
|
|
151
|
+
</p>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return (
|
|
155
|
+
<div className="block block-ui-resource">
|
|
156
|
+
<AppRenderer
|
|
157
|
+
key={block.toolCallId}
|
|
158
|
+
toolName={toolNameFor(message, block.toolCallId)}
|
|
159
|
+
sandbox={{ url: new URL(SANDBOX_URL) }}
|
|
160
|
+
html={html}
|
|
161
|
+
onCallTool={handlers.onCallTool}
|
|
162
|
+
onReadResource={handlers.onReadResource}
|
|
163
|
+
onError={(err: unknown) => console.warn("[AppRenderer]", err)}
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return (
|
|
169
|
+
<p className={`block block-tool-result${block.isError ? " is-error" : ""}`}>
|
|
170
|
+
← {block.isError ? "tool error" : "tool result"}
|
|
171
|
+
</p>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
default:
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function App() {
|
|
180
|
+
const { result, busy, error, needsResync, send } = useAgentChat();
|
|
181
|
+
const [draft, setDraft] = useState("");
|
|
182
|
+
|
|
183
|
+
// Neither relay exists in this minimal template — this worker only exposes
|
|
184
|
+
// `POST /agent/invoke` (see useAgentChat.ts), not a general tool-call/
|
|
185
|
+
// resource-read relay. Rendered UI resources that DON'T round-trip through
|
|
186
|
+
// the model (most simple cards/forms) still work; ones that do get a clear
|
|
187
|
+
// error here instead of a silent hang.
|
|
188
|
+
const onCallTool = useCallback<CallToolHandler>(async (params) => {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`tool-call relay is not wired in this template (requested "${params.name}") — ` +
|
|
191
|
+
`add a relay endpoint on your worker for full MCP Apps interactivity.`
|
|
192
|
+
);
|
|
193
|
+
}, []);
|
|
194
|
+
const onReadResource = useCallback<ReadResourceHandler>(async (params) => {
|
|
195
|
+
throw new Error(`resource-read relay is not wired in this template (requested "${params.uri}")`);
|
|
196
|
+
}, []);
|
|
197
|
+
|
|
198
|
+
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
const text = draft.trim();
|
|
201
|
+
if (!text || busy) return;
|
|
202
|
+
setDraft("");
|
|
203
|
+
void send(text);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div className="app">
|
|
208
|
+
<style>{`
|
|
209
|
+
:root { color-scheme: light dark; }
|
|
210
|
+
body { margin: 0; font-family: system-ui, sans-serif; }
|
|
211
|
+
.app { display: flex; flex-direction: column; height: 100vh; max-width: 720px; margin: 0 auto; }
|
|
212
|
+
header { padding: 12px 16px; border-bottom: 1px solid #8884; }
|
|
213
|
+
header h1 { font-size: 15px; margin: 0; }
|
|
214
|
+
header p { font-size: 12px; opacity: 0.6; margin: 2px 0 0; }
|
|
215
|
+
.history { flex: 1; overflow-y: auto; padding: 12px 16px; display: flex; flex-direction: column; gap: 10px; }
|
|
216
|
+
.empty { opacity: 0.5; font-size: 13px; }
|
|
217
|
+
.message { max-width: 85%; }
|
|
218
|
+
.message-user { align-self: flex-end; text-align: right; }
|
|
219
|
+
.message-assistant, .message-tool { align-self: flex-start; }
|
|
220
|
+
.block { margin: 0; padding: 6px 10px; border-radius: 8px; background: #8882; white-space: pre-wrap; }
|
|
221
|
+
.message-user .block-text { background: #4c8bf522; }
|
|
222
|
+
.block-tool-call, .block-tool-result { font-size: 12px; opacity: 0.7; background: none; padding: 0; }
|
|
223
|
+
.block-tool-result.is-error { color: #d33; }
|
|
224
|
+
.block-ui-resource { padding: 0; background: none; }
|
|
225
|
+
.block-ui-resource iframe { width: 100%; min-height: 240px; border: 1px solid #8884; border-radius: 8px; }
|
|
226
|
+
.banner { margin: 0 16px 8px; padding: 6px 10px; border-radius: 6px; font-size: 12px; }
|
|
227
|
+
.banner-error { background: #d3333322; color: #d33; }
|
|
228
|
+
.banner-warn { background: #d3901922; color: #b8720f; }
|
|
229
|
+
.composer { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid #8884; }
|
|
230
|
+
.composer input { flex: 1; padding: 8px 10px; border-radius: 6px; border: 1px solid #8886; }
|
|
231
|
+
.composer button { padding: 8px 14px; border-radius: 6px; border: none; background: #4c8bf5; color: white; }
|
|
232
|
+
.composer button:disabled { opacity: 0.5; }
|
|
233
|
+
`}</style>
|
|
234
|
+
<header>
|
|
235
|
+
<h1>agentic-app-template</h1>
|
|
236
|
+
<p>{AGENT_URL}</p>
|
|
237
|
+
</header>
|
|
238
|
+
|
|
239
|
+
<main className="history" role="log" aria-live="polite">
|
|
240
|
+
{result.messages.length === 0 ? (
|
|
241
|
+
<p className="empty">Say something to your agent.</p>
|
|
242
|
+
) : (
|
|
243
|
+
result.messages.map((message) => (
|
|
244
|
+
<div key={message.id} className={`message message-${message.role}`}>
|
|
245
|
+
{message.content.map((block, i) => (
|
|
246
|
+
<Fragment key={i}>{renderBlock(message, block, { onCallTool, onReadResource })}</Fragment>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
))
|
|
250
|
+
)}
|
|
251
|
+
</main>
|
|
252
|
+
|
|
253
|
+
{needsResync ? (
|
|
254
|
+
<p className="banner banner-warn">Stream fell out of sync — reload to resync.</p>
|
|
255
|
+
) : null}
|
|
256
|
+
{error ? <p className="banner banner-error">{error}</p> : null}
|
|
257
|
+
|
|
258
|
+
<form className="composer" onSubmit={onSubmit}>
|
|
259
|
+
<input
|
|
260
|
+
value={draft}
|
|
261
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
262
|
+
placeholder="Message your agent…"
|
|
263
|
+
disabled={busy}
|
|
264
|
+
autoFocus
|
|
265
|
+
/>
|
|
266
|
+
<button type="submit" disabled={busy || !draft.trim()}>
|
|
267
|
+
{busy ? "Sending…" : "Send"}
|
|
268
|
+
</button>
|
|
269
|
+
</form>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { App } from "./App";
|
|
4
|
+
|
|
5
|
+
const root = document.getElementById("root");
|
|
6
|
+
if (!root) throw new Error("#root element missing from index.html");
|
|
7
|
+
|
|
8
|
+
createRoot(root).render(
|
|
9
|
+
<StrictMode>
|
|
10
|
+
<App />
|
|
11
|
+
</StrictMode>
|
|
12
|
+
);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The from-scratch AgJSON client for the local dev chat SPA. Talks directly
|
|
3
|
+
* to the agent's `POST /agent/invoke` endpoint (SSE response), parses each
|
|
4
|
+
* `event: session|message|done|error` / `data: <JSON>` frame off the wire,
|
|
5
|
+
* and folds `message` frames (AgJSON `AgEvent[]`) through `@silverprotocol/
|
|
6
|
+
* core`'s `Reducer` into the normative `{messages, turns, artifacts, memory}`
|
|
7
|
+
* snapshot `App.tsx` renders from.
|
|
8
|
+
*
|
|
9
|
+
* Real `@silverprotocol/core` API (verified against `core/README.md` +
|
|
10
|
+
* `core/src/reduce.ts:79`): `new Reducer()` + `reducer.push(event)` are
|
|
11
|
+
* correct, but the fold is read back via `reducer.result()` — there is no
|
|
12
|
+
* `reducer.state` accessor. `ingestAgEvents` is the library's own
|
|
13
|
+
* parse-known-else-skip validator; used here instead of an `as AgEvent[]`
|
|
14
|
+
* cast so a malformed/foreign wire object degrades to "silently skipped"
|
|
15
|
+
* rather than a runtime type lie.
|
|
16
|
+
*/
|
|
17
|
+
import { useCallback, useRef, useState } from "react";
|
|
18
|
+
import { Reducer, ingestAgEvents, type AgReduceResult, type JsonValue } from "@silverprotocol/core";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalize a `VITE_*` env URL: an empty/whitespace declaration (e.g. a
|
|
22
|
+
* `VITE_X=` line copied from `.env.example`) loads as `""`, not `undefined`,
|
|
23
|
+
* which would defeat a `??` fallback — treat it as unset. Shared by this
|
|
24
|
+
* file's `AGENT_URL` and `App.tsx`'s `SANDBOX_URL` so both read sites agree.
|
|
25
|
+
*/
|
|
26
|
+
export function envUrl(value: string | undefined): string | undefined {
|
|
27
|
+
const trimmed = value?.trim();
|
|
28
|
+
return trimmed ? trimmed : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const AGENT_URL =
|
|
32
|
+
envUrl(import.meta.env.VITE_AGENT_ENDPOINT_URL) ?? "http://localhost:6790";
|
|
33
|
+
|
|
34
|
+
/** Parse complete SSE frame blocks ("event: x\ndata: {...}") out of a growing buffer. */
|
|
35
|
+
function parseFrames(buffer: string): {
|
|
36
|
+
frames: Array<{ event: string; data: string }>;
|
|
37
|
+
rest: string;
|
|
38
|
+
} {
|
|
39
|
+
const parts = buffer.split("\n\n");
|
|
40
|
+
const rest = parts.pop() ?? "";
|
|
41
|
+
const frames = parts.map((block) => {
|
|
42
|
+
const event = /^event: (.*)$/m.exec(block)?.[1] ?? "message";
|
|
43
|
+
const data = /^data: (.*)$/m.exec(block)?.[1] ?? "";
|
|
44
|
+
return { event, data };
|
|
45
|
+
});
|
|
46
|
+
return { frames, rest };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UseAgentChatResult {
|
|
50
|
+
/** The reducer's current folded state — fresh on every `message` frame. */
|
|
51
|
+
result: AgReduceResult;
|
|
52
|
+
/** True while a `send()` request is streaming. */
|
|
53
|
+
busy: boolean;
|
|
54
|
+
/** Last transport/agent error, or `null`. */
|
|
55
|
+
error: string | null;
|
|
56
|
+
/** True when the reducer detected a sequence gap and parked (needs a `messages.snapshot` to recover). */
|
|
57
|
+
needsResync: boolean;
|
|
58
|
+
send: (text: string) => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useAgentChat(): UseAgentChatResult {
|
|
62
|
+
const reducerRef = useRef(new Reducer());
|
|
63
|
+
// No payload — just a re-render trigger every time the mutable reducer
|
|
64
|
+
// advances, so `reducerRef.current.result()` below re-derives fresh state.
|
|
65
|
+
const [, bump] = useState(0);
|
|
66
|
+
const [busy, setBusy] = useState(false);
|
|
67
|
+
const [error, setError] = useState<string | null>(null);
|
|
68
|
+
|
|
69
|
+
const send = useCallback(async (text: string) => {
|
|
70
|
+
setBusy(true);
|
|
71
|
+
setError(null);
|
|
72
|
+
const res = await fetch(`${AGENT_URL}/agent/invoke`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({ input: text }),
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok || !res.body) {
|
|
78
|
+
setError(`agent returned ${res.status}`);
|
|
79
|
+
setBusy(false);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const decoder = new TextDecoder();
|
|
83
|
+
let buffer = "";
|
|
84
|
+
const reader = res.body.getReader();
|
|
85
|
+
for (;;) {
|
|
86
|
+
const { done, value } = await reader.read();
|
|
87
|
+
if (done) break;
|
|
88
|
+
buffer += decoder.decode(value, { stream: true });
|
|
89
|
+
const { frames, rest } = parseFrames(buffer);
|
|
90
|
+
buffer = rest;
|
|
91
|
+
for (const frame of frames) {
|
|
92
|
+
if (frame.event === "message") {
|
|
93
|
+
let raw: JsonValue;
|
|
94
|
+
try {
|
|
95
|
+
raw = JSON.parse(frame.data) as JsonValue;
|
|
96
|
+
} catch {
|
|
97
|
+
continue; // malformed frame — skip rather than crash the stream
|
|
98
|
+
}
|
|
99
|
+
const events = ingestAgEvents(Array.isArray(raw) ? raw : []);
|
|
100
|
+
for (const ev of events) reducerRef.current.push(ev);
|
|
101
|
+
bump((v) => v + 1);
|
|
102
|
+
} else if (frame.event === "error") {
|
|
103
|
+
try {
|
|
104
|
+
const payload = JSON.parse(frame.data) as { message?: string };
|
|
105
|
+
setError(payload.message ?? "agent error");
|
|
106
|
+
} catch {
|
|
107
|
+
setError("agent error");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
setBusy(false);
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
result: reducerRef.current.result(),
|
|
117
|
+
busy,
|
|
118
|
+
error,
|
|
119
|
+
needsResync: reducerRef.current.needsResync,
|
|
120
|
+
send,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typed Vite env vars. `import.meta.env` is Vite's analog of Next.js's
|
|
5
|
+
* `process.env.NEXT_PUBLIC_*` — anything declared here is exposed to the
|
|
6
|
+
* browser bundle at build time.
|
|
7
|
+
*/
|
|
8
|
+
interface ImportMetaEnv {
|
|
9
|
+
/**
|
|
10
|
+
* The agent's base URL (e.g. `http://localhost:6790` in local dev, via
|
|
11
|
+
* `guuey dev`). Falls back to that same local-dev default when unset —
|
|
12
|
+
* see `useAgentChat.ts`.
|
|
13
|
+
*/
|
|
14
|
+
readonly VITE_AGENT_ENDPOINT_URL?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Second-origin MCP-Apps sandbox page URL for `<AppRenderer>` (e.g.
|
|
17
|
+
* `https://sandbox.example.com/sandbox.html`). Optional in dev — the
|
|
18
|
+
* `sandboxProxyPlugin` in `vite.config.ts` serves one on :6891. REQUIRED
|
|
19
|
+
* for deployed builds that render UI resources: without it the app shows
|
|
20
|
+
* a plain-text notice instead (see `App.tsx` + `../sandbox-proxy.ts`).
|
|
21
|
+
*/
|
|
22
|
+
readonly VITE_SANDBOX_URL?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ImportMeta {
|
|
26
|
+
readonly env: ImportMetaEnv;
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"allowJs": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"module": "esnext",
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
"types": ["vite/client"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src", "vite.config.ts", "sandbox-proxy.ts"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import { sandboxProxyPlugin } from "./sandbox-proxy";
|
|
4
|
+
|
|
5
|
+
// Port is passed via the `dev` script's `--port 6890` flag (see package.json)
|
|
6
|
+
// so it stays visible in one place; `scripts/dev.mjs` boots this on the same
|
|
7
|
+
// port. The sandbox proxy plugin serves the MCP-Apps sandbox page on :6891 —
|
|
8
|
+
// a SECOND origin, as the spec's double-iframe architecture requires (see
|
|
9
|
+
// ./sandbox-proxy.ts).
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
plugins: [react(), sandboxProxyPlugin()],
|
|
12
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@guuey/create-agentic-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a guuey agentic app: code-mode agent + custom MCP + ggui + web, local pnpm dev, one-command guuey deploy.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-agentic-app": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^24.0.0",
|
|
22
|
+
"tsup": "^8.5.0",
|
|
23
|
+
"typescript": "^5.8.0",
|
|
24
|
+
"vitest": "^3.2.4",
|
|
25
|
+
"@guuey/config": "0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/withguuey/guuey-sdks.git",
|
|
33
|
+
"directory": "packages/create-agentic-app"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://guuey.com",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup && node scripts/build-templates.mjs && node scripts/check-templates.mjs",
|
|
38
|
+
"pretest": "node scripts/build-templates.mjs",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"lint": "eslint src --ext .ts",
|
|
42
|
+
"smoke": "node scripts/scaffold-smoke.mjs",
|
|
43
|
+
"smoke:verdaccio": "node scripts/verdaccio-smoke.mjs"
|
|
44
|
+
}
|
|
45
|
+
}
|