struere 0.3.11 → 0.4.1
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/struere.js +1123 -1168
- package/dist/cli/commands/add.d.ts +3 -0
- package/dist/cli/commands/add.d.ts.map +1 -0
- package/dist/cli/commands/deploy.d.ts.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/index.js +1144 -1184
- package/dist/cli/templates/index.d.ts +8 -0
- package/dist/cli/templates/index.d.ts.map +1 -1
- package/dist/cli/utils/convex.d.ts +101 -2
- package/dist/cli/utils/convex.d.ts.map +1 -1
- package/dist/cli/utils/extractor.d.ts +54 -0
- package/dist/cli/utils/extractor.d.ts.map +1 -0
- package/dist/cli/utils/loader.d.ts +20 -0
- package/dist/cli/utils/loader.d.ts.map +1 -0
- package/dist/cli/utils/project.d.ts +12 -0
- package/dist/cli/utils/project.d.ts.map +1 -1
- package/dist/cli/utils/scaffold.d.ts +11 -0
- package/dist/cli/utils/scaffold.d.ts.map +1 -1
- package/dist/define/entityType.d.ts +3 -0
- package/dist/define/entityType.d.ts.map +1 -0
- package/dist/define/role.d.ts +3 -0
- package/dist/define/role.d.ts.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -8,8 +8,7 @@ import { program } from "commander";
|
|
|
8
8
|
import { Command as Command2 } from "commander";
|
|
9
9
|
import chalk2 from "chalk";
|
|
10
10
|
import ora2 from "ora";
|
|
11
|
-
import {
|
|
12
|
-
import { join as join4, basename } from "path";
|
|
11
|
+
import { basename } from "path";
|
|
13
12
|
|
|
14
13
|
// src/cli/utils/credentials.ts
|
|
15
14
|
import { homedir } from "os";
|
|
@@ -62,110 +61,6 @@ import ora from "ora";
|
|
|
62
61
|
|
|
63
62
|
// src/cli/utils/convex.ts
|
|
64
63
|
var CONVEX_URL = process.env.STRUERE_CONVEX_URL || "https://rapid-wildebeest-172.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
64
|
async function getUserInfo(token) {
|
|
170
65
|
const ensureResponse = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
171
66
|
method: "POST",
|
|
@@ -237,73 +132,6 @@ async function getUserInfo(token) {
|
|
|
237
132
|
}
|
|
238
133
|
};
|
|
239
134
|
}
|
|
240
|
-
function extractConfig(agent) {
|
|
241
|
-
const BUILTIN_TOOLS = [
|
|
242
|
-
"entity.create",
|
|
243
|
-
"entity.get",
|
|
244
|
-
"entity.query",
|
|
245
|
-
"entity.update",
|
|
246
|
-
"entity.delete",
|
|
247
|
-
"entity.link",
|
|
248
|
-
"entity.unlink",
|
|
249
|
-
"event.emit",
|
|
250
|
-
"event.query",
|
|
251
|
-
"job.enqueue",
|
|
252
|
-
"job.status"
|
|
253
|
-
];
|
|
254
|
-
let systemPrompt;
|
|
255
|
-
if (typeof agent.systemPrompt === "function") {
|
|
256
|
-
const result = agent.systemPrompt();
|
|
257
|
-
if (result instanceof Promise) {
|
|
258
|
-
throw new Error("Async system prompts must be resolved before syncing");
|
|
259
|
-
}
|
|
260
|
-
systemPrompt = result;
|
|
261
|
-
} else {
|
|
262
|
-
systemPrompt = agent.systemPrompt;
|
|
263
|
-
}
|
|
264
|
-
const tools = (agent.tools || []).map((tool) => {
|
|
265
|
-
const isBuiltin = BUILTIN_TOOLS.includes(tool.name);
|
|
266
|
-
let handlerCode;
|
|
267
|
-
if (!isBuiltin && tool.handler) {
|
|
268
|
-
handlerCode = extractHandlerCode(tool.handler);
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
271
|
-
name: tool.name,
|
|
272
|
-
description: tool.description,
|
|
273
|
-
parameters: tool.parameters || { type: "object", properties: {} },
|
|
274
|
-
handlerCode,
|
|
275
|
-
isBuiltin
|
|
276
|
-
};
|
|
277
|
-
});
|
|
278
|
-
return {
|
|
279
|
-
name: agent.name,
|
|
280
|
-
version: agent.version || "0.0.1",
|
|
281
|
-
systemPrompt,
|
|
282
|
-
model: {
|
|
283
|
-
provider: agent.model?.provider || "anthropic",
|
|
284
|
-
name: agent.model?.name || "claude-sonnet-4-20250514",
|
|
285
|
-
temperature: agent.model?.temperature,
|
|
286
|
-
maxTokens: agent.model?.maxTokens
|
|
287
|
-
},
|
|
288
|
-
tools
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
function extractHandlerCode(handler) {
|
|
292
|
-
const code = handler.toString();
|
|
293
|
-
const arrowMatch = code.match(/(?:async\s*)?\([^)]*\)\s*=>\s*\{?([\s\S]*)\}?$/);
|
|
294
|
-
if (arrowMatch) {
|
|
295
|
-
let body = arrowMatch[1].trim();
|
|
296
|
-
if (body.startsWith("{") && body.endsWith("}")) {
|
|
297
|
-
body = body.slice(1, -1).trim();
|
|
298
|
-
}
|
|
299
|
-
return body;
|
|
300
|
-
}
|
|
301
|
-
const funcMatch = code.match(/(?:async\s*)?function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
|
|
302
|
-
if (funcMatch) {
|
|
303
|
-
return funcMatch[1].trim();
|
|
304
|
-
}
|
|
305
|
-
return code;
|
|
306
|
-
}
|
|
307
135
|
async function getRecentExecutions(limit = 100) {
|
|
308
136
|
const credentials = loadCredentials();
|
|
309
137
|
const apiKey = getApiKey();
|
|
@@ -393,6 +221,81 @@ async function runTestConversation(agentId, message, threadId) {
|
|
|
393
221
|
threadId: result.threadId
|
|
394
222
|
};
|
|
395
223
|
}
|
|
224
|
+
async function syncOrganization(payload) {
|
|
225
|
+
const credentials = loadCredentials();
|
|
226
|
+
const apiKey = getApiKey();
|
|
227
|
+
const token = apiKey || credentials?.token;
|
|
228
|
+
if (!token) {
|
|
229
|
+
return { success: false, error: "Not authenticated" };
|
|
230
|
+
}
|
|
231
|
+
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: {
|
|
234
|
+
"Content-Type": "application/json",
|
|
235
|
+
Authorization: `Bearer ${token}`
|
|
236
|
+
},
|
|
237
|
+
body: JSON.stringify({
|
|
238
|
+
path: "sync:syncOrganization",
|
|
239
|
+
args: payload
|
|
240
|
+
})
|
|
241
|
+
});
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
const error = await response.text();
|
|
244
|
+
return { success: false, error };
|
|
245
|
+
}
|
|
246
|
+
const result = await response.json();
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
async function getSyncState() {
|
|
250
|
+
const credentials = loadCredentials();
|
|
251
|
+
const apiKey = getApiKey();
|
|
252
|
+
const token = apiKey || credentials?.token;
|
|
253
|
+
if (!token) {
|
|
254
|
+
return { error: "Not authenticated" };
|
|
255
|
+
}
|
|
256
|
+
const response = await fetch(`${CONVEX_URL}/api/query`, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: {
|
|
259
|
+
"Content-Type": "application/json",
|
|
260
|
+
Authorization: `Bearer ${token}`
|
|
261
|
+
},
|
|
262
|
+
body: JSON.stringify({
|
|
263
|
+
path: "sync:getSyncState",
|
|
264
|
+
args: {}
|
|
265
|
+
})
|
|
266
|
+
});
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
const error = await response.text();
|
|
269
|
+
return { error };
|
|
270
|
+
}
|
|
271
|
+
const result = await response.json();
|
|
272
|
+
return { state: result.value };
|
|
273
|
+
}
|
|
274
|
+
async function deployAllAgents() {
|
|
275
|
+
const credentials = loadCredentials();
|
|
276
|
+
const apiKey = getApiKey();
|
|
277
|
+
const token = apiKey || credentials?.token;
|
|
278
|
+
if (!token) {
|
|
279
|
+
return { success: false, error: "Not authenticated" };
|
|
280
|
+
}
|
|
281
|
+
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: {
|
|
284
|
+
"Content-Type": "application/json",
|
|
285
|
+
Authorization: `Bearer ${token}`
|
|
286
|
+
},
|
|
287
|
+
body: JSON.stringify({
|
|
288
|
+
path: "sync:deployAllAgents",
|
|
289
|
+
args: {}
|
|
290
|
+
})
|
|
291
|
+
});
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
const error = await response.text();
|
|
294
|
+
return { success: false, error };
|
|
295
|
+
}
|
|
296
|
+
const result = await response.json();
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
396
299
|
|
|
397
300
|
// src/cli/commands/login.ts
|
|
398
301
|
var AUTH_CALLBACK_PORT = 9876;
|
|
@@ -579,40 +482,50 @@ function loadProject(cwd) {
|
|
|
579
482
|
return null;
|
|
580
483
|
}
|
|
581
484
|
}
|
|
582
|
-
function
|
|
485
|
+
function loadProjectV2(cwd) {
|
|
583
486
|
const projectPath = join2(cwd, PROJECT_FILE);
|
|
584
|
-
|
|
585
|
-
|
|
487
|
+
if (!existsSync2(projectPath)) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const data = readFileSync2(projectPath, "utf-8");
|
|
492
|
+
const parsed = JSON.parse(data);
|
|
493
|
+
if (parsed.version === "2.0") {
|
|
494
|
+
return parsed;
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
} catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
586
500
|
}
|
|
587
501
|
function hasProject(cwd) {
|
|
588
502
|
return existsSync2(join2(cwd, PROJECT_FILE));
|
|
589
503
|
}
|
|
504
|
+
function getProjectVersion(cwd) {
|
|
505
|
+
const projectPath = join2(cwd, PROJECT_FILE);
|
|
506
|
+
if (!existsSync2(projectPath)) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const data = readFileSync2(projectPath, "utf-8");
|
|
511
|
+
const parsed = JSON.parse(data);
|
|
512
|
+
if (parsed.version === "2.0") {
|
|
513
|
+
return "2.0";
|
|
514
|
+
}
|
|
515
|
+
if (parsed.agentId) {
|
|
516
|
+
return "1.0";
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
} catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
590
523
|
|
|
591
524
|
// src/cli/utils/scaffold.ts
|
|
592
525
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync3, appendFileSync } from "fs";
|
|
593
526
|
import { join as join3, dirname } from "path";
|
|
594
527
|
|
|
595
528
|
// src/cli/templates/index.ts
|
|
596
|
-
function getPackageJson(name) {
|
|
597
|
-
return JSON.stringify({
|
|
598
|
-
name,
|
|
599
|
-
version: "0.1.0",
|
|
600
|
-
type: "module",
|
|
601
|
-
scripts: {
|
|
602
|
-
dev: "struere dev",
|
|
603
|
-
build: "struere build",
|
|
604
|
-
test: "struere test",
|
|
605
|
-
deploy: "struere deploy"
|
|
606
|
-
},
|
|
607
|
-
dependencies: {
|
|
608
|
-
struere: "^0.3.0"
|
|
609
|
-
},
|
|
610
|
-
devDependencies: {
|
|
611
|
-
"bun-types": "^1.0.0",
|
|
612
|
-
typescript: "^5.3.0"
|
|
613
|
-
}
|
|
614
|
-
}, null, 2);
|
|
615
|
-
}
|
|
616
529
|
function getTsConfig() {
|
|
617
530
|
return JSON.stringify({
|
|
618
531
|
compilerOptions: {
|
|
@@ -649,36 +562,106 @@ export default defineConfig({
|
|
|
649
562
|
})
|
|
650
563
|
`;
|
|
651
564
|
}
|
|
652
|
-
function
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
import { tools } from './tools'
|
|
656
|
-
|
|
657
|
-
export default defineAgent({
|
|
658
|
-
name: '${name}',
|
|
659
|
-
version: '0.1.0',
|
|
660
|
-
description: '${displayName} Agent',
|
|
661
|
-
model: {
|
|
662
|
-
provider: 'anthropic',
|
|
663
|
-
name: 'claude-sonnet-4-20250514',
|
|
664
|
-
temperature: 0.7,
|
|
665
|
-
maxTokens: 4096,
|
|
666
|
-
},
|
|
667
|
-
systemPrompt: \`You are ${displayName}, a helpful AI assistant.
|
|
565
|
+
function getEnvExample() {
|
|
566
|
+
return `# Anthropic API Key (default provider)
|
|
567
|
+
ANTHROPIC_API_KEY=your_api_key_here
|
|
668
568
|
|
|
669
|
-
|
|
569
|
+
# Optional: OpenAI API Key (if using OpenAI models)
|
|
570
|
+
# OPENAI_API_KEY=your_openai_api_key
|
|
670
571
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
- Use available tools when appropriate
|
|
674
|
-
- Maintain conversation context
|
|
572
|
+
# Optional: Google AI API Key (if using Gemini models)
|
|
573
|
+
# GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key
|
|
675
574
|
|
|
676
|
-
|
|
677
|
-
|
|
575
|
+
# Optional: Custom Convex URL
|
|
576
|
+
# STRUERE_CONVEX_URL=https://struere.convex.cloud
|
|
577
|
+
`;
|
|
578
|
+
}
|
|
579
|
+
function getGitignore() {
|
|
580
|
+
return `node_modules/
|
|
581
|
+
dist/
|
|
582
|
+
.env
|
|
583
|
+
.env.local
|
|
584
|
+
.env.*.local
|
|
585
|
+
.idea/
|
|
586
|
+
.vscode/
|
|
587
|
+
*.swp
|
|
588
|
+
*.swo
|
|
589
|
+
.DS_Store
|
|
590
|
+
Thumbs.db
|
|
591
|
+
*.log
|
|
592
|
+
logs/
|
|
593
|
+
.vercel/
|
|
594
|
+
`;
|
|
595
|
+
}
|
|
596
|
+
function getEntityTypeTs(name, slug) {
|
|
597
|
+
const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
598
|
+
return `import { defineEntityType } from 'struere'
|
|
599
|
+
|
|
600
|
+
export default defineEntityType({
|
|
601
|
+
name: "${displayName}",
|
|
602
|
+
slug: "${slug}",
|
|
603
|
+
schema: {
|
|
604
|
+
type: "object",
|
|
605
|
+
properties: {
|
|
606
|
+
name: { type: "string", description: "Name" },
|
|
607
|
+
email: { type: "string", format: "email", description: "Email address" },
|
|
608
|
+
status: { type: "string", enum: ["active", "inactive"], description: "Status" },
|
|
609
|
+
},
|
|
610
|
+
required: ["name"],
|
|
611
|
+
},
|
|
612
|
+
searchFields: ["name", "email"],
|
|
613
|
+
})
|
|
614
|
+
`;
|
|
615
|
+
}
|
|
616
|
+
function getRoleTs(name) {
|
|
617
|
+
return `import { defineRole } from 'struere'
|
|
618
|
+
|
|
619
|
+
export default defineRole({
|
|
620
|
+
name: "${name}",
|
|
621
|
+
description: "${name.charAt(0).toUpperCase() + name.slice(1)} role",
|
|
622
|
+
policies: [
|
|
623
|
+
{ resource: "*", actions: ["list", "read"], effect: "allow", priority: 50 },
|
|
624
|
+
],
|
|
625
|
+
scopeRules: [],
|
|
626
|
+
fieldMasks: [],
|
|
627
|
+
})
|
|
628
|
+
`;
|
|
629
|
+
}
|
|
630
|
+
function getAgentTsV2(name, slug) {
|
|
631
|
+
const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
632
|
+
return `import { defineAgent } from 'struere'
|
|
633
|
+
|
|
634
|
+
export default defineAgent({
|
|
635
|
+
name: "${displayName}",
|
|
636
|
+
slug: "${slug}",
|
|
637
|
+
version: "0.1.0",
|
|
638
|
+
description: "${displayName} Agent",
|
|
639
|
+
model: {
|
|
640
|
+
provider: "anthropic",
|
|
641
|
+
name: "claude-sonnet-4-20250514",
|
|
642
|
+
temperature: 0.7,
|
|
643
|
+
maxTokens: 4096,
|
|
644
|
+
},
|
|
645
|
+
systemPrompt: \`You are ${displayName}, a helpful AI assistant.
|
|
646
|
+
|
|
647
|
+
Current time: {{datetime}}
|
|
648
|
+
|
|
649
|
+
Your capabilities:
|
|
650
|
+
- Answer questions accurately and helpfully
|
|
651
|
+
- Use available tools when appropriate
|
|
652
|
+
- Maintain conversation context
|
|
653
|
+
|
|
654
|
+
Always be concise, accurate, and helpful.\`,
|
|
655
|
+
tools: ["entity.query", "entity.get", "event.emit"],
|
|
678
656
|
})
|
|
679
657
|
`;
|
|
680
658
|
}
|
|
681
|
-
function
|
|
659
|
+
function getIndexTs(type) {
|
|
660
|
+
return `// Export all ${type} from this directory
|
|
661
|
+
// Example: export { default as myAgent } from './my-agent'
|
|
662
|
+
`;
|
|
663
|
+
}
|
|
664
|
+
function getToolsIndexTs() {
|
|
682
665
|
return `import { defineTools } from 'struere'
|
|
683
666
|
|
|
684
667
|
export const tools = defineTools([
|
|
@@ -704,225 +687,137 @@ export const tools = defineTools([
|
|
|
704
687
|
}
|
|
705
688
|
},
|
|
706
689
|
},
|
|
707
|
-
{
|
|
708
|
-
name: 'calculate',
|
|
709
|
-
description: 'Perform a mathematical calculation',
|
|
710
|
-
parameters: {
|
|
711
|
-
type: 'object',
|
|
712
|
-
properties: {
|
|
713
|
-
expression: {
|
|
714
|
-
type: 'string',
|
|
715
|
-
description: 'Mathematical expression to evaluate (e.g., "2 + 2")',
|
|
716
|
-
},
|
|
717
|
-
},
|
|
718
|
-
required: ['expression'],
|
|
719
|
-
},
|
|
720
|
-
handler: async (params) => {
|
|
721
|
-
const expression = params.expression as string
|
|
722
|
-
const sanitized = expression.replace(/[^0-9+*/().\\s-]/g, '')
|
|
723
|
-
try {
|
|
724
|
-
const result = new Function(\`return \${sanitized}\`)()
|
|
725
|
-
return { expression, result }
|
|
726
|
-
} catch {
|
|
727
|
-
return { expression, error: 'Invalid expression' }
|
|
728
|
-
}
|
|
729
|
-
},
|
|
730
|
-
},
|
|
731
690
|
])
|
|
732
|
-
`;
|
|
733
|
-
}
|
|
734
|
-
function getBasicTestYaml() {
|
|
735
|
-
return `name: Basic conversation test
|
|
736
|
-
description: Verify the agent responds correctly to basic queries
|
|
737
|
-
|
|
738
|
-
conversation:
|
|
739
|
-
- role: user
|
|
740
|
-
content: Hello, what can you do?
|
|
741
|
-
- role: assistant
|
|
742
|
-
assertions:
|
|
743
|
-
- type: contains
|
|
744
|
-
value: help
|
|
745
|
-
|
|
746
|
-
- role: user
|
|
747
|
-
content: What time is it?
|
|
748
|
-
- role: assistant
|
|
749
|
-
assertions:
|
|
750
|
-
- type: toolCalled
|
|
751
|
-
value: get_current_time
|
|
752
|
-
`;
|
|
753
|
-
}
|
|
754
|
-
function getEnvExample() {
|
|
755
|
-
return `# Anthropic API Key (default provider)
|
|
756
|
-
ANTHROPIC_API_KEY=your_api_key_here
|
|
757
691
|
|
|
758
|
-
|
|
759
|
-
# OPENAI_API_KEY=your_openai_api_key
|
|
760
|
-
|
|
761
|
-
# Optional: Google AI API Key (if using Gemini models)
|
|
762
|
-
# GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key
|
|
763
|
-
|
|
764
|
-
# Optional: Custom Convex URL
|
|
765
|
-
# STRUERE_CONVEX_URL=https://struere.convex.cloud
|
|
766
|
-
`;
|
|
767
|
-
}
|
|
768
|
-
function getGitignore() {
|
|
769
|
-
return `node_modules/
|
|
770
|
-
dist/
|
|
771
|
-
.env
|
|
772
|
-
.env.local
|
|
773
|
-
.env.*.local
|
|
774
|
-
.idea/
|
|
775
|
-
.vscode/
|
|
776
|
-
*.swp
|
|
777
|
-
*.swo
|
|
778
|
-
.DS_Store
|
|
779
|
-
Thumbs.db
|
|
780
|
-
*.log
|
|
781
|
-
logs/
|
|
782
|
-
.vercel/
|
|
692
|
+
export default tools
|
|
783
693
|
`;
|
|
784
694
|
}
|
|
785
|
-
function
|
|
695
|
+
function getStruereJsonV2(orgId, orgSlug, orgName) {
|
|
786
696
|
return JSON.stringify({
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
slug,
|
|
791
|
-
name
|
|
697
|
+
version: "2.0",
|
|
698
|
+
organization: {
|
|
699
|
+
id: orgId,
|
|
700
|
+
slug: orgSlug,
|
|
701
|
+
name: orgName
|
|
792
702
|
}
|
|
793
703
|
}, null, 2);
|
|
794
704
|
}
|
|
795
|
-
function
|
|
796
|
-
return
|
|
797
|
-
|
|
705
|
+
function getPackageJsonV2(name) {
|
|
706
|
+
return JSON.stringify({
|
|
707
|
+
name,
|
|
708
|
+
version: "0.1.0",
|
|
709
|
+
type: "module",
|
|
710
|
+
scripts: {
|
|
711
|
+
dev: "struere dev",
|
|
712
|
+
build: "struere build",
|
|
713
|
+
deploy: "struere deploy",
|
|
714
|
+
status: "struere status"
|
|
715
|
+
},
|
|
716
|
+
dependencies: {
|
|
717
|
+
struere: "^0.4.0"
|
|
718
|
+
},
|
|
719
|
+
devDependencies: {
|
|
720
|
+
"bun-types": "^1.0.0",
|
|
721
|
+
typescript: "^5.3.0"
|
|
722
|
+
}
|
|
723
|
+
}, null, 2);
|
|
798
724
|
}
|
|
799
|
-
function
|
|
800
|
-
|
|
801
|
-
return `# ${displayName} Agent
|
|
725
|
+
function getClaudeMDV2(orgName) {
|
|
726
|
+
return `# ${orgName} - Struere Project
|
|
802
727
|
|
|
803
|
-
This is a Struere
|
|
728
|
+
This is a Struere organization project. Struere is a framework for building production AI agents with built-in data management, RBAC permissions, and job scheduling.
|
|
804
729
|
|
|
805
730
|
## Project Structure
|
|
806
731
|
|
|
807
732
|
\`\`\`
|
|
808
|
-
|
|
809
|
-
\u251C\u2500\u2500
|
|
810
|
-
\
|
|
811
|
-
\u2514\u2500\u2500 workflows/ # Multi-step workflow definitions
|
|
812
|
-
tests/
|
|
813
|
-
\u2514\u2500\u2500 *.test.yaml # YAML-based conversation tests
|
|
814
|
-
struere.json # Project configuration (agentId, team, slug)
|
|
815
|
-
struere.config.ts # Framework settings (port, CORS, logging)
|
|
816
|
-
\`\`\`
|
|
817
|
-
|
|
818
|
-
## Agent Definition
|
|
733
|
+
agents/ # Agent definitions
|
|
734
|
+
\u251C\u2500\u2500 scheduler.ts # Example agent
|
|
735
|
+
\u2514\u2500\u2500 index.ts # Re-exports all agents
|
|
819
736
|
|
|
820
|
-
|
|
737
|
+
entity-types/ # Entity type schemas
|
|
738
|
+
\u251C\u2500\u2500 teacher.ts # Example entity type
|
|
739
|
+
\u2514\u2500\u2500 index.ts # Re-exports all entity types
|
|
821
740
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
741
|
+
roles/ # Role + permission definitions
|
|
742
|
+
\u251C\u2500\u2500 admin.ts # Example role with policies
|
|
743
|
+
\u2514\u2500\u2500 index.ts # Re-exports all roles
|
|
825
744
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
version: '0.1.0',
|
|
829
|
-
description: 'My AI Agent',
|
|
830
|
-
model: {
|
|
831
|
-
provider: 'anthropic',
|
|
832
|
-
name: 'claude-sonnet-4-20250514',
|
|
833
|
-
temperature: 0.7,
|
|
834
|
-
maxTokens: 4096,
|
|
835
|
-
},
|
|
836
|
-
systemPrompt: \\\`You are a helpful assistant.
|
|
745
|
+
tools/ # Shared custom tools
|
|
746
|
+
\u2514\u2500\u2500 index.ts # Custom tool definitions
|
|
837
747
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
tools,
|
|
841
|
-
})
|
|
748
|
+
struere.json # Organization configuration
|
|
749
|
+
struere.config.ts # Framework settings
|
|
842
750
|
\`\`\`
|
|
843
751
|
|
|
844
|
-
##
|
|
845
|
-
|
|
846
|
-
System prompts support dynamic \`{{...}}\` templates that are resolved at runtime before the LLM call.
|
|
847
|
-
|
|
848
|
-
### Available Variables
|
|
849
|
-
|
|
850
|
-
| Variable | Description |
|
|
851
|
-
|----------|-------------|
|
|
852
|
-
| \`{{organizationId}}\` | Current organization ID |
|
|
853
|
-
| \`{{userId}}\` | Current user ID |
|
|
854
|
-
| \`{{threadId}}\` | Conversation thread ID |
|
|
855
|
-
| \`{{agentId}}\` | Agent ID |
|
|
856
|
-
| \`{{agent.name}}\` | Agent name |
|
|
857
|
-
| \`{{agent.slug}}\` | Agent slug |
|
|
858
|
-
| \`{{thread.metadata.X}}\` | Thread metadata field X |
|
|
859
|
-
| \`{{message}}\` | Current user message |
|
|
860
|
-
| \`{{timestamp}}\` | Unix timestamp (ms) |
|
|
861
|
-
| \`{{datetime}}\` | ISO 8601 datetime |
|
|
862
|
-
|
|
863
|
-
### Function Calls
|
|
864
|
-
|
|
865
|
-
Call any agent tool directly in the system prompt:
|
|
866
|
-
|
|
867
|
-
\`\`\`
|
|
868
|
-
{{entity.get({"id": "ent_123"})}}
|
|
869
|
-
{{entity.query({"type": "customer", "limit": 5})}}
|
|
870
|
-
{{event.query({"entityId": "ent_123", "limit": 10})}}
|
|
871
|
-
\`\`\`
|
|
752
|
+
## CLI Commands
|
|
872
753
|
|
|
873
|
-
|
|
754
|
+
| Command | Description |
|
|
755
|
+
|---------|-------------|
|
|
756
|
+
| \`struere dev\` | Watch and sync all resources to Convex |
|
|
757
|
+
| \`struere deploy\` | Deploy all agents to production |
|
|
758
|
+
| \`struere add <type> <name>\` | Scaffold new agent/entity-type/role |
|
|
759
|
+
| \`struere status\` | Compare local vs remote state |
|
|
874
760
|
|
|
875
|
-
|
|
761
|
+
## Defining Resources
|
|
876
762
|
|
|
877
|
-
|
|
878
|
-
{{entity.get({"id": "{{thread.metadata.customerId}}"})}}
|
|
879
|
-
\`\`\`
|
|
763
|
+
### Agents (\`agents/*.ts\`)
|
|
880
764
|
|
|
881
|
-
|
|
765
|
+
\`\`\`typescript
|
|
766
|
+
import { defineAgent } from 'struere'
|
|
882
767
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
768
|
+
export default defineAgent({
|
|
769
|
+
name: "Scheduler",
|
|
770
|
+
slug: "scheduler",
|
|
771
|
+
version: "0.1.0",
|
|
772
|
+
systemPrompt: "You are a scheduling assistant...",
|
|
773
|
+
model: { provider: "anthropic", name: "claude-sonnet-4-20250514" },
|
|
774
|
+
tools: ["entity.create", "entity.query", "event.emit"],
|
|
775
|
+
})
|
|
887
776
|
\`\`\`
|
|
888
777
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
Define tools in \`src/tools.ts\`:
|
|
778
|
+
### Entity Types (\`entity-types/*.ts\`)
|
|
892
779
|
|
|
893
780
|
\`\`\`typescript
|
|
894
|
-
import {
|
|
895
|
-
|
|
896
|
-
export
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
},
|
|
906
|
-
required: ['query'],
|
|
907
|
-
},
|
|
908
|
-
handler: async (params) => {
|
|
909
|
-
const results = await searchProducts(params.query, params.limit ?? 10)
|
|
910
|
-
return { products: results }
|
|
781
|
+
import { defineEntityType } from 'struere'
|
|
782
|
+
|
|
783
|
+
export default defineEntityType({
|
|
784
|
+
name: "Teacher",
|
|
785
|
+
slug: "teacher",
|
|
786
|
+
schema: {
|
|
787
|
+
type: "object",
|
|
788
|
+
properties: {
|
|
789
|
+
name: { type: "string" },
|
|
790
|
+
email: { type: "string", format: "email" },
|
|
791
|
+
hourlyRate: { type: "number" },
|
|
911
792
|
},
|
|
793
|
+
required: ["name", "email"],
|
|
912
794
|
},
|
|
913
|
-
]
|
|
795
|
+
searchFields: ["name", "email"],
|
|
796
|
+
})
|
|
914
797
|
\`\`\`
|
|
915
798
|
|
|
916
|
-
|
|
917
|
-
- api.openai.com, api.anthropic.com, api.stripe.com
|
|
918
|
-
- api.sendgrid.com, api.twilio.com, hooks.slack.com
|
|
919
|
-
- discord.com, api.github.com
|
|
920
|
-
|
|
921
|
-
## Built-in Tools
|
|
799
|
+
### Roles (\`roles/*.ts\`)
|
|
922
800
|
|
|
923
|
-
|
|
801
|
+
\`\`\`typescript
|
|
802
|
+
import { defineRole } from 'struere'
|
|
803
|
+
|
|
804
|
+
export default defineRole({
|
|
805
|
+
name: "teacher",
|
|
806
|
+
description: "Tutors who conduct sessions",
|
|
807
|
+
policies: [
|
|
808
|
+
{ resource: "session", actions: ["list", "read", "update"], effect: "allow", priority: 50 },
|
|
809
|
+
{ resource: "payment", actions: ["*"], effect: "deny", priority: 100 },
|
|
810
|
+
],
|
|
811
|
+
scopeRules: [
|
|
812
|
+
{ entityType: "session", field: "data.teacherId", operator: "eq", value: "actor.userId" },
|
|
813
|
+
],
|
|
814
|
+
fieldMasks: [
|
|
815
|
+
{ entityType: "session", fieldPath: "data.paymentId", maskType: "hide" },
|
|
816
|
+
],
|
|
817
|
+
})
|
|
818
|
+
\`\`\`
|
|
924
819
|
|
|
925
|
-
|
|
820
|
+
## Built-in Tools
|
|
926
821
|
|
|
927
822
|
| Tool | Description |
|
|
928
823
|
|------|-------------|
|
|
@@ -933,159 +828,18 @@ Agents have access to these built-in tools for data management:
|
|
|
933
828
|
| \`entity.delete\` | Soft-delete entity |
|
|
934
829
|
| \`entity.link\` | Create entity relation |
|
|
935
830
|
| \`entity.unlink\` | Remove entity relation |
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
// entity.create
|
|
940
|
-
{ "type": "customer", "data": { "name": "John", "email": "john@example.com" } }
|
|
941
|
-
|
|
942
|
-
// entity.query
|
|
943
|
-
{ "type": "customer", "filters": { "status": "active" }, "limit": 10 }
|
|
944
|
-
|
|
945
|
-
// entity.update
|
|
946
|
-
{ "id": "ent_123", "data": { "status": "vip" } }
|
|
947
|
-
\`\`\`
|
|
948
|
-
|
|
949
|
-
### Event Tools
|
|
950
|
-
|
|
951
|
-
| Tool | Description |
|
|
952
|
-
|------|-------------|
|
|
953
|
-
| \`event.emit\` | Emit a custom event |
|
|
954
|
-
| \`event.query\` | Query event history |
|
|
955
|
-
|
|
956
|
-
Example event operations:
|
|
957
|
-
\`\`\`json
|
|
958
|
-
// event.emit
|
|
959
|
-
{ "entityId": "ent_123", "eventType": "order.placed", "payload": { "amount": 99.99 } }
|
|
960
|
-
|
|
961
|
-
// event.query
|
|
962
|
-
{ "entityId": "ent_123", "eventType": "order.*", "limit": 20 }
|
|
963
|
-
\`\`\`
|
|
964
|
-
|
|
965
|
-
### Job Tools
|
|
966
|
-
|
|
967
|
-
| Tool | Description |
|
|
968
|
-
|------|-------------|
|
|
969
|
-
| \`job.enqueue\` | Schedule a background job |
|
|
831
|
+
| \`event.emit\` | Emit custom event |
|
|
832
|
+
| \`event.query\` | Query events |
|
|
833
|
+
| \`job.enqueue\` | Schedule background job |
|
|
970
834
|
| \`job.status\` | Get job status |
|
|
971
835
|
|
|
972
|
-
Example job operations:
|
|
973
|
-
\`\`\`json
|
|
974
|
-
// job.enqueue
|
|
975
|
-
{ "jobType": "send_email", "payload": { "to": "user@example.com" }, "scheduledFor": 1706745600000 }
|
|
976
|
-
|
|
977
|
-
// job.status
|
|
978
|
-
{ "id": "job_abc123" }
|
|
979
|
-
\`\`\`
|
|
980
|
-
|
|
981
|
-
## Testing
|
|
982
|
-
|
|
983
|
-
Write YAML-based conversation tests in \`tests/\`:
|
|
984
|
-
|
|
985
|
-
\`\`\`yaml
|
|
986
|
-
name: Order flow test
|
|
987
|
-
description: Test the complete order flow
|
|
988
|
-
|
|
989
|
-
conversation:
|
|
990
|
-
- role: user
|
|
991
|
-
content: I want to order a pizza
|
|
992
|
-
- role: assistant
|
|
993
|
-
assertions:
|
|
994
|
-
- type: contains
|
|
995
|
-
value: size
|
|
996
|
-
- type: toolCalled
|
|
997
|
-
value: get_menu
|
|
998
|
-
|
|
999
|
-
- role: user
|
|
1000
|
-
content: Large pepperoni please
|
|
1001
|
-
- role: assistant
|
|
1002
|
-
assertions:
|
|
1003
|
-
- type: toolCalled
|
|
1004
|
-
value: entity.create
|
|
1005
|
-
\`\`\`
|
|
1006
|
-
|
|
1007
|
-
### Assertion Types
|
|
1008
|
-
|
|
1009
|
-
| Type | Description |
|
|
1010
|
-
|------|-------------|
|
|
1011
|
-
| \`contains\` | Response contains substring |
|
|
1012
|
-
| \`matches\` | Response matches regex |
|
|
1013
|
-
| \`toolCalled\` | Specific tool was called |
|
|
1014
|
-
| \`noToolCalled\` | No tools were called |
|
|
1015
|
-
|
|
1016
|
-
Run tests with:
|
|
1017
|
-
\`\`\`bash
|
|
1018
|
-
bun run test
|
|
1019
|
-
\`\`\`
|
|
1020
|
-
|
|
1021
|
-
## CLI Commands
|
|
1022
|
-
|
|
1023
|
-
| Command | Description |
|
|
1024
|
-
|---------|-------------|
|
|
1025
|
-
| \`struere dev\` | Start development mode (live sync to Convex) |
|
|
1026
|
-
| \`struere build\` | Validate agent configuration |
|
|
1027
|
-
| \`struere deploy\` | Deploy agent to production |
|
|
1028
|
-
| \`struere test\` | Run YAML conversation tests |
|
|
1029
|
-
| \`struere logs\` | View recent execution logs |
|
|
1030
|
-
| \`struere state\` | Inspect conversation thread state |
|
|
1031
|
-
|
|
1032
|
-
## Thread Metadata
|
|
1033
|
-
|
|
1034
|
-
Set thread metadata when creating conversations to provide context:
|
|
1035
|
-
|
|
1036
|
-
\`\`\`typescript
|
|
1037
|
-
// Via API
|
|
1038
|
-
POST /v1/chat
|
|
1039
|
-
{
|
|
1040
|
-
"agentId": "agent_123",
|
|
1041
|
-
"message": "Hello",
|
|
1042
|
-
"metadata": {
|
|
1043
|
-
"customerId": "ent_customer_456",
|
|
1044
|
-
"channel": "web",
|
|
1045
|
-
"language": "en"
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
\`\`\`
|
|
1049
|
-
|
|
1050
|
-
Access in system prompt:
|
|
1051
|
-
\`\`\`
|
|
1052
|
-
Customer: {{entity.get({"id": "{{thread.metadata.customerId}}"})}}
|
|
1053
|
-
Channel: {{thread.metadata.channel}}
|
|
1054
|
-
\`\`\`
|
|
1055
|
-
|
|
1056
836
|
## Development Workflow
|
|
1057
837
|
|
|
1058
|
-
1.
|
|
1059
|
-
2.
|
|
1060
|
-
3.
|
|
1061
|
-
4.
|
|
1062
|
-
5.
|
|
1063
|
-
|
|
1064
|
-
## API Endpoints
|
|
1065
|
-
|
|
1066
|
-
| Endpoint | Method | Description |
|
|
1067
|
-
|----------|--------|-------------|
|
|
1068
|
-
| \`/v1/chat\` | POST | Chat by agent ID |
|
|
1069
|
-
| \`/v1/agents/:slug/chat\` | POST | Chat by agent slug |
|
|
1070
|
-
|
|
1071
|
-
Authentication: Bearer token (API key from dashboard)
|
|
1072
|
-
|
|
1073
|
-
\`\`\`bash
|
|
1074
|
-
curl -X POST https://your-deployment.convex.cloud/v1/chat \\
|
|
1075
|
-
-H "Authorization: Bearer sk_live_..." \\
|
|
1076
|
-
-H "Content-Type: application/json" \\
|
|
1077
|
-
-d '{"agentId": "...", "message": "Hello"}'
|
|
1078
|
-
\`\`\`
|
|
1079
|
-
|
|
1080
|
-
## Best Practices
|
|
1081
|
-
|
|
1082
|
-
1. **System Prompts**: Use templates for dynamic data instead of hardcoding
|
|
1083
|
-
2. **Tools**: Keep tool handlers focused and stateless
|
|
1084
|
-
3. **Entities**: Model your domain data as entity types
|
|
1085
|
-
4. **Events**: Emit events for audit trails and analytics
|
|
1086
|
-
5. **Jobs**: Use jobs for async operations (emails, notifications)
|
|
1087
|
-
6. **Testing**: Write tests for critical conversation flows
|
|
1088
|
-
7. **Thread Metadata**: Use metadata for user-specific personalization
|
|
838
|
+
1. Run \`struere dev\` to start watching for changes
|
|
839
|
+
2. Edit agents, entity types, or roles
|
|
840
|
+
3. Changes are automatically synced to Convex
|
|
841
|
+
4. Test via API or dashboard
|
|
842
|
+
5. Run \`struere deploy\` when ready for production
|
|
1089
843
|
`;
|
|
1090
844
|
}
|
|
1091
845
|
|
|
@@ -1101,33 +855,35 @@ function writeFile(cwd, relativePath, content) {
|
|
|
1101
855
|
ensureDir(fullPath);
|
|
1102
856
|
writeFileSync3(fullPath, content);
|
|
1103
857
|
}
|
|
1104
|
-
function
|
|
1105
|
-
const result = {
|
|
1106
|
-
createdFiles: [],
|
|
1107
|
-
updatedFiles: []
|
|
1108
|
-
};
|
|
1109
|
-
writeFile(cwd, "struere.json", getStruereJson(options.agentId, options.team, options.agentSlug, options.agentName));
|
|
1110
|
-
result.createdFiles.push("struere.json");
|
|
1111
|
-
writeFile(cwd, ".env.local", getEnvLocal(options.deploymentUrl));
|
|
1112
|
-
result.createdFiles.push(".env.local");
|
|
1113
|
-
updateGitignore(cwd, result);
|
|
1114
|
-
return result;
|
|
1115
|
-
}
|
|
1116
|
-
function scaffoldAgentFiles(cwd, projectName) {
|
|
858
|
+
function scaffoldProjectV2(cwd, options) {
|
|
1117
859
|
const result = {
|
|
1118
860
|
createdFiles: [],
|
|
1119
861
|
updatedFiles: []
|
|
1120
862
|
};
|
|
863
|
+
const directories = [
|
|
864
|
+
"agents",
|
|
865
|
+
"entity-types",
|
|
866
|
+
"roles",
|
|
867
|
+
"tools"
|
|
868
|
+
];
|
|
869
|
+
for (const dir of directories) {
|
|
870
|
+
const dirPath = join3(cwd, dir);
|
|
871
|
+
if (!existsSync3(dirPath)) {
|
|
872
|
+
mkdirSync2(dirPath, { recursive: true });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
1121
875
|
const files = {
|
|
1122
|
-
"
|
|
876
|
+
"struere.json": getStruereJsonV2(options.orgId, options.orgSlug, options.orgName),
|
|
877
|
+
"package.json": getPackageJsonV2(options.projectName),
|
|
1123
878
|
"tsconfig.json": getTsConfig(),
|
|
1124
879
|
"struere.config.ts": getStruereConfig(),
|
|
1125
|
-
"src/agent.ts": getAgentTs(projectName),
|
|
1126
|
-
"src/tools.ts": getToolsTs(),
|
|
1127
|
-
"src/workflows/.gitkeep": "",
|
|
1128
|
-
"tests/basic.test.yaml": getBasicTestYaml(),
|
|
1129
880
|
".env.example": getEnvExample(),
|
|
1130
|
-
"
|
|
881
|
+
".gitignore": getGitignore(),
|
|
882
|
+
"CLAUDE.md": getClaudeMDV2(options.orgName),
|
|
883
|
+
"agents/index.ts": getIndexTs("agents"),
|
|
884
|
+
"entity-types/index.ts": getIndexTs("entity-types"),
|
|
885
|
+
"roles/index.ts": getIndexTs("roles"),
|
|
886
|
+
"tools/index.ts": getToolsIndexTs()
|
|
1131
887
|
};
|
|
1132
888
|
for (const [relativePath, content] of Object.entries(files)) {
|
|
1133
889
|
const fullPath = join3(cwd, relativePath);
|
|
@@ -1137,166 +893,161 @@ function scaffoldAgentFiles(cwd, projectName) {
|
|
|
1137
893
|
writeFile(cwd, relativePath, content);
|
|
1138
894
|
result.createdFiles.push(relativePath);
|
|
1139
895
|
}
|
|
1140
|
-
updateGitignore(cwd, result);
|
|
1141
896
|
return result;
|
|
1142
897
|
}
|
|
1143
|
-
function
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
898
|
+
function scaffoldAgent(cwd, name, slug) {
|
|
899
|
+
const result = {
|
|
900
|
+
createdFiles: [],
|
|
901
|
+
updatedFiles: []
|
|
902
|
+
};
|
|
903
|
+
const agentsDir = join3(cwd, "agents");
|
|
904
|
+
if (!existsSync3(agentsDir)) {
|
|
905
|
+
mkdirSync2(agentsDir, { recursive: true });
|
|
906
|
+
}
|
|
907
|
+
const fileName = `${slug}.ts`;
|
|
908
|
+
const filePath = join3(agentsDir, fileName);
|
|
909
|
+
if (existsSync3(filePath)) {
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
912
|
+
writeFileSync3(filePath, getAgentTsV2(name, slug));
|
|
913
|
+
result.createdFiles.push(`agents/${fileName}`);
|
|
914
|
+
return result;
|
|
915
|
+
}
|
|
916
|
+
function scaffoldEntityType(cwd, name, slug) {
|
|
917
|
+
const result = {
|
|
918
|
+
createdFiles: [],
|
|
919
|
+
updatedFiles: []
|
|
920
|
+
};
|
|
921
|
+
const entityTypesDir = join3(cwd, "entity-types");
|
|
922
|
+
if (!existsSync3(entityTypesDir)) {
|
|
923
|
+
mkdirSync2(entityTypesDir, { recursive: true });
|
|
924
|
+
}
|
|
925
|
+
const fileName = `${slug}.ts`;
|
|
926
|
+
const filePath = join3(entityTypesDir, fileName);
|
|
927
|
+
if (existsSync3(filePath)) {
|
|
928
|
+
return result;
|
|
1162
929
|
}
|
|
930
|
+
writeFileSync3(filePath, getEntityTypeTs(name, slug));
|
|
931
|
+
result.createdFiles.push(`entity-types/${fileName}`);
|
|
932
|
+
return result;
|
|
1163
933
|
}
|
|
1164
|
-
function
|
|
1165
|
-
|
|
934
|
+
function scaffoldRole(cwd, name) {
|
|
935
|
+
const result = {
|
|
936
|
+
createdFiles: [],
|
|
937
|
+
updatedFiles: []
|
|
938
|
+
};
|
|
939
|
+
const rolesDir = join3(cwd, "roles");
|
|
940
|
+
if (!existsSync3(rolesDir)) {
|
|
941
|
+
mkdirSync2(rolesDir, { recursive: true });
|
|
942
|
+
}
|
|
943
|
+
const fileName = `${name}.ts`;
|
|
944
|
+
const filePath = join3(rolesDir, fileName);
|
|
945
|
+
if (existsSync3(filePath)) {
|
|
946
|
+
return result;
|
|
947
|
+
}
|
|
948
|
+
writeFileSync3(filePath, getRoleTs(name));
|
|
949
|
+
result.createdFiles.push(`roles/${fileName}`);
|
|
950
|
+
return result;
|
|
1166
951
|
}
|
|
1167
952
|
|
|
1168
953
|
// src/cli/commands/init.ts
|
|
1169
|
-
|
|
1170
|
-
const cwd = process.cwd();
|
|
954
|
+
async function runInit(cwd) {
|
|
1171
955
|
const spinner = ora2();
|
|
1172
|
-
console.log();
|
|
1173
|
-
console.log(chalk2.bold("Struere CLI"));
|
|
1174
|
-
console.log();
|
|
1175
|
-
if (hasProject(cwd)) {
|
|
1176
|
-
const existingProject = loadProject(cwd);
|
|
1177
|
-
if (existingProject) {
|
|
1178
|
-
console.log(chalk2.yellow("This project is already initialized."));
|
|
1179
|
-
console.log();
|
|
1180
|
-
console.log(chalk2.gray(" Agent:"), chalk2.cyan(existingProject.agent.name));
|
|
1181
|
-
console.log(chalk2.gray(" ID:"), chalk2.gray(existingProject.agentId));
|
|
1182
|
-
console.log(chalk2.gray(" Team:"), chalk2.cyan(existingProject.team));
|
|
1183
|
-
console.log();
|
|
1184
|
-
const shouldRelink = await promptYesNo("Would you like to relink to a different agent?");
|
|
1185
|
-
if (!shouldRelink) {
|
|
1186
|
-
console.log();
|
|
1187
|
-
console.log(chalk2.gray("Run"), chalk2.cyan("struere dev"), chalk2.gray("to start development"));
|
|
1188
|
-
console.log();
|
|
1189
|
-
return;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
956
|
let credentials = loadCredentials();
|
|
1194
957
|
if (!credentials) {
|
|
1195
|
-
console.log(chalk2.
|
|
958
|
+
console.log(chalk2.yellow("Not logged in - authenticating..."));
|
|
1196
959
|
console.log();
|
|
1197
960
|
credentials = await performLogin();
|
|
1198
961
|
if (!credentials) {
|
|
1199
962
|
console.log(chalk2.red("Authentication failed"));
|
|
1200
|
-
|
|
963
|
+
return false;
|
|
1201
964
|
}
|
|
1202
|
-
} else {
|
|
1203
|
-
console.log(chalk2.green("\u2713"), "Logged in as", chalk2.cyan(credentials.user.name || credentials.user.email));
|
|
1204
965
|
console.log();
|
|
1205
966
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
967
|
+
console.log(chalk2.green("\u2713"), "Logged in as", chalk2.cyan(credentials.user.name || credentials.user.email));
|
|
968
|
+
console.log(chalk2.gray(" Organization:"), chalk2.cyan(credentials.organization.name));
|
|
969
|
+
console.log();
|
|
970
|
+
const projectName = slugify(basename(cwd));
|
|
971
|
+
spinner.start("Creating project structure");
|
|
972
|
+
const scaffoldResult = scaffoldProjectV2(cwd, {
|
|
973
|
+
projectName,
|
|
974
|
+
orgId: credentials.organization.id,
|
|
975
|
+
orgSlug: credentials.organization.slug,
|
|
976
|
+
orgName: credentials.organization.name
|
|
977
|
+
});
|
|
978
|
+
spinner.succeed("Project structure created");
|
|
979
|
+
for (const file of scaffoldResult.createdFiles) {
|
|
980
|
+
console.log(chalk2.green("\u2713"), `Created ${file}`);
|
|
1213
981
|
}
|
|
1214
|
-
|
|
1215
|
-
spinner.start("
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
982
|
+
console.log();
|
|
983
|
+
spinner.start("Installing dependencies");
|
|
984
|
+
const installResult = Bun.spawnSync(["bun", "install"], {
|
|
985
|
+
cwd,
|
|
986
|
+
stdout: "pipe",
|
|
987
|
+
stderr: "pipe"
|
|
988
|
+
});
|
|
989
|
+
if (installResult.exitCode === 0) {
|
|
990
|
+
spinner.succeed("Dependencies installed");
|
|
991
|
+
} else {
|
|
992
|
+
spinner.warn("Could not install dependencies automatically");
|
|
993
|
+
console.log(chalk2.gray(" Run"), chalk2.cyan("bun install"), chalk2.gray("manually"));
|
|
1222
994
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
spinner.fail("Failed to create agent");
|
|
995
|
+
console.log();
|
|
996
|
+
console.log(chalk2.green("\u2713"), "Project initialized");
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
999
|
+
var initCommand = new Command2("init").description("Initialize a new Struere organization project").argument("[project-name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").action(async (projectNameArg, options) => {
|
|
1000
|
+
const cwd = process.cwd();
|
|
1001
|
+
const spinner = ora2();
|
|
1002
|
+
console.log();
|
|
1003
|
+
console.log(chalk2.bold("Struere CLI"));
|
|
1004
|
+
console.log();
|
|
1005
|
+
if (hasProject(cwd)) {
|
|
1006
|
+
const version = getProjectVersion(cwd);
|
|
1007
|
+
if (version === "2.0") {
|
|
1008
|
+
console.log(chalk2.yellow("This project is already initialized (v2.0)."));
|
|
1009
|
+
console.log();
|
|
1010
|
+
console.log(chalk2.gray("Run"), chalk2.cyan("struere dev"), chalk2.gray("to start development"));
|
|
1011
|
+
console.log();
|
|
1012
|
+
return;
|
|
1013
|
+
} else if (version === "1.0") {
|
|
1014
|
+
console.log(chalk2.yellow("This is a v1 agent-centric project."));
|
|
1015
|
+
console.log(chalk2.yellow("The new CLI uses an organization-centric structure."));
|
|
1016
|
+
console.log();
|
|
1017
|
+
console.log(chalk2.gray("Please create a new project directory for the v2 structure."));
|
|
1247
1018
|
console.log();
|
|
1248
|
-
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
let credentials = loadCredentials();
|
|
1023
|
+
if (!credentials) {
|
|
1024
|
+
console.log(chalk2.yellow("Not logged in - authenticating..."));
|
|
1025
|
+
console.log();
|
|
1026
|
+
credentials = await performLogin();
|
|
1027
|
+
if (!credentials) {
|
|
1028
|
+
console.log(chalk2.red("Authentication failed"));
|
|
1249
1029
|
process.exit(1);
|
|
1250
1030
|
}
|
|
1251
|
-
selectedAgent = { id: agentId, name: displayName2, slug: projectName };
|
|
1252
|
-
deploymentUrl = `https://${projectName}-dev.struere.dev`;
|
|
1253
|
-
spinner.succeed(`Created agent "${projectName}"`);
|
|
1254
|
-
} else {
|
|
1255
|
-
deploymentUrl = `https://${selectedAgent.slug}-dev.struere.dev`;
|
|
1256
1031
|
console.log();
|
|
1257
|
-
console.log(chalk2.green("\u2713"), `Linked to "${selectedAgent.name}"`);
|
|
1258
1032
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
const
|
|
1033
|
+
console.log(chalk2.green("\u2713"), "Logged in as", chalk2.cyan(credentials.user.name || credentials.user.email));
|
|
1034
|
+
console.log(chalk2.gray(" Organization:"), chalk2.cyan(credentials.organization.name));
|
|
1035
|
+
console.log();
|
|
1036
|
+
let projectName = projectNameArg;
|
|
1037
|
+
if (!projectName) {
|
|
1038
|
+
projectName = slugify(basename(cwd));
|
|
1039
|
+
}
|
|
1040
|
+
projectName = slugify(projectName);
|
|
1041
|
+
spinner.start("Creating project structure");
|
|
1042
|
+
const scaffoldResult = scaffoldProjectV2(cwd, {
|
|
1269
1043
|
projectName,
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
agentName: selectedAgent.name,
|
|
1274
|
-
deploymentUrl
|
|
1044
|
+
orgId: credentials.organization.id,
|
|
1045
|
+
orgSlug: credentials.organization.slug,
|
|
1046
|
+
orgName: credentials.organization.name
|
|
1275
1047
|
});
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
for (const file of configResult.updatedFiles) {
|
|
1282
|
-
console.log(chalk2.green("\u2713"), `Updated ${file}`);
|
|
1283
|
-
}
|
|
1284
|
-
if (!hasAgentFiles(cwd)) {
|
|
1285
|
-
let shouldScaffold = options.yes;
|
|
1286
|
-
if (!options.yes) {
|
|
1287
|
-
console.log();
|
|
1288
|
-
shouldScaffold = await promptYesNo("Scaffold starter files?");
|
|
1289
|
-
}
|
|
1290
|
-
if (shouldScaffold) {
|
|
1291
|
-
const scaffoldResult = scaffoldAgentFiles(cwd, projectName);
|
|
1292
|
-
console.log();
|
|
1293
|
-
for (const file of scaffoldResult.createdFiles) {
|
|
1294
|
-
console.log(chalk2.green("\u2713"), `Created ${file}`);
|
|
1295
|
-
}
|
|
1296
|
-
for (const file of scaffoldResult.updatedFiles) {
|
|
1297
|
-
console.log(chalk2.green("\u2713"), `Updated ${file}`);
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1048
|
+
spinner.succeed("Project structure created");
|
|
1049
|
+
for (const file of scaffoldResult.createdFiles) {
|
|
1050
|
+
console.log(chalk2.green("\u2713"), `Created ${file}`);
|
|
1300
1051
|
}
|
|
1301
1052
|
console.log();
|
|
1302
1053
|
spinner.start("Installing dependencies");
|
|
@@ -1311,244 +1062,343 @@ var initCommand = new Command2("init").description("Initialize a new Struere pro
|
|
|
1311
1062
|
spinner.warn("Could not install dependencies automatically");
|
|
1312
1063
|
console.log(chalk2.gray(" Run"), chalk2.cyan("bun install"), chalk2.gray("manually"));
|
|
1313
1064
|
}
|
|
1314
|
-
spinner.start("Syncing initial config to Convex");
|
|
1315
|
-
const displayName = projectName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1316
|
-
const defaultConfig = {
|
|
1317
|
-
name: displayName,
|
|
1318
|
-
version: "0.1.0",
|
|
1319
|
-
systemPrompt: `You are ${displayName}, a helpful AI assistant. You help users with their questions and tasks.`,
|
|
1320
|
-
model: {
|
|
1321
|
-
provider: "anthropic",
|
|
1322
|
-
name: "claude-sonnet-4-20250514",
|
|
1323
|
-
temperature: 0.7,
|
|
1324
|
-
maxTokens: 4096
|
|
1325
|
-
},
|
|
1326
|
-
tools: []
|
|
1327
|
-
};
|
|
1328
|
-
const syncResult = await syncToConvex(selectedAgent.id, defaultConfig);
|
|
1329
|
-
if (syncResult.success) {
|
|
1330
|
-
spinner.succeed("Initial config synced");
|
|
1331
|
-
} else {
|
|
1332
|
-
spinner.warn("Could not sync initial config");
|
|
1333
|
-
console.log(chalk2.gray(" Run"), chalk2.cyan("struere dev"), chalk2.gray("to sync manually"));
|
|
1334
|
-
}
|
|
1335
1065
|
console.log();
|
|
1336
1066
|
console.log(chalk2.green("Success!"), "Project initialized");
|
|
1337
1067
|
console.log();
|
|
1068
|
+
console.log(chalk2.gray("Project structure:"));
|
|
1069
|
+
console.log(chalk2.gray(" agents/ "), chalk2.cyan("Agent definitions"));
|
|
1070
|
+
console.log(chalk2.gray(" entity-types/ "), chalk2.cyan("Entity type schemas"));
|
|
1071
|
+
console.log(chalk2.gray(" roles/ "), chalk2.cyan("Role + permission definitions"));
|
|
1072
|
+
console.log(chalk2.gray(" tools/ "), chalk2.cyan("Shared custom tools"));
|
|
1073
|
+
console.log();
|
|
1338
1074
|
console.log(chalk2.gray("Next steps:"));
|
|
1339
|
-
console.log(chalk2.gray("
|
|
1075
|
+
console.log(chalk2.gray(" 1."), chalk2.cyan("struere add agent my-agent"), chalk2.gray("- Create an agent"));
|
|
1076
|
+
console.log(chalk2.gray(" 2."), chalk2.cyan("struere dev"), chalk2.gray("- Start development"));
|
|
1340
1077
|
console.log();
|
|
1341
1078
|
});
|
|
1342
|
-
async function deriveProjectName(cwd) {
|
|
1343
|
-
const packageJsonPath = join4(cwd, "package.json");
|
|
1344
|
-
if (existsSync4(packageJsonPath)) {
|
|
1345
|
-
try {
|
|
1346
|
-
const pkg = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
|
|
1347
|
-
if (pkg.name && typeof pkg.name === "string") {
|
|
1348
|
-
return slugify(pkg.name);
|
|
1349
|
-
}
|
|
1350
|
-
} catch {}
|
|
1351
|
-
}
|
|
1352
|
-
return slugify(basename(cwd));
|
|
1353
|
-
}
|
|
1354
1079
|
function slugify(name) {
|
|
1355
1080
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1356
1081
|
}
|
|
1357
|
-
async function promptYesNo(message) {
|
|
1358
|
-
process.stdout.write(chalk2.gray(`${message} (Y/n) `));
|
|
1359
|
-
const answer = await readLine();
|
|
1360
|
-
return answer === "" || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
1361
|
-
}
|
|
1362
|
-
async function promptText(message, defaultValue) {
|
|
1363
|
-
process.stdout.write(chalk2.gray(`${message} `));
|
|
1364
|
-
process.stdout.write(chalk2.cyan(`(${defaultValue}) `));
|
|
1365
|
-
const answer = await readLine();
|
|
1366
|
-
return answer || defaultValue;
|
|
1367
|
-
}
|
|
1368
|
-
async function promptChoice(message, choices) {
|
|
1369
|
-
console.log(chalk2.gray(message));
|
|
1370
|
-
console.log();
|
|
1371
|
-
for (let i = 0;i < choices.length; i++) {
|
|
1372
|
-
const prefix = i === 0 ? chalk2.cyan("\u276F") : chalk2.gray(" ");
|
|
1373
|
-
console.log(`${prefix} ${i + 1}. ${choices[i].label}`);
|
|
1374
|
-
}
|
|
1375
|
-
console.log();
|
|
1376
|
-
process.stdout.write(chalk2.gray("Enter choice (1-" + choices.length + "): "));
|
|
1377
|
-
const answer = await readLine();
|
|
1378
|
-
const num = parseInt(answer, 10);
|
|
1379
|
-
if (num >= 1 && num <= choices.length) {
|
|
1380
|
-
return choices[num - 1].value;
|
|
1381
|
-
}
|
|
1382
|
-
return choices[0].value;
|
|
1383
|
-
}
|
|
1384
|
-
function readLine() {
|
|
1385
|
-
return new Promise((resolve) => {
|
|
1386
|
-
let buffer = "";
|
|
1387
|
-
if (process.stdin.isTTY) {
|
|
1388
|
-
process.stdin.setRawMode(false);
|
|
1389
|
-
}
|
|
1390
|
-
process.stdin.setEncoding("utf8");
|
|
1391
|
-
process.stdin.resume();
|
|
1392
|
-
const onData = (data) => {
|
|
1393
|
-
buffer += data;
|
|
1394
|
-
if (buffer.includes(`
|
|
1395
|
-
`)) {
|
|
1396
|
-
process.stdin.removeListener("data", onData);
|
|
1397
|
-
process.stdin.pause();
|
|
1398
|
-
resolve(buffer.replace(/[\r\n]/g, "").trim());
|
|
1399
|
-
}
|
|
1400
|
-
};
|
|
1401
|
-
process.stdin.on("data", onData);
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
1082
|
|
|
1405
1083
|
// src/cli/commands/dev.ts
|
|
1406
1084
|
import { Command as Command3 } from "commander";
|
|
1407
1085
|
import chalk3 from "chalk";
|
|
1408
1086
|
import ora3 from "ora";
|
|
1409
1087
|
import chokidar from "chokidar";
|
|
1410
|
-
import { join as
|
|
1088
|
+
import { join as join5 } from "path";
|
|
1411
1089
|
import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
1412
1090
|
|
|
1413
|
-
// src/cli/utils/
|
|
1414
|
-
import {
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1091
|
+
// src/cli/utils/loader.ts
|
|
1092
|
+
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
1093
|
+
import { join as join4 } from "path";
|
|
1094
|
+
async function loadAllResources(cwd) {
|
|
1095
|
+
const agents = await loadAllAgents(join4(cwd, "agents"));
|
|
1096
|
+
const entityTypes = await loadAllEntityTypes(join4(cwd, "entity-types"));
|
|
1097
|
+
const roles = await loadAllRoles(join4(cwd, "roles"));
|
|
1098
|
+
const customTools = await loadCustomTools(join4(cwd, "tools"));
|
|
1099
|
+
return { agents, entityTypes, roles, customTools };
|
|
1100
|
+
}
|
|
1101
|
+
async function loadAllAgents(dir) {
|
|
1102
|
+
if (!existsSync4(dir)) {
|
|
1103
|
+
return [];
|
|
1104
|
+
}
|
|
1105
|
+
const indexPath = join4(dir, "index.ts");
|
|
1106
|
+
if (existsSync4(indexPath)) {
|
|
1107
|
+
return loadFromIndex(indexPath);
|
|
1108
|
+
}
|
|
1109
|
+
return loadFromDirectory(dir);
|
|
1110
|
+
}
|
|
1111
|
+
async function loadAllEntityTypes(dir) {
|
|
1112
|
+
if (!existsSync4(dir)) {
|
|
1113
|
+
return [];
|
|
1114
|
+
}
|
|
1115
|
+
const indexPath = join4(dir, "index.ts");
|
|
1116
|
+
if (existsSync4(indexPath)) {
|
|
1117
|
+
return loadFromIndex(indexPath);
|
|
1118
|
+
}
|
|
1119
|
+
return loadFromDirectory(dir);
|
|
1120
|
+
}
|
|
1121
|
+
async function loadAllRoles(dir) {
|
|
1122
|
+
if (!existsSync4(dir)) {
|
|
1123
|
+
return [];
|
|
1124
|
+
}
|
|
1125
|
+
const indexPath = join4(dir, "index.ts");
|
|
1126
|
+
if (existsSync4(indexPath)) {
|
|
1127
|
+
return loadFromIndex(indexPath);
|
|
1128
|
+
}
|
|
1129
|
+
return loadFromDirectory(dir);
|
|
1130
|
+
}
|
|
1131
|
+
async function loadCustomTools(dir) {
|
|
1132
|
+
if (!existsSync4(dir)) {
|
|
1133
|
+
return [];
|
|
1134
|
+
}
|
|
1135
|
+
const indexPath = join4(dir, "index.ts");
|
|
1136
|
+
if (!existsSync4(indexPath)) {
|
|
1137
|
+
return [];
|
|
1428
1138
|
}
|
|
1429
|
-
};
|
|
1430
|
-
async function loadConfig(cwd) {
|
|
1431
|
-
const configPath = join5(cwd, "struere.config.ts");
|
|
1432
1139
|
try {
|
|
1433
|
-
const module = await import(
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
},
|
|
1442
|
-
logging: {
|
|
1443
|
-
...defaultConfig.logging,
|
|
1444
|
-
...config.logging
|
|
1445
|
-
},
|
|
1446
|
-
auth: {
|
|
1447
|
-
...defaultConfig.auth,
|
|
1448
|
-
...config.auth
|
|
1449
|
-
}
|
|
1450
|
-
};
|
|
1140
|
+
const module = await import(indexPath);
|
|
1141
|
+
if (Array.isArray(module.default)) {
|
|
1142
|
+
return module.default;
|
|
1143
|
+
}
|
|
1144
|
+
if (module.tools && Array.isArray(module.tools)) {
|
|
1145
|
+
return module.tools;
|
|
1146
|
+
}
|
|
1147
|
+
return [];
|
|
1451
1148
|
} catch {
|
|
1452
|
-
return
|
|
1149
|
+
return [];
|
|
1453
1150
|
}
|
|
1454
1151
|
}
|
|
1455
|
-
|
|
1456
|
-
// src/cli/utils/agent.ts
|
|
1457
|
-
import { join as join6 } from "path";
|
|
1458
|
-
async function loadAgent(cwd) {
|
|
1459
|
-
const agentPath = join6(cwd, "src/agent.ts");
|
|
1152
|
+
async function loadFromIndex(indexPath) {
|
|
1460
1153
|
try {
|
|
1461
|
-
const module = await import(
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1154
|
+
const module = await import(indexPath);
|
|
1155
|
+
if (Array.isArray(module.default)) {
|
|
1156
|
+
return module.default;
|
|
1157
|
+
}
|
|
1158
|
+
const items = [];
|
|
1159
|
+
for (const key of Object.keys(module)) {
|
|
1160
|
+
if (key === "default")
|
|
1161
|
+
continue;
|
|
1162
|
+
const value = module[key];
|
|
1163
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1164
|
+
items.push(value);
|
|
1165
|
+
}
|
|
1465
1166
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1167
|
+
return items;
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
throw new Error(`Failed to load index at ${indexPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async function loadFromDirectory(dir) {
|
|
1173
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".ts") && f !== "index.ts" && !f.endsWith(".d.ts"));
|
|
1174
|
+
const items = [];
|
|
1175
|
+
for (const file of files) {
|
|
1176
|
+
const filePath = join4(dir, file);
|
|
1177
|
+
try {
|
|
1178
|
+
const module = await import(filePath);
|
|
1179
|
+
if (module.default) {
|
|
1180
|
+
items.push(module.default);
|
|
1181
|
+
}
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
throw new Error(`Failed to load ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1468
1184
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1185
|
+
}
|
|
1186
|
+
return items;
|
|
1187
|
+
}
|
|
1188
|
+
function getResourceDirectories(cwd) {
|
|
1189
|
+
return {
|
|
1190
|
+
agents: join4(cwd, "agents"),
|
|
1191
|
+
entityTypes: join4(cwd, "entity-types"),
|
|
1192
|
+
roles: join4(cwd, "roles"),
|
|
1193
|
+
tools: join4(cwd, "tools")
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/cli/utils/extractor.ts
|
|
1198
|
+
var BUILTIN_TOOLS = [
|
|
1199
|
+
"entity.create",
|
|
1200
|
+
"entity.get",
|
|
1201
|
+
"entity.query",
|
|
1202
|
+
"entity.update",
|
|
1203
|
+
"entity.delete",
|
|
1204
|
+
"entity.link",
|
|
1205
|
+
"entity.unlink",
|
|
1206
|
+
"event.emit",
|
|
1207
|
+
"event.query",
|
|
1208
|
+
"job.enqueue",
|
|
1209
|
+
"job.status"
|
|
1210
|
+
];
|
|
1211
|
+
function extractSyncPayload(resources) {
|
|
1212
|
+
const customToolsMap = new Map;
|
|
1213
|
+
for (const tool of resources.customTools) {
|
|
1214
|
+
customToolsMap.set(tool.name, tool);
|
|
1215
|
+
}
|
|
1216
|
+
const agents = resources.agents.map((agent) => extractAgentPayload(agent, customToolsMap));
|
|
1217
|
+
const entityTypes = resources.entityTypes.map((et) => ({
|
|
1218
|
+
name: et.name,
|
|
1219
|
+
slug: et.slug,
|
|
1220
|
+
schema: et.schema,
|
|
1221
|
+
searchFields: et.searchFields,
|
|
1222
|
+
displayConfig: et.displayConfig
|
|
1223
|
+
}));
|
|
1224
|
+
const roles = resources.roles.map((role) => ({
|
|
1225
|
+
name: role.name,
|
|
1226
|
+
description: role.description,
|
|
1227
|
+
policies: role.policies.map((p) => ({
|
|
1228
|
+
resource: p.resource,
|
|
1229
|
+
actions: p.actions,
|
|
1230
|
+
effect: p.effect,
|
|
1231
|
+
priority: p.priority
|
|
1232
|
+
})),
|
|
1233
|
+
scopeRules: role.scopeRules?.map((sr) => ({
|
|
1234
|
+
entityType: sr.entityType,
|
|
1235
|
+
field: sr.field,
|
|
1236
|
+
operator: sr.operator,
|
|
1237
|
+
value: sr.value
|
|
1238
|
+
})),
|
|
1239
|
+
fieldMasks: role.fieldMasks?.map((fm) => ({
|
|
1240
|
+
entityType: fm.entityType,
|
|
1241
|
+
fieldPath: fm.fieldPath,
|
|
1242
|
+
maskType: fm.maskType,
|
|
1243
|
+
maskConfig: fm.maskConfig
|
|
1244
|
+
}))
|
|
1245
|
+
}));
|
|
1246
|
+
return { agents, entityTypes, roles };
|
|
1247
|
+
}
|
|
1248
|
+
function extractAgentPayload(agent, customToolsMap) {
|
|
1249
|
+
let systemPrompt;
|
|
1250
|
+
if (typeof agent.systemPrompt === "function") {
|
|
1251
|
+
const result = agent.systemPrompt();
|
|
1252
|
+
if (result instanceof Promise) {
|
|
1253
|
+
throw new Error("Async system prompts must be resolved before syncing");
|
|
1471
1254
|
}
|
|
1472
|
-
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1255
|
+
systemPrompt = result;
|
|
1256
|
+
} else {
|
|
1257
|
+
systemPrompt = agent.systemPrompt;
|
|
1258
|
+
}
|
|
1259
|
+
const tools = (agent.tools || []).map((toolName) => {
|
|
1260
|
+
const isBuiltin = BUILTIN_TOOLS.includes(toolName);
|
|
1261
|
+
if (isBuiltin) {
|
|
1262
|
+
return {
|
|
1263
|
+
name: toolName,
|
|
1264
|
+
description: getBuiltinToolDescription(toolName),
|
|
1265
|
+
parameters: { type: "object", properties: {} },
|
|
1266
|
+
isBuiltin: true
|
|
1267
|
+
};
|
|
1476
1268
|
}
|
|
1477
|
-
|
|
1269
|
+
const customTool = customToolsMap.get(toolName);
|
|
1270
|
+
if (!customTool) {
|
|
1271
|
+
throw new Error(`Tool "${toolName}" not found in custom tools`);
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
name: customTool.name,
|
|
1275
|
+
description: customTool.description,
|
|
1276
|
+
parameters: customTool.parameters || { type: "object", properties: {} },
|
|
1277
|
+
handlerCode: extractHandlerCode(customTool.handler),
|
|
1278
|
+
isBuiltin: false
|
|
1279
|
+
};
|
|
1280
|
+
});
|
|
1281
|
+
return {
|
|
1282
|
+
name: agent.name,
|
|
1283
|
+
slug: agent.slug,
|
|
1284
|
+
version: agent.version,
|
|
1285
|
+
description: agent.description,
|
|
1286
|
+
systemPrompt,
|
|
1287
|
+
model: {
|
|
1288
|
+
provider: agent.model?.provider || "anthropic",
|
|
1289
|
+
name: agent.model?.name || "claude-sonnet-4-20250514",
|
|
1290
|
+
temperature: agent.model?.temperature,
|
|
1291
|
+
maxTokens: agent.model?.maxTokens
|
|
1292
|
+
},
|
|
1293
|
+
tools
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function getBuiltinToolDescription(name) {
|
|
1297
|
+
const descriptions = {
|
|
1298
|
+
"entity.create": "Create a new entity",
|
|
1299
|
+
"entity.get": "Get an entity by ID",
|
|
1300
|
+
"entity.query": "Query entities by type and filters",
|
|
1301
|
+
"entity.update": "Update an entity",
|
|
1302
|
+
"entity.delete": "Delete an entity",
|
|
1303
|
+
"entity.link": "Link two entities",
|
|
1304
|
+
"entity.unlink": "Unlink two entities",
|
|
1305
|
+
"event.emit": "Emit an event",
|
|
1306
|
+
"event.query": "Query events",
|
|
1307
|
+
"job.enqueue": "Schedule a background job",
|
|
1308
|
+
"job.status": "Get job status"
|
|
1309
|
+
};
|
|
1310
|
+
return descriptions[name] || name;
|
|
1311
|
+
}
|
|
1312
|
+
function extractHandlerCode(handler) {
|
|
1313
|
+
const code = handler.toString();
|
|
1314
|
+
const arrowMatch = code.match(/(?:async\s*)?\([^)]*\)\s*=>\s*\{?([\s\S]*)\}?$/);
|
|
1315
|
+
if (arrowMatch) {
|
|
1316
|
+
let body = arrowMatch[1].trim();
|
|
1317
|
+
if (body.startsWith("{") && body.endsWith("}")) {
|
|
1318
|
+
body = body.slice(1, -1).trim();
|
|
1319
|
+
}
|
|
1320
|
+
return body;
|
|
1478
1321
|
}
|
|
1322
|
+
const funcMatch = code.match(/(?:async\s*)?function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
|
|
1323
|
+
if (funcMatch) {
|
|
1324
|
+
return funcMatch[1].trim();
|
|
1325
|
+
}
|
|
1326
|
+
return code;
|
|
1479
1327
|
}
|
|
1480
1328
|
|
|
1481
1329
|
// src/cli/commands/dev.ts
|
|
1482
|
-
var devCommand = new Command3("dev").description("Sync
|
|
1330
|
+
var devCommand = new Command3("dev").description("Sync all resources to development environment").action(async () => {
|
|
1483
1331
|
const spinner = ora3();
|
|
1484
1332
|
const cwd = process.cwd();
|
|
1485
1333
|
console.log();
|
|
1486
1334
|
console.log(chalk3.bold("Struere Dev"));
|
|
1487
1335
|
console.log();
|
|
1488
|
-
let project = loadProject(cwd);
|
|
1489
1336
|
if (!hasProject(cwd)) {
|
|
1490
|
-
console.log(chalk3.yellow("No struere.json found"));
|
|
1337
|
+
console.log(chalk3.yellow("No struere.json found - initializing project..."));
|
|
1338
|
+
console.log();
|
|
1339
|
+
await runInit(cwd);
|
|
1491
1340
|
console.log();
|
|
1492
|
-
const setupResult = await interactiveSetup(cwd);
|
|
1493
|
-
if (!setupResult) {
|
|
1494
|
-
process.exit(0);
|
|
1495
|
-
}
|
|
1496
|
-
project = setupResult;
|
|
1497
1341
|
}
|
|
1498
|
-
|
|
1342
|
+
const version = getProjectVersion(cwd);
|
|
1343
|
+
if (version === "1.0") {
|
|
1344
|
+
console.log(chalk3.yellow("This is a v1 agent-centric project."));
|
|
1345
|
+
console.log(chalk3.yellow("Please migrate to v2 structure or use an older CLI version."));
|
|
1346
|
+
console.log();
|
|
1347
|
+
process.exit(1);
|
|
1348
|
+
}
|
|
1349
|
+
const project = loadProjectV2(cwd);
|
|
1499
1350
|
if (!project) {
|
|
1500
1351
|
console.log(chalk3.red("Failed to load struere.json"));
|
|
1501
1352
|
process.exit(1);
|
|
1502
1353
|
}
|
|
1503
|
-
console.log(chalk3.gray("
|
|
1354
|
+
console.log(chalk3.gray("Organization:"), chalk3.cyan(project.organization.name));
|
|
1504
1355
|
console.log();
|
|
1505
|
-
spinner.start("Loading configuration");
|
|
1506
|
-
await loadConfig(cwd);
|
|
1507
|
-
spinner.succeed("Configuration loaded");
|
|
1508
|
-
spinner.start("Loading agent");
|
|
1509
|
-
let agent = await loadAgent(cwd);
|
|
1510
|
-
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
1511
|
-
const claudeMdPath = join7(cwd, "CLAUDE.md");
|
|
1512
|
-
if (!existsSync5(claudeMdPath)) {
|
|
1513
|
-
writeFileSync4(claudeMdPath, getClaudeMD(project.agent.slug));
|
|
1514
|
-
console.log(chalk3.green("\u2713"), "Created CLAUDE.md");
|
|
1515
|
-
}
|
|
1516
1356
|
let credentials = loadCredentials();
|
|
1517
1357
|
const apiKey = getApiKey();
|
|
1518
1358
|
if (!credentials && !apiKey) {
|
|
1519
|
-
console.log(chalk3.
|
|
1359
|
+
console.log(chalk3.yellow("Not logged in - authenticating..."));
|
|
1520
1360
|
console.log();
|
|
1521
1361
|
credentials = await performLogin();
|
|
1522
1362
|
if (!credentials) {
|
|
1523
1363
|
console.log(chalk3.red("Authentication failed"));
|
|
1524
1364
|
process.exit(1);
|
|
1525
1365
|
}
|
|
1366
|
+
console.log();
|
|
1367
|
+
}
|
|
1368
|
+
const claudeMdPath = join5(cwd, "CLAUDE.md");
|
|
1369
|
+
if (!existsSync5(claudeMdPath)) {
|
|
1370
|
+
writeFileSync4(claudeMdPath, getClaudeMDV2(project.organization.name));
|
|
1371
|
+
console.log(chalk3.green("\u2713"), "Created CLAUDE.md");
|
|
1526
1372
|
}
|
|
1527
|
-
spinner.start("Syncing to Convex");
|
|
1528
|
-
const performSync = async () => {
|
|
1529
|
-
try {
|
|
1530
|
-
const config = extractConfig(agent);
|
|
1531
|
-
const result = await syncToConvex(project.agentId, config);
|
|
1532
|
-
if (!result.success) {
|
|
1533
|
-
throw new Error(result.error || "Sync failed");
|
|
1534
|
-
}
|
|
1535
|
-
return true;
|
|
1536
|
-
} catch (error) {
|
|
1537
|
-
throw error;
|
|
1538
|
-
}
|
|
1539
|
-
};
|
|
1540
1373
|
const isAuthError = (error) => {
|
|
1541
1374
|
const message = error instanceof Error ? error.message : String(error);
|
|
1542
1375
|
return message.includes("Unauthenticated") || message.includes("OIDC") || message.includes("token") || message.includes("expired");
|
|
1543
1376
|
};
|
|
1377
|
+
const performSync = async () => {
|
|
1378
|
+
const resources = await loadAllResources(cwd);
|
|
1379
|
+
const payload = extractSyncPayload(resources);
|
|
1380
|
+
const result = await syncOrganization(payload);
|
|
1381
|
+
if (!result.success) {
|
|
1382
|
+
throw new Error(result.error || "Sync failed");
|
|
1383
|
+
}
|
|
1384
|
+
return true;
|
|
1385
|
+
};
|
|
1386
|
+
spinner.start("Loading resources");
|
|
1387
|
+
try {
|
|
1388
|
+
const resources = await loadAllResources(cwd);
|
|
1389
|
+
spinner.succeed(`Loaded ${resources.agents.length} agents, ${resources.entityTypes.length} entity types, ${resources.roles.length} roles`);
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
spinner.fail("Failed to load resources");
|
|
1392
|
+
console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
1393
|
+
process.exit(1);
|
|
1394
|
+
}
|
|
1395
|
+
spinner.start("Syncing to Convex");
|
|
1544
1396
|
try {
|
|
1545
1397
|
await performSync();
|
|
1546
1398
|
spinner.succeed("Synced to development");
|
|
1547
1399
|
} catch (error) {
|
|
1548
1400
|
if (isAuthError(error)) {
|
|
1549
|
-
spinner.fail("Session expired");
|
|
1550
|
-
console.log();
|
|
1551
|
-
console.log(chalk3.gray("Re-authenticating..."));
|
|
1401
|
+
spinner.fail("Session expired - re-authenticating...");
|
|
1552
1402
|
clearCredentials();
|
|
1553
1403
|
credentials = await performLogin();
|
|
1554
1404
|
if (!credentials) {
|
|
@@ -1570,13 +1420,18 @@ var devCommand = new Command3("dev").description("Sync agent to development envi
|
|
|
1570
1420
|
process.exit(1);
|
|
1571
1421
|
}
|
|
1572
1422
|
}
|
|
1573
|
-
const devUrl = `https://${project.agent.slug}-dev.struere.dev`;
|
|
1574
|
-
console.log();
|
|
1575
|
-
console.log(chalk3.green("Development URL:"), chalk3.cyan(devUrl));
|
|
1576
1423
|
console.log();
|
|
1577
1424
|
console.log(chalk3.gray("Watching for changes... Press Ctrl+C to stop"));
|
|
1578
1425
|
console.log();
|
|
1579
|
-
const
|
|
1426
|
+
const dirs = getResourceDirectories(cwd);
|
|
1427
|
+
const watchPaths = [
|
|
1428
|
+
dirs.agents,
|
|
1429
|
+
dirs.entityTypes,
|
|
1430
|
+
dirs.roles,
|
|
1431
|
+
dirs.tools,
|
|
1432
|
+
join5(cwd, "struere.config.ts")
|
|
1433
|
+
].filter((p) => existsSync5(p));
|
|
1434
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
1580
1435
|
ignoreInitial: true,
|
|
1581
1436
|
ignored: /node_modules/
|
|
1582
1437
|
});
|
|
@@ -1585,14 +1440,11 @@ var devCommand = new Command3("dev").description("Sync agent to development envi
|
|
|
1585
1440
|
console.log(chalk3.gray(`Changed: ${relativePath}`));
|
|
1586
1441
|
const syncSpinner = ora3("Syncing...").start();
|
|
1587
1442
|
try {
|
|
1588
|
-
agent = await loadAgent(cwd);
|
|
1589
1443
|
await performSync();
|
|
1590
1444
|
syncSpinner.succeed("Synced");
|
|
1591
1445
|
} catch (error) {
|
|
1592
1446
|
if (isAuthError(error)) {
|
|
1593
|
-
syncSpinner.fail("Session expired");
|
|
1594
|
-
console.log();
|
|
1595
|
-
console.log(chalk3.gray("Re-authenticating..."));
|
|
1447
|
+
syncSpinner.fail("Session expired - re-authenticating...");
|
|
1596
1448
|
clearCredentials();
|
|
1597
1449
|
const newCredentials = await performLogin();
|
|
1598
1450
|
if (!newCredentials) {
|
|
@@ -1600,257 +1452,50 @@ var devCommand = new Command3("dev").description("Sync agent to development envi
|
|
|
1600
1452
|
return;
|
|
1601
1453
|
}
|
|
1602
1454
|
const retrySyncSpinner = ora3("Syncing...").start();
|
|
1603
|
-
try {
|
|
1604
|
-
await performSync();
|
|
1605
|
-
retrySyncSpinner.succeed("Synced");
|
|
1606
|
-
} catch (retryError) {
|
|
1607
|
-
retrySyncSpinner.fail("Sync failed");
|
|
1608
|
-
console.log(chalk3.red("Error:"), retryError instanceof Error ? retryError.message : String(retryError));
|
|
1609
|
-
}
|
|
1610
|
-
} else {
|
|
1611
|
-
syncSpinner.fail("Sync failed");
|
|
1612
|
-
console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
});
|
|
1616
|
-
process.on("SIGINT", () => {
|
|
1617
|
-
console.log();
|
|
1618
|
-
watcher.close();
|
|
1619
|
-
console.log(chalk3.gray("Stopped"));
|
|
1620
|
-
process.exit(0);
|
|
1621
|
-
});
|
|
1622
|
-
});
|
|
1623
|
-
async function interactiveSetup(cwd) {
|
|
1624
|
-
const spinner = ora3();
|
|
1625
|
-
let credentials = loadCredentials();
|
|
1626
|
-
if (!credentials) {
|
|
1627
|
-
console.log(chalk3.gray("Authentication required"));
|
|
1628
|
-
console.log();
|
|
1629
|
-
credentials = await performLogin();
|
|
1630
|
-
if (!credentials) {
|
|
1631
|
-
console.log(chalk3.red("Authentication failed"));
|
|
1632
|
-
return null;
|
|
1633
|
-
}
|
|
1634
|
-
} else {
|
|
1635
|
-
console.log(chalk3.green("\u2713"), "Logged in as", chalk3.cyan(credentials.user.name));
|
|
1636
|
-
console.log();
|
|
1637
|
-
}
|
|
1638
|
-
spinner.start("Fetching agents");
|
|
1639
|
-
let { agents: existingAgents, error: listError } = await listAgents();
|
|
1640
|
-
if (listError) {
|
|
1641
|
-
const isAuthError = listError.includes("Unauthenticated") || listError.includes("OIDC") || listError.includes("token") || listError.includes("expired");
|
|
1642
|
-
if (isAuthError) {
|
|
1643
|
-
spinner.fail("Session expired");
|
|
1644
|
-
console.log();
|
|
1645
|
-
console.log(chalk3.gray("Re-authenticating..."));
|
|
1646
|
-
clearCredentials();
|
|
1647
|
-
credentials = await performLogin();
|
|
1648
|
-
if (!credentials) {
|
|
1649
|
-
console.log(chalk3.red("Authentication failed"));
|
|
1650
|
-
return null;
|
|
1651
|
-
}
|
|
1652
|
-
spinner.start("Fetching agents");
|
|
1653
|
-
const retryResult = await listAgents();
|
|
1654
|
-
existingAgents = retryResult.agents;
|
|
1655
|
-
listError = retryResult.error;
|
|
1656
|
-
}
|
|
1657
|
-
if (listError) {
|
|
1658
|
-
spinner.fail("Failed to fetch agents");
|
|
1659
|
-
console.log();
|
|
1660
|
-
console.log(chalk3.red("Error:"), listError);
|
|
1661
|
-
return null;
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
const agents = existingAgents.map((a) => ({ id: a._id, name: a.name, slug: a.slug }));
|
|
1665
|
-
spinner.succeed(`Found ${agents.length} existing agent(s)`);
|
|
1666
|
-
let selectedAgent = null;
|
|
1667
|
-
if (agents.length === 0) {
|
|
1668
|
-
console.log(chalk3.gray("No existing agents found. Creating a new one..."));
|
|
1669
|
-
} else {
|
|
1670
|
-
console.log();
|
|
1671
|
-
const choices = [
|
|
1672
|
-
{ value: "link", label: "Link to an existing agent" },
|
|
1673
|
-
{ value: "create", label: "Create a new agent" },
|
|
1674
|
-
{ value: "cancel", label: "Cancel" }
|
|
1675
|
-
];
|
|
1676
|
-
const action = await promptChoiceArrows("No agent configured. Would you like to:", choices);
|
|
1677
|
-
if (action === "cancel") {
|
|
1678
|
-
console.log();
|
|
1679
|
-
console.log(chalk3.gray("Run"), chalk3.cyan("struere init"), chalk3.gray("when ready to set up"));
|
|
1680
|
-
return null;
|
|
1681
|
-
}
|
|
1682
|
-
if (action === "link") {
|
|
1683
|
-
console.log();
|
|
1684
|
-
const agentChoices = agents.map((a) => ({ value: a.id, label: `${a.name} (${a.slug})` }));
|
|
1685
|
-
const agentId = await promptChoiceArrows("Select an agent:", agentChoices);
|
|
1686
|
-
selectedAgent = agents.find((a) => a.id === agentId) || null;
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
if (!selectedAgent) {
|
|
1690
|
-
console.log();
|
|
1691
|
-
const projectName = slugify2(basename2(cwd));
|
|
1692
|
-
const name = await promptText2("Agent name:", projectName);
|
|
1693
|
-
const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1694
|
-
spinner.start("Creating agent");
|
|
1695
|
-
let { agentId, error: createError } = await createAgent({
|
|
1696
|
-
name: displayName,
|
|
1697
|
-
slug: name,
|
|
1698
|
-
description: `${displayName} Agent`
|
|
1699
|
-
});
|
|
1700
|
-
if (createError) {
|
|
1701
|
-
const isAuthError = createError.includes("Unauthenticated") || createError.includes("OIDC") || createError.includes("token") || createError.includes("expired");
|
|
1702
|
-
if (isAuthError) {
|
|
1703
|
-
spinner.fail("Session expired");
|
|
1704
|
-
console.log();
|
|
1705
|
-
console.log(chalk3.gray("Re-authenticating..."));
|
|
1706
|
-
clearCredentials();
|
|
1707
|
-
credentials = await performLogin();
|
|
1708
|
-
if (!credentials) {
|
|
1709
|
-
console.log(chalk3.red("Authentication failed"));
|
|
1710
|
-
return null;
|
|
1711
|
-
}
|
|
1712
|
-
spinner.start("Creating agent");
|
|
1713
|
-
const retryResult = await createAgent({
|
|
1714
|
-
name: displayName,
|
|
1715
|
-
slug: name,
|
|
1716
|
-
description: `${displayName} Agent`
|
|
1717
|
-
});
|
|
1718
|
-
agentId = retryResult.agentId;
|
|
1719
|
-
createError = retryResult.error;
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
if (createError || !agentId) {
|
|
1723
|
-
spinner.fail("Failed to create agent");
|
|
1724
|
-
console.log();
|
|
1725
|
-
console.log(chalk3.red("Error:"), createError || "Unknown error");
|
|
1726
|
-
return null;
|
|
1727
|
-
}
|
|
1728
|
-
selectedAgent = { id: agentId, name: displayName, slug: name };
|
|
1729
|
-
spinner.succeed(`Created agent "${name}"`);
|
|
1730
|
-
}
|
|
1731
|
-
if (!selectedAgent) {
|
|
1732
|
-
return null;
|
|
1733
|
-
}
|
|
1734
|
-
const projectData = {
|
|
1735
|
-
agentId: selectedAgent.id,
|
|
1736
|
-
team: credentials.organization.slug,
|
|
1737
|
-
agent: {
|
|
1738
|
-
slug: selectedAgent.slug,
|
|
1739
|
-
name: selectedAgent.name
|
|
1740
|
-
}
|
|
1741
|
-
};
|
|
1742
|
-
saveProject(cwd, projectData);
|
|
1743
|
-
console.log(chalk3.green("\u2713"), "Created struere.json");
|
|
1744
|
-
if (!hasAgentFiles(cwd)) {
|
|
1745
|
-
const scaffoldResult = scaffoldAgentFiles(cwd, selectedAgent.slug);
|
|
1746
|
-
for (const file of scaffoldResult.createdFiles) {
|
|
1747
|
-
console.log(chalk3.green("\u2713"), `Created ${file}`);
|
|
1748
|
-
}
|
|
1749
|
-
console.log();
|
|
1750
|
-
spinner.start("Installing dependencies");
|
|
1751
|
-
try {
|
|
1752
|
-
const proc = Bun.spawn(["bun", "install"], {
|
|
1753
|
-
cwd,
|
|
1754
|
-
stdout: "pipe",
|
|
1755
|
-
stderr: "pipe"
|
|
1756
|
-
});
|
|
1757
|
-
await proc.exited;
|
|
1758
|
-
if (proc.exitCode === 0) {
|
|
1759
|
-
spinner.succeed("Dependencies installed");
|
|
1455
|
+
try {
|
|
1456
|
+
await performSync();
|
|
1457
|
+
retrySyncSpinner.succeed("Synced");
|
|
1458
|
+
} catch (retryError) {
|
|
1459
|
+
retrySyncSpinner.fail("Sync failed");
|
|
1460
|
+
console.log(chalk3.red("Error:"), retryError instanceof Error ? retryError.message : String(retryError));
|
|
1461
|
+
}
|
|
1760
1462
|
} else {
|
|
1761
|
-
|
|
1762
|
-
console.log(chalk3.
|
|
1763
|
-
}
|
|
1764
|
-
} catch {
|
|
1765
|
-
spinner.fail("Failed to install dependencies");
|
|
1766
|
-
console.log(chalk3.yellow("Run"), chalk3.cyan("bun install"), chalk3.yellow("manually"));
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
console.log();
|
|
1770
|
-
return projectData;
|
|
1771
|
-
}
|
|
1772
|
-
function slugify2(name) {
|
|
1773
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1774
|
-
}
|
|
1775
|
-
async function promptChoiceArrows(message, choices) {
|
|
1776
|
-
return new Promise((resolve) => {
|
|
1777
|
-
let selectedIndex = 0;
|
|
1778
|
-
const render = () => {
|
|
1779
|
-
process.stdout.write("\x1B[?25l");
|
|
1780
|
-
process.stdout.write(`\x1B[${choices.length + 2}A`);
|
|
1781
|
-
console.log(chalk3.gray(message));
|
|
1782
|
-
console.log();
|
|
1783
|
-
for (let i = 0;i < choices.length; i++) {
|
|
1784
|
-
const prefix = i === selectedIndex ? chalk3.cyan("\u276F") : " ";
|
|
1785
|
-
const label = i === selectedIndex ? chalk3.cyan(choices[i].label) : choices[i].label;
|
|
1786
|
-
console.log(`${prefix} ${label}`);
|
|
1463
|
+
syncSpinner.fail("Sync failed");
|
|
1464
|
+
console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
1787
1465
|
}
|
|
1788
|
-
};
|
|
1789
|
-
console.log(chalk3.gray(message));
|
|
1790
|
-
console.log();
|
|
1791
|
-
for (let i = 0;i < choices.length; i++) {
|
|
1792
|
-
const prefix = i === selectedIndex ? chalk3.cyan("\u276F") : " ";
|
|
1793
|
-
const label = i === selectedIndex ? chalk3.cyan(choices[i].label) : choices[i].label;
|
|
1794
|
-
console.log(`${prefix} ${label}`);
|
|
1795
1466
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1467
|
+
});
|
|
1468
|
+
watcher.on("add", async (path) => {
|
|
1469
|
+
const relativePath = path.replace(cwd, ".");
|
|
1470
|
+
console.log(chalk3.gray(`Added: ${relativePath}`));
|
|
1471
|
+
const syncSpinner = ora3("Syncing...").start();
|
|
1472
|
+
try {
|
|
1473
|
+
await performSync();
|
|
1474
|
+
syncSpinner.succeed("Synced");
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
syncSpinner.fail("Sync failed");
|
|
1477
|
+
console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
1799
1478
|
}
|
|
1800
|
-
process.stdin.setRawMode?.(true);
|
|
1801
|
-
process.stdin.resume();
|
|
1802
|
-
const onKeypress = (key) => {
|
|
1803
|
-
const char = key.toString();
|
|
1804
|
-
if (char === "\x1B[A" || char === "k") {
|
|
1805
|
-
selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
|
|
1806
|
-
render();
|
|
1807
|
-
} else if (char === "\x1B[B" || char === "j") {
|
|
1808
|
-
selectedIndex = (selectedIndex + 1) % choices.length;
|
|
1809
|
-
render();
|
|
1810
|
-
} else if (char === "\r" || char === `
|
|
1811
|
-
`) {
|
|
1812
|
-
process.stdin.removeListener("data", onKeypress);
|
|
1813
|
-
process.stdin.setRawMode?.(false);
|
|
1814
|
-
process.stdin.pause();
|
|
1815
|
-
process.stdout.write("\x1B[?25h");
|
|
1816
|
-
resolve(choices[selectedIndex].value);
|
|
1817
|
-
} else if (char === "\x03") {
|
|
1818
|
-
process.stdin.removeListener("data", onKeypress);
|
|
1819
|
-
process.stdin.setRawMode?.(false);
|
|
1820
|
-
process.stdout.write("\x1B[?25h");
|
|
1821
|
-
process.exit(0);
|
|
1822
|
-
}
|
|
1823
|
-
};
|
|
1824
|
-
process.stdin.on("data", onKeypress);
|
|
1825
1479
|
});
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
const onData = (chunk) => {
|
|
1837
|
-
const str = chunk.toString();
|
|
1838
|
-
buffer += str;
|
|
1839
|
-
if (str.includes(`
|
|
1840
|
-
`) || str.includes("\r")) {
|
|
1841
|
-
process.stdin.removeListener("data", onData);
|
|
1842
|
-
process.stdin.pause();
|
|
1843
|
-
process.stdin.setRawMode?.(false);
|
|
1844
|
-
resolve(buffer.replace(/[\r\n]/g, "").trim());
|
|
1845
|
-
}
|
|
1846
|
-
};
|
|
1847
|
-
if (process.stdin.isTTY) {
|
|
1848
|
-
process.stdin.setRawMode?.(false);
|
|
1480
|
+
watcher.on("unlink", async (path) => {
|
|
1481
|
+
const relativePath = path.replace(cwd, ".");
|
|
1482
|
+
console.log(chalk3.gray(`Removed: ${relativePath}`));
|
|
1483
|
+
const syncSpinner = ora3("Syncing...").start();
|
|
1484
|
+
try {
|
|
1485
|
+
await performSync();
|
|
1486
|
+
syncSpinner.succeed("Synced");
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
syncSpinner.fail("Sync failed");
|
|
1489
|
+
console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
1849
1490
|
}
|
|
1850
|
-
process.stdin.resume();
|
|
1851
|
-
process.stdin.on("data", onData);
|
|
1852
1491
|
});
|
|
1853
|
-
|
|
1492
|
+
process.on("SIGINT", () => {
|
|
1493
|
+
console.log();
|
|
1494
|
+
watcher.close();
|
|
1495
|
+
console.log(chalk3.gray("Stopped"));
|
|
1496
|
+
process.exit(0);
|
|
1497
|
+
});
|
|
1498
|
+
});
|
|
1854
1499
|
|
|
1855
1500
|
// src/cli/commands/build.ts
|
|
1856
1501
|
import { Command as Command4 } from "commander";
|
|
@@ -1858,6 +1503,74 @@ import chalk4 from "chalk";
|
|
|
1858
1503
|
import ora4 from "ora";
|
|
1859
1504
|
import { join as join8 } from "path";
|
|
1860
1505
|
|
|
1506
|
+
// src/cli/utils/config.ts
|
|
1507
|
+
import { join as join6 } from "path";
|
|
1508
|
+
var defaultConfig = {
|
|
1509
|
+
port: 3000,
|
|
1510
|
+
host: "localhost",
|
|
1511
|
+
cors: {
|
|
1512
|
+
origins: ["http://localhost:3000"],
|
|
1513
|
+
credentials: true
|
|
1514
|
+
},
|
|
1515
|
+
logging: {
|
|
1516
|
+
level: "info",
|
|
1517
|
+
format: "pretty"
|
|
1518
|
+
},
|
|
1519
|
+
auth: {
|
|
1520
|
+
type: "none"
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
async function loadConfig(cwd) {
|
|
1524
|
+
const configPath = join6(cwd, "struere.config.ts");
|
|
1525
|
+
try {
|
|
1526
|
+
const module = await import(configPath);
|
|
1527
|
+
const config = module.default || module;
|
|
1528
|
+
return {
|
|
1529
|
+
...defaultConfig,
|
|
1530
|
+
...config,
|
|
1531
|
+
cors: {
|
|
1532
|
+
...defaultConfig.cors,
|
|
1533
|
+
...config.cors
|
|
1534
|
+
},
|
|
1535
|
+
logging: {
|
|
1536
|
+
...defaultConfig.logging,
|
|
1537
|
+
...config.logging
|
|
1538
|
+
},
|
|
1539
|
+
auth: {
|
|
1540
|
+
...defaultConfig.auth,
|
|
1541
|
+
...config.auth
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
} catch {
|
|
1545
|
+
return defaultConfig;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/cli/utils/agent.ts
|
|
1550
|
+
import { join as join7 } from "path";
|
|
1551
|
+
async function loadAgent(cwd) {
|
|
1552
|
+
const agentPath = join7(cwd, "src/agent.ts");
|
|
1553
|
+
try {
|
|
1554
|
+
const module = await import(`${agentPath}?t=${Date.now()}`);
|
|
1555
|
+
const agent = module.default || module;
|
|
1556
|
+
if (!agent.name) {
|
|
1557
|
+
throw new Error("Agent must have a name");
|
|
1558
|
+
}
|
|
1559
|
+
if (!agent.version) {
|
|
1560
|
+
throw new Error("Agent must have a version");
|
|
1561
|
+
}
|
|
1562
|
+
if (!agent.systemPrompt) {
|
|
1563
|
+
throw new Error("Agent must have a systemPrompt");
|
|
1564
|
+
}
|
|
1565
|
+
return agent;
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
if (error instanceof Error && error.message.includes("Cannot find module")) {
|
|
1568
|
+
throw new Error(`Agent not found at ${agentPath}`);
|
|
1569
|
+
}
|
|
1570
|
+
throw error;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1861
1574
|
// src/cli/utils/validate.ts
|
|
1862
1575
|
function validateAgent(agent) {
|
|
1863
1576
|
const errors = [];
|
|
@@ -2178,73 +1891,90 @@ function formatAssertionError(assertion, context) {
|
|
|
2178
1891
|
import { Command as Command6 } from "commander";
|
|
2179
1892
|
import chalk6 from "chalk";
|
|
2180
1893
|
import ora6 from "ora";
|
|
2181
|
-
var deployCommand = new Command6("deploy").description("Deploy
|
|
2182
|
-
const environment = "production";
|
|
1894
|
+
var deployCommand = new Command6("deploy").description("Deploy all agents to production").option("--dry-run", "Show what would be deployed without deploying").action(async (options) => {
|
|
2183
1895
|
const spinner = ora6();
|
|
2184
1896
|
const cwd = process.cwd();
|
|
2185
1897
|
console.log();
|
|
2186
|
-
console.log(chalk6.bold("Deploying
|
|
1898
|
+
console.log(chalk6.bold("Deploying Agents"));
|
|
2187
1899
|
console.log();
|
|
2188
1900
|
if (!hasProject(cwd)) {
|
|
2189
|
-
console.log(chalk6.yellow("No struere.json found"));
|
|
1901
|
+
console.log(chalk6.yellow("No struere.json found - initializing project..."));
|
|
1902
|
+
console.log();
|
|
1903
|
+
const success = await runInit(cwd);
|
|
1904
|
+
if (!success) {
|
|
1905
|
+
process.exit(1);
|
|
1906
|
+
}
|
|
2190
1907
|
console.log();
|
|
2191
|
-
|
|
1908
|
+
}
|
|
1909
|
+
const version = getProjectVersion(cwd);
|
|
1910
|
+
if (version === "1.0") {
|
|
1911
|
+
console.log(chalk6.yellow("This is a v1 agent-centric project."));
|
|
1912
|
+
console.log(chalk6.yellow("Please migrate to v2 structure or use an older CLI version."));
|
|
2192
1913
|
console.log();
|
|
2193
1914
|
process.exit(1);
|
|
2194
1915
|
}
|
|
2195
|
-
const project =
|
|
1916
|
+
const project = loadProjectV2(cwd);
|
|
2196
1917
|
if (!project) {
|
|
2197
1918
|
console.log(chalk6.red("Failed to load struere.json"));
|
|
2198
1919
|
process.exit(1);
|
|
2199
1920
|
}
|
|
2200
|
-
console.log(chalk6.gray("
|
|
1921
|
+
console.log(chalk6.gray("Organization:"), chalk6.cyan(project.organization.name));
|
|
2201
1922
|
console.log();
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
const agent = await loadAgent(cwd);
|
|
2207
|
-
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
2208
|
-
spinner.start("Validating agent");
|
|
2209
|
-
const errors = validateAgent(agent);
|
|
2210
|
-
if (errors.length > 0) {
|
|
2211
|
-
spinner.fail("Validation failed");
|
|
1923
|
+
let credentials = loadCredentials();
|
|
1924
|
+
const apiKey = getApiKey();
|
|
1925
|
+
if (!credentials && !apiKey) {
|
|
1926
|
+
console.log(chalk6.yellow("Not logged in - authenticating..."));
|
|
2212
1927
|
console.log();
|
|
2213
|
-
|
|
2214
|
-
|
|
1928
|
+
credentials = await performLogin();
|
|
1929
|
+
if (!credentials) {
|
|
1930
|
+
console.log(chalk6.red("Authentication failed"));
|
|
1931
|
+
process.exit(1);
|
|
2215
1932
|
}
|
|
2216
1933
|
console.log();
|
|
1934
|
+
}
|
|
1935
|
+
spinner.start("Loading resources");
|
|
1936
|
+
let resources;
|
|
1937
|
+
try {
|
|
1938
|
+
resources = await loadAllResources(cwd);
|
|
1939
|
+
spinner.succeed(`Loaded ${resources.agents.length} agents, ${resources.entityTypes.length} entity types, ${resources.roles.length} roles`);
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
spinner.fail("Failed to load resources");
|
|
1942
|
+
console.log(chalk6.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
2217
1943
|
process.exit(1);
|
|
2218
1944
|
}
|
|
2219
|
-
|
|
1945
|
+
if (resources.agents.length === 0) {
|
|
1946
|
+
console.log();
|
|
1947
|
+
console.log(chalk6.yellow("No agents found to deploy"));
|
|
1948
|
+
console.log();
|
|
1949
|
+
console.log(chalk6.gray("Run"), chalk6.cyan("struere add agent my-agent"), chalk6.gray("to create an agent"));
|
|
1950
|
+
console.log();
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
2220
1953
|
if (options.dryRun) {
|
|
2221
1954
|
console.log();
|
|
2222
1955
|
console.log(chalk6.yellow("Dry run mode - no changes will be made"));
|
|
2223
1956
|
console.log();
|
|
2224
1957
|
console.log("Would deploy:");
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
console.log(chalk6.gray(" -"), `Agent ID: ${chalk6.cyan(project.agentId)}`);
|
|
1958
|
+
for (const agent of resources.agents) {
|
|
1959
|
+
console.log(chalk6.gray(" -"), `${chalk6.cyan(agent.name)} (${agent.slug}) v${agent.version}`);
|
|
1960
|
+
}
|
|
2229
1961
|
console.log();
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
if (!credentials && !apiKey) {
|
|
2235
|
-
spinner.fail("Not authenticated");
|
|
1962
|
+
console.log("Entity types:");
|
|
1963
|
+
for (const et of resources.entityTypes) {
|
|
1964
|
+
console.log(chalk6.gray(" -"), chalk6.cyan(et.name), `(${et.slug})`);
|
|
1965
|
+
}
|
|
2236
1966
|
console.log();
|
|
2237
|
-
console.log(
|
|
2238
|
-
|
|
1967
|
+
console.log("Roles:");
|
|
1968
|
+
for (const role of resources.roles) {
|
|
1969
|
+
console.log(chalk6.gray(" -"), chalk6.cyan(role.name));
|
|
1970
|
+
}
|
|
2239
1971
|
console.log();
|
|
2240
|
-
|
|
1972
|
+
return;
|
|
2241
1973
|
}
|
|
2242
|
-
spinner.start("Extracting agent configuration");
|
|
2243
|
-
const config = extractConfig(agent);
|
|
2244
|
-
spinner.succeed("Configuration extracted");
|
|
2245
1974
|
spinner.start("Syncing to development");
|
|
2246
1975
|
try {
|
|
2247
|
-
const
|
|
1976
|
+
const payload = extractSyncPayload(resources);
|
|
1977
|
+
const syncResult = await syncOrganization(payload);
|
|
2248
1978
|
if (!syncResult.success) {
|
|
2249
1979
|
throw new Error(syncResult.error || "Sync failed");
|
|
2250
1980
|
}
|
|
@@ -2254,32 +1984,40 @@ var deployCommand = new Command6("deploy").description("Deploy agent to producti
|
|
|
2254
1984
|
console.log(chalk6.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
2255
1985
|
process.exit(1);
|
|
2256
1986
|
}
|
|
2257
|
-
spinner.start(
|
|
1987
|
+
spinner.start("Deploying to production");
|
|
2258
1988
|
try {
|
|
2259
|
-
const deployResult = await
|
|
1989
|
+
const deployResult = await deployAllAgents();
|
|
2260
1990
|
if (!deployResult.success) {
|
|
2261
1991
|
throw new Error(deployResult.error || "Deployment failed");
|
|
2262
1992
|
}
|
|
2263
|
-
spinner.succeed(
|
|
2264
|
-
const prodUrl = `https://${project.agent.slug}.struere.dev`;
|
|
1993
|
+
spinner.succeed("Deployed to production");
|
|
2265
1994
|
console.log();
|
|
2266
|
-
console.log(chalk6.green("Success!"), "
|
|
1995
|
+
console.log(chalk6.green("Success!"), "All agents deployed");
|
|
2267
1996
|
console.log();
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
1997
|
+
if (deployResult.deployed && deployResult.deployed.length > 0) {
|
|
1998
|
+
console.log("Deployed agents:");
|
|
1999
|
+
for (const slug of deployResult.deployed) {
|
|
2000
|
+
const agent = resources.agents.find((a) => a.slug === slug);
|
|
2001
|
+
const prodUrl = `https://${slug}.struere.dev`;
|
|
2002
|
+
console.log(chalk6.gray(" -"), chalk6.cyan(agent?.name || slug), chalk6.gray(`\u2192 ${prodUrl}`));
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (deployResult.skipped && deployResult.skipped.length > 0) {
|
|
2006
|
+
console.log();
|
|
2007
|
+
console.log(chalk6.yellow("Skipped (no development config):"));
|
|
2008
|
+
for (const slug of deployResult.skipped) {
|
|
2009
|
+
console.log(chalk6.gray(" -"), slug);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2272
2012
|
console.log();
|
|
2273
|
-
console.log(chalk6.gray("Test your
|
|
2274
|
-
console.log(chalk6.gray(" $"), chalk6.cyan(`curl -X POST
|
|
2013
|
+
console.log(chalk6.gray("Test your agents:"));
|
|
2014
|
+
console.log(chalk6.gray(" $"), chalk6.cyan(`curl -X POST https://<agent-slug>.struere.dev/chat -H "Authorization: Bearer YOUR_API_KEY" -d '{"message": "Hello"}'`));
|
|
2275
2015
|
console.log();
|
|
2276
2016
|
} catch (error) {
|
|
2277
2017
|
spinner.fail("Deployment failed");
|
|
2278
2018
|
console.log();
|
|
2279
2019
|
console.log(chalk6.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
2280
2020
|
console.log();
|
|
2281
|
-
console.log(chalk6.gray("Try running"), chalk6.cyan("struere login"), chalk6.gray("to re-authenticate"));
|
|
2282
|
-
console.log();
|
|
2283
2021
|
process.exit(1);
|
|
2284
2022
|
}
|
|
2285
2023
|
});
|
|
@@ -2509,10 +2247,230 @@ var whoamiCommand = new Command11("whoami").description("Show current logged in
|
|
|
2509
2247
|
console.log();
|
|
2510
2248
|
}
|
|
2511
2249
|
});
|
|
2250
|
+
|
|
2251
|
+
// src/cli/commands/add.ts
|
|
2252
|
+
import { Command as Command12 } from "commander";
|
|
2253
|
+
import chalk12 from "chalk";
|
|
2254
|
+
var addCommand = new Command12("add").description("Scaffold a new resource").argument("<type>", "Resource type: agent, entity-type, or role").argument("<name>", "Resource name").action(async (type, name) => {
|
|
2255
|
+
const cwd = process.cwd();
|
|
2256
|
+
console.log();
|
|
2257
|
+
if (!hasProject(cwd)) {
|
|
2258
|
+
console.log(chalk12.yellow("No struere.json found - initializing project..."));
|
|
2259
|
+
console.log();
|
|
2260
|
+
const success = await runInit(cwd);
|
|
2261
|
+
if (!success) {
|
|
2262
|
+
process.exit(1);
|
|
2263
|
+
}
|
|
2264
|
+
console.log();
|
|
2265
|
+
}
|
|
2266
|
+
const version = getProjectVersion(cwd);
|
|
2267
|
+
if (version === "1.0") {
|
|
2268
|
+
console.log(chalk12.yellow("This is a v1 agent-centric project."));
|
|
2269
|
+
console.log(chalk12.yellow("The add command requires v2 structure."));
|
|
2270
|
+
console.log();
|
|
2271
|
+
process.exit(1);
|
|
2272
|
+
}
|
|
2273
|
+
const slug = slugify2(name);
|
|
2274
|
+
const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2275
|
+
let result;
|
|
2276
|
+
switch (type.toLowerCase()) {
|
|
2277
|
+
case "agent":
|
|
2278
|
+
result = scaffoldAgent(cwd, displayName, slug);
|
|
2279
|
+
if (result.createdFiles.length > 0) {
|
|
2280
|
+
console.log(chalk12.green("\u2713"), `Created agent "${displayName}"`);
|
|
2281
|
+
for (const file of result.createdFiles) {
|
|
2282
|
+
console.log(chalk12.gray(" \u2192"), file);
|
|
2283
|
+
}
|
|
2284
|
+
} else {
|
|
2285
|
+
console.log(chalk12.yellow("Agent already exists:"), `agents/${slug}.ts`);
|
|
2286
|
+
}
|
|
2287
|
+
break;
|
|
2288
|
+
case "entity-type":
|
|
2289
|
+
case "entitytype":
|
|
2290
|
+
case "type":
|
|
2291
|
+
result = scaffoldEntityType(cwd, displayName, slug);
|
|
2292
|
+
if (result.createdFiles.length > 0) {
|
|
2293
|
+
console.log(chalk12.green("\u2713"), `Created entity type "${displayName}"`);
|
|
2294
|
+
for (const file of result.createdFiles) {
|
|
2295
|
+
console.log(chalk12.gray(" \u2192"), file);
|
|
2296
|
+
}
|
|
2297
|
+
} else {
|
|
2298
|
+
console.log(chalk12.yellow("Entity type already exists:"), `entity-types/${slug}.ts`);
|
|
2299
|
+
}
|
|
2300
|
+
break;
|
|
2301
|
+
case "role":
|
|
2302
|
+
result = scaffoldRole(cwd, slug);
|
|
2303
|
+
if (result.createdFiles.length > 0) {
|
|
2304
|
+
console.log(chalk12.green("\u2713"), `Created role "${slug}"`);
|
|
2305
|
+
for (const file of result.createdFiles) {
|
|
2306
|
+
console.log(chalk12.gray(" \u2192"), file);
|
|
2307
|
+
}
|
|
2308
|
+
} else {
|
|
2309
|
+
console.log(chalk12.yellow("Role already exists:"), `roles/${slug}.ts`);
|
|
2310
|
+
}
|
|
2311
|
+
break;
|
|
2312
|
+
default:
|
|
2313
|
+
console.log(chalk12.red("Unknown resource type:"), type);
|
|
2314
|
+
console.log();
|
|
2315
|
+
console.log("Available types:");
|
|
2316
|
+
console.log(chalk12.gray(" -"), chalk12.cyan("agent"), "- Create an AI agent");
|
|
2317
|
+
console.log(chalk12.gray(" -"), chalk12.cyan("entity-type"), "- Create an entity type schema");
|
|
2318
|
+
console.log(chalk12.gray(" -"), chalk12.cyan("role"), "- Create a role with permissions");
|
|
2319
|
+
console.log();
|
|
2320
|
+
process.exit(1);
|
|
2321
|
+
}
|
|
2322
|
+
console.log();
|
|
2323
|
+
console.log(chalk12.gray("Run"), chalk12.cyan("struere dev"), chalk12.gray("to sync changes"));
|
|
2324
|
+
console.log();
|
|
2325
|
+
});
|
|
2326
|
+
function slugify2(name) {
|
|
2327
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// src/cli/commands/status.ts
|
|
2331
|
+
import { Command as Command13 } from "commander";
|
|
2332
|
+
import chalk13 from "chalk";
|
|
2333
|
+
import ora11 from "ora";
|
|
2334
|
+
var statusCommand = new Command13("status").description("Compare local vs remote state").action(async () => {
|
|
2335
|
+
const spinner = ora11();
|
|
2336
|
+
const cwd = process.cwd();
|
|
2337
|
+
console.log();
|
|
2338
|
+
console.log(chalk13.bold("Struere Status"));
|
|
2339
|
+
console.log();
|
|
2340
|
+
if (!hasProject(cwd)) {
|
|
2341
|
+
console.log(chalk13.yellow("No struere.json found - initializing project..."));
|
|
2342
|
+
console.log();
|
|
2343
|
+
const success = await runInit(cwd);
|
|
2344
|
+
if (!success) {
|
|
2345
|
+
process.exit(1);
|
|
2346
|
+
}
|
|
2347
|
+
console.log();
|
|
2348
|
+
}
|
|
2349
|
+
const version = getProjectVersion(cwd);
|
|
2350
|
+
if (version === "1.0") {
|
|
2351
|
+
console.log(chalk13.yellow("This is a v1 agent-centric project."));
|
|
2352
|
+
console.log(chalk13.yellow("The status command requires v2 structure."));
|
|
2353
|
+
console.log();
|
|
2354
|
+
process.exit(1);
|
|
2355
|
+
}
|
|
2356
|
+
const project = loadProjectV2(cwd);
|
|
2357
|
+
if (!project) {
|
|
2358
|
+
console.log(chalk13.red("Failed to load struere.json"));
|
|
2359
|
+
process.exit(1);
|
|
2360
|
+
}
|
|
2361
|
+
console.log(chalk13.gray("Organization:"), chalk13.cyan(project.organization.name));
|
|
2362
|
+
console.log();
|
|
2363
|
+
let credentials = loadCredentials();
|
|
2364
|
+
const apiKey = getApiKey();
|
|
2365
|
+
if (!credentials && !apiKey) {
|
|
2366
|
+
console.log(chalk13.yellow("Not logged in - authenticating..."));
|
|
2367
|
+
console.log();
|
|
2368
|
+
credentials = await performLogin();
|
|
2369
|
+
if (!credentials) {
|
|
2370
|
+
console.log(chalk13.red("Authentication failed"));
|
|
2371
|
+
process.exit(1);
|
|
2372
|
+
}
|
|
2373
|
+
console.log();
|
|
2374
|
+
}
|
|
2375
|
+
spinner.start("Loading local resources");
|
|
2376
|
+
let localResources;
|
|
2377
|
+
try {
|
|
2378
|
+
localResources = await loadAllResources(cwd);
|
|
2379
|
+
spinner.succeed("Local resources loaded");
|
|
2380
|
+
} catch (error) {
|
|
2381
|
+
spinner.fail("Failed to load local resources");
|
|
2382
|
+
console.log(chalk13.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
2383
|
+
process.exit(1);
|
|
2384
|
+
}
|
|
2385
|
+
spinner.start("Fetching remote state");
|
|
2386
|
+
const { state: remoteState, error: fetchError } = await getSyncState();
|
|
2387
|
+
if (fetchError || !remoteState) {
|
|
2388
|
+
spinner.fail("Failed to fetch remote state");
|
|
2389
|
+
console.log(chalk13.red("Error:"), fetchError || "Unknown error");
|
|
2390
|
+
process.exit(1);
|
|
2391
|
+
}
|
|
2392
|
+
spinner.succeed("Remote state fetched");
|
|
2393
|
+
console.log();
|
|
2394
|
+
const localAgentSlugs = new Set(localResources.agents.map((a) => a.slug));
|
|
2395
|
+
const remoteAgentSlugs = new Set(remoteState.agents.map((a) => a.slug));
|
|
2396
|
+
const localEntityTypeSlugs = new Set(localResources.entityTypes.map((et) => et.slug));
|
|
2397
|
+
const remoteEntityTypeSlugs = new Set(remoteState.entityTypes.map((et) => et.slug));
|
|
2398
|
+
const localRoleNames = new Set(localResources.roles.map((r) => r.name));
|
|
2399
|
+
const remoteRoleNames = new Set(remoteState.roles.map((r) => r.name));
|
|
2400
|
+
console.log(chalk13.bold("Agents"));
|
|
2401
|
+
console.log(chalk13.gray("\u2500".repeat(60)));
|
|
2402
|
+
if (localResources.agents.length === 0 && remoteState.agents.length === 0) {
|
|
2403
|
+
console.log(chalk13.gray(" No agents"));
|
|
2404
|
+
} else {
|
|
2405
|
+
for (const agent of localResources.agents) {
|
|
2406
|
+
const remote = remoteState.agents.find((a) => a.slug === agent.slug);
|
|
2407
|
+
if (remote) {
|
|
2408
|
+
const statusIcon = remote.hasProdConfig ? chalk13.green("\u25CF") : chalk13.yellow("\u25CB");
|
|
2409
|
+
console.log(` ${statusIcon} ${chalk13.cyan(agent.name)} (${agent.slug}) - v${agent.version}`);
|
|
2410
|
+
if (!remote.hasProdConfig) {
|
|
2411
|
+
console.log(chalk13.gray(" Not deployed to production"));
|
|
2412
|
+
}
|
|
2413
|
+
} else {
|
|
2414
|
+
console.log(` ${chalk13.blue("+")} ${chalk13.cyan(agent.name)} (${agent.slug}) - ${chalk13.blue("new")}`);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
for (const remote of remoteState.agents) {
|
|
2418
|
+
if (!localAgentSlugs.has(remote.slug)) {
|
|
2419
|
+
console.log(` ${chalk13.red("-")} ${remote.name} (${remote.slug}) - ${chalk13.red("will be deleted")}`);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
console.log();
|
|
2424
|
+
console.log(chalk13.bold("Entity Types"));
|
|
2425
|
+
console.log(chalk13.gray("\u2500".repeat(60)));
|
|
2426
|
+
if (localResources.entityTypes.length === 0 && remoteState.entityTypes.length === 0) {
|
|
2427
|
+
console.log(chalk13.gray(" No entity types"));
|
|
2428
|
+
} else {
|
|
2429
|
+
for (const et of localResources.entityTypes) {
|
|
2430
|
+
const remote = remoteState.entityTypes.find((r) => r.slug === et.slug);
|
|
2431
|
+
if (remote) {
|
|
2432
|
+
console.log(` ${chalk13.green("\u25CF")} ${chalk13.cyan(et.name)} (${et.slug})`);
|
|
2433
|
+
} else {
|
|
2434
|
+
console.log(` ${chalk13.blue("+")} ${chalk13.cyan(et.name)} (${et.slug}) - ${chalk13.blue("new")}`);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
for (const remote of remoteState.entityTypes) {
|
|
2438
|
+
if (!localEntityTypeSlugs.has(remote.slug)) {
|
|
2439
|
+
console.log(` ${chalk13.red("-")} ${remote.name} (${remote.slug}) - ${chalk13.red("will be deleted")}`);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
console.log();
|
|
2444
|
+
console.log(chalk13.bold("Roles"));
|
|
2445
|
+
console.log(chalk13.gray("\u2500".repeat(60)));
|
|
2446
|
+
if (localResources.roles.length === 0 && remoteState.roles.length === 0) {
|
|
2447
|
+
console.log(chalk13.gray(" No roles"));
|
|
2448
|
+
} else {
|
|
2449
|
+
for (const role of localResources.roles) {
|
|
2450
|
+
const remote = remoteState.roles.find((r) => r.name === role.name);
|
|
2451
|
+
if (remote) {
|
|
2452
|
+
console.log(` ${chalk13.green("\u25CF")} ${chalk13.cyan(role.name)} (${role.policies.length} policies)`);
|
|
2453
|
+
} else {
|
|
2454
|
+
console.log(` ${chalk13.blue("+")} ${chalk13.cyan(role.name)} - ${chalk13.blue("new")}`);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
for (const remote of remoteState.roles) {
|
|
2458
|
+
if (!localRoleNames.has(remote.name)) {
|
|
2459
|
+
console.log(` ${chalk13.red("-")} ${remote.name} - ${chalk13.red("will be deleted")}`);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
console.log();
|
|
2464
|
+
console.log(chalk13.gray("Legend:"));
|
|
2465
|
+
console.log(chalk13.gray(" "), chalk13.green("\u25CF"), "Synced", chalk13.yellow("\u25CB"), "Not deployed", chalk13.blue("+"), "New", chalk13.red("-"), "Will be deleted");
|
|
2466
|
+
console.log();
|
|
2467
|
+
console.log(chalk13.gray("Run"), chalk13.cyan("struere dev"), chalk13.gray("to sync changes"));
|
|
2468
|
+
console.log();
|
|
2469
|
+
});
|
|
2512
2470
|
// package.json
|
|
2513
2471
|
var package_default = {
|
|
2514
2472
|
name: "struere",
|
|
2515
|
-
version: "0.
|
|
2473
|
+
version: "0.4.1",
|
|
2516
2474
|
description: "Build, test, and deploy AI agents",
|
|
2517
2475
|
keywords: [
|
|
2518
2476
|
"ai",
|
|
@@ -2617,4 +2575,6 @@ program.addCommand(deployCommand);
|
|
|
2617
2575
|
program.addCommand(validateCommand);
|
|
2618
2576
|
program.addCommand(logsCommand);
|
|
2619
2577
|
program.addCommand(stateCommand);
|
|
2578
|
+
program.addCommand(addCommand);
|
|
2579
|
+
program.addCommand(statusCommand);
|
|
2620
2580
|
program.parse();
|