struere 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/fsevents-hj42pnne.node +0 -0
- package/dist/bin/struere.d.ts +3 -0
- package/dist/bin/struere.d.ts.map +1 -0
- package/dist/bin/struere.js +18761 -0
- package/dist/cli/commands/build.d.ts +3 -0
- package/dist/cli/commands/build.d.ts.map +1 -0
- package/dist/cli/commands/deploy.d.ts +3 -0
- package/dist/cli/commands/deploy.d.ts.map +1 -0
- package/dist/cli/commands/dev.d.ts +3 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/login.d.ts +5 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/logout.d.ts +3 -0
- package/dist/cli/commands/logout.d.ts.map +1 -0
- package/dist/cli/commands/logs.d.ts +3 -0
- package/dist/cli/commands/logs.d.ts.map +1 -0
- package/dist/cli/commands/state.d.ts +3 -0
- package/dist/cli/commands/state.d.ts.map +1 -0
- package/dist/cli/commands/test.d.ts +3 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/whoami.d.ts +3 -0
- package/dist/cli/commands/whoami.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2126 -0
- package/dist/cli/templates/index.d.ts +12 -0
- package/dist/cli/templates/index.d.ts.map +1 -0
- package/dist/cli/utils/agent.d.ts +3 -0
- package/dist/cli/utils/agent.d.ts.map +1 -0
- package/dist/cli/utils/config.d.ts +3 -0
- package/dist/cli/utils/config.d.ts.map +1 -0
- package/dist/cli/utils/convex.d.ts +130 -0
- package/dist/cli/utils/convex.d.ts.map +1 -0
- package/dist/cli/utils/credentials.d.ts +23 -0
- package/dist/cli/utils/credentials.d.ts.map +1 -0
- package/dist/cli/utils/project.d.ts +12 -0
- package/dist/cli/utils/project.d.ts.map +1 -0
- package/dist/cli/utils/scaffold.d.ts +17 -0
- package/dist/cli/utils/scaffold.d.ts.map +1 -0
- package/dist/cli/utils/validate.d.ts +3 -0
- package/dist/cli/utils/validate.d.ts.map +1 -0
- package/dist/define/agent.d.ts +3 -0
- package/dist/define/agent.d.ts.map +1 -0
- package/dist/define/config.d.ts +3 -0
- package/dist/define/config.d.ts.map +1 -0
- package/dist/define/context.d.ts +3 -0
- package/dist/define/context.d.ts.map +1 -0
- package/dist/define/index.d.ts +5 -0
- package/dist/define/index.d.ts.map +1 -0
- package/dist/define/tools.d.ts +10 -0
- package/dist/define/tools.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +98 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,2126 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/cli/index.ts
|
|
5
|
+
import { program } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/cli/commands/init.ts
|
|
8
|
+
import { Command as Command2 } from "commander";
|
|
9
|
+
import chalk2 from "chalk";
|
|
10
|
+
import ora2 from "ora";
|
|
11
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
12
|
+
import { join as join4, basename } from "path";
|
|
13
|
+
|
|
14
|
+
// src/cli/utils/credentials.ts
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
18
|
+
var CONFIG_DIR = join(homedir(), ".struere");
|
|
19
|
+
var CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
|
|
20
|
+
function ensureConfigDir() {
|
|
21
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
22
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function saveCredentials(credentials) {
|
|
26
|
+
ensureConfigDir();
|
|
27
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 384 });
|
|
28
|
+
}
|
|
29
|
+
function loadCredentials() {
|
|
30
|
+
if (!existsSync(CREDENTIALS_FILE)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const data = readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
35
|
+
const credentials = JSON.parse(data);
|
|
36
|
+
if (new Date(credentials.expiresAt) < new Date) {
|
|
37
|
+
clearCredentials();
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return credentials;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function clearCredentials() {
|
|
46
|
+
if (existsSync(CREDENTIALS_FILE)) {
|
|
47
|
+
unlinkSync(CREDENTIALS_FILE);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function getApiKey() {
|
|
51
|
+
const credentials = loadCredentials();
|
|
52
|
+
if (credentials?.apiKey) {
|
|
53
|
+
return credentials.apiKey;
|
|
54
|
+
}
|
|
55
|
+
return process.env.STRUERE_API_KEY || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/cli/commands/login.ts
|
|
59
|
+
import { Command } from "commander";
|
|
60
|
+
import chalk from "chalk";
|
|
61
|
+
import ora from "ora";
|
|
62
|
+
|
|
63
|
+
// src/cli/utils/convex.ts
|
|
64
|
+
var CONVEX_URL = process.env.STRUERE_CONVEX_URL || "https://struere.convex.cloud";
|
|
65
|
+
async function syncToConvex(agentId, config) {
|
|
66
|
+
const credentials = loadCredentials();
|
|
67
|
+
const apiKey = getApiKey();
|
|
68
|
+
const token = apiKey || credentials?.token;
|
|
69
|
+
if (!token) {
|
|
70
|
+
return { success: false, error: "Not authenticated" };
|
|
71
|
+
}
|
|
72
|
+
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
Authorization: `Bearer ${token}`
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
path: "agents:syncDevelopment",
|
|
80
|
+
args: {
|
|
81
|
+
agentId,
|
|
82
|
+
config
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const error = await response.text();
|
|
88
|
+
return { success: false, error };
|
|
89
|
+
}
|
|
90
|
+
const result = await response.json();
|
|
91
|
+
return { success: result.success ?? true };
|
|
92
|
+
}
|
|
93
|
+
async function deployToProduction(agentId) {
|
|
94
|
+
const credentials = loadCredentials();
|
|
95
|
+
const apiKey = getApiKey();
|
|
96
|
+
const token = apiKey || credentials?.token;
|
|
97
|
+
if (!token) {
|
|
98
|
+
return { success: false, error: "Not authenticated" };
|
|
99
|
+
}
|
|
100
|
+
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
Authorization: `Bearer ${token}`
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
path: "agents:deploy",
|
|
108
|
+
args: { agentId }
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const error = await response.text();
|
|
113
|
+
return { success: false, error };
|
|
114
|
+
}
|
|
115
|
+
const result = await response.json();
|
|
116
|
+
return { success: result.success ?? true, configId: result.configId };
|
|
117
|
+
}
|
|
118
|
+
async function listAgents() {
|
|
119
|
+
const credentials = loadCredentials();
|
|
120
|
+
const apiKey = getApiKey();
|
|
121
|
+
const token = apiKey || credentials?.token;
|
|
122
|
+
if (!token) {
|
|
123
|
+
return { agents: [], error: "Not authenticated" };
|
|
124
|
+
}
|
|
125
|
+
const response = await fetch(`${CONVEX_URL}/api/query`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
Authorization: `Bearer ${token}`
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
path: "agents:list",
|
|
133
|
+
args: {}
|
|
134
|
+
})
|
|
135
|
+
});
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
const error = await response.text();
|
|
138
|
+
return { agents: [], error };
|
|
139
|
+
}
|
|
140
|
+
const result = await response.json();
|
|
141
|
+
const agents = Array.isArray(result) ? result : result?.value || [];
|
|
142
|
+
return { agents };
|
|
143
|
+
}
|
|
144
|
+
async function createAgent(data) {
|
|
145
|
+
const credentials = loadCredentials();
|
|
146
|
+
const apiKey = getApiKey();
|
|
147
|
+
const token = apiKey || credentials?.token;
|
|
148
|
+
if (!token) {
|
|
149
|
+
return { error: "Not authenticated" };
|
|
150
|
+
}
|
|
151
|
+
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: {
|
|
154
|
+
"Content-Type": "application/json",
|
|
155
|
+
Authorization: `Bearer ${token}`
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
path: "agents:create",
|
|
159
|
+
args: data
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
const error = await response.text();
|
|
164
|
+
return { error };
|
|
165
|
+
}
|
|
166
|
+
const agentId = await response.json();
|
|
167
|
+
return { agentId };
|
|
168
|
+
}
|
|
169
|
+
async function getUserInfo(token) {
|
|
170
|
+
const response = await fetch(`${CONVEX_URL}/api/query`, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: {
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
Authorization: `Bearer ${token}`
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
path: "users:getCurrent",
|
|
178
|
+
args: {}
|
|
179
|
+
})
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const error = await response.text();
|
|
183
|
+
return { error };
|
|
184
|
+
}
|
|
185
|
+
const user = await response.json();
|
|
186
|
+
if (!user) {
|
|
187
|
+
return { error: "User not found" };
|
|
188
|
+
}
|
|
189
|
+
const orgResponse = await fetch(`${CONVEX_URL}/api/query`, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: {
|
|
192
|
+
"Content-Type": "application/json",
|
|
193
|
+
Authorization: `Bearer ${token}`
|
|
194
|
+
},
|
|
195
|
+
body: JSON.stringify({
|
|
196
|
+
path: "organizations:getCurrent",
|
|
197
|
+
args: {}
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
if (!orgResponse.ok) {
|
|
201
|
+
return { error: "Failed to fetch organization" };
|
|
202
|
+
}
|
|
203
|
+
const org = await orgResponse.json();
|
|
204
|
+
return {
|
|
205
|
+
userInfo: {
|
|
206
|
+
user: {
|
|
207
|
+
id: user._id,
|
|
208
|
+
email: user.email,
|
|
209
|
+
name: user.name,
|
|
210
|
+
organizationId: user.organizationId
|
|
211
|
+
},
|
|
212
|
+
organization: {
|
|
213
|
+
id: org._id,
|
|
214
|
+
name: org.name,
|
|
215
|
+
slug: org.slug
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function extractConfig(agent) {
|
|
221
|
+
const BUILTIN_TOOLS = [
|
|
222
|
+
"entity.create",
|
|
223
|
+
"entity.get",
|
|
224
|
+
"entity.query",
|
|
225
|
+
"entity.update",
|
|
226
|
+
"entity.delete",
|
|
227
|
+
"entity.link",
|
|
228
|
+
"entity.unlink",
|
|
229
|
+
"event.emit",
|
|
230
|
+
"event.query",
|
|
231
|
+
"job.enqueue",
|
|
232
|
+
"job.status"
|
|
233
|
+
];
|
|
234
|
+
let systemPrompt;
|
|
235
|
+
if (typeof agent.systemPrompt === "function") {
|
|
236
|
+
const result = agent.systemPrompt();
|
|
237
|
+
if (result instanceof Promise) {
|
|
238
|
+
throw new Error("Async system prompts must be resolved before syncing");
|
|
239
|
+
}
|
|
240
|
+
systemPrompt = result;
|
|
241
|
+
} else {
|
|
242
|
+
systemPrompt = agent.systemPrompt;
|
|
243
|
+
}
|
|
244
|
+
const tools = (agent.tools || []).map((tool) => {
|
|
245
|
+
const isBuiltin = BUILTIN_TOOLS.includes(tool.name);
|
|
246
|
+
let handlerCode;
|
|
247
|
+
if (!isBuiltin && tool.handler) {
|
|
248
|
+
handlerCode = extractHandlerCode(tool.handler);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
name: tool.name,
|
|
252
|
+
description: tool.description,
|
|
253
|
+
parameters: tool.parameters || { type: "object", properties: {} },
|
|
254
|
+
handlerCode,
|
|
255
|
+
isBuiltin
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
name: agent.name,
|
|
260
|
+
version: agent.version || "0.0.1",
|
|
261
|
+
systemPrompt,
|
|
262
|
+
model: {
|
|
263
|
+
provider: agent.model?.provider || "anthropic",
|
|
264
|
+
name: agent.model?.name || "claude-sonnet-4-20250514",
|
|
265
|
+
temperature: agent.model?.temperature,
|
|
266
|
+
maxTokens: agent.model?.maxTokens
|
|
267
|
+
},
|
|
268
|
+
tools
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function extractHandlerCode(handler) {
|
|
272
|
+
const code = handler.toString();
|
|
273
|
+
const arrowMatch = code.match(/(?:async\s*)?\([^)]*\)\s*=>\s*\{?([\s\S]*)\}?$/);
|
|
274
|
+
if (arrowMatch) {
|
|
275
|
+
let body = arrowMatch[1].trim();
|
|
276
|
+
if (body.startsWith("{") && body.endsWith("}")) {
|
|
277
|
+
body = body.slice(1, -1).trim();
|
|
278
|
+
}
|
|
279
|
+
return body;
|
|
280
|
+
}
|
|
281
|
+
const funcMatch = code.match(/(?:async\s*)?function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
|
|
282
|
+
if (funcMatch) {
|
|
283
|
+
return funcMatch[1].trim();
|
|
284
|
+
}
|
|
285
|
+
return code;
|
|
286
|
+
}
|
|
287
|
+
async function getRecentExecutions(limit = 100) {
|
|
288
|
+
const credentials = loadCredentials();
|
|
289
|
+
const apiKey = getApiKey();
|
|
290
|
+
const token = apiKey || credentials?.token;
|
|
291
|
+
if (!token) {
|
|
292
|
+
return { executions: [], error: "Not authenticated" };
|
|
293
|
+
}
|
|
294
|
+
const response = await fetch(`${CONVEX_URL}/api/query`, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: {
|
|
297
|
+
"Content-Type": "application/json",
|
|
298
|
+
Authorization: `Bearer ${token}`
|
|
299
|
+
},
|
|
300
|
+
body: JSON.stringify({
|
|
301
|
+
path: "executions:list",
|
|
302
|
+
args: { limit }
|
|
303
|
+
})
|
|
304
|
+
});
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
const error = await response.text();
|
|
307
|
+
return { executions: [], error };
|
|
308
|
+
}
|
|
309
|
+
const executions = await response.json();
|
|
310
|
+
return { executions: executions || [] };
|
|
311
|
+
}
|
|
312
|
+
async function getThreadState(threadId) {
|
|
313
|
+
const credentials = loadCredentials();
|
|
314
|
+
const apiKey = getApiKey();
|
|
315
|
+
const token = apiKey || credentials?.token;
|
|
316
|
+
if (!token) {
|
|
317
|
+
return { error: "Not authenticated" };
|
|
318
|
+
}
|
|
319
|
+
const response = await fetch(`${CONVEX_URL}/api/query`, {
|
|
320
|
+
method: "POST",
|
|
321
|
+
headers: {
|
|
322
|
+
"Content-Type": "application/json",
|
|
323
|
+
Authorization: `Bearer ${token}`
|
|
324
|
+
},
|
|
325
|
+
body: JSON.stringify({
|
|
326
|
+
path: "threads:getWithMessages",
|
|
327
|
+
args: { threadId }
|
|
328
|
+
})
|
|
329
|
+
});
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
const error = await response.text();
|
|
332
|
+
return { error };
|
|
333
|
+
}
|
|
334
|
+
const state = await response.json();
|
|
335
|
+
if (!state) {
|
|
336
|
+
return { error: "Thread not found" };
|
|
337
|
+
}
|
|
338
|
+
return { state };
|
|
339
|
+
}
|
|
340
|
+
async function runTestConversation(agentId, message, threadId) {
|
|
341
|
+
const credentials = loadCredentials();
|
|
342
|
+
const apiKey = getApiKey();
|
|
343
|
+
const token = apiKey || credentials?.token;
|
|
344
|
+
if (!token) {
|
|
345
|
+
return { error: "Not authenticated" };
|
|
346
|
+
}
|
|
347
|
+
const response = await fetch(`${CONVEX_URL}/api/action`, {
|
|
348
|
+
method: "POST",
|
|
349
|
+
headers: {
|
|
350
|
+
"Content-Type": "application/json",
|
|
351
|
+
Authorization: `Bearer ${token}`
|
|
352
|
+
},
|
|
353
|
+
body: JSON.stringify({
|
|
354
|
+
path: "agent:chat",
|
|
355
|
+
args: {
|
|
356
|
+
agentId,
|
|
357
|
+
message,
|
|
358
|
+
threadId,
|
|
359
|
+
environment: "development"
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
});
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
const error = await response.text();
|
|
365
|
+
return { error };
|
|
366
|
+
}
|
|
367
|
+
const result = await response.json();
|
|
368
|
+
return {
|
|
369
|
+
response: {
|
|
370
|
+
message: result.message,
|
|
371
|
+
toolCalls: result.toolCalls
|
|
372
|
+
},
|
|
373
|
+
threadId: result.threadId
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/cli/commands/login.ts
|
|
378
|
+
var AUTH_CALLBACK_PORT = 9876;
|
|
379
|
+
var loginCommand = new Command("login").description("Log in to Struere").action(async () => {
|
|
380
|
+
const spinner = ora();
|
|
381
|
+
console.log();
|
|
382
|
+
console.log(chalk.bold("Struere Login"));
|
|
383
|
+
console.log();
|
|
384
|
+
const existing = loadCredentials();
|
|
385
|
+
if (existing) {
|
|
386
|
+
console.log(chalk.yellow("Already logged in as"), chalk.cyan(existing.user.email));
|
|
387
|
+
console.log(chalk.gray("Run"), chalk.cyan("struere logout"), chalk.gray("to log out first"));
|
|
388
|
+
console.log();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
await browserLogin(spinner);
|
|
392
|
+
});
|
|
393
|
+
async function performLogin() {
|
|
394
|
+
const spinner = ora();
|
|
395
|
+
console.log();
|
|
396
|
+
console.log(chalk.bold("Struere Login"));
|
|
397
|
+
console.log();
|
|
398
|
+
return browserLoginInternal(spinner);
|
|
399
|
+
}
|
|
400
|
+
async function browserLogin(spinner) {
|
|
401
|
+
const result = await browserLoginInternal(spinner);
|
|
402
|
+
if (result) {
|
|
403
|
+
printNextSteps();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function browserLoginInternal(spinner) {
|
|
407
|
+
spinner.start("Starting authentication server");
|
|
408
|
+
const authPromise = new Promise((resolve, reject) => {
|
|
409
|
+
const server = Bun.serve({
|
|
410
|
+
port: AUTH_CALLBACK_PORT,
|
|
411
|
+
async fetch(req) {
|
|
412
|
+
const url = new URL(req.url);
|
|
413
|
+
if (url.pathname === "/callback") {
|
|
414
|
+
const token = url.searchParams.get("token");
|
|
415
|
+
const sessionId = url.searchParams.get("session_id");
|
|
416
|
+
if (token && sessionId) {
|
|
417
|
+
resolve({ token, sessionId });
|
|
418
|
+
setTimeout(() => server.stop(), 1000);
|
|
419
|
+
return new Response(getSuccessHtml(), {
|
|
420
|
+
headers: { "Content-Type": "text/html" }
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return new Response(getErrorHtml("Missing token"), {
|
|
424
|
+
status: 400,
|
|
425
|
+
headers: { "Content-Type": "text/html" }
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (url.pathname === "/") {
|
|
429
|
+
const authUrl = getAuthUrl();
|
|
430
|
+
return Response.redirect(authUrl, 302);
|
|
431
|
+
}
|
|
432
|
+
return new Response("Not Found", { status: 404 });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
setTimeout(() => {
|
|
436
|
+
server.stop();
|
|
437
|
+
reject(new Error("Authentication timed out"));
|
|
438
|
+
}, 5 * 60 * 1000);
|
|
439
|
+
});
|
|
440
|
+
spinner.succeed("Authentication server started");
|
|
441
|
+
const loginUrl = `http://localhost:${AUTH_CALLBACK_PORT}`;
|
|
442
|
+
console.log();
|
|
443
|
+
console.log(chalk.gray("Opening browser to log in..."));
|
|
444
|
+
console.log(chalk.gray("If browser does not open, visit:"), chalk.cyan(loginUrl));
|
|
445
|
+
console.log();
|
|
446
|
+
if (process.platform === "darwin") {
|
|
447
|
+
Bun.spawn(["open", loginUrl]);
|
|
448
|
+
} else if (process.platform === "linux") {
|
|
449
|
+
Bun.spawn(["xdg-open", loginUrl]);
|
|
450
|
+
} else if (process.platform === "win32") {
|
|
451
|
+
Bun.spawn(["cmd", "/c", "start", loginUrl]);
|
|
452
|
+
}
|
|
453
|
+
spinner.start("Waiting for authentication");
|
|
454
|
+
try {
|
|
455
|
+
const { token } = await authPromise;
|
|
456
|
+
spinner.text = "Fetching user info";
|
|
457
|
+
const { userInfo, error } = await getUserInfo(token);
|
|
458
|
+
if (error || !userInfo) {
|
|
459
|
+
throw new Error(error || "Failed to fetch user info");
|
|
460
|
+
}
|
|
461
|
+
const { user, organization } = userInfo;
|
|
462
|
+
const credentials = {
|
|
463
|
+
token,
|
|
464
|
+
user: {
|
|
465
|
+
id: user.id,
|
|
466
|
+
email: user.email,
|
|
467
|
+
name: user.name || user.email,
|
|
468
|
+
organizationId: user.organizationId
|
|
469
|
+
},
|
|
470
|
+
organization: {
|
|
471
|
+
id: organization.id,
|
|
472
|
+
name: organization.name,
|
|
473
|
+
slug: organization.slug
|
|
474
|
+
},
|
|
475
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
|
|
476
|
+
};
|
|
477
|
+
saveCredentials(credentials);
|
|
478
|
+
spinner.succeed("Logged in successfully");
|
|
479
|
+
console.log();
|
|
480
|
+
console.log(chalk.green("Welcome,"), chalk.cyan(user.name || user.email));
|
|
481
|
+
console.log(chalk.gray("Organization:"), organization.name);
|
|
482
|
+
console.log();
|
|
483
|
+
return credentials;
|
|
484
|
+
} catch (error) {
|
|
485
|
+
spinner.fail("Login failed");
|
|
486
|
+
console.log();
|
|
487
|
+
console.log(chalk.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
488
|
+
console.log();
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function printNextSteps() {
|
|
493
|
+
console.log(chalk.gray("You can now use:"));
|
|
494
|
+
console.log(chalk.gray(" \u2022"), chalk.cyan("struere dev"), chalk.gray("- Start cloud-connected dev server"));
|
|
495
|
+
console.log(chalk.gray(" \u2022"), chalk.cyan("struere deploy"), chalk.gray("- Deploy your agent"));
|
|
496
|
+
console.log(chalk.gray(" \u2022"), chalk.cyan("struere logs"), chalk.gray("- View agent logs"));
|
|
497
|
+
console.log();
|
|
498
|
+
}
|
|
499
|
+
function getAuthUrl() {
|
|
500
|
+
const baseUrl = process.env.STRUERE_AUTH_URL || "https://app.struere.dev";
|
|
501
|
+
const callbackUrl = `http://localhost:${AUTH_CALLBACK_PORT}/callback`;
|
|
502
|
+
return `${baseUrl}/authorize?callback=${encodeURIComponent(callbackUrl)}`;
|
|
503
|
+
}
|
|
504
|
+
function getSuccessHtml() {
|
|
505
|
+
return `<!DOCTYPE html>
|
|
506
|
+
<html>
|
|
507
|
+
<head>
|
|
508
|
+
<title>Login Successful</title>
|
|
509
|
+
<style>
|
|
510
|
+
body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa; }
|
|
511
|
+
.container { text-align: center; }
|
|
512
|
+
h1 { color: #22c55e; }
|
|
513
|
+
p { color: #888; }
|
|
514
|
+
</style>
|
|
515
|
+
</head>
|
|
516
|
+
<body>
|
|
517
|
+
<div class="container">
|
|
518
|
+
<h1>Login Successful</h1>
|
|
519
|
+
<p>You can close this window and return to the terminal.</p>
|
|
520
|
+
</div>
|
|
521
|
+
<script>setTimeout(() => window.close(), 3000)</script>
|
|
522
|
+
</body>
|
|
523
|
+
</html>`;
|
|
524
|
+
}
|
|
525
|
+
function getErrorHtml(message) {
|
|
526
|
+
return `<!DOCTYPE html>
|
|
527
|
+
<html>
|
|
528
|
+
<head>
|
|
529
|
+
<title>Login Failed</title>
|
|
530
|
+
<style>
|
|
531
|
+
body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa; }
|
|
532
|
+
.container { text-align: center; }
|
|
533
|
+
h1 { color: #ef4444; }
|
|
534
|
+
p { color: #888; }
|
|
535
|
+
</style>
|
|
536
|
+
</head>
|
|
537
|
+
<body>
|
|
538
|
+
<div class="container">
|
|
539
|
+
<h1>Login Failed</h1>
|
|
540
|
+
<p>${message}</p>
|
|
541
|
+
</div>
|
|
542
|
+
</body>
|
|
543
|
+
</html>`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/cli/utils/project.ts
|
|
547
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
548
|
+
import { join as join2 } from "path";
|
|
549
|
+
var PROJECT_FILE = "struere.json";
|
|
550
|
+
function loadProject(cwd) {
|
|
551
|
+
const projectPath = join2(cwd, PROJECT_FILE);
|
|
552
|
+
if (!existsSync2(projectPath)) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const data = readFileSync2(projectPath, "utf-8");
|
|
557
|
+
return JSON.parse(data);
|
|
558
|
+
} catch {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function saveProject(cwd, project) {
|
|
563
|
+
const projectPath = join2(cwd, PROJECT_FILE);
|
|
564
|
+
writeFileSync2(projectPath, JSON.stringify(project, null, 2) + `
|
|
565
|
+
`);
|
|
566
|
+
}
|
|
567
|
+
function hasProject(cwd) {
|
|
568
|
+
return existsSync2(join2(cwd, PROJECT_FILE));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/cli/utils/scaffold.ts
|
|
572
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync3, appendFileSync } from "fs";
|
|
573
|
+
import { join as join3, dirname } from "path";
|
|
574
|
+
|
|
575
|
+
// src/cli/templates/index.ts
|
|
576
|
+
function getPackageJson(name) {
|
|
577
|
+
return JSON.stringify({
|
|
578
|
+
name,
|
|
579
|
+
version: "0.1.0",
|
|
580
|
+
type: "module",
|
|
581
|
+
scripts: {
|
|
582
|
+
dev: "struere dev",
|
|
583
|
+
build: "struere build",
|
|
584
|
+
test: "struere test",
|
|
585
|
+
deploy: "struere deploy"
|
|
586
|
+
},
|
|
587
|
+
dependencies: {
|
|
588
|
+
struere: "^0.3.0"
|
|
589
|
+
},
|
|
590
|
+
devDependencies: {
|
|
591
|
+
"bun-types": "^1.0.0",
|
|
592
|
+
typescript: "^5.3.0"
|
|
593
|
+
}
|
|
594
|
+
}, null, 2);
|
|
595
|
+
}
|
|
596
|
+
function getTsConfig() {
|
|
597
|
+
return JSON.stringify({
|
|
598
|
+
compilerOptions: {
|
|
599
|
+
target: "ES2022",
|
|
600
|
+
module: "ESNext",
|
|
601
|
+
moduleResolution: "bundler",
|
|
602
|
+
lib: ["ES2022"],
|
|
603
|
+
strict: true,
|
|
604
|
+
esModuleInterop: true,
|
|
605
|
+
skipLibCheck: true,
|
|
606
|
+
forceConsistentCasingInFileNames: true,
|
|
607
|
+
outDir: "dist",
|
|
608
|
+
rootDir: "src",
|
|
609
|
+
types: ["bun-types"]
|
|
610
|
+
},
|
|
611
|
+
include: ["src/**/*"],
|
|
612
|
+
exclude: ["node_modules", "dist"]
|
|
613
|
+
}, null, 2);
|
|
614
|
+
}
|
|
615
|
+
function getStruereConfig() {
|
|
616
|
+
return `import { defineConfig } from 'struere'
|
|
617
|
+
|
|
618
|
+
export default defineConfig({
|
|
619
|
+
port: 3000,
|
|
620
|
+
host: 'localhost',
|
|
621
|
+
cors: {
|
|
622
|
+
origins: ['http://localhost:3000'],
|
|
623
|
+
credentials: true,
|
|
624
|
+
},
|
|
625
|
+
logging: {
|
|
626
|
+
level: 'info',
|
|
627
|
+
format: 'pretty',
|
|
628
|
+
},
|
|
629
|
+
})
|
|
630
|
+
`;
|
|
631
|
+
}
|
|
632
|
+
function getAgentTs(name) {
|
|
633
|
+
const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
634
|
+
return `import { defineAgent } from 'struere'
|
|
635
|
+
import { context } from './context'
|
|
636
|
+
import { tools } from './tools'
|
|
637
|
+
|
|
638
|
+
export default defineAgent({
|
|
639
|
+
name: '${name}',
|
|
640
|
+
version: '0.1.0',
|
|
641
|
+
description: '${displayName} Agent',
|
|
642
|
+
model: {
|
|
643
|
+
provider: 'anthropic',
|
|
644
|
+
name: 'claude-sonnet-4-20250514',
|
|
645
|
+
temperature: 0.7,
|
|
646
|
+
maxTokens: 4096,
|
|
647
|
+
},
|
|
648
|
+
systemPrompt: \`You are ${displayName}, a helpful AI assistant.
|
|
649
|
+
|
|
650
|
+
Your capabilities:
|
|
651
|
+
- Answer questions accurately and helpfully
|
|
652
|
+
- Use available tools when appropriate
|
|
653
|
+
- Maintain conversation context
|
|
654
|
+
|
|
655
|
+
Always be concise, accurate, and helpful.\`,
|
|
656
|
+
tools,
|
|
657
|
+
context,
|
|
658
|
+
state: {
|
|
659
|
+
storage: 'memory',
|
|
660
|
+
ttl: 3600,
|
|
661
|
+
},
|
|
662
|
+
})
|
|
663
|
+
`;
|
|
664
|
+
}
|
|
665
|
+
function getContextTs() {
|
|
666
|
+
return `import { defineContext } from 'struere'
|
|
667
|
+
|
|
668
|
+
export const context = defineContext(async (request) => {
|
|
669
|
+
const { conversationId, userId, channel, state } = request
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
additionalContext: \`
|
|
673
|
+
Current conversation: \${conversationId}
|
|
674
|
+
Channel: \${channel}
|
|
675
|
+
\`,
|
|
676
|
+
variables: {
|
|
677
|
+
userId,
|
|
678
|
+
timestamp: new Date().toISOString(),
|
|
679
|
+
},
|
|
680
|
+
}
|
|
681
|
+
})
|
|
682
|
+
`;
|
|
683
|
+
}
|
|
684
|
+
function getToolsTs() {
|
|
685
|
+
return `import { defineTools } from 'struere'
|
|
686
|
+
|
|
687
|
+
export const tools = defineTools([
|
|
688
|
+
{
|
|
689
|
+
name: 'get_current_time',
|
|
690
|
+
description: 'Get the current date and time',
|
|
691
|
+
parameters: {
|
|
692
|
+
type: 'object',
|
|
693
|
+
properties: {
|
|
694
|
+
timezone: {
|
|
695
|
+
type: 'string',
|
|
696
|
+
description: 'Timezone (e.g., "America/New_York", "UTC")',
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
handler: async (params) => {
|
|
701
|
+
const timezone = (params.timezone as string) || 'UTC'
|
|
702
|
+
const now = new Date()
|
|
703
|
+
return {
|
|
704
|
+
timestamp: now.toISOString(),
|
|
705
|
+
formatted: now.toLocaleString('en-US', { timeZone: timezone }),
|
|
706
|
+
timezone,
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
name: 'calculate',
|
|
712
|
+
description: 'Perform a mathematical calculation',
|
|
713
|
+
parameters: {
|
|
714
|
+
type: 'object',
|
|
715
|
+
properties: {
|
|
716
|
+
expression: {
|
|
717
|
+
type: 'string',
|
|
718
|
+
description: 'Mathematical expression to evaluate (e.g., "2 + 2")',
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
required: ['expression'],
|
|
722
|
+
},
|
|
723
|
+
handler: async (params) => {
|
|
724
|
+
const expression = params.expression as string
|
|
725
|
+
const sanitized = expression.replace(/[^0-9+*/().\\s-]/g, '')
|
|
726
|
+
try {
|
|
727
|
+
const result = new Function(\`return \${sanitized}\`)()
|
|
728
|
+
return { expression, result }
|
|
729
|
+
} catch {
|
|
730
|
+
return { expression, error: 'Invalid expression' }
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
])
|
|
735
|
+
`;
|
|
736
|
+
}
|
|
737
|
+
function getBasicTestYaml() {
|
|
738
|
+
return `name: Basic conversation test
|
|
739
|
+
description: Verify the agent responds correctly to basic queries
|
|
740
|
+
|
|
741
|
+
conversation:
|
|
742
|
+
- role: user
|
|
743
|
+
content: Hello, what can you do?
|
|
744
|
+
- role: assistant
|
|
745
|
+
assertions:
|
|
746
|
+
- type: contains
|
|
747
|
+
value: help
|
|
748
|
+
|
|
749
|
+
- role: user
|
|
750
|
+
content: What time is it?
|
|
751
|
+
- role: assistant
|
|
752
|
+
assertions:
|
|
753
|
+
- type: toolCalled
|
|
754
|
+
value: get_current_time
|
|
755
|
+
`;
|
|
756
|
+
}
|
|
757
|
+
function getEnvExample() {
|
|
758
|
+
return `# Anthropic API Key (default provider)
|
|
759
|
+
ANTHROPIC_API_KEY=your_api_key_here
|
|
760
|
+
|
|
761
|
+
# Optional: OpenAI API Key (if using OpenAI models)
|
|
762
|
+
# OPENAI_API_KEY=your_openai_api_key
|
|
763
|
+
|
|
764
|
+
# Optional: Google AI API Key (if using Gemini models)
|
|
765
|
+
# GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key
|
|
766
|
+
|
|
767
|
+
# Optional: Custom Convex URL
|
|
768
|
+
# STRUERE_CONVEX_URL=https://struere.convex.cloud
|
|
769
|
+
`;
|
|
770
|
+
}
|
|
771
|
+
function getGitignore() {
|
|
772
|
+
return `node_modules/
|
|
773
|
+
dist/
|
|
774
|
+
.env
|
|
775
|
+
.env.local
|
|
776
|
+
.env.*.local
|
|
777
|
+
.idea/
|
|
778
|
+
.vscode/
|
|
779
|
+
*.swp
|
|
780
|
+
*.swo
|
|
781
|
+
.DS_Store
|
|
782
|
+
Thumbs.db
|
|
783
|
+
*.log
|
|
784
|
+
logs/
|
|
785
|
+
.vercel/
|
|
786
|
+
`;
|
|
787
|
+
}
|
|
788
|
+
function getStruereJson(agentId, team, slug, name) {
|
|
789
|
+
return JSON.stringify({
|
|
790
|
+
agentId,
|
|
791
|
+
team,
|
|
792
|
+
agent: {
|
|
793
|
+
slug,
|
|
794
|
+
name
|
|
795
|
+
}
|
|
796
|
+
}, null, 2);
|
|
797
|
+
}
|
|
798
|
+
function getEnvLocal(deploymentUrl) {
|
|
799
|
+
return `STRUERE_DEPLOYMENT_URL=${deploymentUrl}
|
|
800
|
+
`;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/cli/utils/scaffold.ts
|
|
804
|
+
function ensureDir(filePath) {
|
|
805
|
+
const dir = dirname(filePath);
|
|
806
|
+
if (!existsSync3(dir)) {
|
|
807
|
+
mkdirSync2(dir, { recursive: true });
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function writeFile(cwd, relativePath, content) {
|
|
811
|
+
const fullPath = join3(cwd, relativePath);
|
|
812
|
+
ensureDir(fullPath);
|
|
813
|
+
writeFileSync3(fullPath, content);
|
|
814
|
+
}
|
|
815
|
+
function writeProjectConfig(cwd, options) {
|
|
816
|
+
const result = {
|
|
817
|
+
createdFiles: [],
|
|
818
|
+
updatedFiles: []
|
|
819
|
+
};
|
|
820
|
+
writeFile(cwd, "struere.json", getStruereJson(options.agentId, options.team, options.agentSlug, options.agentName));
|
|
821
|
+
result.createdFiles.push("struere.json");
|
|
822
|
+
writeFile(cwd, ".env.local", getEnvLocal(options.deploymentUrl));
|
|
823
|
+
result.createdFiles.push(".env.local");
|
|
824
|
+
updateGitignore(cwd, result);
|
|
825
|
+
return result;
|
|
826
|
+
}
|
|
827
|
+
function scaffoldAgentFiles(cwd, projectName) {
|
|
828
|
+
const result = {
|
|
829
|
+
createdFiles: [],
|
|
830
|
+
updatedFiles: []
|
|
831
|
+
};
|
|
832
|
+
const files = {
|
|
833
|
+
"package.json": getPackageJson(projectName),
|
|
834
|
+
"tsconfig.json": getTsConfig(),
|
|
835
|
+
"struere.config.ts": getStruereConfig(),
|
|
836
|
+
"src/agent.ts": getAgentTs(projectName),
|
|
837
|
+
"src/context.ts": getContextTs(),
|
|
838
|
+
"src/tools.ts": getToolsTs(),
|
|
839
|
+
"src/workflows/.gitkeep": "",
|
|
840
|
+
"tests/basic.test.yaml": getBasicTestYaml(),
|
|
841
|
+
".env.example": getEnvExample()
|
|
842
|
+
};
|
|
843
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
844
|
+
const fullPath = join3(cwd, relativePath);
|
|
845
|
+
if (existsSync3(fullPath)) {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
writeFile(cwd, relativePath, content);
|
|
849
|
+
result.createdFiles.push(relativePath);
|
|
850
|
+
}
|
|
851
|
+
updateGitignore(cwd, result);
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
function updateGitignore(cwd, result) {
|
|
855
|
+
const gitignorePath = join3(cwd, ".gitignore");
|
|
856
|
+
const linesToAdd = [".env.local"];
|
|
857
|
+
if (existsSync3(gitignorePath)) {
|
|
858
|
+
const content = readFileSync3(gitignorePath, "utf-8");
|
|
859
|
+
const lines = content.split(`
|
|
860
|
+
`);
|
|
861
|
+
const missingLines = linesToAdd.filter((line) => !lines.some((l) => l.trim() === line));
|
|
862
|
+
if (missingLines.length > 0) {
|
|
863
|
+
const toAppend = `
|
|
864
|
+
` + missingLines.join(`
|
|
865
|
+
`) + `
|
|
866
|
+
`;
|
|
867
|
+
appendFileSync(gitignorePath, toAppend);
|
|
868
|
+
result.updatedFiles.push(".gitignore");
|
|
869
|
+
}
|
|
870
|
+
} else {
|
|
871
|
+
writeFile(cwd, ".gitignore", getGitignore());
|
|
872
|
+
result.createdFiles.push(".gitignore");
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function hasAgentFiles(cwd) {
|
|
876
|
+
return existsSync3(join3(cwd, "src", "agent.ts"));
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/cli/commands/init.ts
|
|
880
|
+
var initCommand = new Command2("init").description("Initialize a new Struere project").argument("[project-name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").action(async (projectNameArg, options) => {
|
|
881
|
+
const cwd = process.cwd();
|
|
882
|
+
const spinner = ora2();
|
|
883
|
+
console.log();
|
|
884
|
+
console.log(chalk2.bold("Struere CLI"));
|
|
885
|
+
console.log();
|
|
886
|
+
if (hasProject(cwd)) {
|
|
887
|
+
const existingProject = loadProject(cwd);
|
|
888
|
+
if (existingProject) {
|
|
889
|
+
console.log(chalk2.yellow("This project is already initialized."));
|
|
890
|
+
console.log();
|
|
891
|
+
console.log(chalk2.gray(" Agent:"), chalk2.cyan(existingProject.agent.name));
|
|
892
|
+
console.log(chalk2.gray(" ID:"), chalk2.gray(existingProject.agentId));
|
|
893
|
+
console.log(chalk2.gray(" Team:"), chalk2.cyan(existingProject.team));
|
|
894
|
+
console.log();
|
|
895
|
+
const shouldRelink = await promptYesNo("Would you like to relink to a different agent?");
|
|
896
|
+
if (!shouldRelink) {
|
|
897
|
+
console.log();
|
|
898
|
+
console.log(chalk2.gray("Run"), chalk2.cyan("struere dev"), chalk2.gray("to start development"));
|
|
899
|
+
console.log();
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
let credentials = loadCredentials();
|
|
905
|
+
if (!credentials) {
|
|
906
|
+
console.log(chalk2.gray("Authentication required"));
|
|
907
|
+
console.log();
|
|
908
|
+
credentials = await performLogin();
|
|
909
|
+
if (!credentials) {
|
|
910
|
+
console.log(chalk2.red("Authentication failed"));
|
|
911
|
+
process.exit(1);
|
|
912
|
+
}
|
|
913
|
+
} else {
|
|
914
|
+
console.log(chalk2.green("\u2713"), "Logged in as", chalk2.cyan(credentials.user.name || credentials.user.email));
|
|
915
|
+
console.log();
|
|
916
|
+
}
|
|
917
|
+
let projectName = projectNameArg;
|
|
918
|
+
if (!projectName) {
|
|
919
|
+
projectName = await deriveProjectName(cwd);
|
|
920
|
+
if (!options.yes) {
|
|
921
|
+
const confirmed = await promptText("Agent name:", projectName);
|
|
922
|
+
projectName = confirmed || projectName;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
projectName = slugify(projectName);
|
|
926
|
+
spinner.start("Fetching agents");
|
|
927
|
+
const { agents: existingAgents, error: listError } = await listAgents();
|
|
928
|
+
if (listError) {
|
|
929
|
+
spinner.fail("Failed to fetch agents");
|
|
930
|
+
console.log();
|
|
931
|
+
console.log(chalk2.gray("Run"), chalk2.cyan("struere login"), chalk2.gray("to re-authenticate"));
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
const agents = existingAgents.map((a) => ({ id: a._id, name: a.name, slug: a.slug }));
|
|
935
|
+
spinner.succeed(`Found ${agents.length} existing agent(s)`);
|
|
936
|
+
let selectedAgent = null;
|
|
937
|
+
let deploymentUrl = "";
|
|
938
|
+
if (agents.length > 0 && !options.yes) {
|
|
939
|
+
console.log();
|
|
940
|
+
const choice = await promptChoice("Create new agent or link existing?", [
|
|
941
|
+
{ value: "new", label: "Create new agent" },
|
|
942
|
+
...agents.map((a) => ({ value: a.id, label: `${a.name} (${a.slug})` }))
|
|
943
|
+
]);
|
|
944
|
+
if (choice !== "new") {
|
|
945
|
+
selectedAgent = agents.find((a) => a.id === choice) || null;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (!selectedAgent) {
|
|
949
|
+
const displayName = projectName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
950
|
+
spinner.start("Creating agent");
|
|
951
|
+
const { agentId, error: createError } = await createAgent({
|
|
952
|
+
name: displayName,
|
|
953
|
+
slug: projectName,
|
|
954
|
+
description: `${displayName} Agent`
|
|
955
|
+
});
|
|
956
|
+
if (createError || !agentId) {
|
|
957
|
+
spinner.fail("Failed to create agent");
|
|
958
|
+
console.log();
|
|
959
|
+
console.log(chalk2.red("Error:"), createError || "Unknown error");
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
selectedAgent = { id: agentId, name: displayName, slug: projectName };
|
|
963
|
+
deploymentUrl = `https://${projectName}-dev.struere.dev`;
|
|
964
|
+
spinner.succeed(`Created agent "${projectName}"`);
|
|
965
|
+
} else {
|
|
966
|
+
deploymentUrl = `https://${selectedAgent.slug}-dev.struere.dev`;
|
|
967
|
+
console.log();
|
|
968
|
+
console.log(chalk2.green("\u2713"), `Linked to "${selectedAgent.name}"`);
|
|
969
|
+
}
|
|
970
|
+
saveProject(cwd, {
|
|
971
|
+
agentId: selectedAgent.id,
|
|
972
|
+
team: credentials.organization.slug,
|
|
973
|
+
agent: {
|
|
974
|
+
slug: selectedAgent.slug,
|
|
975
|
+
name: selectedAgent.name
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
console.log(chalk2.green("\u2713"), "Created struere.json");
|
|
979
|
+
const configResult = writeProjectConfig(cwd, {
|
|
980
|
+
projectName,
|
|
981
|
+
agentId: selectedAgent.id,
|
|
982
|
+
team: credentials.organization.slug,
|
|
983
|
+
agentSlug: selectedAgent.slug,
|
|
984
|
+
agentName: selectedAgent.name,
|
|
985
|
+
deploymentUrl
|
|
986
|
+
});
|
|
987
|
+
for (const file of configResult.createdFiles) {
|
|
988
|
+
if (file !== "struere.json") {
|
|
989
|
+
console.log(chalk2.green("\u2713"), `Created ${file}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
for (const file of configResult.updatedFiles) {
|
|
993
|
+
console.log(chalk2.green("\u2713"), `Updated ${file}`);
|
|
994
|
+
}
|
|
995
|
+
if (!hasAgentFiles(cwd)) {
|
|
996
|
+
let shouldScaffold = options.yes;
|
|
997
|
+
if (!options.yes) {
|
|
998
|
+
console.log();
|
|
999
|
+
shouldScaffold = await promptYesNo("Scaffold starter files?");
|
|
1000
|
+
}
|
|
1001
|
+
if (shouldScaffold) {
|
|
1002
|
+
const scaffoldResult = scaffoldAgentFiles(cwd, projectName);
|
|
1003
|
+
console.log();
|
|
1004
|
+
for (const file of scaffoldResult.createdFiles) {
|
|
1005
|
+
console.log(chalk2.green("\u2713"), `Created ${file}`);
|
|
1006
|
+
}
|
|
1007
|
+
for (const file of scaffoldResult.updatedFiles) {
|
|
1008
|
+
console.log(chalk2.green("\u2713"), `Updated ${file}`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
console.log();
|
|
1013
|
+
console.log(chalk2.green("Success!"), "Project initialized");
|
|
1014
|
+
console.log();
|
|
1015
|
+
console.log(chalk2.gray("Next steps:"));
|
|
1016
|
+
console.log(chalk2.gray(" $"), chalk2.cyan("bun install"));
|
|
1017
|
+
console.log(chalk2.gray(" $"), chalk2.cyan("struere dev"));
|
|
1018
|
+
console.log();
|
|
1019
|
+
});
|
|
1020
|
+
async function deriveProjectName(cwd) {
|
|
1021
|
+
const packageJsonPath = join4(cwd, "package.json");
|
|
1022
|
+
if (existsSync4(packageJsonPath)) {
|
|
1023
|
+
try {
|
|
1024
|
+
const pkg = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
|
|
1025
|
+
if (pkg.name && typeof pkg.name === "string") {
|
|
1026
|
+
return slugify(pkg.name);
|
|
1027
|
+
}
|
|
1028
|
+
} catch {}
|
|
1029
|
+
}
|
|
1030
|
+
return slugify(basename(cwd));
|
|
1031
|
+
}
|
|
1032
|
+
function slugify(name) {
|
|
1033
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1034
|
+
}
|
|
1035
|
+
async function promptYesNo(message) {
|
|
1036
|
+
process.stdout.write(chalk2.gray(`${message} (Y/n) `));
|
|
1037
|
+
const answer = await readLine();
|
|
1038
|
+
return answer === "" || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
1039
|
+
}
|
|
1040
|
+
async function promptText(message, defaultValue) {
|
|
1041
|
+
process.stdout.write(chalk2.gray(`${message} `));
|
|
1042
|
+
process.stdout.write(chalk2.cyan(`(${defaultValue}) `));
|
|
1043
|
+
const answer = await readLine();
|
|
1044
|
+
return answer || defaultValue;
|
|
1045
|
+
}
|
|
1046
|
+
async function promptChoice(message, choices) {
|
|
1047
|
+
console.log(chalk2.gray(message));
|
|
1048
|
+
console.log();
|
|
1049
|
+
for (let i = 0;i < choices.length; i++) {
|
|
1050
|
+
const prefix = i === 0 ? chalk2.cyan("\u276F") : chalk2.gray(" ");
|
|
1051
|
+
console.log(`${prefix} ${i + 1}. ${choices[i].label}`);
|
|
1052
|
+
}
|
|
1053
|
+
console.log();
|
|
1054
|
+
process.stdout.write(chalk2.gray("Enter choice (1-" + choices.length + "): "));
|
|
1055
|
+
const answer = await readLine();
|
|
1056
|
+
const num = parseInt(answer, 10);
|
|
1057
|
+
if (num >= 1 && num <= choices.length) {
|
|
1058
|
+
return choices[num - 1].value;
|
|
1059
|
+
}
|
|
1060
|
+
return choices[0].value;
|
|
1061
|
+
}
|
|
1062
|
+
function readLine() {
|
|
1063
|
+
return new Promise((resolve) => {
|
|
1064
|
+
let buffer = "";
|
|
1065
|
+
if (process.stdin.isTTY) {
|
|
1066
|
+
process.stdin.setRawMode(false);
|
|
1067
|
+
}
|
|
1068
|
+
process.stdin.setEncoding("utf8");
|
|
1069
|
+
process.stdin.resume();
|
|
1070
|
+
const onData = (data) => {
|
|
1071
|
+
buffer += data;
|
|
1072
|
+
if (buffer.includes(`
|
|
1073
|
+
`)) {
|
|
1074
|
+
process.stdin.removeListener("data", onData);
|
|
1075
|
+
process.stdin.pause();
|
|
1076
|
+
resolve(buffer.replace(/[\r\n]/g, "").trim());
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
process.stdin.on("data", onData);
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/cli/commands/dev.ts
|
|
1084
|
+
import { Command as Command3 } from "commander";
|
|
1085
|
+
import chalk3 from "chalk";
|
|
1086
|
+
import ora3 from "ora";
|
|
1087
|
+
import chokidar from "chokidar";
|
|
1088
|
+
import { join as join7, basename as basename2 } from "path";
|
|
1089
|
+
|
|
1090
|
+
// src/cli/utils/config.ts
|
|
1091
|
+
import { join as join5 } from "path";
|
|
1092
|
+
var defaultConfig = {
|
|
1093
|
+
port: 3000,
|
|
1094
|
+
host: "localhost",
|
|
1095
|
+
cors: {
|
|
1096
|
+
origins: ["http://localhost:3000"],
|
|
1097
|
+
credentials: true
|
|
1098
|
+
},
|
|
1099
|
+
logging: {
|
|
1100
|
+
level: "info",
|
|
1101
|
+
format: "pretty"
|
|
1102
|
+
},
|
|
1103
|
+
auth: {
|
|
1104
|
+
type: "none"
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
async function loadConfig(cwd) {
|
|
1108
|
+
const configPath = join5(cwd, "struere.config.ts");
|
|
1109
|
+
try {
|
|
1110
|
+
const module = await import(configPath);
|
|
1111
|
+
const config = module.default || module;
|
|
1112
|
+
return {
|
|
1113
|
+
...defaultConfig,
|
|
1114
|
+
...config,
|
|
1115
|
+
cors: {
|
|
1116
|
+
...defaultConfig.cors,
|
|
1117
|
+
...config.cors
|
|
1118
|
+
},
|
|
1119
|
+
logging: {
|
|
1120
|
+
...defaultConfig.logging,
|
|
1121
|
+
...config.logging
|
|
1122
|
+
},
|
|
1123
|
+
auth: {
|
|
1124
|
+
...defaultConfig.auth,
|
|
1125
|
+
...config.auth
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
} catch {
|
|
1129
|
+
return defaultConfig;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/cli/utils/agent.ts
|
|
1134
|
+
import { join as join6 } from "path";
|
|
1135
|
+
async function loadAgent(cwd) {
|
|
1136
|
+
const agentPath = join6(cwd, "src/agent.ts");
|
|
1137
|
+
try {
|
|
1138
|
+
const module = await import(agentPath);
|
|
1139
|
+
const agent = module.default || module;
|
|
1140
|
+
if (!agent.name) {
|
|
1141
|
+
throw new Error("Agent must have a name");
|
|
1142
|
+
}
|
|
1143
|
+
if (!agent.version) {
|
|
1144
|
+
throw new Error("Agent must have a version");
|
|
1145
|
+
}
|
|
1146
|
+
if (!agent.systemPrompt) {
|
|
1147
|
+
throw new Error("Agent must have a systemPrompt");
|
|
1148
|
+
}
|
|
1149
|
+
return agent;
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
if (error instanceof Error && error.message.includes("Cannot find module")) {
|
|
1152
|
+
throw new Error(`Agent not found at ${agentPath}`);
|
|
1153
|
+
}
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/cli/commands/dev.ts
|
|
1159
|
+
var devCommand = new Command3("dev").description("Sync agent to development environment").action(async () => {
|
|
1160
|
+
const spinner = ora3();
|
|
1161
|
+
const cwd = process.cwd();
|
|
1162
|
+
console.log();
|
|
1163
|
+
console.log(chalk3.bold("Struere Dev"));
|
|
1164
|
+
console.log();
|
|
1165
|
+
let project = loadProject(cwd);
|
|
1166
|
+
if (!hasProject(cwd)) {
|
|
1167
|
+
console.log(chalk3.yellow("No struere.json found"));
|
|
1168
|
+
console.log();
|
|
1169
|
+
const setupResult = await interactiveSetup(cwd);
|
|
1170
|
+
if (!setupResult) {
|
|
1171
|
+
process.exit(0);
|
|
1172
|
+
}
|
|
1173
|
+
project = setupResult;
|
|
1174
|
+
}
|
|
1175
|
+
project = loadProject(cwd);
|
|
1176
|
+
if (!project) {
|
|
1177
|
+
console.log(chalk3.red("Failed to load struere.json"));
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
console.log(chalk3.gray("Agent:"), chalk3.cyan(project.agent.name));
|
|
1181
|
+
console.log();
|
|
1182
|
+
spinner.start("Loading configuration");
|
|
1183
|
+
await loadConfig(cwd);
|
|
1184
|
+
spinner.succeed("Configuration loaded");
|
|
1185
|
+
spinner.start("Loading agent");
|
|
1186
|
+
let agent = await loadAgent(cwd);
|
|
1187
|
+
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
1188
|
+
const credentials = loadCredentials();
|
|
1189
|
+
const apiKey = getApiKey();
|
|
1190
|
+
if (!credentials && !apiKey) {
|
|
1191
|
+
spinner.fail("Not logged in");
|
|
1192
|
+
console.log();
|
|
1193
|
+
console.log(chalk3.gray("Run"), chalk3.cyan("struere login"), chalk3.gray("to authenticate"));
|
|
1194
|
+
console.log();
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
}
|
|
1197
|
+
spinner.start("Syncing to Convex");
|
|
1198
|
+
const performSync = async () => {
|
|
1199
|
+
try {
|
|
1200
|
+
const config = extractConfig(agent);
|
|
1201
|
+
const result = await syncToConvex(project.agentId, config);
|
|
1202
|
+
if (!result.success) {
|
|
1203
|
+
throw new Error(result.error || "Sync failed");
|
|
1204
|
+
}
|
|
1205
|
+
return true;
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
throw error;
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
try {
|
|
1211
|
+
await performSync();
|
|
1212
|
+
spinner.succeed("Synced to development");
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
spinner.fail("Sync failed");
|
|
1215
|
+
console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
1216
|
+
process.exit(1);
|
|
1217
|
+
}
|
|
1218
|
+
const devUrl = `https://${project.agent.slug}-dev.struere.dev`;
|
|
1219
|
+
console.log();
|
|
1220
|
+
console.log(chalk3.green("Development URL:"), chalk3.cyan(devUrl));
|
|
1221
|
+
console.log();
|
|
1222
|
+
console.log(chalk3.gray("Watching for changes... Press Ctrl+C to stop"));
|
|
1223
|
+
console.log();
|
|
1224
|
+
const watcher = chokidar.watch([join7(cwd, "src"), join7(cwd, "struere.config.ts")], {
|
|
1225
|
+
ignoreInitial: true,
|
|
1226
|
+
ignored: /node_modules/
|
|
1227
|
+
});
|
|
1228
|
+
watcher.on("change", async (path) => {
|
|
1229
|
+
const relativePath = path.replace(cwd, ".");
|
|
1230
|
+
console.log(chalk3.gray(`Changed: ${relativePath}`));
|
|
1231
|
+
const syncSpinner = ora3("Syncing...").start();
|
|
1232
|
+
try {
|
|
1233
|
+
agent = await loadAgent(cwd);
|
|
1234
|
+
await performSync();
|
|
1235
|
+
syncSpinner.succeed("Synced");
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
syncSpinner.fail("Sync failed");
|
|
1238
|
+
console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
process.on("SIGINT", () => {
|
|
1242
|
+
console.log();
|
|
1243
|
+
watcher.close();
|
|
1244
|
+
console.log(chalk3.gray("Stopped"));
|
|
1245
|
+
process.exit(0);
|
|
1246
|
+
});
|
|
1247
|
+
});
|
|
1248
|
+
async function interactiveSetup(cwd) {
|
|
1249
|
+
const spinner = ora3();
|
|
1250
|
+
let credentials = loadCredentials();
|
|
1251
|
+
if (!credentials) {
|
|
1252
|
+
console.log(chalk3.gray("Authentication required"));
|
|
1253
|
+
console.log();
|
|
1254
|
+
credentials = await performLogin();
|
|
1255
|
+
if (!credentials) {
|
|
1256
|
+
console.log(chalk3.red("Authentication failed"));
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
} else {
|
|
1260
|
+
console.log(chalk3.green("\u2713"), "Logged in as", chalk3.cyan(credentials.user.name));
|
|
1261
|
+
console.log();
|
|
1262
|
+
}
|
|
1263
|
+
spinner.start("Fetching agents");
|
|
1264
|
+
const { agents: existingAgents, error: listError } = await listAgents();
|
|
1265
|
+
if (listError) {
|
|
1266
|
+
spinner.fail("Failed to fetch agents");
|
|
1267
|
+
console.log();
|
|
1268
|
+
console.log(chalk3.gray("Run"), chalk3.cyan("struere login"), chalk3.gray("to re-authenticate"));
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
const agents = existingAgents.map((a) => ({ id: a._id, name: a.name, slug: a.slug }));
|
|
1272
|
+
spinner.succeed(`Found ${agents.length} existing agent(s)`);
|
|
1273
|
+
let selectedAgent = null;
|
|
1274
|
+
if (agents.length === 0) {
|
|
1275
|
+
console.log(chalk3.gray("No existing agents found. Creating a new one..."));
|
|
1276
|
+
} else {
|
|
1277
|
+
console.log();
|
|
1278
|
+
const choices = [
|
|
1279
|
+
{ value: "link", label: "Link to an existing agent" },
|
|
1280
|
+
{ value: "create", label: "Create a new agent" },
|
|
1281
|
+
{ value: "cancel", label: "Cancel" }
|
|
1282
|
+
];
|
|
1283
|
+
const action = await promptChoiceArrows("No agent configured. Would you like to:", choices);
|
|
1284
|
+
if (action === "cancel") {
|
|
1285
|
+
console.log();
|
|
1286
|
+
console.log(chalk3.gray("Run"), chalk3.cyan("struere init"), chalk3.gray("when ready to set up"));
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
if (action === "link") {
|
|
1290
|
+
console.log();
|
|
1291
|
+
const agentChoices = agents.map((a) => ({ value: a.id, label: `${a.name} (${a.slug})` }));
|
|
1292
|
+
const agentId = await promptChoiceArrows("Select an agent:", agentChoices);
|
|
1293
|
+
selectedAgent = agents.find((a) => a.id === agentId) || null;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (!selectedAgent) {
|
|
1297
|
+
console.log();
|
|
1298
|
+
const projectName = slugify2(basename2(cwd));
|
|
1299
|
+
const name = await promptText2("Agent name:", projectName);
|
|
1300
|
+
const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1301
|
+
spinner.start("Creating agent");
|
|
1302
|
+
const { agentId, error: createError } = await createAgent({
|
|
1303
|
+
name: displayName,
|
|
1304
|
+
slug: name,
|
|
1305
|
+
description: `${displayName} Agent`
|
|
1306
|
+
});
|
|
1307
|
+
if (createError || !agentId) {
|
|
1308
|
+
spinner.fail("Failed to create agent");
|
|
1309
|
+
console.log();
|
|
1310
|
+
console.log(chalk3.red("Error:"), createError || "Unknown error");
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
selectedAgent = { id: agentId, name: displayName, slug: name };
|
|
1314
|
+
spinner.succeed(`Created agent "${name}"`);
|
|
1315
|
+
}
|
|
1316
|
+
if (!selectedAgent) {
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
const projectData = {
|
|
1320
|
+
agentId: selectedAgent.id,
|
|
1321
|
+
team: credentials.organization.slug,
|
|
1322
|
+
agent: {
|
|
1323
|
+
slug: selectedAgent.slug,
|
|
1324
|
+
name: selectedAgent.name
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
saveProject(cwd, projectData);
|
|
1328
|
+
console.log(chalk3.green("\u2713"), "Created struere.json");
|
|
1329
|
+
if (!hasAgentFiles(cwd)) {
|
|
1330
|
+
const scaffoldResult = scaffoldAgentFiles(cwd, selectedAgent.slug);
|
|
1331
|
+
for (const file of scaffoldResult.createdFiles) {
|
|
1332
|
+
console.log(chalk3.green("\u2713"), `Created ${file}`);
|
|
1333
|
+
}
|
|
1334
|
+
console.log();
|
|
1335
|
+
console.log(chalk3.yellow("Run"), chalk3.cyan("bun install"), chalk3.yellow("to install dependencies"));
|
|
1336
|
+
}
|
|
1337
|
+
console.log();
|
|
1338
|
+
return projectData;
|
|
1339
|
+
}
|
|
1340
|
+
function slugify2(name) {
|
|
1341
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1342
|
+
}
|
|
1343
|
+
async function promptChoiceArrows(message, choices) {
|
|
1344
|
+
return new Promise((resolve) => {
|
|
1345
|
+
let selectedIndex = 0;
|
|
1346
|
+
const render = () => {
|
|
1347
|
+
process.stdout.write("\x1B[?25l");
|
|
1348
|
+
process.stdout.write(`\x1B[${choices.length + 2}A`);
|
|
1349
|
+
console.log(chalk3.gray(message));
|
|
1350
|
+
console.log();
|
|
1351
|
+
for (let i = 0;i < choices.length; i++) {
|
|
1352
|
+
const prefix = i === selectedIndex ? chalk3.cyan("\u276F") : " ";
|
|
1353
|
+
const label = i === selectedIndex ? chalk3.cyan(choices[i].label) : choices[i].label;
|
|
1354
|
+
console.log(`${prefix} ${label}`);
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
console.log(chalk3.gray(message));
|
|
1358
|
+
console.log();
|
|
1359
|
+
for (let i = 0;i < choices.length; i++) {
|
|
1360
|
+
const prefix = i === selectedIndex ? chalk3.cyan("\u276F") : " ";
|
|
1361
|
+
const label = i === selectedIndex ? chalk3.cyan(choices[i].label) : choices[i].label;
|
|
1362
|
+
console.log(`${prefix} ${label}`);
|
|
1363
|
+
}
|
|
1364
|
+
if (!process.stdin.isTTY) {
|
|
1365
|
+
resolve(choices[0].value);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
process.stdin.setRawMode?.(true);
|
|
1369
|
+
process.stdin.resume();
|
|
1370
|
+
const onKeypress = (key) => {
|
|
1371
|
+
const char = key.toString();
|
|
1372
|
+
if (char === "\x1B[A" || char === "k") {
|
|
1373
|
+
selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
|
|
1374
|
+
render();
|
|
1375
|
+
} else if (char === "\x1B[B" || char === "j") {
|
|
1376
|
+
selectedIndex = (selectedIndex + 1) % choices.length;
|
|
1377
|
+
render();
|
|
1378
|
+
} else if (char === "\r" || char === `
|
|
1379
|
+
`) {
|
|
1380
|
+
process.stdin.removeListener("data", onKeypress);
|
|
1381
|
+
process.stdin.setRawMode?.(false);
|
|
1382
|
+
process.stdin.pause();
|
|
1383
|
+
process.stdout.write("\x1B[?25h");
|
|
1384
|
+
resolve(choices[selectedIndex].value);
|
|
1385
|
+
} else if (char === "\x03") {
|
|
1386
|
+
process.stdin.removeListener("data", onKeypress);
|
|
1387
|
+
process.stdin.setRawMode?.(false);
|
|
1388
|
+
process.stdout.write("\x1B[?25h");
|
|
1389
|
+
process.exit(0);
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
process.stdin.on("data", onKeypress);
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
async function promptText2(message, defaultValue) {
|
|
1396
|
+
process.stdout.write(chalk3.gray(`${message} `));
|
|
1397
|
+
process.stdout.write(chalk3.cyan(`(${defaultValue}) `));
|
|
1398
|
+
const answer = await readLine2();
|
|
1399
|
+
return answer || defaultValue;
|
|
1400
|
+
}
|
|
1401
|
+
function readLine2() {
|
|
1402
|
+
return new Promise((resolve) => {
|
|
1403
|
+
let buffer = "";
|
|
1404
|
+
const onData = (chunk) => {
|
|
1405
|
+
const str = chunk.toString();
|
|
1406
|
+
buffer += str;
|
|
1407
|
+
if (str.includes(`
|
|
1408
|
+
`) || str.includes("\r")) {
|
|
1409
|
+
process.stdin.removeListener("data", onData);
|
|
1410
|
+
process.stdin.pause();
|
|
1411
|
+
process.stdin.setRawMode?.(false);
|
|
1412
|
+
resolve(buffer.replace(/[\r\n]/g, "").trim());
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
if (process.stdin.isTTY) {
|
|
1416
|
+
process.stdin.setRawMode?.(false);
|
|
1417
|
+
}
|
|
1418
|
+
process.stdin.resume();
|
|
1419
|
+
process.stdin.on("data", onData);
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/cli/commands/build.ts
|
|
1424
|
+
import { Command as Command4 } from "commander";
|
|
1425
|
+
import chalk4 from "chalk";
|
|
1426
|
+
import ora4 from "ora";
|
|
1427
|
+
import { join as join8 } from "path";
|
|
1428
|
+
|
|
1429
|
+
// src/cli/utils/validate.ts
|
|
1430
|
+
function validateAgent(agent) {
|
|
1431
|
+
const errors = [];
|
|
1432
|
+
if (!agent.name) {
|
|
1433
|
+
errors.push("Agent name is required");
|
|
1434
|
+
} else if (!/^[a-z0-9-]+$/.test(agent.name)) {
|
|
1435
|
+
errors.push("Agent name must be lowercase alphanumeric with hyphens only");
|
|
1436
|
+
}
|
|
1437
|
+
if (!agent.version) {
|
|
1438
|
+
errors.push("Agent version is required");
|
|
1439
|
+
} else if (!/^\d+\.\d+\.\d+/.test(agent.version)) {
|
|
1440
|
+
errors.push("Agent version must follow semver format (e.g., 1.0.0)");
|
|
1441
|
+
}
|
|
1442
|
+
if (!agent.systemPrompt) {
|
|
1443
|
+
errors.push("System prompt is required");
|
|
1444
|
+
} else if (typeof agent.systemPrompt === "string" && agent.systemPrompt.trim().length === 0) {
|
|
1445
|
+
errors.push("System prompt cannot be empty");
|
|
1446
|
+
}
|
|
1447
|
+
if (agent.model) {
|
|
1448
|
+
const validProviders = ["anthropic", "openai", "google", "custom"];
|
|
1449
|
+
if (!validProviders.includes(agent.model.provider)) {
|
|
1450
|
+
errors.push(`Invalid model provider: ${agent.model.provider}`);
|
|
1451
|
+
}
|
|
1452
|
+
if (!agent.model.name) {
|
|
1453
|
+
errors.push("Model name is required when model is specified");
|
|
1454
|
+
}
|
|
1455
|
+
if (agent.model.temperature !== undefined) {
|
|
1456
|
+
if (agent.model.temperature < 0 || agent.model.temperature > 2) {
|
|
1457
|
+
errors.push("Model temperature must be between 0 and 2");
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (agent.model.maxTokens !== undefined) {
|
|
1461
|
+
if (agent.model.maxTokens < 1) {
|
|
1462
|
+
errors.push("Model maxTokens must be at least 1");
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (agent.tools) {
|
|
1467
|
+
for (const tool of agent.tools) {
|
|
1468
|
+
const toolErrors = validateTool(tool);
|
|
1469
|
+
errors.push(...toolErrors);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (agent.state) {
|
|
1473
|
+
const validStorage = ["memory", "redis", "postgres", "custom"];
|
|
1474
|
+
if (!validStorage.includes(agent.state.storage)) {
|
|
1475
|
+
errors.push(`Invalid state storage: ${agent.state.storage}`);
|
|
1476
|
+
}
|
|
1477
|
+
if (agent.state.ttl !== undefined && agent.state.ttl < 0) {
|
|
1478
|
+
errors.push("State TTL must be non-negative");
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return errors;
|
|
1482
|
+
}
|
|
1483
|
+
function validateTool(tool) {
|
|
1484
|
+
const errors = [];
|
|
1485
|
+
if (!tool.name) {
|
|
1486
|
+
errors.push("Tool name is required");
|
|
1487
|
+
} else if (!/^[a-z_][a-z0-9_]*$/.test(tool.name)) {
|
|
1488
|
+
errors.push(`Tool name "${tool.name}" must be snake_case`);
|
|
1489
|
+
}
|
|
1490
|
+
if (!tool.description) {
|
|
1491
|
+
errors.push(`Tool "${tool.name || "unknown"}" requires a description`);
|
|
1492
|
+
}
|
|
1493
|
+
if (!tool.parameters) {
|
|
1494
|
+
errors.push(`Tool "${tool.name || "unknown"}" requires parameters definition`);
|
|
1495
|
+
} else if (tool.parameters.type !== "object") {
|
|
1496
|
+
errors.push(`Tool "${tool.name || "unknown"}" parameters type must be "object"`);
|
|
1497
|
+
}
|
|
1498
|
+
if (typeof tool.handler !== "function") {
|
|
1499
|
+
errors.push(`Tool "${tool.name || "unknown"}" requires a handler function`);
|
|
1500
|
+
}
|
|
1501
|
+
return errors;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// src/cli/commands/build.ts
|
|
1505
|
+
var buildCommand = new Command4("build").description("Build and validate agent for production").option("-o, --outdir <dir>", "Output directory", "dist").action(async (options) => {
|
|
1506
|
+
const spinner = ora4();
|
|
1507
|
+
const cwd = process.cwd();
|
|
1508
|
+
console.log();
|
|
1509
|
+
console.log(chalk4.bold("Building Agent"));
|
|
1510
|
+
console.log();
|
|
1511
|
+
spinner.start("Loading configuration");
|
|
1512
|
+
const config = await loadConfig(cwd);
|
|
1513
|
+
spinner.succeed("Configuration loaded");
|
|
1514
|
+
spinner.start("Loading agent");
|
|
1515
|
+
const agent = await loadAgent(cwd);
|
|
1516
|
+
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
1517
|
+
spinner.start("Validating agent");
|
|
1518
|
+
const errors = validateAgent(agent);
|
|
1519
|
+
if (errors.length > 0) {
|
|
1520
|
+
spinner.fail("Validation failed");
|
|
1521
|
+
console.log();
|
|
1522
|
+
for (const error of errors) {
|
|
1523
|
+
console.log(chalk4.red(" \u2717"), error);
|
|
1524
|
+
}
|
|
1525
|
+
console.log();
|
|
1526
|
+
process.exit(1);
|
|
1527
|
+
}
|
|
1528
|
+
spinner.succeed("Agent validated");
|
|
1529
|
+
spinner.start("Building");
|
|
1530
|
+
const outdir = join8(cwd, options.outdir);
|
|
1531
|
+
const result = await Bun.build({
|
|
1532
|
+
entrypoints: [join8(cwd, "src/agent.ts")],
|
|
1533
|
+
outdir,
|
|
1534
|
+
target: "node",
|
|
1535
|
+
minify: true
|
|
1536
|
+
});
|
|
1537
|
+
if (!result.success) {
|
|
1538
|
+
spinner.fail("Build failed");
|
|
1539
|
+
console.log();
|
|
1540
|
+
for (const log of result.logs) {
|
|
1541
|
+
console.log(chalk4.red(" \u2717"), log.message);
|
|
1542
|
+
}
|
|
1543
|
+
process.exit(1);
|
|
1544
|
+
}
|
|
1545
|
+
spinner.succeed("Build completed");
|
|
1546
|
+
console.log();
|
|
1547
|
+
console.log(chalk4.green("Success!"), `Built to ${chalk4.cyan(options.outdir)}`);
|
|
1548
|
+
console.log();
|
|
1549
|
+
console.log("Output files:");
|
|
1550
|
+
for (const output of result.outputs) {
|
|
1551
|
+
console.log(chalk4.gray(" \u2022"), output.path.replace(cwd, "."));
|
|
1552
|
+
}
|
|
1553
|
+
console.log();
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
// src/cli/commands/test.ts
|
|
1557
|
+
import { Command as Command5 } from "commander";
|
|
1558
|
+
import chalk5 from "chalk";
|
|
1559
|
+
import ora5 from "ora";
|
|
1560
|
+
import { join as join9 } from "path";
|
|
1561
|
+
import { readdir, readFile } from "fs/promises";
|
|
1562
|
+
import YAML from "yaml";
|
|
1563
|
+
var testCommand = new Command5("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) => {
|
|
1564
|
+
const spinner = ora5();
|
|
1565
|
+
const cwd = process.cwd();
|
|
1566
|
+
console.log();
|
|
1567
|
+
console.log(chalk5.bold("Running Tests"));
|
|
1568
|
+
console.log();
|
|
1569
|
+
if (!hasProject(cwd)) {
|
|
1570
|
+
console.log(chalk5.yellow("No struere.json found"));
|
|
1571
|
+
console.log();
|
|
1572
|
+
console.log(chalk5.gray("Run"), chalk5.cyan("struere init"), chalk5.gray("to initialize this project"));
|
|
1573
|
+
console.log();
|
|
1574
|
+
process.exit(1);
|
|
1575
|
+
}
|
|
1576
|
+
const project = loadProject(cwd);
|
|
1577
|
+
if (!project) {
|
|
1578
|
+
console.log(chalk5.red("Failed to load struere.json"));
|
|
1579
|
+
process.exit(1);
|
|
1580
|
+
}
|
|
1581
|
+
spinner.start("Loading agent");
|
|
1582
|
+
const agent = await loadAgent(cwd);
|
|
1583
|
+
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
1584
|
+
spinner.start("Finding test files");
|
|
1585
|
+
const testsDir = join9(cwd, "tests");
|
|
1586
|
+
let testFiles = [];
|
|
1587
|
+
try {
|
|
1588
|
+
const files = await readdir(testsDir);
|
|
1589
|
+
testFiles = files.filter((f) => f.endsWith(".test.yaml") || f.endsWith(".test.yml"));
|
|
1590
|
+
} catch {
|
|
1591
|
+
spinner.warn("No tests directory found");
|
|
1592
|
+
console.log();
|
|
1593
|
+
console.log(chalk5.gray("Create tests in"), chalk5.cyan("tests/*.test.yaml"));
|
|
1594
|
+
console.log();
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
if (testFiles.length === 0) {
|
|
1598
|
+
spinner.warn("No test files found");
|
|
1599
|
+
console.log();
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
spinner.succeed(`Found ${testFiles.length} test file(s)`);
|
|
1603
|
+
if (options.dryRun) {
|
|
1604
|
+
console.log();
|
|
1605
|
+
console.log(chalk5.yellow("Dry run mode - skipping execution"));
|
|
1606
|
+
console.log();
|
|
1607
|
+
}
|
|
1608
|
+
const results = [];
|
|
1609
|
+
for (const file of testFiles) {
|
|
1610
|
+
const filePath = join9(testsDir, file);
|
|
1611
|
+
const content = await readFile(filePath, "utf-8");
|
|
1612
|
+
const testCase = YAML.parse(content);
|
|
1613
|
+
if (options.verbose) {
|
|
1614
|
+
console.log();
|
|
1615
|
+
console.log(chalk5.gray("Running:"), testCase.name);
|
|
1616
|
+
}
|
|
1617
|
+
const result = options.dryRun ? await runDryTest(testCase) : await runTest(testCase, project.agentId, options.verbose);
|
|
1618
|
+
results.push(result);
|
|
1619
|
+
if (result.passed) {
|
|
1620
|
+
console.log(chalk5.green(" \u2713"), result.name);
|
|
1621
|
+
} else {
|
|
1622
|
+
console.log(chalk5.red(" \u2717"), result.name);
|
|
1623
|
+
for (const error of result.errors) {
|
|
1624
|
+
console.log(chalk5.red(" \u2192"), error);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
const passed = results.filter((r) => r.passed).length;
|
|
1629
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
1630
|
+
console.log();
|
|
1631
|
+
if (failed === 0) {
|
|
1632
|
+
console.log(chalk5.green("All tests passed!"), chalk5.gray(`(${passed}/${results.length})`));
|
|
1633
|
+
} else {
|
|
1634
|
+
console.log(chalk5.red("Tests failed:"), chalk5.gray(`${passed}/${results.length} passed`));
|
|
1635
|
+
}
|
|
1636
|
+
console.log();
|
|
1637
|
+
if (failed > 0) {
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
async function runDryTest(testCase) {
|
|
1642
|
+
return {
|
|
1643
|
+
name: testCase.name,
|
|
1644
|
+
passed: true,
|
|
1645
|
+
errors: []
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
async function runTest(testCase, agentId, verbose) {
|
|
1649
|
+
const errors = [];
|
|
1650
|
+
let threadId;
|
|
1651
|
+
const context = {
|
|
1652
|
+
lastResponse: "",
|
|
1653
|
+
toolCalls: [],
|
|
1654
|
+
state: {}
|
|
1655
|
+
};
|
|
1656
|
+
try {
|
|
1657
|
+
for (const message of testCase.conversation || []) {
|
|
1658
|
+
if (message.role === "user") {
|
|
1659
|
+
if (verbose) {
|
|
1660
|
+
console.log(chalk5.cyan(" User:"), message.content.slice(0, 50) + (message.content.length > 50 ? "..." : ""));
|
|
1661
|
+
}
|
|
1662
|
+
const { response, threadId: newThreadId, error } = await runTestConversation(agentId, message.content, threadId);
|
|
1663
|
+
if (error || !response) {
|
|
1664
|
+
errors.push(`API error: ${error || "Unknown error"}`);
|
|
1665
|
+
break;
|
|
1666
|
+
}
|
|
1667
|
+
threadId = newThreadId;
|
|
1668
|
+
context.lastResponse = response.message;
|
|
1669
|
+
context.toolCalls = response.toolCalls || [];
|
|
1670
|
+
if (verbose) {
|
|
1671
|
+
console.log(chalk5.green(" Assistant:"), response.message.slice(0, 50) + (response.message.length > 50 ? "..." : ""));
|
|
1672
|
+
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
1673
|
+
console.log(chalk5.yellow(" Tools:"), response.toolCalls.map((t) => t.name).join(", "));
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
if (message.assertions) {
|
|
1678
|
+
for (const assertion of message.assertions) {
|
|
1679
|
+
const passed = checkAssertion(assertion, context);
|
|
1680
|
+
if (!passed) {
|
|
1681
|
+
errors.push(formatAssertionError(assertion, context));
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
if (testCase.assertions) {
|
|
1687
|
+
for (const assertion of testCase.assertions) {
|
|
1688
|
+
const passed = checkAssertion(assertion, context);
|
|
1689
|
+
if (!passed) {
|
|
1690
|
+
errors.push(formatAssertionError(assertion, context));
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1696
|
+
errors.push(`Execution error: ${errorMsg}`);
|
|
1697
|
+
}
|
|
1698
|
+
return {
|
|
1699
|
+
name: testCase.name,
|
|
1700
|
+
passed: errors.length === 0,
|
|
1701
|
+
errors
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
function checkAssertion(assertion, context) {
|
|
1705
|
+
switch (assertion.type) {
|
|
1706
|
+
case "contains":
|
|
1707
|
+
return typeof assertion.value === "string" && context.lastResponse.toLowerCase().includes(assertion.value.toLowerCase());
|
|
1708
|
+
case "matches":
|
|
1709
|
+
return typeof assertion.value === "string" && new RegExp(assertion.value, "i").test(context.lastResponse);
|
|
1710
|
+
case "toolCalled":
|
|
1711
|
+
if (typeof assertion.value === "string") {
|
|
1712
|
+
return context.toolCalls.some((tc) => tc.name === assertion.value);
|
|
1713
|
+
}
|
|
1714
|
+
return false;
|
|
1715
|
+
case "stateEquals":
|
|
1716
|
+
if (typeof assertion.value === "object" && assertion.value !== null) {
|
|
1717
|
+
for (const [key, expected] of Object.entries(assertion.value)) {
|
|
1718
|
+
if (context.state[key] !== expected) {
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
return true;
|
|
1723
|
+
}
|
|
1724
|
+
return false;
|
|
1725
|
+
default:
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
function formatAssertionError(assertion, context) {
|
|
1730
|
+
switch (assertion.type) {
|
|
1731
|
+
case "contains":
|
|
1732
|
+
return `Expected response to contain "${assertion.value}", got: "${context.lastResponse.slice(0, 100)}..."`;
|
|
1733
|
+
case "matches":
|
|
1734
|
+
return `Expected response to match /${assertion.value}/, got: "${context.lastResponse.slice(0, 100)}..."`;
|
|
1735
|
+
case "toolCalled":
|
|
1736
|
+
const calledTools = context.toolCalls.map((tc) => tc.name).join(", ") || "none";
|
|
1737
|
+
return `Expected tool "${assertion.value}" to be called, called: [${calledTools}]`;
|
|
1738
|
+
case "stateEquals":
|
|
1739
|
+
return `State mismatch: expected ${JSON.stringify(assertion.value)}, got ${JSON.stringify(context.state)}`;
|
|
1740
|
+
default:
|
|
1741
|
+
return `Assertion failed: ${assertion.type} - ${JSON.stringify(assertion.value)}`;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/cli/commands/deploy.ts
|
|
1746
|
+
import { Command as Command6 } from "commander";
|
|
1747
|
+
import chalk6 from "chalk";
|
|
1748
|
+
import ora6 from "ora";
|
|
1749
|
+
var deployCommand = new Command6("deploy").description("Deploy agent to production").option("--dry-run", "Show what would be deployed without deploying").action(async (options) => {
|
|
1750
|
+
const environment = "production";
|
|
1751
|
+
const spinner = ora6();
|
|
1752
|
+
const cwd = process.cwd();
|
|
1753
|
+
console.log();
|
|
1754
|
+
console.log(chalk6.bold("Deploying Agent"));
|
|
1755
|
+
console.log();
|
|
1756
|
+
if (!hasProject(cwd)) {
|
|
1757
|
+
console.log(chalk6.yellow("No struere.json found"));
|
|
1758
|
+
console.log();
|
|
1759
|
+
console.log(chalk6.gray("Run"), chalk6.cyan("struere init"), chalk6.gray("to initialize this project"));
|
|
1760
|
+
console.log();
|
|
1761
|
+
process.exit(1);
|
|
1762
|
+
}
|
|
1763
|
+
const project = loadProject(cwd);
|
|
1764
|
+
if (!project) {
|
|
1765
|
+
console.log(chalk6.red("Failed to load struere.json"));
|
|
1766
|
+
process.exit(1);
|
|
1767
|
+
}
|
|
1768
|
+
console.log(chalk6.gray("Agent:"), chalk6.cyan(project.agent.name));
|
|
1769
|
+
console.log();
|
|
1770
|
+
spinner.start("Loading configuration");
|
|
1771
|
+
await loadConfig(cwd);
|
|
1772
|
+
spinner.succeed("Configuration loaded");
|
|
1773
|
+
spinner.start("Loading agent");
|
|
1774
|
+
const agent = await loadAgent(cwd);
|
|
1775
|
+
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
1776
|
+
spinner.start("Validating agent");
|
|
1777
|
+
const errors = validateAgent(agent);
|
|
1778
|
+
if (errors.length > 0) {
|
|
1779
|
+
spinner.fail("Validation failed");
|
|
1780
|
+
console.log();
|
|
1781
|
+
for (const error of errors) {
|
|
1782
|
+
console.log(chalk6.red(" x"), error);
|
|
1783
|
+
}
|
|
1784
|
+
console.log();
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
spinner.succeed("Agent validated");
|
|
1788
|
+
if (options.dryRun) {
|
|
1789
|
+
console.log();
|
|
1790
|
+
console.log(chalk6.yellow("Dry run mode - no changes will be made"));
|
|
1791
|
+
console.log();
|
|
1792
|
+
console.log("Would deploy:");
|
|
1793
|
+
console.log(chalk6.gray(" -"), `Agent: ${chalk6.cyan(agent.name)}`);
|
|
1794
|
+
console.log(chalk6.gray(" -"), `Version: ${chalk6.cyan(agent.version)}`);
|
|
1795
|
+
console.log(chalk6.gray(" -"), `Environment: ${chalk6.cyan(environment)}`);
|
|
1796
|
+
console.log(chalk6.gray(" -"), `Agent ID: ${chalk6.cyan(project.agentId)}`);
|
|
1797
|
+
console.log();
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const credentials = loadCredentials();
|
|
1801
|
+
const apiKey = getApiKey();
|
|
1802
|
+
if (!credentials && !apiKey) {
|
|
1803
|
+
spinner.fail("Not authenticated");
|
|
1804
|
+
console.log();
|
|
1805
|
+
console.log(chalk6.gray("Run"), chalk6.cyan("struere login"), chalk6.gray("to authenticate"));
|
|
1806
|
+
console.log(chalk6.gray("Or set"), chalk6.cyan("STRUERE_API_KEY"), chalk6.gray("environment variable"));
|
|
1807
|
+
console.log();
|
|
1808
|
+
process.exit(1);
|
|
1809
|
+
}
|
|
1810
|
+
spinner.start("Extracting agent configuration");
|
|
1811
|
+
const config = extractConfig(agent);
|
|
1812
|
+
spinner.succeed("Configuration extracted");
|
|
1813
|
+
spinner.start("Syncing to development");
|
|
1814
|
+
try {
|
|
1815
|
+
const syncResult = await syncToConvex(project.agentId, config);
|
|
1816
|
+
if (!syncResult.success) {
|
|
1817
|
+
throw new Error(syncResult.error || "Sync failed");
|
|
1818
|
+
}
|
|
1819
|
+
spinner.succeed("Synced to development");
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
spinner.fail("Sync failed");
|
|
1822
|
+
console.log(chalk6.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
1823
|
+
process.exit(1);
|
|
1824
|
+
}
|
|
1825
|
+
spinner.start(`Deploying to ${environment}`);
|
|
1826
|
+
try {
|
|
1827
|
+
const deployResult = await deployToProduction(project.agentId);
|
|
1828
|
+
if (!deployResult.success) {
|
|
1829
|
+
throw new Error(deployResult.error || "Deployment failed");
|
|
1830
|
+
}
|
|
1831
|
+
spinner.succeed(`Deployed to ${environment}`);
|
|
1832
|
+
const prodUrl = `https://${project.agent.slug}.struere.dev`;
|
|
1833
|
+
console.log();
|
|
1834
|
+
console.log(chalk6.green("Success!"), "Agent deployed");
|
|
1835
|
+
console.log();
|
|
1836
|
+
console.log("Deployment details:");
|
|
1837
|
+
console.log(chalk6.gray(" -"), `Version: ${chalk6.cyan(agent.version)}`);
|
|
1838
|
+
console.log(chalk6.gray(" -"), `Environment: ${chalk6.cyan(environment)}`);
|
|
1839
|
+
console.log(chalk6.gray(" -"), `URL: ${chalk6.cyan(prodUrl)}`);
|
|
1840
|
+
console.log();
|
|
1841
|
+
console.log(chalk6.gray("Test your agent:"));
|
|
1842
|
+
console.log(chalk6.gray(" $"), chalk6.cyan(`curl -X POST ${prodUrl}/chat -H "Authorization: Bearer YOUR_API_KEY" -d '{"message": "Hello"}'`));
|
|
1843
|
+
console.log();
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
spinner.fail("Deployment failed");
|
|
1846
|
+
console.log();
|
|
1847
|
+
console.log(chalk6.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
1848
|
+
console.log();
|
|
1849
|
+
console.log(chalk6.gray("Try running"), chalk6.cyan("struere login"), chalk6.gray("to re-authenticate"));
|
|
1850
|
+
console.log();
|
|
1851
|
+
process.exit(1);
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
// src/cli/commands/validate.ts
|
|
1856
|
+
import { Command as Command7 } from "commander";
|
|
1857
|
+
import chalk7 from "chalk";
|
|
1858
|
+
import ora7 from "ora";
|
|
1859
|
+
var validateCommand = new Command7("validate").description("Validate agent configuration").option("--strict", "Enable strict validation").action(async (options) => {
|
|
1860
|
+
const spinner = ora7();
|
|
1861
|
+
const cwd = process.cwd();
|
|
1862
|
+
console.log();
|
|
1863
|
+
console.log(chalk7.bold("Validating Agent"));
|
|
1864
|
+
console.log();
|
|
1865
|
+
spinner.start("Loading agent");
|
|
1866
|
+
let agent;
|
|
1867
|
+
try {
|
|
1868
|
+
agent = await loadAgent(cwd);
|
|
1869
|
+
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
spinner.fail("Failed to load agent");
|
|
1872
|
+
console.log();
|
|
1873
|
+
console.log(chalk7.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
1874
|
+
console.log();
|
|
1875
|
+
process.exit(1);
|
|
1876
|
+
}
|
|
1877
|
+
spinner.start("Validating configuration");
|
|
1878
|
+
const errors = validateAgent(agent);
|
|
1879
|
+
const warnings = options.strict ? getStrictWarnings(agent) : [];
|
|
1880
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
1881
|
+
spinner.succeed("Agent is valid");
|
|
1882
|
+
console.log();
|
|
1883
|
+
console.log(chalk7.green("\u2713"), "No issues found");
|
|
1884
|
+
console.log();
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
if (errors.length > 0) {
|
|
1888
|
+
spinner.fail("Validation failed");
|
|
1889
|
+
console.log();
|
|
1890
|
+
console.log(chalk7.red("Errors:"));
|
|
1891
|
+
for (const error of errors) {
|
|
1892
|
+
console.log(chalk7.red(" \u2717"), error);
|
|
1893
|
+
}
|
|
1894
|
+
} else {
|
|
1895
|
+
spinner.succeed("Validation passed with warnings");
|
|
1896
|
+
}
|
|
1897
|
+
if (warnings.length > 0) {
|
|
1898
|
+
console.log();
|
|
1899
|
+
console.log(chalk7.yellow("Warnings:"));
|
|
1900
|
+
for (const warning of warnings) {
|
|
1901
|
+
console.log(chalk7.yellow(" \u26A0"), warning);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
console.log();
|
|
1905
|
+
if (errors.length > 0) {
|
|
1906
|
+
process.exit(1);
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
function getStrictWarnings(agent) {
|
|
1910
|
+
const warnings = [];
|
|
1911
|
+
if (!agent.description) {
|
|
1912
|
+
warnings.push("Agent is missing a description");
|
|
1913
|
+
}
|
|
1914
|
+
if (!agent.tools || agent.tools.length === 0) {
|
|
1915
|
+
warnings.push("Agent has no tools defined");
|
|
1916
|
+
}
|
|
1917
|
+
return warnings;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// src/cli/commands/logs.ts
|
|
1921
|
+
import { Command as Command8 } from "commander";
|
|
1922
|
+
import chalk8 from "chalk";
|
|
1923
|
+
import ora8 from "ora";
|
|
1924
|
+
var logsCommand = new Command8("logs").description("View recent execution logs").option("-n, --lines <number>", "Number of executions to show", "50").option("--json", "Output as JSON").action(async (options) => {
|
|
1925
|
+
const spinner = ora8();
|
|
1926
|
+
console.log();
|
|
1927
|
+
console.log(chalk8.bold("Execution Logs"));
|
|
1928
|
+
console.log();
|
|
1929
|
+
spinner.start("Fetching recent executions");
|
|
1930
|
+
const { executions, error } = await getRecentExecutions(parseInt(options.lines, 10));
|
|
1931
|
+
if (error) {
|
|
1932
|
+
spinner.fail("Failed to fetch executions");
|
|
1933
|
+
console.log();
|
|
1934
|
+
console.log(chalk8.red("Error:"), error);
|
|
1935
|
+
console.log();
|
|
1936
|
+
process.exit(1);
|
|
1937
|
+
}
|
|
1938
|
+
spinner.succeed(`Fetched ${executions.length} executions`);
|
|
1939
|
+
console.log();
|
|
1940
|
+
if (options.json) {
|
|
1941
|
+
console.log(JSON.stringify(executions, null, 2));
|
|
1942
|
+
console.log();
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
if (executions.length === 0) {
|
|
1946
|
+
console.log(chalk8.gray("No executions found"));
|
|
1947
|
+
console.log();
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
for (const exec of executions) {
|
|
1951
|
+
const statusColor = exec.status === "success" ? chalk8.green : chalk8.red;
|
|
1952
|
+
const timestamp = new Date(exec.createdAt).toISOString();
|
|
1953
|
+
console.log(chalk8.gray(timestamp), statusColor(`[${exec.status}]`), chalk8.cyan(`${exec.inputTokens}/${exec.outputTokens} tokens`), chalk8.gray(`${exec.durationMs}ms`));
|
|
1954
|
+
if (exec.errorMessage) {
|
|
1955
|
+
console.log(chalk8.red(` Error: ${exec.errorMessage}`));
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
console.log();
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
// src/cli/commands/state.ts
|
|
1962
|
+
import { Command as Command9 } from "commander";
|
|
1963
|
+
import chalk9 from "chalk";
|
|
1964
|
+
import ora9 from "ora";
|
|
1965
|
+
var stateCommand = new Command9("state").description("Inspect conversation thread state").argument("<id>", "Thread ID").option("--json", "Output as JSON").action(async (id, options) => {
|
|
1966
|
+
const spinner = ora9();
|
|
1967
|
+
console.log();
|
|
1968
|
+
console.log(chalk9.bold("Thread State"));
|
|
1969
|
+
console.log();
|
|
1970
|
+
spinner.start("Fetching thread state");
|
|
1971
|
+
const { state, error } = await getThreadState(id);
|
|
1972
|
+
if (error) {
|
|
1973
|
+
spinner.fail("Failed to fetch state");
|
|
1974
|
+
console.log();
|
|
1975
|
+
console.log(chalk9.red("Error:"), error);
|
|
1976
|
+
console.log();
|
|
1977
|
+
process.exit(1);
|
|
1978
|
+
}
|
|
1979
|
+
if (!state) {
|
|
1980
|
+
spinner.fail("Thread not found");
|
|
1981
|
+
console.log();
|
|
1982
|
+
process.exit(1);
|
|
1983
|
+
}
|
|
1984
|
+
spinner.succeed("State retrieved");
|
|
1985
|
+
if (options.json) {
|
|
1986
|
+
console.log();
|
|
1987
|
+
console.log(JSON.stringify(state, null, 2));
|
|
1988
|
+
console.log();
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
console.log();
|
|
1992
|
+
console.log(chalk9.gray("Thread:"), chalk9.cyan(state.thread._id));
|
|
1993
|
+
console.log(chalk9.gray("Agent:"), state.thread.agentId);
|
|
1994
|
+
console.log(chalk9.gray("Created:"), new Date(state.thread.createdAt).toLocaleString());
|
|
1995
|
+
console.log(chalk9.gray("Updated:"), new Date(state.thread.updatedAt).toLocaleString());
|
|
1996
|
+
console.log(chalk9.gray("Messages:"), state.messages.length);
|
|
1997
|
+
console.log();
|
|
1998
|
+
if (state.messages.length > 0) {
|
|
1999
|
+
console.log(chalk9.bold("Messages:"));
|
|
2000
|
+
console.log();
|
|
2001
|
+
for (const msg of state.messages) {
|
|
2002
|
+
const roleColor = msg.role === "user" ? chalk9.blue : msg.role === "assistant" ? chalk9.green : chalk9.gray;
|
|
2003
|
+
console.log(roleColor(`[${msg.role}]`));
|
|
2004
|
+
console.log(msg.content.slice(0, 200) + (msg.content.length > 200 ? "..." : ""));
|
|
2005
|
+
console.log();
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
// src/cli/commands/logout.ts
|
|
2011
|
+
import { Command as Command10 } from "commander";
|
|
2012
|
+
import chalk10 from "chalk";
|
|
2013
|
+
var logoutCommand = new Command10("logout").description("Log out of Struere").action(async () => {
|
|
2014
|
+
console.log();
|
|
2015
|
+
const credentials = loadCredentials();
|
|
2016
|
+
if (!credentials) {
|
|
2017
|
+
console.log(chalk10.yellow("Not currently logged in"));
|
|
2018
|
+
console.log();
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
clearCredentials();
|
|
2022
|
+
console.log(chalk10.green("Logged out successfully"));
|
|
2023
|
+
console.log(chalk10.gray("Goodbye,"), chalk10.cyan(credentials.user.name));
|
|
2024
|
+
console.log();
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
// src/cli/commands/whoami.ts
|
|
2028
|
+
import { Command as Command11 } from "commander";
|
|
2029
|
+
import chalk11 from "chalk";
|
|
2030
|
+
import ora10 from "ora";
|
|
2031
|
+
var whoamiCommand = new Command11("whoami").description("Show current logged in user").option("--refresh", "Refresh user info from server").action(async (options) => {
|
|
2032
|
+
console.log();
|
|
2033
|
+
const credentials = loadCredentials();
|
|
2034
|
+
if (!credentials) {
|
|
2035
|
+
console.log(chalk11.yellow("Not logged in"));
|
|
2036
|
+
console.log();
|
|
2037
|
+
console.log(chalk11.gray("Run"), chalk11.cyan("struere login"), chalk11.gray("to log in"));
|
|
2038
|
+
console.log();
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
if (options.refresh) {
|
|
2042
|
+
const spinner = ora10("Fetching user info").start();
|
|
2043
|
+
const { userInfo, error } = await getUserInfo(credentials.token);
|
|
2044
|
+
if (error || !userInfo) {
|
|
2045
|
+
spinner.fail("Failed to fetch user info");
|
|
2046
|
+
console.log();
|
|
2047
|
+
if (error?.includes("401") || error?.includes("unauthorized")) {
|
|
2048
|
+
console.log(chalk11.red("Session expired. Please log in again."));
|
|
2049
|
+
} else {
|
|
2050
|
+
console.log(chalk11.red("Error:"), error || "Unknown error");
|
|
2051
|
+
}
|
|
2052
|
+
console.log();
|
|
2053
|
+
process.exit(1);
|
|
2054
|
+
}
|
|
2055
|
+
spinner.stop();
|
|
2056
|
+
const { user, organization } = userInfo;
|
|
2057
|
+
console.log(chalk11.bold("Logged in as:"));
|
|
2058
|
+
console.log();
|
|
2059
|
+
console.log(chalk11.gray(" User: "), chalk11.cyan(user.name || user.email), chalk11.gray(`<${user.email}>`));
|
|
2060
|
+
console.log(chalk11.gray(" User ID: "), chalk11.gray(user.id));
|
|
2061
|
+
console.log();
|
|
2062
|
+
console.log(chalk11.gray(" Organization:"), chalk11.cyan(organization.name));
|
|
2063
|
+
console.log(chalk11.gray(" Org ID: "), chalk11.gray(organization.id));
|
|
2064
|
+
console.log(chalk11.gray(" Slug: "), chalk11.cyan(organization.slug));
|
|
2065
|
+
console.log();
|
|
2066
|
+
} else {
|
|
2067
|
+
console.log(chalk11.bold("Logged in as:"));
|
|
2068
|
+
console.log();
|
|
2069
|
+
console.log(chalk11.gray(" User: "), chalk11.cyan(credentials.user.name), chalk11.gray(`<${credentials.user.email}>`));
|
|
2070
|
+
console.log(chalk11.gray(" User ID: "), chalk11.gray(credentials.user.id));
|
|
2071
|
+
console.log();
|
|
2072
|
+
console.log(chalk11.gray(" Organization:"), chalk11.cyan(credentials.organization.name));
|
|
2073
|
+
console.log(chalk11.gray(" Org ID: "), chalk11.gray(credentials.organization.id));
|
|
2074
|
+
console.log(chalk11.gray(" Slug: "), chalk11.cyan(credentials.organization.slug));
|
|
2075
|
+
console.log();
|
|
2076
|
+
console.log(chalk11.gray("Use"), chalk11.cyan("struere whoami --refresh"), chalk11.gray("to fetch latest info"));
|
|
2077
|
+
console.log();
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
// src/cli/index.ts
|
|
2082
|
+
var CURRENT_VERSION = "0.3.0";
|
|
2083
|
+
async function checkForUpdates() {
|
|
2084
|
+
if (process.env.STRUERE_SKIP_UPDATE_CHECK)
|
|
2085
|
+
return;
|
|
2086
|
+
try {
|
|
2087
|
+
const response = await fetch("https://registry.npmjs.org/struere/latest", {
|
|
2088
|
+
signal: AbortSignal.timeout(2000)
|
|
2089
|
+
});
|
|
2090
|
+
if (response.ok) {
|
|
2091
|
+
const data = await response.json();
|
|
2092
|
+
if (data.version !== CURRENT_VERSION) {
|
|
2093
|
+
const semverCompare = (a, b) => {
|
|
2094
|
+
const pa = a.split(".").map(Number);
|
|
2095
|
+
const pb = b.split(".").map(Number);
|
|
2096
|
+
for (let i = 0;i < 3; i++) {
|
|
2097
|
+
if (pa[i] > pb[i])
|
|
2098
|
+
return 1;
|
|
2099
|
+
if (pa[i] < pb[i])
|
|
2100
|
+
return -1;
|
|
2101
|
+
}
|
|
2102
|
+
return 0;
|
|
2103
|
+
};
|
|
2104
|
+
if (semverCompare(data.version, CURRENT_VERSION) > 0) {
|
|
2105
|
+
console.log(`\x1B[33m\u26A0 Update available: ${CURRENT_VERSION} \u2192 ${data.version}\x1B[0m`);
|
|
2106
|
+
console.log(`\x1B[90m Run: npm install -g struere@${data.version}\x1B[0m`);
|
|
2107
|
+
console.log();
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
} catch {}
|
|
2112
|
+
}
|
|
2113
|
+
checkForUpdates();
|
|
2114
|
+
program.name("struere").description("Struere CLI - Build, test, and deploy AI agents").version(CURRENT_VERSION);
|
|
2115
|
+
program.addCommand(initCommand);
|
|
2116
|
+
program.addCommand(loginCommand);
|
|
2117
|
+
program.addCommand(logoutCommand);
|
|
2118
|
+
program.addCommand(whoamiCommand);
|
|
2119
|
+
program.addCommand(devCommand);
|
|
2120
|
+
program.addCommand(buildCommand);
|
|
2121
|
+
program.addCommand(testCommand);
|
|
2122
|
+
program.addCommand(deployCommand);
|
|
2123
|
+
program.addCommand(validateCommand);
|
|
2124
|
+
program.addCommand(logsCommand);
|
|
2125
|
+
program.addCommand(stateCommand);
|
|
2126
|
+
program.parse();
|