@struere/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.
Files changed (65) hide show
  1. package/dist/commands/build.d.ts +3 -0
  2. package/dist/commands/build.d.ts.map +1 -0
  3. package/dist/commands/build.js +61 -0
  4. package/dist/commands/build.js.map +1 -0
  5. package/dist/commands/deploy.d.ts +3 -0
  6. package/dist/commands/deploy.d.ts.map +1 -0
  7. package/dist/commands/deploy.js +147 -0
  8. package/dist/commands/deploy.js.map +1 -0
  9. package/dist/commands/dev.d.ts +3 -0
  10. package/dist/commands/dev.d.ts.map +1 -0
  11. package/dist/commands/dev.js +500 -0
  12. package/dist/commands/dev.js.map +1 -0
  13. package/dist/commands/login.d.ts +3 -0
  14. package/dist/commands/login.d.ts.map +1 -0
  15. package/dist/commands/login.js +251 -0
  16. package/dist/commands/login.js.map +1 -0
  17. package/dist/commands/logout.d.ts +3 -0
  18. package/dist/commands/logout.d.ts.map +1 -0
  19. package/dist/commands/logout.js +19 -0
  20. package/dist/commands/logout.js.map +1 -0
  21. package/dist/commands/logs.d.ts +3 -0
  22. package/dist/commands/logs.d.ts.map +1 -0
  23. package/dist/commands/logs.js +103 -0
  24. package/dist/commands/logs.js.map +1 -0
  25. package/dist/commands/state.d.ts +3 -0
  26. package/dist/commands/state.d.ts.map +1 -0
  27. package/dist/commands/state.js +71 -0
  28. package/dist/commands/state.js.map +1 -0
  29. package/dist/commands/test.d.ts +3 -0
  30. package/dist/commands/test.d.ts.map +1 -0
  31. package/dist/commands/test.js +188 -0
  32. package/dist/commands/test.js.map +1 -0
  33. package/dist/commands/validate.d.ts +3 -0
  34. package/dist/commands/validate.d.ts.map +1 -0
  35. package/dist/commands/validate.js +71 -0
  36. package/dist/commands/validate.js.map +1 -0
  37. package/dist/commands/whoami.d.ts +3 -0
  38. package/dist/commands/whoami.d.ts.map +1 -0
  39. package/dist/commands/whoami.js +69 -0
  40. package/dist/commands/whoami.js.map +1 -0
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +1703 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/utils/agent.d.ts +3 -0
  46. package/dist/utils/agent.d.ts.map +1 -0
  47. package/dist/utils/agent.js +25 -0
  48. package/dist/utils/agent.js.map +1 -0
  49. package/dist/utils/api.d.ts +147 -0
  50. package/dist/utils/api.d.ts.map +1 -0
  51. package/dist/utils/api.js +102 -0
  52. package/dist/utils/api.js.map +1 -0
  53. package/dist/utils/config.d.ts +3 -0
  54. package/dist/utils/config.d.ts.map +1 -0
  55. package/dist/utils/config.js +43 -0
  56. package/dist/utils/config.js.map +1 -0
  57. package/dist/utils/credentials.d.ts +23 -0
  58. package/dist/utils/credentials.d.ts.map +1 -0
  59. package/dist/utils/credentials.js +51 -0
  60. package/dist/utils/credentials.js.map +1 -0
  61. package/dist/utils/validate.d.ts +3 -0
  62. package/dist/utils/validate.d.ts.map +1 -0
  63. package/dist/utils/validate.js +79 -0
  64. package/dist/utils/validate.js.map +1 -0
  65. package/package.json +44 -0
package/dist/index.js ADDED
@@ -0,0 +1,1703 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { program } from "commander";
5
+
6
+ // src/commands/dev.ts
7
+ import { Command } from "commander";
8
+ import chalk from "chalk";
9
+ import ora from "ora";
10
+ import chokidar from "chokidar";
11
+ import { join as join4 } from "path";
12
+
13
+ // src/utils/config.ts
14
+ import { join } from "path";
15
+ var defaultConfig = {
16
+ port: 3000,
17
+ host: "localhost",
18
+ cors: {
19
+ origins: ["http://localhost:3000"],
20
+ credentials: true
21
+ },
22
+ logging: {
23
+ level: "info",
24
+ format: "pretty"
25
+ },
26
+ auth: {
27
+ type: "none"
28
+ }
29
+ };
30
+ async function loadConfig(cwd) {
31
+ const configPath = join(cwd, "af.config.ts");
32
+ try {
33
+ const module = await import(configPath);
34
+ const config = module.default || module;
35
+ return {
36
+ ...defaultConfig,
37
+ ...config,
38
+ cors: {
39
+ ...defaultConfig.cors,
40
+ ...config.cors
41
+ },
42
+ logging: {
43
+ ...defaultConfig.logging,
44
+ ...config.logging
45
+ },
46
+ auth: {
47
+ ...defaultConfig.auth,
48
+ ...config.auth
49
+ }
50
+ };
51
+ } catch {
52
+ return defaultConfig;
53
+ }
54
+ }
55
+
56
+ // src/utils/agent.ts
57
+ import { join as join2 } from "path";
58
+ async function loadAgent(cwd) {
59
+ const agentPath = join2(cwd, "src/agent.ts");
60
+ try {
61
+ const module = await import(agentPath);
62
+ const agent = module.default || module;
63
+ if (!agent.name) {
64
+ throw new Error("Agent must have a name");
65
+ }
66
+ if (!agent.version) {
67
+ throw new Error("Agent must have a version");
68
+ }
69
+ if (!agent.systemPrompt) {
70
+ throw new Error("Agent must have a systemPrompt");
71
+ }
72
+ return agent;
73
+ } catch (error) {
74
+ if (error instanceof Error && error.message.includes("Cannot find module")) {
75
+ throw new Error(`Agent not found at ${agentPath}`);
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ // src/commands/dev.ts
82
+ import { AgentExecutor } from "@struere/runtime";
83
+
84
+ // src/utils/credentials.ts
85
+ import { homedir } from "os";
86
+ import { join as join3 } from "path";
87
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
88
+ var CONFIG_DIR = join3(homedir(), ".struere");
89
+ var CREDENTIALS_FILE = join3(CONFIG_DIR, "credentials.json");
90
+ function ensureConfigDir() {
91
+ if (!existsSync(CONFIG_DIR)) {
92
+ mkdirSync(CONFIG_DIR, { recursive: true });
93
+ }
94
+ }
95
+ function saveCredentials(credentials) {
96
+ ensureConfigDir();
97
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 384 });
98
+ }
99
+ function loadCredentials() {
100
+ if (!existsSync(CREDENTIALS_FILE)) {
101
+ return null;
102
+ }
103
+ try {
104
+ const data = readFileSync(CREDENTIALS_FILE, "utf-8");
105
+ const credentials = JSON.parse(data);
106
+ if (new Date(credentials.expiresAt) < new Date) {
107
+ clearCredentials();
108
+ return null;
109
+ }
110
+ return credentials;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+ function clearCredentials() {
116
+ if (existsSync(CREDENTIALS_FILE)) {
117
+ unlinkSync(CREDENTIALS_FILE);
118
+ }
119
+ }
120
+ function getApiKey() {
121
+ const credentials = loadCredentials();
122
+ if (credentials?.apiKey) {
123
+ return credentials.apiKey;
124
+ }
125
+ return process.env.STRUERE_API_KEY || null;
126
+ }
127
+ function getToken() {
128
+ const credentials = loadCredentials();
129
+ return credentials?.token || null;
130
+ }
131
+ function isLoggedIn() {
132
+ return loadCredentials() !== null;
133
+ }
134
+
135
+ // src/utils/api.ts
136
+ var DEFAULT_API_URL = "https://api.struere.dev";
137
+ var DEFAULT_SYNC_URL = "wss://sync.struere.dev";
138
+ function getApiUrl() {
139
+ return process.env.STRUERE_API_URL || DEFAULT_API_URL;
140
+ }
141
+ function getSyncUrl() {
142
+ return process.env.STRUERE_SYNC_URL || DEFAULT_SYNC_URL;
143
+ }
144
+
145
+ class ApiClient {
146
+ baseUrl;
147
+ constructor(baseUrl) {
148
+ this.baseUrl = baseUrl || getApiUrl();
149
+ }
150
+ async request(path, options = {}) {
151
+ const token = getToken();
152
+ const apiKey = getApiKey();
153
+ const headers = {
154
+ "Content-Type": "application/json",
155
+ ...options.headers || {}
156
+ };
157
+ if (token) {
158
+ headers["Authorization"] = `Bearer ${token}`;
159
+ } else if (apiKey) {
160
+ headers["Authorization"] = `Bearer ${apiKey}`;
161
+ }
162
+ const response = await fetch(`${this.baseUrl}${path}`, {
163
+ ...options,
164
+ headers
165
+ });
166
+ const data = await response.json();
167
+ if (!response.ok) {
168
+ const error = data;
169
+ throw new ApiError(error.error?.message || `HTTP ${response.status}`, error.error?.code || "UNKNOWN_ERROR", response.status);
170
+ }
171
+ return data;
172
+ }
173
+ async login(email, password) {
174
+ return this.request("/v1/auth/login", {
175
+ method: "POST",
176
+ body: JSON.stringify({ email, password })
177
+ });
178
+ }
179
+ async signup(email, name, password) {
180
+ return this.request("/v1/auth/signup", {
181
+ method: "POST",
182
+ body: JSON.stringify({ email, name, password })
183
+ });
184
+ }
185
+ async getMe() {
186
+ return this.request("/v1/auth/me");
187
+ }
188
+ async refreshToken() {
189
+ return this.request("/v1/auth/refresh", { method: "POST" });
190
+ }
191
+ async listAgents() {
192
+ return this.request("/v1/agents");
193
+ }
194
+ async createAgent(data) {
195
+ return this.request("/v1/agents", {
196
+ method: "POST",
197
+ body: JSON.stringify(data)
198
+ });
199
+ }
200
+ async getAgent(agentId) {
201
+ return this.request(`/v1/agents/${agentId}`);
202
+ }
203
+ async deployAgent(agentId, data) {
204
+ return this.request(`/v1/deployments/agents/${agentId}/deploy`, {
205
+ method: "POST",
206
+ body: JSON.stringify(data)
207
+ });
208
+ }
209
+ async createApiKey(data) {
210
+ return this.request("/v1/api-keys", {
211
+ method: "POST",
212
+ body: JSON.stringify(data)
213
+ });
214
+ }
215
+ async listApiKeys() {
216
+ return this.request("/v1/api-keys");
217
+ }
218
+ async getUsage(period = "day") {
219
+ return this.request(`/v1/usage?period=${period}`);
220
+ }
221
+ }
222
+
223
+ class ApiError extends Error {
224
+ code;
225
+ status;
226
+ constructor(message, code, status) {
227
+ super(message);
228
+ this.code = code;
229
+ this.status = status;
230
+ this.name = "ApiError";
231
+ }
232
+ }
233
+
234
+ // src/commands/dev.ts
235
+ var devCommand = new Command("dev").description("Start development server with hot reload").option("-p, --port <port>", "Port to run on", "3000").option("-c, --channel <channel>", "Channel to open (web, api)", "web").option("--no-open", "Do not open browser").option("--local", "Run locally without cloud sync").option("--cloud", "Force cloud-connected mode").action(async (options) => {
236
+ const spinner = ora();
237
+ const cwd = process.cwd();
238
+ console.log();
239
+ console.log(chalk.bold("Struere Dev Server"));
240
+ console.log();
241
+ spinner.start("Loading configuration");
242
+ const config = await loadConfig(cwd);
243
+ const port = parseInt(options.port) || config.port || 3000;
244
+ spinner.succeed("Configuration loaded");
245
+ spinner.start("Loading agent");
246
+ let agent = await loadAgent(cwd);
247
+ spinner.succeed(`Agent "${agent.name}" loaded`);
248
+ const useCloud = options.cloud || !options.local && isLoggedIn();
249
+ if (useCloud) {
250
+ await runCloudDev(agent, cwd, port, options, spinner);
251
+ } else {
252
+ await runLocalDev(agent, cwd, port, options, spinner);
253
+ }
254
+ });
255
+ async function runLocalDev(agent, cwd, port, options, spinner) {
256
+ let executor = new AgentExecutor(agent);
257
+ const server = Bun.serve({
258
+ port,
259
+ async fetch(req) {
260
+ const url = new URL(req.url);
261
+ if (url.pathname === "/health") {
262
+ return Response.json({ status: "ok", agent: agent.name, mode: "local" });
263
+ }
264
+ if (url.pathname === "/api/chat" && req.method === "POST") {
265
+ const body = await req.json();
266
+ const conversationId = body.conversationId || crypto.randomUUID();
267
+ if (body.stream) {
268
+ const stream = new ReadableStream({
269
+ async start(controller) {
270
+ const encoder = new TextEncoder;
271
+ const sendEvent = (event, data) => {
272
+ controller.enqueue(encoder.encode(`event: ${event}
273
+ data: ${JSON.stringify(data)}
274
+
275
+ `));
276
+ };
277
+ sendEvent("start", { conversationId });
278
+ for await (const chunk of executor.stream({ conversationId, message: body.message })) {
279
+ sendEvent(chunk.type, chunk);
280
+ }
281
+ controller.close();
282
+ }
283
+ });
284
+ return new Response(stream, {
285
+ headers: {
286
+ "Content-Type": "text/event-stream",
287
+ "Cache-Control": "no-cache",
288
+ Connection: "keep-alive"
289
+ }
290
+ });
291
+ }
292
+ const response = await executor.execute({ conversationId, message: body.message });
293
+ return Response.json({
294
+ response: response.message,
295
+ conversationId: response.conversationId,
296
+ toolCalls: response.toolCalls,
297
+ usage: response.usage
298
+ });
299
+ }
300
+ if (url.pathname === "/" && options.channel === "web") {
301
+ return new Response(getDevHtml(agent.name, "local"), {
302
+ headers: { "Content-Type": "text/html" }
303
+ });
304
+ }
305
+ return new Response("Not Found", { status: 404 });
306
+ }
307
+ });
308
+ console.log();
309
+ console.log(chalk.gray("Mode:"), chalk.yellow("Local"));
310
+ console.log(chalk.green("Server running at"), chalk.cyan(`http://localhost:${port}`));
311
+ console.log();
312
+ if (options.channel === "web" && options.open) {
313
+ const openUrl = `http://localhost:${port}`;
314
+ if (process.platform === "darwin") {
315
+ Bun.spawn(["open", openUrl]);
316
+ } else if (process.platform === "linux") {
317
+ Bun.spawn(["xdg-open", openUrl]);
318
+ }
319
+ }
320
+ spinner.start("Watching for changes");
321
+ const watcher = chokidar.watch([join4(cwd, "src"), join4(cwd, "af.config.ts")], {
322
+ ignoreInitial: true,
323
+ ignored: /node_modules/
324
+ });
325
+ watcher.on("change", async (path) => {
326
+ spinner.text = `Reloading (${path.replace(cwd, ".")})`;
327
+ try {
328
+ agent = await loadAgent(cwd);
329
+ executor = new AgentExecutor(agent);
330
+ spinner.succeed(`Reloaded "${agent.name}"`);
331
+ spinner.start("Watching for changes");
332
+ } catch (error) {
333
+ spinner.fail(`Reload failed: ${error}`);
334
+ spinner.start("Watching for changes");
335
+ }
336
+ });
337
+ process.on("SIGINT", () => {
338
+ console.log();
339
+ spinner.stop();
340
+ watcher.close();
341
+ server.stop();
342
+ console.log(chalk.gray("Server stopped"));
343
+ process.exit(0);
344
+ });
345
+ }
346
+ async function runCloudDev(agent, cwd, port, options, spinner) {
347
+ const credentials = loadCredentials();
348
+ const apiKey = getApiKey();
349
+ if (!credentials && !apiKey) {
350
+ spinner.fail("Not logged in");
351
+ console.log();
352
+ console.log(chalk.gray("Run"), chalk.cyan("af login"), chalk.gray("to connect to Struere Cloud"));
353
+ console.log(chalk.gray("Or use"), chalk.cyan("af dev --local"), chalk.gray("for local development"));
354
+ console.log();
355
+ process.exit(1);
356
+ }
357
+ spinner.start("Connecting to Struere Cloud");
358
+ const syncUrl = getSyncUrl();
359
+ const ws = new WebSocket(`${syncUrl}/v1/dev/sync`);
360
+ let cloudUrl = null;
361
+ let sessionId = null;
362
+ let isConnected = false;
363
+ ws.onopen = () => {
364
+ ws.send(JSON.stringify({
365
+ type: "auth",
366
+ apiKey: apiKey || credentials?.token
367
+ }));
368
+ };
369
+ ws.onmessage = async (event) => {
370
+ const data = JSON.parse(event.data);
371
+ switch (data.type) {
372
+ case "authenticated":
373
+ spinner.text = "Syncing agent";
374
+ const bundle = await bundleAgent(cwd);
375
+ const configHash = hashString(bundle);
376
+ ws.send(JSON.stringify({
377
+ type: "sync",
378
+ agentSlug: agent.name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
379
+ bundle,
380
+ configHash
381
+ }));
382
+ break;
383
+ case "synced":
384
+ isConnected = true;
385
+ cloudUrl = data.url || null;
386
+ sessionId = data.agentId || null;
387
+ spinner.succeed("Connected to Struere Cloud");
388
+ console.log();
389
+ console.log(chalk.gray("Mode:"), chalk.green("Cloud"));
390
+ console.log(chalk.green("Agent running at"), chalk.cyan(cloudUrl));
391
+ console.log(chalk.green("Local server at"), chalk.cyan(`http://localhost:${port}`));
392
+ console.log();
393
+ spinner.start("Watching for changes");
394
+ break;
395
+ case "log":
396
+ const logColor = data.level === "error" ? chalk.red : data.level === "warn" ? chalk.yellow : data.level === "debug" ? chalk.gray : chalk.blue;
397
+ spinner.stop();
398
+ console.log(logColor(`[${data.level}]`), data.message);
399
+ spinner.start("Watching for changes");
400
+ break;
401
+ case "error":
402
+ spinner.fail(`Cloud error: ${data.message}`);
403
+ if (data.code === "INVALID_API_KEY" || data.code === "NOT_AUTHENTICATED") {
404
+ console.log();
405
+ console.log(chalk.gray("Run"), chalk.cyan("af login"), chalk.gray("to authenticate"));
406
+ }
407
+ break;
408
+ }
409
+ };
410
+ ws.onerror = (error) => {
411
+ spinner.fail("WebSocket error");
412
+ console.log(chalk.red("Connection error. Falling back to local mode."));
413
+ };
414
+ ws.onclose = () => {
415
+ if (isConnected) {
416
+ spinner.stop();
417
+ console.log(chalk.yellow("Disconnected from cloud"));
418
+ }
419
+ };
420
+ const server = Bun.serve({
421
+ port,
422
+ async fetch(req) {
423
+ const url = new URL(req.url);
424
+ if (url.pathname === "/health") {
425
+ return Response.json({
426
+ status: "ok",
427
+ agent: agent.name,
428
+ mode: "cloud",
429
+ cloudUrl
430
+ });
431
+ }
432
+ if (url.pathname === "/api/chat" && req.method === "POST") {
433
+ if (!cloudUrl || !sessionId) {
434
+ return Response.json({ error: "Not connected to cloud" }, { status: 503 });
435
+ }
436
+ const body = await req.json();
437
+ const gatewayUrl = cloudUrl.replace("https://", "https://gateway.").replace(/-dev\.struere\.dev.*/, ".struere.dev");
438
+ const response = await fetch(`${process.env.STRUERE_GATEWAY_URL || "https://gateway.struere.dev"}/v1/dev/${sessionId}/chat`, {
439
+ method: "POST",
440
+ headers: {
441
+ "Content-Type": "application/json",
442
+ Authorization: `Bearer ${apiKey || credentials?.token}`
443
+ },
444
+ body: JSON.stringify(body)
445
+ });
446
+ if (body.stream) {
447
+ return new Response(response.body, {
448
+ headers: {
449
+ "Content-Type": "text/event-stream",
450
+ "Cache-Control": "no-cache",
451
+ Connection: "keep-alive"
452
+ }
453
+ });
454
+ }
455
+ const data = await response.json();
456
+ return Response.json(data);
457
+ }
458
+ if (url.pathname === "/" && options.channel === "web") {
459
+ return new Response(getDevHtml(agent.name, "cloud", cloudUrl), {
460
+ headers: { "Content-Type": "text/html" }
461
+ });
462
+ }
463
+ return new Response("Not Found", { status: 404 });
464
+ }
465
+ });
466
+ if (options.channel === "web" && options.open) {
467
+ const openUrl = `http://localhost:${port}`;
468
+ if (process.platform === "darwin") {
469
+ Bun.spawn(["open", openUrl]);
470
+ } else if (process.platform === "linux") {
471
+ Bun.spawn(["xdg-open", openUrl]);
472
+ }
473
+ }
474
+ const watcher = chokidar.watch([join4(cwd, "src"), join4(cwd, "af.config.ts")], {
475
+ ignoreInitial: true,
476
+ ignored: /node_modules/
477
+ });
478
+ watcher.on("change", async (path) => {
479
+ spinner.text = `Syncing (${path.replace(cwd, ".")})`;
480
+ try {
481
+ agent = await loadAgent(cwd);
482
+ const bundle = await bundleAgent(cwd);
483
+ const configHash = hashString(bundle);
484
+ if (ws.readyState === WebSocket.OPEN) {
485
+ ws.send(JSON.stringify({
486
+ type: "sync",
487
+ agentSlug: agent.name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
488
+ bundle,
489
+ configHash
490
+ }));
491
+ }
492
+ } catch (error) {
493
+ spinner.fail(`Sync failed: ${error}`);
494
+ spinner.start("Watching for changes");
495
+ }
496
+ });
497
+ process.on("SIGINT", () => {
498
+ console.log();
499
+ spinner.stop();
500
+ if (ws.readyState === WebSocket.OPEN) {
501
+ ws.send(JSON.stringify({ type: "unsync" }));
502
+ ws.close();
503
+ }
504
+ watcher.close();
505
+ server.stop();
506
+ console.log(chalk.gray("Server stopped"));
507
+ process.exit(0);
508
+ });
509
+ }
510
+ async function bundleAgent(cwd) {
511
+ const result = await Bun.build({
512
+ entrypoints: [join4(cwd, "src", "agent.ts")],
513
+ target: "browser",
514
+ minify: true
515
+ });
516
+ if (!result.success) {
517
+ throw new Error("Bundle failed: " + result.logs.join(`
518
+ `));
519
+ }
520
+ return await result.outputs[0].text();
521
+ }
522
+ function hashString(str) {
523
+ let hash = 0;
524
+ for (let i = 0;i < str.length; i++) {
525
+ const char = str.charCodeAt(i);
526
+ hash = (hash << 5) - hash + char;
527
+ hash = hash & hash;
528
+ }
529
+ return Math.abs(hash).toString(16);
530
+ }
531
+ function getDevHtml(agentName, mode, cloudUrl) {
532
+ const modeLabel = mode === "cloud" ? `<span style="color: #22c55e;">Cloud</span>${cloudUrl ? ` - <a href="${cloudUrl}" target="_blank" style="color: #60a5fa;">${cloudUrl}</a>` : ""}` : '<span style="color: #eab308;">Local</span>';
533
+ return `<!DOCTYPE html>
534
+ <html lang="en">
535
+ <head>
536
+ <meta charset="UTF-8">
537
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
538
+ <title>${agentName} - Dev</title>
539
+ <style>
540
+ * { box-sizing: border-box; margin: 0; padding: 0; }
541
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0a0a0a; color: #fafafa; height: 100vh; display: flex; flex-direction: column; }
542
+ header { padding: 1rem; border-bottom: 1px solid #333; display: flex; justify-content: space-between; align-items: center; }
543
+ header h1 { font-size: 1rem; font-weight: 500; }
544
+ header .mode { font-size: 0.875rem; }
545
+ header a { text-decoration: none; }
546
+ #messages { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
547
+ .message { max-width: 80%; padding: 0.75rem 1rem; border-radius: 0.75rem; line-height: 1.5; white-space: pre-wrap; }
548
+ .message.user { align-self: flex-end; background: #2563eb; }
549
+ .message.assistant { align-self: flex-start; background: #27272a; }
550
+ .message.tool { align-self: flex-start; background: #1e3a5f; font-family: monospace; font-size: 0.875rem; border-left: 3px solid #3b82f6; }
551
+ .message.streaming { opacity: 0.9; }
552
+ form { padding: 1rem; border-top: 1px solid #333; display: flex; gap: 0.5rem; }
553
+ input { flex: 1; padding: 0.75rem 1rem; background: #18181b; border: 1px solid #333; border-radius: 0.5rem; color: #fafafa; font-size: 1rem; outline: none; }
554
+ input:focus { border-color: #2563eb; }
555
+ input:disabled { opacity: 0.5; }
556
+ button { padding: 0.75rem 1.5rem; background: #2563eb; border: none; border-radius: 0.5rem; color: white; font-size: 1rem; cursor: pointer; }
557
+ button:hover { background: #1d4ed8; }
558
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
559
+ .toggle-container { padding: 0.5rem 1rem; display: flex; align-items: center; gap: 0.5rem; border-top: 1px solid #333; }
560
+ .toggle-container label { font-size: 0.875rem; color: #888; }
561
+ .toggle-container input[type="checkbox"] { width: 1rem; height: 1rem; }
562
+ </style>
563
+ </head>
564
+ <body>
565
+ <header>
566
+ <h1>${agentName}</h1>
567
+ <span class="mode">${modeLabel}</span>
568
+ </header>
569
+ <div id="messages"></div>
570
+ <div class="toggle-container">
571
+ <input type="checkbox" id="stream-toggle" checked />
572
+ <label for="stream-toggle">Enable streaming</label>
573
+ </div>
574
+ <form id="chat-form">
575
+ <input type="text" id="input" placeholder="Type a message..." autocomplete="off" />
576
+ <button type="submit">Send</button>
577
+ </form>
578
+ <script>
579
+ const messages = document.getElementById('messages');
580
+ const form = document.getElementById('chat-form');
581
+ const input = document.getElementById('input');
582
+ const button = form.querySelector('button');
583
+ const streamToggle = document.getElementById('stream-toggle');
584
+ let conversationId = null;
585
+ let isProcessing = false;
586
+
587
+ function addMessage(role, content, isStreaming = false) {
588
+ const div = document.createElement('div');
589
+ div.className = 'message ' + role + (isStreaming ? ' streaming' : '');
590
+ div.textContent = content;
591
+ messages.appendChild(div);
592
+ messages.scrollTop = messages.scrollHeight;
593
+ return div;
594
+ }
595
+
596
+ function setProcessing(processing) {
597
+ isProcessing = processing;
598
+ input.disabled = processing;
599
+ button.disabled = processing;
600
+ }
601
+
602
+ async function sendWithStreaming(message) {
603
+ const assistantDiv = addMessage('assistant', '', true);
604
+
605
+ const response = await fetch('/api/chat', {
606
+ method: 'POST',
607
+ headers: { 'Content-Type': 'application/json' },
608
+ body: JSON.stringify({ message, conversationId, stream: true }),
609
+ });
610
+
611
+ const reader = response.body.getReader();
612
+ const decoder = new TextDecoder();
613
+ let buffer = '';
614
+ let fullText = '';
615
+
616
+ while (true) {
617
+ const { done, value } = await reader.read();
618
+ if (done) break;
619
+
620
+ buffer += decoder.decode(value, { stream: true });
621
+ const lines = buffer.split('\\n');
622
+ buffer = lines.pop() || '';
623
+
624
+ for (const line of lines) {
625
+ if (line.startsWith('data: ')) {
626
+ try {
627
+ const data = JSON.parse(line.slice(6));
628
+
629
+ if (data.conversationId) {
630
+ conversationId = data.conversationId;
631
+ }
632
+
633
+ if (data.type === 'text-delta' && (data.textDelta || data.content)) {
634
+ fullText += data.textDelta || data.content;
635
+ assistantDiv.textContent = fullText;
636
+ messages.scrollTop = messages.scrollHeight;
637
+ } else if (data.type === 'tool-call-start') {
638
+ addMessage('tool', '\uD83D\uDD27 Calling tool: ' + (data.toolName || data.toolCall?.name));
639
+ } else if (data.type === 'tool-result') {
640
+ const resultText = typeof data.toolResult === 'string'
641
+ ? data.toolResult
642
+ : JSON.stringify(data.toolResult || data.toolCall?.result, null, 2);
643
+ addMessage('tool', '✓ Result: ' + resultText);
644
+ } else if (data.type === 'finish') {
645
+ assistantDiv.classList.remove('streaming');
646
+ } else if (data.type === 'error') {
647
+ assistantDiv.textContent = 'Error: ' + (data.error || data.message);
648
+ assistantDiv.classList.remove('streaming');
649
+ }
650
+ } catch (e) {}
651
+ }
652
+ }
653
+ }
654
+
655
+ if (!fullText) {
656
+ assistantDiv.remove();
657
+ }
658
+ }
659
+
660
+ async function sendWithoutStreaming(message) {
661
+ const res = await fetch('/api/chat', {
662
+ method: 'POST',
663
+ headers: { 'Content-Type': 'application/json' },
664
+ body: JSON.stringify({ message, conversationId }),
665
+ });
666
+ const data = await res.json();
667
+ conversationId = data.conversationId;
668
+
669
+ if (data.toolCalls && data.toolCalls.length > 0) {
670
+ for (const tc of data.toolCalls) {
671
+ const resultText = typeof tc.result === 'string'
672
+ ? tc.result
673
+ : JSON.stringify(tc.result, null, 2);
674
+ addMessage('tool', '\uD83D\uDD27 ' + tc.name + ': ' + resultText);
675
+ }
676
+ }
677
+
678
+ addMessage('assistant', data.response || data.content);
679
+ }
680
+
681
+ form.addEventListener('submit', async (e) => {
682
+ e.preventDefault();
683
+ if (isProcessing) return;
684
+
685
+ const message = input.value.trim();
686
+ if (!message) return;
687
+
688
+ addMessage('user', message);
689
+ input.value = '';
690
+ setProcessing(true);
691
+
692
+ try {
693
+ if (streamToggle.checked) {
694
+ await sendWithStreaming(message);
695
+ } else {
696
+ await sendWithoutStreaming(message);
697
+ }
698
+ } catch (err) {
699
+ addMessage('assistant', 'Error: ' + err.message);
700
+ } finally {
701
+ setProcessing(false);
702
+ }
703
+ });
704
+
705
+ input.focus();
706
+ </script>
707
+ </body>
708
+ </html>`;
709
+ }
710
+
711
+ // src/commands/build.ts
712
+ import { Command as Command2 } from "commander";
713
+ import chalk2 from "chalk";
714
+ import ora2 from "ora";
715
+ import { join as join5 } from "path";
716
+
717
+ // src/utils/validate.ts
718
+ function validateAgent(agent) {
719
+ const errors = [];
720
+ if (!agent.name) {
721
+ errors.push("Agent name is required");
722
+ } else if (!/^[a-z0-9-]+$/.test(agent.name)) {
723
+ errors.push("Agent name must be lowercase alphanumeric with hyphens only");
724
+ }
725
+ if (!agent.version) {
726
+ errors.push("Agent version is required");
727
+ } else if (!/^\d+\.\d+\.\d+/.test(agent.version)) {
728
+ errors.push("Agent version must follow semver format (e.g., 1.0.0)");
729
+ }
730
+ if (!agent.systemPrompt) {
731
+ errors.push("System prompt is required");
732
+ } else if (typeof agent.systemPrompt === "string" && agent.systemPrompt.trim().length === 0) {
733
+ errors.push("System prompt cannot be empty");
734
+ }
735
+ if (agent.model) {
736
+ const validProviders = ["anthropic", "openai", "google", "custom"];
737
+ if (!validProviders.includes(agent.model.provider)) {
738
+ errors.push(`Invalid model provider: ${agent.model.provider}`);
739
+ }
740
+ if (!agent.model.name) {
741
+ errors.push("Model name is required when model is specified");
742
+ }
743
+ if (agent.model.temperature !== undefined) {
744
+ if (agent.model.temperature < 0 || agent.model.temperature > 2) {
745
+ errors.push("Model temperature must be between 0 and 2");
746
+ }
747
+ }
748
+ if (agent.model.maxTokens !== undefined) {
749
+ if (agent.model.maxTokens < 1) {
750
+ errors.push("Model maxTokens must be at least 1");
751
+ }
752
+ }
753
+ }
754
+ if (agent.tools) {
755
+ for (const tool of agent.tools) {
756
+ const toolErrors = validateTool(tool);
757
+ errors.push(...toolErrors);
758
+ }
759
+ }
760
+ if (agent.state) {
761
+ const validStorage = ["memory", "redis", "postgres", "custom"];
762
+ if (!validStorage.includes(agent.state.storage)) {
763
+ errors.push(`Invalid state storage: ${agent.state.storage}`);
764
+ }
765
+ if (agent.state.ttl !== undefined && agent.state.ttl < 0) {
766
+ errors.push("State TTL must be non-negative");
767
+ }
768
+ }
769
+ return errors;
770
+ }
771
+ function validateTool(tool) {
772
+ const errors = [];
773
+ if (!tool.name) {
774
+ errors.push("Tool name is required");
775
+ } else if (!/^[a-z_][a-z0-9_]*$/.test(tool.name)) {
776
+ errors.push(`Tool name "${tool.name}" must be snake_case`);
777
+ }
778
+ if (!tool.description) {
779
+ errors.push(`Tool "${tool.name || "unknown"}" requires a description`);
780
+ }
781
+ if (!tool.parameters) {
782
+ errors.push(`Tool "${tool.name || "unknown"}" requires parameters definition`);
783
+ } else if (tool.parameters.type !== "object") {
784
+ errors.push(`Tool "${tool.name || "unknown"}" parameters type must be "object"`);
785
+ }
786
+ if (typeof tool.handler !== "function") {
787
+ errors.push(`Tool "${tool.name || "unknown"}" requires a handler function`);
788
+ }
789
+ return errors;
790
+ }
791
+
792
+ // src/commands/build.ts
793
+ var buildCommand = new Command2("build").description("Build and validate agent for production").option("-o, --outdir <dir>", "Output directory", "dist").action(async (options) => {
794
+ const spinner = ora2();
795
+ const cwd = process.cwd();
796
+ console.log();
797
+ console.log(chalk2.bold("Building Agent"));
798
+ console.log();
799
+ spinner.start("Loading configuration");
800
+ const config = await loadConfig(cwd);
801
+ spinner.succeed("Configuration loaded");
802
+ spinner.start("Loading agent");
803
+ const agent = await loadAgent(cwd);
804
+ spinner.succeed(`Agent "${agent.name}" loaded`);
805
+ spinner.start("Validating agent");
806
+ const errors = validateAgent(agent);
807
+ if (errors.length > 0) {
808
+ spinner.fail("Validation failed");
809
+ console.log();
810
+ for (const error of errors) {
811
+ console.log(chalk2.red(" ✗"), error);
812
+ }
813
+ console.log();
814
+ process.exit(1);
815
+ }
816
+ spinner.succeed("Agent validated");
817
+ spinner.start("Building");
818
+ const outdir = join5(cwd, options.outdir);
819
+ const result = await Bun.build({
820
+ entrypoints: [join5(cwd, "src/agent.ts")],
821
+ outdir,
822
+ target: "node",
823
+ minify: true
824
+ });
825
+ if (!result.success) {
826
+ spinner.fail("Build failed");
827
+ console.log();
828
+ for (const log of result.logs) {
829
+ console.log(chalk2.red(" ✗"), log.message);
830
+ }
831
+ process.exit(1);
832
+ }
833
+ spinner.succeed("Build completed");
834
+ console.log();
835
+ console.log(chalk2.green("Success!"), `Built to ${chalk2.cyan(options.outdir)}`);
836
+ console.log();
837
+ console.log("Output files:");
838
+ for (const output of result.outputs) {
839
+ console.log(chalk2.gray(" •"), output.path.replace(cwd, "."));
840
+ }
841
+ console.log();
842
+ });
843
+
844
+ // src/commands/test.ts
845
+ import { Command as Command3 } from "commander";
846
+ import chalk3 from "chalk";
847
+ import ora3 from "ora";
848
+ import { join as join6 } from "path";
849
+ import { readdir, readFile } from "fs/promises";
850
+ import YAML from "yaml";
851
+ import { AgentExecutor as AgentExecutor2 } from "@struere/runtime";
852
+ var testCommand = new Command3("test").description("Run test conversations").argument("[pattern]", "Test file pattern", "*.test.yaml").option("-v, --verbose", "Show detailed output").option("--dry-run", "Parse tests without executing (no API calls)").action(async (pattern, options) => {
853
+ const spinner = ora3();
854
+ const cwd = process.cwd();
855
+ console.log();
856
+ console.log(chalk3.bold("Running Tests"));
857
+ console.log();
858
+ spinner.start("Loading agent");
859
+ const agent = await loadAgent(cwd);
860
+ spinner.succeed(`Agent "${agent.name}" loaded`);
861
+ spinner.start("Finding test files");
862
+ const testsDir = join6(cwd, "tests");
863
+ let testFiles = [];
864
+ try {
865
+ const files = await readdir(testsDir);
866
+ testFiles = files.filter((f) => f.endsWith(".test.yaml") || f.endsWith(".test.yml"));
867
+ } catch {
868
+ spinner.warn("No tests directory found");
869
+ console.log();
870
+ console.log(chalk3.gray("Create tests in"), chalk3.cyan("tests/*.test.yaml"));
871
+ console.log();
872
+ return;
873
+ }
874
+ if (testFiles.length === 0) {
875
+ spinner.warn("No test files found");
876
+ console.log();
877
+ return;
878
+ }
879
+ spinner.succeed(`Found ${testFiles.length} test file(s)`);
880
+ if (options.dryRun) {
881
+ console.log();
882
+ console.log(chalk3.yellow("Dry run mode - skipping execution"));
883
+ console.log();
884
+ }
885
+ const results = [];
886
+ for (const file of testFiles) {
887
+ const filePath = join6(testsDir, file);
888
+ const content = await readFile(filePath, "utf-8");
889
+ const testCase = YAML.parse(content);
890
+ if (options.verbose) {
891
+ console.log();
892
+ console.log(chalk3.gray("Running:"), testCase.name);
893
+ }
894
+ const result = options.dryRun ? await runDryTest(testCase) : await runTest(testCase, agent, options.verbose);
895
+ results.push(result);
896
+ if (result.passed) {
897
+ console.log(chalk3.green(" ✓"), result.name);
898
+ } else {
899
+ console.log(chalk3.red(" ✗"), result.name);
900
+ for (const error of result.errors) {
901
+ console.log(chalk3.red(" →"), error);
902
+ }
903
+ }
904
+ }
905
+ const passed = results.filter((r) => r.passed).length;
906
+ const failed = results.filter((r) => !r.passed).length;
907
+ console.log();
908
+ if (failed === 0) {
909
+ console.log(chalk3.green("All tests passed!"), chalk3.gray(`(${passed}/${results.length})`));
910
+ } else {
911
+ console.log(chalk3.red("Tests failed:"), chalk3.gray(`${passed}/${results.length} passed`));
912
+ }
913
+ console.log();
914
+ if (failed > 0) {
915
+ process.exit(1);
916
+ }
917
+ });
918
+ async function runDryTest(testCase) {
919
+ return {
920
+ name: testCase.name,
921
+ passed: true,
922
+ errors: []
923
+ };
924
+ }
925
+ async function runTest(testCase, agent, verbose) {
926
+ const errors = [];
927
+ const executor = new AgentExecutor2(agent);
928
+ const conversationId = `test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
929
+ const context = {
930
+ lastResponse: "",
931
+ toolCalls: [],
932
+ state: {}
933
+ };
934
+ try {
935
+ for (const message of testCase.conversation || []) {
936
+ if (message.role === "user") {
937
+ if (verbose) {
938
+ console.log(chalk3.cyan(" User:"), message.content.slice(0, 50) + (message.content.length > 50 ? "..." : ""));
939
+ }
940
+ const result = await executor.execute({
941
+ conversationId,
942
+ message: message.content
943
+ });
944
+ context.lastResponse = result.message;
945
+ context.toolCalls = result.toolCalls || [];
946
+ if (verbose) {
947
+ console.log(chalk3.green(" Assistant:"), result.message.slice(0, 50) + (result.message.length > 50 ? "..." : ""));
948
+ if (result.toolCalls && result.toolCalls.length > 0) {
949
+ console.log(chalk3.yellow(" Tools:"), result.toolCalls.map((t) => t.name).join(", "));
950
+ }
951
+ }
952
+ }
953
+ if (message.assertions) {
954
+ for (const assertion of message.assertions) {
955
+ const passed = checkAssertion(assertion, context);
956
+ if (!passed) {
957
+ errors.push(formatAssertionError(assertion, context));
958
+ }
959
+ }
960
+ }
961
+ }
962
+ if (testCase.assertions) {
963
+ for (const assertion of testCase.assertions) {
964
+ const passed = checkAssertion(assertion, context);
965
+ if (!passed) {
966
+ errors.push(formatAssertionError(assertion, context));
967
+ }
968
+ }
969
+ }
970
+ } catch (error) {
971
+ const errorMsg = error instanceof Error ? error.message : String(error);
972
+ errors.push(`Execution error: ${errorMsg}`);
973
+ }
974
+ return {
975
+ name: testCase.name,
976
+ passed: errors.length === 0,
977
+ errors
978
+ };
979
+ }
980
+ function checkAssertion(assertion, context) {
981
+ switch (assertion.type) {
982
+ case "contains":
983
+ return typeof assertion.value === "string" && context.lastResponse.toLowerCase().includes(assertion.value.toLowerCase());
984
+ case "matches":
985
+ return typeof assertion.value === "string" && new RegExp(assertion.value, "i").test(context.lastResponse);
986
+ case "toolCalled":
987
+ if (typeof assertion.value === "string") {
988
+ return context.toolCalls.some((tc) => tc.name === assertion.value);
989
+ }
990
+ return false;
991
+ case "stateEquals":
992
+ if (typeof assertion.value === "object" && assertion.value !== null) {
993
+ for (const [key, expected] of Object.entries(assertion.value)) {
994
+ if (context.state[key] !== expected) {
995
+ return false;
996
+ }
997
+ }
998
+ return true;
999
+ }
1000
+ return false;
1001
+ default:
1002
+ return false;
1003
+ }
1004
+ }
1005
+ function formatAssertionError(assertion, context) {
1006
+ switch (assertion.type) {
1007
+ case "contains":
1008
+ return `Expected response to contain "${assertion.value}", got: "${context.lastResponse.slice(0, 100)}..."`;
1009
+ case "matches":
1010
+ return `Expected response to match /${assertion.value}/, got: "${context.lastResponse.slice(0, 100)}..."`;
1011
+ case "toolCalled":
1012
+ const calledTools = context.toolCalls.map((tc) => tc.name).join(", ") || "none";
1013
+ return `Expected tool "${assertion.value}" to be called, called: [${calledTools}]`;
1014
+ case "stateEquals":
1015
+ return `State mismatch: expected ${JSON.stringify(assertion.value)}, got ${JSON.stringify(context.state)}`;
1016
+ default:
1017
+ return `Assertion failed: ${assertion.type} - ${JSON.stringify(assertion.value)}`;
1018
+ }
1019
+ }
1020
+
1021
+ // src/commands/deploy.ts
1022
+ import { Command as Command4 } from "commander";
1023
+ import chalk4 from "chalk";
1024
+ import ora4 from "ora";
1025
+ import { join as join7 } from "path";
1026
+ var deployCommand = new Command4("deploy").description("Deploy agent to Agent Factory cloud").option("-e, --env <environment>", "Target environment (preview, staging, production)", "preview").option("--dry-run", "Show what would be deployed without deploying").action(async (options) => {
1027
+ const spinner = ora4();
1028
+ const cwd = process.cwd();
1029
+ console.log();
1030
+ console.log(chalk4.bold("Deploying Agent"));
1031
+ console.log();
1032
+ spinner.start("Loading configuration");
1033
+ const config = await loadConfig(cwd);
1034
+ spinner.succeed("Configuration loaded");
1035
+ spinner.start("Loading agent");
1036
+ const agent = await loadAgent(cwd);
1037
+ spinner.succeed(`Agent "${agent.name}" loaded`);
1038
+ spinner.start("Validating agent");
1039
+ const errors = validateAgent(agent);
1040
+ if (errors.length > 0) {
1041
+ spinner.fail("Validation failed");
1042
+ console.log();
1043
+ for (const error of errors) {
1044
+ console.log(chalk4.red(" ✗"), error);
1045
+ }
1046
+ console.log();
1047
+ process.exit(1);
1048
+ }
1049
+ spinner.succeed("Agent validated");
1050
+ if (options.dryRun) {
1051
+ console.log();
1052
+ console.log(chalk4.yellow("Dry run mode - no changes will be made"));
1053
+ console.log();
1054
+ console.log("Would deploy:");
1055
+ console.log(chalk4.gray(" •"), `Agent: ${chalk4.cyan(agent.name)}`);
1056
+ console.log(chalk4.gray(" •"), `Version: ${chalk4.cyan(agent.version)}`);
1057
+ console.log(chalk4.gray(" •"), `Environment: ${chalk4.cyan(options.env)}`);
1058
+ console.log();
1059
+ return;
1060
+ }
1061
+ const credentials = loadCredentials();
1062
+ const apiKey = getApiKey();
1063
+ if (!credentials && !apiKey) {
1064
+ spinner.fail("Not authenticated");
1065
+ console.log();
1066
+ console.log(chalk4.gray("Run"), chalk4.cyan("af login"), chalk4.gray("to authenticate"));
1067
+ console.log(chalk4.gray("Or set"), chalk4.cyan("AGENT_FACTORY_API_KEY"), chalk4.gray("environment variable"));
1068
+ console.log();
1069
+ process.exit(1);
1070
+ }
1071
+ spinner.start("Building agent bundle");
1072
+ const result = await Bun.build({
1073
+ entrypoints: [join7(cwd, "src", "agent.ts")],
1074
+ target: "browser",
1075
+ minify: true
1076
+ });
1077
+ if (!result.success) {
1078
+ spinner.fail("Build failed");
1079
+ console.log();
1080
+ for (const log of result.logs) {
1081
+ console.log(chalk4.red(" •"), log);
1082
+ }
1083
+ console.log();
1084
+ process.exit(1);
1085
+ }
1086
+ const bundle = await result.outputs[0].text();
1087
+ spinner.succeed(`Bundle created (${formatBytes(bundle.length)})`);
1088
+ spinner.start(`Deploying to ${options.env}`);
1089
+ try {
1090
+ const api = new ApiClient;
1091
+ const agentSlug = agent.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
1092
+ let agentRecord = null;
1093
+ try {
1094
+ const { agents } = await api.listAgents();
1095
+ agentRecord = agents.find((a) => a.slug === agentSlug) || null;
1096
+ } catch {}
1097
+ if (!agentRecord) {
1098
+ spinner.text = "Creating agent";
1099
+ const { agent: newAgent } = await api.createAgent({
1100
+ name: agent.name,
1101
+ slug: agentSlug,
1102
+ description: agent.description
1103
+ });
1104
+ agentRecord = newAgent;
1105
+ }
1106
+ spinner.text = `Deploying to ${options.env}`;
1107
+ const { deployment } = await api.deployAgent(agentRecord.id, {
1108
+ bundle,
1109
+ version: agent.version,
1110
+ environment: options.env,
1111
+ metadata: {
1112
+ modelProvider: agent.model?.provider || "anthropic",
1113
+ modelName: agent.model?.name || "claude-sonnet-4-20250514",
1114
+ toolCount: agent.tools?.length || 0,
1115
+ bundleSize: bundle.length
1116
+ }
1117
+ });
1118
+ spinner.succeed(`Deployed to ${options.env}`);
1119
+ console.log();
1120
+ console.log(chalk4.green("Success!"), "Agent deployed");
1121
+ console.log();
1122
+ console.log("Deployment details:");
1123
+ console.log(chalk4.gray(" •"), `ID: ${chalk4.cyan(deployment.id)}`);
1124
+ console.log(chalk4.gray(" •"), `Version: ${chalk4.cyan(deployment.version)}`);
1125
+ console.log(chalk4.gray(" •"), `Environment: ${chalk4.cyan(deployment.environment)}`);
1126
+ console.log(chalk4.gray(" •"), `URL: ${chalk4.cyan(deployment.url)}`);
1127
+ console.log();
1128
+ console.log(chalk4.gray("Test your agent:"));
1129
+ console.log(chalk4.gray(" $"), chalk4.cyan(`curl -X POST ${deployment.url}/chat -H "Authorization: Bearer YOUR_API_KEY" -d '{"message": "Hello"}'`));
1130
+ console.log();
1131
+ } catch (error) {
1132
+ spinner.fail("Deployment failed");
1133
+ console.log();
1134
+ if (error instanceof ApiError) {
1135
+ console.log(chalk4.red("Error:"), error.message);
1136
+ if (error.status === 401) {
1137
+ console.log();
1138
+ console.log(chalk4.gray("Try running"), chalk4.cyan("af login"), chalk4.gray("to re-authenticate"));
1139
+ }
1140
+ } else {
1141
+ console.log(chalk4.red("Error:"), error instanceof Error ? error.message : String(error));
1142
+ }
1143
+ console.log();
1144
+ process.exit(1);
1145
+ }
1146
+ });
1147
+ function formatBytes(bytes) {
1148
+ if (bytes === 0)
1149
+ return "0 B";
1150
+ const k = 1024;
1151
+ const sizes = ["B", "KB", "MB", "GB"];
1152
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1153
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
1154
+ }
1155
+
1156
+ // src/commands/validate.ts
1157
+ import { Command as Command5 } from "commander";
1158
+ import chalk5 from "chalk";
1159
+ import ora5 from "ora";
1160
+ var validateCommand = new Command5("validate").description("Validate agent configuration").option("--strict", "Enable strict validation").action(async (options) => {
1161
+ const spinner = ora5();
1162
+ const cwd = process.cwd();
1163
+ console.log();
1164
+ console.log(chalk5.bold("Validating Agent"));
1165
+ console.log();
1166
+ spinner.start("Loading agent");
1167
+ let agent;
1168
+ try {
1169
+ agent = await loadAgent(cwd);
1170
+ spinner.succeed(`Agent "${agent.name}" loaded`);
1171
+ } catch (error) {
1172
+ spinner.fail("Failed to load agent");
1173
+ console.log();
1174
+ console.log(chalk5.red("Error:"), error instanceof Error ? error.message : String(error));
1175
+ console.log();
1176
+ process.exit(1);
1177
+ }
1178
+ spinner.start("Validating configuration");
1179
+ const errors = validateAgent(agent);
1180
+ const warnings = options.strict ? getStrictWarnings(agent) : [];
1181
+ if (errors.length === 0 && warnings.length === 0) {
1182
+ spinner.succeed("Agent is valid");
1183
+ console.log();
1184
+ console.log(chalk5.green("✓"), "No issues found");
1185
+ console.log();
1186
+ return;
1187
+ }
1188
+ if (errors.length > 0) {
1189
+ spinner.fail("Validation failed");
1190
+ console.log();
1191
+ console.log(chalk5.red("Errors:"));
1192
+ for (const error of errors) {
1193
+ console.log(chalk5.red(" ✗"), error);
1194
+ }
1195
+ } else {
1196
+ spinner.succeed("Validation passed with warnings");
1197
+ }
1198
+ if (warnings.length > 0) {
1199
+ console.log();
1200
+ console.log(chalk5.yellow("Warnings:"));
1201
+ for (const warning of warnings) {
1202
+ console.log(chalk5.yellow(" ⚠"), warning);
1203
+ }
1204
+ }
1205
+ console.log();
1206
+ if (errors.length > 0) {
1207
+ process.exit(1);
1208
+ }
1209
+ });
1210
+ function getStrictWarnings(agent) {
1211
+ const warnings = [];
1212
+ if (!agent.description) {
1213
+ warnings.push("Agent is missing a description");
1214
+ }
1215
+ if (!agent.tools || agent.tools.length === 0) {
1216
+ warnings.push("Agent has no tools defined");
1217
+ }
1218
+ return warnings;
1219
+ }
1220
+
1221
+ // src/commands/logs.ts
1222
+ import { Command as Command6 } from "commander";
1223
+ import chalk6 from "chalk";
1224
+ import ora6 from "ora";
1225
+ var logsCommand = new Command6("logs").description("Stream production logs").option("-e, --env <environment>", "Environment to stream from", "production").option("-f, --follow", "Follow log output").option("-n, --lines <number>", "Number of lines to show", "100").action(async (options) => {
1226
+ const spinner = ora6();
1227
+ console.log();
1228
+ console.log(chalk6.bold("Streaming Logs"));
1229
+ console.log();
1230
+ const apiKey = process.env.STRUERE_API_KEY;
1231
+ if (!apiKey) {
1232
+ console.log(chalk6.red("Error:"), "Missing STRUERE_API_KEY environment variable");
1233
+ console.log();
1234
+ console.log("Set your API key:");
1235
+ console.log(chalk6.gray(" $"), chalk6.cyan("export STRUERE_API_KEY=your_api_key"));
1236
+ console.log();
1237
+ process.exit(1);
1238
+ }
1239
+ const apiUrl = process.env.STRUERE_API_URL || "https://api.struere.dev";
1240
+ if (options.follow) {
1241
+ spinner.start(`Connecting to ${options.env} logs`);
1242
+ try {
1243
+ const wsUrl = apiUrl.replace("https://", "wss://").replace("http://", "ws://");
1244
+ const ws = new WebSocket(`${wsUrl}/v1/logs/stream?env=${options.env}`, {
1245
+ headers: {
1246
+ Authorization: `Bearer ${apiKey}`
1247
+ }
1248
+ });
1249
+ ws.onopen = () => {
1250
+ spinner.succeed(`Connected to ${options.env}`);
1251
+ console.log();
1252
+ console.log(chalk6.gray("Streaming logs... (Ctrl+C to stop)"));
1253
+ console.log();
1254
+ };
1255
+ ws.onmessage = (event) => {
1256
+ const log = JSON.parse(event.data);
1257
+ const levelColor = log.level === "error" ? chalk6.red : log.level === "warn" ? chalk6.yellow : chalk6.gray;
1258
+ console.log(chalk6.gray(new Date(log.timestamp).toISOString()), levelColor(`[${log.level}]`), log.message);
1259
+ };
1260
+ ws.onerror = () => {
1261
+ spinner.fail("Connection error");
1262
+ process.exit(1);
1263
+ };
1264
+ ws.onclose = () => {
1265
+ console.log();
1266
+ console.log(chalk6.gray("Connection closed"));
1267
+ process.exit(0);
1268
+ };
1269
+ process.on("SIGINT", () => {
1270
+ console.log();
1271
+ ws.close();
1272
+ });
1273
+ } catch (error) {
1274
+ spinner.fail("Failed to connect");
1275
+ console.log();
1276
+ console.log(chalk6.red("Error:"), error instanceof Error ? error.message : String(error));
1277
+ console.log();
1278
+ process.exit(1);
1279
+ }
1280
+ } else {
1281
+ spinner.start("Fetching logs");
1282
+ try {
1283
+ const response = await fetch(`${apiUrl}/v1/logs?env=${options.env}&lines=${options.lines}`, {
1284
+ headers: {
1285
+ Authorization: `Bearer ${apiKey}`
1286
+ }
1287
+ });
1288
+ if (!response.ok) {
1289
+ throw new Error(`HTTP ${response.status}`);
1290
+ }
1291
+ const logs = await response.json();
1292
+ spinner.succeed(`Fetched ${logs.length} log entries`);
1293
+ console.log();
1294
+ for (const log of logs) {
1295
+ const levelColor = log.level === "error" ? chalk6.red : log.level === "warn" ? chalk6.yellow : chalk6.gray;
1296
+ console.log(chalk6.gray(new Date(log.timestamp).toISOString()), levelColor(`[${log.level}]`), log.message);
1297
+ }
1298
+ console.log();
1299
+ } catch (error) {
1300
+ spinner.fail("Failed to fetch logs");
1301
+ console.log();
1302
+ console.log(chalk6.red("Error:"), error instanceof Error ? error.message : String(error));
1303
+ console.log();
1304
+ process.exit(1);
1305
+ }
1306
+ }
1307
+ });
1308
+
1309
+ // src/commands/state.ts
1310
+ import { Command as Command7 } from "commander";
1311
+ import chalk7 from "chalk";
1312
+ import ora7 from "ora";
1313
+ var stateCommand = new Command7("state").description("Inspect conversation state").argument("<id>", "Conversation ID").option("-e, --env <environment>", "Environment", "production").option("--json", "Output as JSON").action(async (id, options) => {
1314
+ const spinner = ora7();
1315
+ console.log();
1316
+ console.log(chalk7.bold("Conversation State"));
1317
+ console.log();
1318
+ const apiKey = process.env.STRUERE_API_KEY;
1319
+ if (!apiKey) {
1320
+ console.log(chalk7.red("Error:"), "Missing STRUERE_API_KEY environment variable");
1321
+ console.log();
1322
+ console.log("Set your API key:");
1323
+ console.log(chalk7.gray(" $"), chalk7.cyan("export STRUERE_API_KEY=your_api_key"));
1324
+ console.log();
1325
+ process.exit(1);
1326
+ }
1327
+ const apiUrl = process.env.STRUERE_API_URL || "https://api.struere.dev";
1328
+ spinner.start("Fetching conversation state");
1329
+ try {
1330
+ const response = await fetch(`${apiUrl}/v1/conversations/${id}/state?env=${options.env}`, {
1331
+ headers: {
1332
+ Authorization: `Bearer ${apiKey}`
1333
+ }
1334
+ });
1335
+ if (!response.ok) {
1336
+ if (response.status === 404) {
1337
+ throw new Error("Conversation not found");
1338
+ }
1339
+ throw new Error(`HTTP ${response.status}`);
1340
+ }
1341
+ const state = await response.json();
1342
+ spinner.succeed("State retrieved");
1343
+ if (options.json) {
1344
+ console.log();
1345
+ console.log(JSON.stringify(state, null, 2));
1346
+ console.log();
1347
+ return;
1348
+ }
1349
+ console.log();
1350
+ console.log(chalk7.gray("Conversation:"), chalk7.cyan(state.conversationId));
1351
+ console.log(chalk7.gray("Created:"), new Date(state.createdAt).toLocaleString());
1352
+ console.log(chalk7.gray("Updated:"), new Date(state.updatedAt).toLocaleString());
1353
+ console.log(chalk7.gray("Messages:"), state.messageCount);
1354
+ console.log();
1355
+ console.log(chalk7.bold("State:"));
1356
+ if (Object.keys(state.state).length === 0) {
1357
+ console.log(chalk7.gray(" (empty)"));
1358
+ } else {
1359
+ for (const [key, value] of Object.entries(state.state)) {
1360
+ const displayValue = typeof value === "object" ? JSON.stringify(value) : String(value);
1361
+ console.log(chalk7.gray(" •"), `${key}:`, chalk7.cyan(displayValue));
1362
+ }
1363
+ }
1364
+ console.log();
1365
+ } catch (error) {
1366
+ spinner.fail("Failed to fetch state");
1367
+ console.log();
1368
+ console.log(chalk7.red("Error:"), error instanceof Error ? error.message : String(error));
1369
+ console.log();
1370
+ process.exit(1);
1371
+ }
1372
+ });
1373
+
1374
+ // src/commands/login.ts
1375
+ import { Command as Command8 } from "commander";
1376
+ import chalk8 from "chalk";
1377
+ import ora8 from "ora";
1378
+ var CLERK_PUBLISHABLE_KEY = process.env.CLERK_PUBLISHABLE_KEY || "pk_test_placeholder";
1379
+ var AUTH_CALLBACK_PORT = 9876;
1380
+ var loginCommand = new Command8("login").description("Log in to Agent Factory").option("--headless", "Login with email/password (no browser)").action(async (options) => {
1381
+ const spinner = ora8();
1382
+ console.log();
1383
+ console.log(chalk8.bold("Struere Login"));
1384
+ console.log();
1385
+ const existing = loadCredentials();
1386
+ if (existing) {
1387
+ console.log(chalk8.yellow("Already logged in as"), chalk8.cyan(existing.user.email));
1388
+ console.log(chalk8.gray("Run"), chalk8.cyan("af logout"), chalk8.gray("to log out first"));
1389
+ console.log();
1390
+ return;
1391
+ }
1392
+ if (options.headless) {
1393
+ await headlessLogin(spinner);
1394
+ } else {
1395
+ await browserLogin(spinner);
1396
+ }
1397
+ });
1398
+ async function browserLogin(spinner) {
1399
+ spinner.start("Starting authentication server");
1400
+ const authPromise = new Promise((resolve, reject) => {
1401
+ const server = Bun.serve({
1402
+ port: AUTH_CALLBACK_PORT,
1403
+ async fetch(req) {
1404
+ const url = new URL(req.url);
1405
+ if (url.pathname === "/callback") {
1406
+ const token = url.searchParams.get("token");
1407
+ const sessionId = url.searchParams.get("session_id");
1408
+ if (token && sessionId) {
1409
+ resolve({ token, sessionId });
1410
+ return new Response(getSuccessHtml(), {
1411
+ headers: { "Content-Type": "text/html" }
1412
+ });
1413
+ }
1414
+ return new Response(getErrorHtml("Missing token"), {
1415
+ status: 400,
1416
+ headers: { "Content-Type": "text/html" }
1417
+ });
1418
+ }
1419
+ if (url.pathname === "/") {
1420
+ const authUrl = getAuthUrl();
1421
+ return Response.redirect(authUrl, 302);
1422
+ }
1423
+ return new Response("Not Found", { status: 404 });
1424
+ }
1425
+ });
1426
+ setTimeout(() => {
1427
+ server.stop();
1428
+ reject(new Error("Authentication timed out"));
1429
+ }, 5 * 60 * 1000);
1430
+ });
1431
+ spinner.succeed("Authentication server started");
1432
+ const loginUrl = `http://localhost:${AUTH_CALLBACK_PORT}`;
1433
+ console.log();
1434
+ console.log(chalk8.gray("Opening browser to log in..."));
1435
+ console.log(chalk8.gray("If browser does not open, visit:"), chalk8.cyan(loginUrl));
1436
+ console.log();
1437
+ if (process.platform === "darwin") {
1438
+ Bun.spawn(["open", loginUrl]);
1439
+ } else if (process.platform === "linux") {
1440
+ Bun.spawn(["xdg-open", loginUrl]);
1441
+ } else if (process.platform === "win32") {
1442
+ Bun.spawn(["cmd", "/c", "start", loginUrl]);
1443
+ }
1444
+ spinner.start("Waiting for authentication");
1445
+ try {
1446
+ const { token, sessionId } = await authPromise;
1447
+ spinner.text = "Fetching user info";
1448
+ const api = new ApiClient;
1449
+ const { user, organization } = await api.getMe();
1450
+ saveCredentials({
1451
+ token,
1452
+ user: {
1453
+ id: user.id,
1454
+ email: user.email,
1455
+ name: user.name,
1456
+ organizationId: user.organizationId
1457
+ },
1458
+ organization: {
1459
+ id: organization.id,
1460
+ name: organization.name,
1461
+ slug: organization.slug
1462
+ },
1463
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()
1464
+ });
1465
+ spinner.succeed("Logged in successfully");
1466
+ console.log();
1467
+ console.log(chalk8.green("Welcome,"), chalk8.cyan(user.name));
1468
+ console.log(chalk8.gray("Organization:"), organization.name);
1469
+ console.log();
1470
+ printNextSteps();
1471
+ } catch (error) {
1472
+ spinner.fail("Login failed");
1473
+ console.log();
1474
+ console.log(chalk8.red("Error:"), error instanceof Error ? error.message : String(error));
1475
+ console.log();
1476
+ console.log(chalk8.gray("Try"), chalk8.cyan("af login --headless"), chalk8.gray("for email/password login"));
1477
+ console.log();
1478
+ process.exit(1);
1479
+ }
1480
+ }
1481
+ async function headlessLogin(spinner) {
1482
+ const email = await prompt("Email: ");
1483
+ const password = await prompt("Password: ", true);
1484
+ if (!email || !password) {
1485
+ console.log(chalk8.red("Email and password are required"));
1486
+ process.exit(1);
1487
+ }
1488
+ spinner.start("Logging in");
1489
+ try {
1490
+ const api = new ApiClient;
1491
+ const { token, user } = await api.login(email, password);
1492
+ const { organization } = await api.getMe();
1493
+ saveCredentials({
1494
+ token,
1495
+ user: {
1496
+ id: user.id,
1497
+ email: user.email,
1498
+ name: user.name,
1499
+ organizationId: user.organizationId
1500
+ },
1501
+ organization: {
1502
+ id: organization.id,
1503
+ name: organization.name,
1504
+ slug: organization.slug
1505
+ },
1506
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()
1507
+ });
1508
+ spinner.succeed("Logged in successfully");
1509
+ console.log();
1510
+ console.log(chalk8.green("Welcome,"), chalk8.cyan(user.name));
1511
+ console.log(chalk8.gray("Organization:"), organization.name);
1512
+ console.log();
1513
+ printNextSteps();
1514
+ } catch (error) {
1515
+ spinner.fail("Login failed");
1516
+ console.log();
1517
+ if (error instanceof ApiError) {
1518
+ console.log(chalk8.red("Error:"), error.message);
1519
+ } else {
1520
+ console.log(chalk8.red("Error:"), error instanceof Error ? error.message : String(error));
1521
+ }
1522
+ console.log();
1523
+ process.exit(1);
1524
+ }
1525
+ }
1526
+ function printNextSteps() {
1527
+ console.log(chalk8.gray("You can now use:"));
1528
+ console.log(chalk8.gray(" •"), chalk8.cyan("af dev"), chalk8.gray("- Start cloud-connected dev server"));
1529
+ console.log(chalk8.gray(" •"), chalk8.cyan("af deploy"), chalk8.gray("- Deploy your agent"));
1530
+ console.log(chalk8.gray(" •"), chalk8.cyan("af logs"), chalk8.gray("- View agent logs"));
1531
+ console.log();
1532
+ }
1533
+ function getAuthUrl() {
1534
+ const baseUrl = process.env.STRUERE_AUTH_URL || "https://struere.dev";
1535
+ const callbackUrl = `http://localhost:${AUTH_CALLBACK_PORT}/callback`;
1536
+ return `${baseUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
1537
+ }
1538
+ function getSuccessHtml() {
1539
+ return `<!DOCTYPE html>
1540
+ <html>
1541
+ <head>
1542
+ <title>Login Successful</title>
1543
+ <style>
1544
+ body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa; }
1545
+ .container { text-align: center; }
1546
+ h1 { color: #22c55e; }
1547
+ p { color: #888; }
1548
+ </style>
1549
+ </head>
1550
+ <body>
1551
+ <div class="container">
1552
+ <h1>Login Successful</h1>
1553
+ <p>You can close this window and return to the terminal.</p>
1554
+ </div>
1555
+ <script>setTimeout(() => window.close(), 3000)</script>
1556
+ </body>
1557
+ </html>`;
1558
+ }
1559
+ function getErrorHtml(message) {
1560
+ return `<!DOCTYPE html>
1561
+ <html>
1562
+ <head>
1563
+ <title>Login Failed</title>
1564
+ <style>
1565
+ body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa; }
1566
+ .container { text-align: center; }
1567
+ h1 { color: #ef4444; }
1568
+ p { color: #888; }
1569
+ </style>
1570
+ </head>
1571
+ <body>
1572
+ <div class="container">
1573
+ <h1>Login Failed</h1>
1574
+ <p>${message}</p>
1575
+ </div>
1576
+ </body>
1577
+ </html>`;
1578
+ }
1579
+ async function prompt(message, hidden = false) {
1580
+ process.stdout.write(chalk8.gray(message));
1581
+ return new Promise((resolve) => {
1582
+ let input = "";
1583
+ if (hidden) {
1584
+ process.stdin.setRawMode(true);
1585
+ }
1586
+ process.stdin.resume();
1587
+ process.stdin.setEncoding("utf8");
1588
+ const onData = (char) => {
1589
+ if (char === `
1590
+ ` || char === "\r") {
1591
+ process.stdin.removeListener("data", onData);
1592
+ process.stdin.pause();
1593
+ if (hidden) {
1594
+ process.stdin.setRawMode(false);
1595
+ console.log();
1596
+ }
1597
+ resolve(input);
1598
+ } else if (char === "\x03") {
1599
+ process.exit();
1600
+ } else if (char === "") {
1601
+ input = input.slice(0, -1);
1602
+ } else {
1603
+ input += char;
1604
+ if (!hidden) {
1605
+ process.stdout.write(char);
1606
+ }
1607
+ }
1608
+ };
1609
+ process.stdin.on("data", onData);
1610
+ });
1611
+ }
1612
+
1613
+ // src/commands/logout.ts
1614
+ import { Command as Command9 } from "commander";
1615
+ import chalk9 from "chalk";
1616
+ var logoutCommand = new Command9("logout").description("Log out of Agent Factory").action(async () => {
1617
+ console.log();
1618
+ const credentials = loadCredentials();
1619
+ if (!credentials) {
1620
+ console.log(chalk9.yellow("Not currently logged in"));
1621
+ console.log();
1622
+ return;
1623
+ }
1624
+ clearCredentials();
1625
+ console.log(chalk9.green("Logged out successfully"));
1626
+ console.log(chalk9.gray("Goodbye,"), chalk9.cyan(credentials.user.name));
1627
+ console.log();
1628
+ });
1629
+
1630
+ // src/commands/whoami.ts
1631
+ import { Command as Command10 } from "commander";
1632
+ import chalk10 from "chalk";
1633
+ import ora9 from "ora";
1634
+ var whoamiCommand = new Command10("whoami").description("Show current logged in user").option("--refresh", "Refresh user info from server").action(async (options) => {
1635
+ console.log();
1636
+ const credentials = loadCredentials();
1637
+ if (!credentials) {
1638
+ console.log(chalk10.yellow("Not logged in"));
1639
+ console.log();
1640
+ console.log(chalk10.gray("Run"), chalk10.cyan("af login"), chalk10.gray("to log in"));
1641
+ console.log();
1642
+ return;
1643
+ }
1644
+ if (options.refresh) {
1645
+ const spinner = ora9("Fetching user info").start();
1646
+ try {
1647
+ const api = new ApiClient;
1648
+ const { user, organization } = await api.getMe();
1649
+ spinner.stop();
1650
+ console.log(chalk10.bold("Logged in as:"));
1651
+ console.log();
1652
+ console.log(chalk10.gray(" User: "), chalk10.cyan(user.name), chalk10.gray(`<${user.email}>`));
1653
+ console.log(chalk10.gray(" User ID: "), chalk10.gray(user.id));
1654
+ console.log(chalk10.gray(" Role: "), chalk10.cyan(user.role));
1655
+ console.log();
1656
+ console.log(chalk10.gray(" Organization:"), chalk10.cyan(organization.name));
1657
+ console.log(chalk10.gray(" Org ID: "), chalk10.gray(organization.id));
1658
+ console.log(chalk10.gray(" Slug: "), chalk10.cyan(organization.slug));
1659
+ console.log(chalk10.gray(" Plan: "), chalk10.cyan(organization.plan));
1660
+ console.log();
1661
+ } catch (error) {
1662
+ spinner.fail("Failed to fetch user info");
1663
+ console.log();
1664
+ if (error instanceof ApiError) {
1665
+ if (error.status === 401) {
1666
+ console.log(chalk10.red("Session expired. Please log in again."));
1667
+ } else {
1668
+ console.log(chalk10.red("Error:"), error.message);
1669
+ }
1670
+ } else {
1671
+ console.log(chalk10.red("Error:"), error instanceof Error ? error.message : String(error));
1672
+ }
1673
+ console.log();
1674
+ process.exit(1);
1675
+ }
1676
+ } else {
1677
+ console.log(chalk10.bold("Logged in as:"));
1678
+ console.log();
1679
+ console.log(chalk10.gray(" User: "), chalk10.cyan(credentials.user.name), chalk10.gray(`<${credentials.user.email}>`));
1680
+ console.log(chalk10.gray(" User ID: "), chalk10.gray(credentials.user.id));
1681
+ console.log();
1682
+ console.log(chalk10.gray(" Organization:"), chalk10.cyan(credentials.organization.name));
1683
+ console.log(chalk10.gray(" Org ID: "), chalk10.gray(credentials.organization.id));
1684
+ console.log(chalk10.gray(" Slug: "), chalk10.cyan(credentials.organization.slug));
1685
+ console.log();
1686
+ console.log(chalk10.gray("Use"), chalk10.cyan("af whoami --refresh"), chalk10.gray("to fetch latest info"));
1687
+ console.log();
1688
+ }
1689
+ });
1690
+
1691
+ // src/index.ts
1692
+ program.name("struere").description("Struere CLI - Build, test, and deploy AI agents").version("0.1.0");
1693
+ program.addCommand(loginCommand);
1694
+ program.addCommand(logoutCommand);
1695
+ program.addCommand(whoamiCommand);
1696
+ program.addCommand(devCommand);
1697
+ program.addCommand(buildCommand);
1698
+ program.addCommand(testCommand);
1699
+ program.addCommand(deployCommand);
1700
+ program.addCommand(validateCommand);
1701
+ program.addCommand(logsCommand);
1702
+ program.addCommand(stateCommand);
1703
+ program.parse();