@agentskit/chat-cli 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.
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, writeFile } from "fs/promises";
5
+ import path from "path";
6
+ import { STANDARD_COMPONENT_KEYS } from "@agentskit/chat";
7
+ import { z } from "zod";
8
+ var CHAT_RENDERERS = ["react", "react-native", "ink", "vue", "svelte", "solid", "angular"];
9
+ var ChatRendererSchema = z.enum(CHAT_RENDERERS);
10
+ var ChatCliError = class extends Error {
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.code = code;
14
+ this.name = "ChatCliError";
15
+ }
16
+ code;
17
+ };
18
+ var packageJson = (renderer) => JSON.stringify({
19
+ name: "agentskit-chat-app",
20
+ private: true,
21
+ version: "0.0.0",
22
+ type: "module",
23
+ ...renderer === "react-native" ? { main: "expo/AppEntry.js" } : {},
24
+ scripts: { typecheck: renderer === "svelte" ? "svelte-check --tsconfig ./tsconfig.json" : "tsc --noEmit", test: "vitest run", ...["react", "vue", "svelte", "solid", "angular"].includes(renderer) ? { dev: "vite", build: "vite build" } : renderer === "react-native" ? { dev: "expo start", build: "expo export --platform web" } : { dev: "tsx src/index.tsx" } },
25
+ dependencies: {
26
+ "@agentskit/chat": "latest",
27
+ "@agentskit/chat-protocol": "latest",
28
+ "@agentskit/chat-server": "latest",
29
+ "@agentskit/core": "^1.12.2",
30
+ zod: "^4.3.6",
31
+ ...renderer === "react" ? { "@agentskit/chat-react": "latest", "@agentskit/react": "^0.7.1", react: "^19.0.0", "react-dom": "^19.0.0" } : renderer === "react-native" ? { "@agentskit/chat-react-native": "latest", "@agentskit/react-native": "^0.4.4", expo: "^57.0.4", react: "19.2.3", "react-dom": "19.2.3", "react-native": "^0.86.0", "react-native-web": "^0.21.2" } : renderer === "ink" ? { "@agentskit/chat-ink": "latest", "@agentskit/ink": "^0.10.1", ink: "^7.0.0", react: "^19.0.0" } : renderer === "vue" ? { "@agentskit/chat-vue": "latest", "@agentskit/vue": "^0.4.4", vue: "^3.5.0" } : renderer === "svelte" ? { "@agentskit/chat-svelte": "latest", "@agentskit/svelte": "^0.4.4", svelte: "^5.0.0" } : renderer === "solid" ? { "@agentskit/chat-solid": "latest", "@agentskit/solid": "^0.4.4", "solid-js": "^1.9.0" } : { "@agentskit/chat-angular": "latest", "@agentskit/angular": "^0.4.6", "@angular/common": "^21.0.0", "@angular/compiler": "^21.0.0", "@angular/core": "^21.0.0", "@angular/platform-browser": "^21.0.0", rxjs: "^7.8.0", tslib: "^2.8.0", "zone.js": "^0.16.0" }
32
+ },
33
+ devDependencies: { "@types/node": "^25.0.0", tsx: "^4.20.0", typescript: renderer === "angular" ? "^5.9.0" : "^6.0.0", vitest: "^4.0.0", ...["react", "react-native", "ink"].includes(renderer) ? { "@types/react": "^19.0.0" } : {}, ...renderer === "react" ? { "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^5.0.0", vite: "^8.0.0" } : renderer === "vue" ? { "@vitejs/plugin-vue": "^6.0.0", vite: "^8.0.0" } : renderer === "svelte" ? { "@sveltejs/vite-plugin-svelte": "^7.0.0", "svelte-check": "^4.0.0", vite: "^8.0.0" } : renderer === "solid" ? { "vite-plugin-solid": "^2.11.0", vite: "^8.0.0" } : renderer === "angular" ? { vite: "^8.0.0" } : {} }
34
+ }, null, 2) + "\n";
35
+ var sharedFiles = () => ({
36
+ "src/chat.ts": `import type { AdapterFactory } from '@agentskit/core'
37
+ import { defineChat } from '@agentskit/chat'
38
+
39
+ const demoAdapter: AdapterFactory = {
40
+ createSource: request => ({
41
+ async *stream() {
42
+ const input = request.messages.filter(message => message.role === 'user').at(-1)?.content ?? ''
43
+ yield { type: 'text', content: \`Echo: \${input}\` }
44
+ yield { type: 'done' }
45
+ },
46
+ abort() {},
47
+ }),
48
+ }
49
+
50
+ export const chat = defineChat({ id: 'starter', chat: { adapter: demoAdapter } })
51
+ `,
52
+ "src/server.ts": `import type { SessionSnapshot } from '@agentskit/chat-protocol'
53
+ import type { SessionStorage } from '@agentskit/chat'
54
+ import { createChatHandler } from '@agentskit/chat-server'
55
+ import { chat } from './chat.js'
56
+
57
+ let snapshot: SessionSnapshot | undefined
58
+ const storage: SessionStorage = {
59
+ load: () => snapshot,
60
+ save: (next, expected) => {
61
+ if (snapshot?.cursor !== expected) return false
62
+ snapshot = structuredClone(next)
63
+ return true
64
+ },
65
+ }
66
+
67
+ export const handleChat = createChatHandler({ resolveDefinition: () => chat, sessionStorage: () => storage })
68
+ `,
69
+ "tests/chat.test.ts": `import { describe, expect, it } from 'vitest'
70
+ import { chat } from '../src/chat.js'
71
+ import { handleChat } from '../src/server.js'
72
+
73
+ describe('generated chat', () => {
74
+ it('exports one runnable shared definition', () => expect(chat.id).toBe('starter'))
75
+ it('serves one complete turn through the Web handler', async () => {
76
+ const event = { protocol: 'agentskit.chat.turn', version: 1, eventId: 'submit', sessionId: 'session', turnId: 'turn', sequence: 0, emittedAt: new Date(0).toISOString(), event: 'client.turn.submit', payload: { input: 'hello' } }
77
+ const response = await handleChat(new Request('http://localhost/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(event) }))
78
+ expect(response.status).toBe(200)
79
+ expect(await response.text()).toContain('Echo: hello')
80
+ })
81
+ })
82
+ `,
83
+ "README.md": `# AgentsKit Chat starter
84
+
85
+ The shared definition lives in \`src/chat.ts\`, the Web-standard server seam in \`src/server.ts\`, and the native renderer entry under \`src\`.
86
+
87
+ Run \`pnpm typecheck && pnpm test\`, then \`pnpm dev\`. Replace the demo adapter in the shared definition with any published AgentsKit adapter.
88
+ `,
89
+ ".gitignore": "node_modules\ndist\n.expo\n.env\n",
90
+ "vitest.config.ts": `import { defineConfig } from 'vitest/config'
91
+ export default defineConfig({ test: { environment: 'node' } })
92
+ `
93
+ });
94
+ var rendererFiles = (renderer) => renderer === "react" ? {
95
+ "index.html": '<!doctype html><html><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>\n',
96
+ "src/main.tsx": `import '@agentskit/react/theme'
97
+ import { StrictMode } from 'react'
98
+ import { createRoot } from 'react-dom/client'
99
+ import { AgentChat } from '@agentskit/chat-react'
100
+ import { chat } from './chat.js'
101
+
102
+ createRoot(document.getElementById('root')!).render(<StrictMode><AgentChat definition={chat} /></StrictMode>)
103
+ `,
104
+ "src/vite-env.d.ts": `/// <reference types="vite/client" />
105
+ declare module '@agentskit/react/theme'
106
+ `,
107
+ "vite.config.ts": `import react from '@vitejs/plugin-react'
108
+ import { defineConfig } from 'vite'
109
+ export default defineConfig({ plugins: [react()] })
110
+ `
111
+ } : renderer === "react-native" ? {
112
+ "App.tsx": `import { AgentChatNative } from '@agentskit/chat-react-native'
113
+ import { chat } from './src/chat'
114
+ export default function App() { return <AgentChatNative definition={chat} /> }
115
+ `,
116
+ "app.json": JSON.stringify({ expo: { name: "AgentsKit Chat", slug: "agentskit-chat" } }, null, 2) + "\n"
117
+ } : renderer === "ink" ? {
118
+ "src/index.tsx": `import { AgentChat } from '@agentskit/chat-ink'
119
+ import { render } from 'ink'
120
+ import { chat } from './chat.js'
121
+ const app = render(<AgentChat definition={chat} />)
122
+ if (process.env.CI) setTimeout(() => app.unmount(), 0)
123
+ `
124
+ } : renderer === "vue" ? {
125
+ "index.html": '<!doctype html><html><body><div id="app"></div><script type="module" src="/src/main.ts"></script></body></html>\n',
126
+ "src/main.ts": `import { createApp, h } from 'vue'
127
+ import { AgentChat } from '@agentskit/chat-vue'
128
+ import { chat } from './chat.js'
129
+ createApp({ render: () => h(AgentChat, { definition: chat }) }).mount('#app')
130
+ `,
131
+ "vite.config.ts": `import vue from '@vitejs/plugin-vue'
132
+ import { defineConfig } from 'vite'
133
+ export default defineConfig({ plugins: [vue()] })
134
+ `
135
+ } : renderer === "svelte" ? {
136
+ "index.html": '<!doctype html><html><body><div id="app"></div><script type="module" src="/src/main.ts"></script></body></html>\n',
137
+ "src/App.svelte": `<script lang="ts">
138
+ import { AgentChat } from '@agentskit/chat-svelte'
139
+ import { chat } from './chat.js'
140
+ </script>
141
+ <AgentChat definition={chat} />
142
+ `,
143
+ "src/main.ts": `import { mount } from 'svelte'
144
+ import App from './App.svelte'
145
+ mount(App, { target: document.getElementById('app')! })
146
+ `,
147
+ "src/vite-env.d.ts": `/// <reference types="svelte" />
148
+ `,
149
+ "vite.config.ts": `import { svelte } from '@sveltejs/vite-plugin-svelte'
150
+ import { defineConfig } from 'vite'
151
+ export default defineConfig({ plugins: [svelte()] })
152
+ `
153
+ } : renderer === "solid" ? {
154
+ "index.html": '<!doctype html><html><body><div id="app"></div><script type="module" src="/src/main.tsx"></script></body></html>\n',
155
+ "src/main.tsx": `import { render } from 'solid-js/web'
156
+ import { AgentChat } from '@agentskit/chat-solid'
157
+ import { chat } from './chat.js'
158
+ render(() => <AgentChat definition={chat} />, document.getElementById('app')!)
159
+ `,
160
+ "vite.config.ts": `import { defineConfig } from 'vite'
161
+ import solid from 'vite-plugin-solid'
162
+ export default defineConfig({ plugins: [solid()] })
163
+ `
164
+ } : {
165
+ "index.html": '<!doctype html><html><body><app-root></app-root><script type="module" src="/src/main.ts"></script></body></html>\n',
166
+ "src/main.ts": `import 'zone.js'
167
+ import '@angular/compiler'
168
+ import { Component } from '@angular/core'
169
+ import { bootstrapApplication } from '@angular/platform-browser'
170
+ import { AgentChatComponent } from '@agentskit/chat-angular'
171
+ import { chat } from './chat.js'
172
+ @Component({ selector: 'app-root', standalone: true, imports: [AgentChatComponent], template: '<ak-agent-chat [definition]="chat" />' })
173
+ class AppComponent { readonly chat = chat }
174
+ void bootstrapApplication(AppComponent)
175
+ `
176
+ };
177
+ var tsconfig = (renderer) => JSON.stringify({ compilerOptions: {
178
+ target: "ES2022",
179
+ module: "ESNext",
180
+ moduleResolution: "bundler",
181
+ jsx: "react-jsx",
182
+ strict: true,
183
+ noEmit: true,
184
+ skipLibCheck: true,
185
+ ...renderer === "solid" ? { jsx: "preserve", jsxImportSource: "solid-js" } : {},
186
+ ...["react", "vue", "svelte", "solid", "angular"].includes(renderer) ? { lib: ["ES2022", "DOM"] } : {},
187
+ ...renderer === "ink" ? { types: ["node"] } : {}
188
+ }, include: renderer === "react-native" ? ["App.tsx", "src", "tests"] : ["src", "tests"] }, null, 2) + "\n";
189
+ var detectRenderer = async (targetDir) => {
190
+ try {
191
+ const input = JSON.parse(await readFile(path.join(targetDir, "package.json"), "utf8"));
192
+ const manifest = z.object({ dependencies: z.record(z.string(), z.string()).optional(), devDependencies: z.record(z.string(), z.string()).optional() }).passthrough().parse(input);
193
+ const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
194
+ if (dependencies.expo || dependencies["react-native"]) return "react-native";
195
+ if (dependencies.ink) return "ink";
196
+ if (dependencies["@angular/core"]) return "angular";
197
+ if (dependencies.svelte) return "svelte";
198
+ if (dependencies.vue) return "vue";
199
+ if (dependencies["solid-js"]) return "solid";
200
+ return dependencies.react ? "react" : void 0;
201
+ } catch (error) {
202
+ if (error.code === "ENOENT") return void 0;
203
+ throw new ChatCliError("MANIFEST_INVALID", "Could not read a valid package.json for renderer detection.");
204
+ }
205
+ };
206
+ var initChatProject = async (options) => {
207
+ const targetDir = path.resolve(options.targetDir);
208
+ const candidate = options.renderer ?? await detectRenderer(options.detectFromDir ?? targetDir);
209
+ if (!candidate) throw new ChatCliError("RENDERER_REQUIRED", `Choose a renderer with --renderer ${CHAT_RENDERERS.join(", ")}.`);
210
+ const renderer = ChatRendererSchema.parse(candidate);
211
+ const files = { ...sharedFiles(), ...rendererFiles(renderer), "package.json": packageJson(renderer), "tsconfig.json": tsconfig(renderer) };
212
+ const parent = path.dirname(targetDir);
213
+ await mkdir(parent, { recursive: true });
214
+ const staging = await mkdtemp(path.join(parent, `.${path.basename(targetDir)}-`));
215
+ try {
216
+ const writes = await Promise.allSettled(Object.entries(files).map(async ([relative, content]) => {
217
+ const destination = path.join(staging, relative);
218
+ await mkdir(path.dirname(destination), { recursive: true });
219
+ await writeFile(destination, content, { encoding: "utf8", flag: "wx" });
220
+ }));
221
+ const failure = writes.find((result) => result.status === "rejected");
222
+ if (failure?.status === "rejected") throw failure.reason;
223
+ await rename(staging, targetDir);
224
+ } catch (error) {
225
+ await rm(staging, { recursive: true, force: true });
226
+ const code = error.code;
227
+ if (code === "EEXIST" || code === "ENOTEMPTY" || code === "EISDIR" || code === "ENOTDIR") throw new ChatCliError("TARGET_EXISTS", `Refusing to overwrite existing path: ${targetDir}`);
228
+ throw error;
229
+ }
230
+ return Object.keys(files).sort();
231
+ };
232
+ var componentName = (input) => {
233
+ const kebab = input.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
234
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(kebab)) throw new ChatCliError("COMPONENT_INVALID", "Component name must start with a letter and contain only letters, numbers, or dashes.");
235
+ if (STANDARD_COMPONENT_KEYS.includes(kebab)) throw new ChatCliError("COMPONENT_INVALID", "Component name is reserved by the standard catalog.");
236
+ return { kebab, pascal: kebab.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join("") };
237
+ };
238
+ var componentFiles = (name, renderers) => {
239
+ const { kebab, pascal } = componentName(name);
240
+ const shared = `import { z } from 'zod'
241
+ import type { ComponentDefinition } from '@agentskit/chat'
242
+
243
+ export const ${pascal}PropsSchema = z.object({ label: z.string().min(1).max(256) }).strict().readonly()
244
+ export type ${pascal}Props = z.infer<typeof ${pascal}PropsSchema>
245
+ export const ${pascal}Component = { key: '${kebab}', propsSchema: ${pascal}PropsSchema, events: [], accessibility: { role: 'group', keyboard: false, live: 'none' }, capabilities: ['display'], fallback: props => props.label } satisfies ComponentDefinition<${pascal}Props>
246
+ `;
247
+ const files = { [`src/components/${kebab}.ts`]: shared };
248
+ for (const renderer of [...new Set(renderers)]) {
249
+ const relative = `src/components/${renderer}/${kebab}.${renderer === "svelte" ? "svelte" : renderer === "vue" || renderer === "angular" ? "ts" : "tsx"}`;
250
+ files[relative] = renderer === "svelte" ? `<script lang="ts">
251
+ import { StandardComponent } from '@agentskit/chat-svelte'
252
+ import type { ComponentManifest } from '@agentskit/chat'
253
+ import type { ComponentInteractionEvent, ComponentRenderFrame } from '@agentskit/chat-protocol'
254
+ import { ${pascal}PropsSchema } from '../${kebab}.js'
255
+ let { frame, manifest, onInteract, disabled = false }: { frame: ComponentRenderFrame; manifest: ComponentManifest; onInteract: (event: ComponentInteractionEvent) => void; disabled?: boolean } = $props()
256
+ const value = $derived(frame.componentKey === '${kebab}' ? ${pascal}PropsSchema.parse(frame.props) : undefined)
257
+ </script>
258
+ {#if value}<section aria-label={value.label}>{value.label}</section>{:else}<StandardComponent {frame} {manifest} {onInteract} {disabled} />{/if}
259
+ ` : renderer === "vue" ? `import { h, type VNode } from 'vue'
260
+ import { StandardComponent, type StandardComponentProps } from '@agentskit/chat-vue'
261
+ import { ${pascal}PropsSchema } from '../${kebab}.js'
262
+ export const ${pascal} = (slot: StandardComponentProps): VNode[] => { if (slot.frame.componentKey !== '${kebab}') return [h(StandardComponent, slot)]; const props = ${pascal}PropsSchema.parse(slot.frame.props); return [h('section', { 'aria-label': props.label }, props.label)] }
263
+ ` : renderer === "angular" ? `import { Component, Input } from '@angular/core'
264
+ import { StandardComponentComponent } from '@agentskit/chat-angular'
265
+ import type { ComponentManifest } from '@agentskit/chat'
266
+ import type { ComponentInteractionEvent, ComponentRenderFrame } from '@agentskit/chat-protocol'
267
+ import { ${pascal}PropsSchema } from '../${kebab}.js'
268
+ @Component({ selector: 'ak-${kebab}', standalone: true, imports: [StandardComponentComponent], template: '@if (value; as item) { <section [attr.aria-label]="item.label">{{ item.label }}</section> } @else { <ak-standard-component [frame]="frame" [manifest]="manifest" [onInteract]="onInteract" [disabled]="disabled" /> }' })
269
+ export class ${pascal}Component { @Input({ required: true }) frame!: ComponentRenderFrame; @Input({ required: true }) manifest!: ComponentManifest; @Input({ required: true }) onInteract!: (event: ComponentInteractionEvent) => void; @Input() disabled = false; get value() { return this.frame.componentKey === '${kebab}' ? ${pascal}PropsSchema.parse(this.frame.props) : undefined } }
270
+ ` : renderer === "solid" ? `import { StandardComponent, type StandardComponentProps } from '@agentskit/chat-solid'
271
+ import { ${pascal}PropsSchema } from '../${kebab}.js'
272
+ export const ${pascal} = (slot: StandardComponentProps) => { if (slot.frame.componentKey !== '${kebab}') return <StandardComponent {...slot} />; const props = ${pascal}PropsSchema.parse(slot.frame.props); return <section aria-label={props.label}>{props.label}</section> }
273
+ ` : `import { ${renderer === "react-native" ? "StandardComponentNative as DefaultStandardComponent, type StandardComponentNativeProps" : "StandardComponent as DefaultStandardComponent, type StandardComponentProps"} } from '@agentskit/chat-${renderer}'
274
+ import { ${pascal}PropsSchema } from '../${kebab}.js'
275
+ ${renderer === "react-native" ? "import { Text } from 'react-native'\n" : renderer === "ink" ? "import { Text } from 'ink'\n" : ""}export const ${pascal} = (slot: ${renderer === "react-native" ? "StandardComponentNativeProps" : "StandardComponentProps"}) => { if (slot.frame.componentKey !== '${kebab}') return <DefaultStandardComponent {...slot} />; const props = ${pascal}PropsSchema.parse(slot.frame.props); return <${renderer === "react-native" || renderer === "ink" ? "Text" : "section"}${renderer === "react-native" ? " accessibilityLabel={props.label}" : renderer === "ink" ? "" : " aria-label={props.label}"}>{props.label}</${renderer === "react-native" || renderer === "ink" ? "Text" : "section"}> }
276
+ `;
277
+ }
278
+ return files;
279
+ };
280
+ var addChatComponent = async (options) => {
281
+ const projectDir = await realpath(path.resolve(options.projectDir)).catch(() => {
282
+ throw new ChatCliError("MANIFEST_INVALID", "Component generation requires an existing project directory.");
283
+ });
284
+ const renderers = z.array(ChatRendererSchema).min(1).parse(options.renderers);
285
+ const files = componentFiles(options.name, renderers);
286
+ const destinations = Object.keys(files).map((relative) => path.join(projectDir, relative));
287
+ for (const destination of destinations) {
288
+ let current = path.dirname(destination);
289
+ while (current !== projectDir) {
290
+ const status = await lstat(current).catch((error) => error.code === "ENOENT" ? void 0 : Promise.reject(error));
291
+ if (status?.isSymbolicLink()) throw new ChatCliError("MANIFEST_INVALID", "Refusing to write through a symbolic-link component directory.");
292
+ current = path.dirname(current);
293
+ }
294
+ }
295
+ const checks = await Promise.all(destinations.map((destination) => readFile(destination).then(() => true, (error) => error.code === "ENOENT" ? false : Promise.reject(error))));
296
+ if (checks.some(Boolean)) throw new ChatCliError("COMPONENT_EXISTS", "Refusing to overwrite an existing component file.");
297
+ const created = [];
298
+ try {
299
+ for (const [relative, content] of Object.entries(files)) {
300
+ const destination = path.join(projectDir, relative);
301
+ await mkdir(path.dirname(destination), { recursive: true });
302
+ await writeFile(destination, content, { encoding: "utf8", flag: "wx" });
303
+ created.push(destination);
304
+ }
305
+ } catch (error) {
306
+ await Promise.all(created.map((destination) => rm(destination, { force: true })));
307
+ if (error.code === "EEXIST") throw new ChatCliError("COMPONENT_EXISTS", "Refusing to overwrite an existing component file.");
308
+ throw error;
309
+ }
310
+ return Object.keys(files).sort();
311
+ };
312
+
313
+ export {
314
+ CHAT_RENDERERS,
315
+ ChatRendererSchema,
316
+ ChatCliError,
317
+ detectRenderer,
318
+ initChatProject,
319
+ addChatComponent
320
+ };