struere 0.3.10 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
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 saveProject(cwd, project) {
485
+ function loadProjectV2(cwd) {
583
486
  const projectPath = join2(cwd, PROJECT_FILE);
584
- writeFileSync2(projectPath, JSON.stringify(project, null, 2) + `
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 getAgentTs(name) {
653
- const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
654
- return `import { defineAgent } from 'struere'
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
- Current time: {{datetime}}
569
+ # Optional: OpenAI API Key (if using OpenAI models)
570
+ # OPENAI_API_KEY=your_openai_api_key
670
571
 
671
- Your capabilities:
672
- - Answer questions accurately and helpfully
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
- Always be concise, accurate, and helpful.\`,
677
- tools,
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 getToolsTs() {
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
-
758
- # Optional: OpenAI API Key (if using OpenAI models)
759
- # OPENAI_API_KEY=your_openai_api_key
760
691
 
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
692
+ export default tools
766
693
  `;
767
694
  }
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/
783
- `;
784
- }
785
- function getStruereJson(agentId, team, slug, name) {
695
+ function getStruereJsonV2(orgId, orgSlug, orgName) {
786
696
  return JSON.stringify({
787
- agentId,
788
- team,
789
- agent: {
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 getEnvLocal(deploymentUrl) {
796
- return `STRUERE_DEPLOYMENT_URL=${deploymentUrl}
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 getClaudeMD(name) {
800
- const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
801
- return `# ${displayName} Agent
725
+ function getClaudeMDV2(orgName) {
726
+ return `# ${orgName} - Struere Project
802
727
 
803
- This is a Struere AI agent project. Struere is a framework for building production AI agents with built-in data management, event tracking, and job scheduling.
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
- src/
809
- \u251C\u2500\u2500 agent.ts # Agent definition (system prompt, model, tools)
810
- \u251C\u2500\u2500 tools.ts # Custom tool definitions
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
- Define your agent in \`src/agent.ts\`:
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
- \`\`\`typescript
823
- import { defineAgent } from 'struere'
824
- import { tools } from './tools'
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
- export default defineAgent({
827
- name: 'my-agent',
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
- Current time: {{datetime}}
839
- Customer: {{entity.get({"id": "{{thread.metadata.customerId}}"})}}\\\`,
840
- tools,
841
- })
748
+ struere.json # Organization configuration
749
+ struere.config.ts # Framework settings
842
750
  \`\`\`
843
751
 
844
- ## System Prompt Templates
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
- ### Nested Templates
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
- Variables can be used inside function arguments:
761
+ ## Defining Resources
876
762
 
877
- \`\`\`
878
- {{entity.get({"id": "{{thread.metadata.customerId}}"})}}
879
- \`\`\`
763
+ ### Agents (\`agents/*.ts\`)
880
764
 
881
- ### Error Handling
765
+ \`\`\`typescript
766
+ import { defineAgent } from 'struere'
882
767
 
883
- Failed templates are replaced with inline errors:
884
- \`\`\`
885
- [TEMPLATE_ERROR: variableName not found]
886
- [TEMPLATE_ERROR: toolName - error message]
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
- ## Custom Tools
890
-
891
- Define tools in \`src/tools.ts\`:
778
+ ### Entity Types (\`entity-types/*.ts\`)
892
779
 
893
780
  \`\`\`typescript
894
- import { defineTools } from 'struere'
895
-
896
- export const tools = defineTools([
897
- {
898
- name: 'search_products',
899
- description: 'Search the product catalog',
900
- parameters: {
901
- type: 'object',
902
- properties: {
903
- query: { type: 'string', description: 'Search query' },
904
- limit: { type: 'number', description: 'Max results' },
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
- Custom tool handlers are executed in a sandboxed Cloudflare Worker environment. They can make HTTP requests to allowlisted domains:
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
- Agents have access to these built-in tools for data management:
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
- ### Entity Tools
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
- Example entity operations:
938
- \`\`\`json
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. **Edit agent configuration** in \`src/agent.ts\`
1059
- 2. **Run \`bun run dev\`** to sync changes to Convex
1060
- 3. **Test via API** or dashboard chat interface
1061
- 4. **Write tests** in \`tests/*.test.yaml\`
1062
- 5. **Deploy** with \`bun run deploy\`
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 writeProjectConfig(cwd, options) {
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
- "package.json": getPackageJson(projectName),
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
- "CLAUDE.md": getClaudeMD(projectName)
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,57 +893,85 @@ 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 updateGitignore(cwd, result) {
1144
- const gitignorePath = join3(cwd, ".gitignore");
1145
- const linesToAdd = [".env.local"];
1146
- if (existsSync3(gitignorePath)) {
1147
- const content = readFileSync3(gitignorePath, "utf-8");
1148
- const lines = content.split(`
1149
- `);
1150
- const missingLines = linesToAdd.filter((line) => !lines.some((l) => l.trim() === line));
1151
- if (missingLines.length > 0) {
1152
- const toAppend = `
1153
- ` + missingLines.join(`
1154
- `) + `
1155
- `;
1156
- appendFileSync(gitignorePath, toAppend);
1157
- result.updatedFiles.push(".gitignore");
1158
- }
1159
- } else {
1160
- writeFile(cwd, ".gitignore", getGitignore());
1161
- result.createdFiles.push(".gitignore");
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 });
1162
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;
1163
915
  }
1164
- function hasAgentFiles(cwd) {
1165
- return existsSync3(join3(cwd, "src", "agent.ts"));
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;
929
+ }
930
+ writeFileSync3(filePath, getEntityTypeTs(name, slug));
931
+ result.createdFiles.push(`entity-types/${fileName}`);
932
+ return result;
933
+ }
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
- var initCommand = new Command2("init").description("Initialize a new Struere project").argument("[project-name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").action(async (projectNameArg, options) => {
954
+ 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) => {
1170
955
  const cwd = process.cwd();
1171
956
  const spinner = ora2();
1172
957
  console.log();
1173
958
  console.log(chalk2.bold("Struere CLI"));
1174
959
  console.log();
1175
960
  if (hasProject(cwd)) {
1176
- const existingProject = loadProject(cwd);
1177
- if (existingProject) {
1178
- console.log(chalk2.yellow("This project is already initialized."));
961
+ const version = getProjectVersion(cwd);
962
+ if (version === "2.0") {
963
+ console.log(chalk2.yellow("This project is already initialized (v2.0)."));
1179
964
  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));
965
+ console.log(chalk2.gray("Run"), chalk2.cyan("struere dev"), chalk2.gray("to start development"));
1183
966
  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
- }
967
+ return;
968
+ } else if (version === "1.0") {
969
+ console.log(chalk2.yellow("This is a v1 agent-centric project."));
970
+ console.log(chalk2.yellow("The new CLI uses an organization-centric structure."));
971
+ console.log();
972
+ console.log(chalk2.gray("Please create a new project directory for the v2 structure."));
973
+ console.log();
974
+ return;
1191
975
  }
1192
976
  }
1193
977
  let credentials = loadCredentials();
@@ -1199,105 +983,39 @@ var initCommand = new Command2("init").description("Initialize a new Struere pro
1199
983
  console.log(chalk2.red("Authentication failed"));
1200
984
  process.exit(1);
1201
985
  }
1202
- } else {
1203
- console.log(chalk2.green("\u2713"), "Logged in as", chalk2.cyan(credentials.user.name || credentials.user.email));
1204
- console.log();
1205
- }
1206
- let projectName = projectNameArg;
1207
- if (!projectName) {
1208
- projectName = await deriveProjectName(cwd);
1209
- if (!options.yes) {
1210
- const confirmed = await promptText("Agent name:", projectName);
1211
- projectName = confirmed || projectName;
1212
- }
1213
- }
1214
- projectName = slugify(projectName);
1215
- spinner.start("Fetching agents");
1216
- const { agents: existingAgents, error: listError } = await listAgents();
1217
- if (listError) {
1218
- spinner.fail("Failed to fetch agents");
1219
- console.log();
1220
- console.log(chalk2.gray("Run"), chalk2.cyan("struere login"), chalk2.gray("to re-authenticate"));
1221
- process.exit(1);
1222
- }
1223
- const agents = existingAgents.map((a) => ({ id: a._id, name: a.name, slug: a.slug }));
1224
- spinner.succeed(`Found ${agents.length} existing agent(s)`);
1225
- let selectedAgent = null;
1226
- let deploymentUrl = "";
1227
- if (agents.length > 0 && !options.yes) {
1228
- console.log();
1229
- const choice = await promptChoice("Create new agent or link existing?", [
1230
- { value: "new", label: "Create new agent" },
1231
- ...agents.map((a) => ({ value: a.id, label: `${a.name} (${a.slug})` }))
1232
- ]);
1233
- if (choice !== "new") {
1234
- selectedAgent = agents.find((a) => a.id === choice) || null;
1235
- }
1236
- }
1237
- if (!selectedAgent) {
1238
- const displayName2 = projectName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1239
- spinner.start("Creating agent");
1240
- const { agentId, error: createError } = await createAgent({
1241
- name: displayName2,
1242
- slug: projectName,
1243
- description: `${displayName2} Agent`
1244
- });
1245
- if (createError || !agentId) {
1246
- spinner.fail("Failed to create agent");
1247
- console.log();
1248
- console.log(chalk2.red("Error:"), createError || "Unknown error");
1249
- process.exit(1);
1250
- }
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
- console.log();
1257
- console.log(chalk2.green("\u2713"), `Linked to "${selectedAgent.name}"`);
1258
- }
1259
- saveProject(cwd, {
1260
- agentId: selectedAgent.id,
1261
- team: credentials.organization.slug,
1262
- agent: {
1263
- slug: selectedAgent.slug,
1264
- name: selectedAgent.name
1265
- }
1266
- });
1267
- console.log(chalk2.green("\u2713"), "Created struere.json");
1268
- const configResult = writeProjectConfig(cwd, {
1269
- projectName,
1270
- agentId: selectedAgent.id,
1271
- team: credentials.organization.slug,
1272
- agentSlug: selectedAgent.slug,
1273
- agentName: selectedAgent.name,
1274
- deploymentUrl
1275
- });
1276
- for (const file of configResult.createdFiles) {
1277
- if (file !== "struere.json") {
1278
- console.log(chalk2.green("\u2713"), `Created ${file}`);
1279
- }
1280
- }
1281
- for (const file of configResult.updatedFiles) {
1282
- console.log(chalk2.green("\u2713"), `Updated ${file}`);
986
+ } else {
987
+ console.log(chalk2.green("\u2713"), "Logged in as", chalk2.cyan(credentials.user.name || credentials.user.email));
988
+ console.log(chalk2.gray(" Organization:"), chalk2.cyan(credentials.organization.name));
989
+ console.log();
1283
990
  }
1284
- if (!hasAgentFiles(cwd)) {
1285
- let shouldScaffold = options.yes;
1286
- if (!options.yes) {
991
+ if (!options.yes) {
992
+ const confirmed = await promptYesNo(`Initialize project for organization "${credentials.organization.name}"?`);
993
+ if (!confirmed) {
1287
994
  console.log();
1288
- shouldScaffold = await promptYesNo("Scaffold starter files?");
995
+ console.log(chalk2.gray("Cancelled"));
996
+ return;
1289
997
  }
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
- }
998
+ }
999
+ let projectName = projectNameArg;
1000
+ if (!projectName) {
1001
+ projectName = slugify(basename(cwd));
1002
+ if (!options.yes) {
1003
+ const confirmed = await promptText("Project name:", projectName);
1004
+ projectName = confirmed || projectName;
1299
1005
  }
1300
1006
  }
1007
+ projectName = slugify(projectName);
1008
+ spinner.start("Creating project structure");
1009
+ const scaffoldResult = scaffoldProjectV2(cwd, {
1010
+ projectName,
1011
+ orgId: credentials.organization.id,
1012
+ orgSlug: credentials.organization.slug,
1013
+ orgName: credentials.organization.name
1014
+ });
1015
+ spinner.succeed("Project structure created");
1016
+ for (const file of scaffoldResult.createdFiles) {
1017
+ console.log(chalk2.green("\u2713"), `Created ${file}`);
1018
+ }
1301
1019
  console.log();
1302
1020
  spinner.start("Installing dependencies");
1303
1021
  const installResult = Bun.spawnSync(["bun", "install"], {
@@ -1311,46 +1029,20 @@ var initCommand = new Command2("init").description("Initialize a new Struere pro
1311
1029
  spinner.warn("Could not install dependencies automatically");
1312
1030
  console.log(chalk2.gray(" Run"), chalk2.cyan("bun install"), chalk2.gray("manually"));
1313
1031
  }
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
1032
  console.log();
1336
1033
  console.log(chalk2.green("Success!"), "Project initialized");
1337
1034
  console.log();
1035
+ console.log(chalk2.gray("Project structure:"));
1036
+ console.log(chalk2.gray(" agents/ "), chalk2.cyan("Agent definitions"));
1037
+ console.log(chalk2.gray(" entity-types/ "), chalk2.cyan("Entity type schemas"));
1038
+ console.log(chalk2.gray(" roles/ "), chalk2.cyan("Role + permission definitions"));
1039
+ console.log(chalk2.gray(" tools/ "), chalk2.cyan("Shared custom tools"));
1040
+ console.log();
1338
1041
  console.log(chalk2.gray("Next steps:"));
1339
- console.log(chalk2.gray(" $"), chalk2.cyan("struere dev"));
1042
+ console.log(chalk2.gray(" 1."), chalk2.cyan("struere add agent my-agent"), chalk2.gray("- Create an agent"));
1043
+ console.log(chalk2.gray(" 2."), chalk2.cyan("struere dev"), chalk2.gray("- Start development"));
1340
1044
  console.log();
1341
1045
  });
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
1046
  function slugify(name) {
1355
1047
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1356
1048
  }
@@ -1365,22 +1057,6 @@ async function promptText(message, defaultValue) {
1365
1057
  const answer = await readLine();
1366
1058
  return answer || defaultValue;
1367
1059
  }
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
1060
  function readLine() {
1385
1061
  return new Promise((resolve) => {
1386
1062
  let buffer = "";
@@ -1407,112 +1083,275 @@ import { Command as Command3 } from "commander";
1407
1083
  import chalk3 from "chalk";
1408
1084
  import ora3 from "ora";
1409
1085
  import chokidar from "chokidar";
1410
- import { join as join7, basename as basename2 } from "path";
1086
+ import { join as join5 } from "path";
1411
1087
  import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
1412
1088
 
1413
- // src/cli/utils/config.ts
1414
- import { join as join5 } from "path";
1415
- var defaultConfig = {
1416
- port: 3000,
1417
- host: "localhost",
1418
- cors: {
1419
- origins: ["http://localhost:3000"],
1420
- credentials: true
1421
- },
1422
- logging: {
1423
- level: "info",
1424
- format: "pretty"
1425
- },
1426
- auth: {
1427
- type: "none"
1089
+ // src/cli/utils/loader.ts
1090
+ import { existsSync as existsSync4, readdirSync } from "fs";
1091
+ import { join as join4 } from "path";
1092
+ async function loadAllResources(cwd) {
1093
+ const agents = await loadAllAgents(join4(cwd, "agents"));
1094
+ const entityTypes = await loadAllEntityTypes(join4(cwd, "entity-types"));
1095
+ const roles = await loadAllRoles(join4(cwd, "roles"));
1096
+ const customTools = await loadCustomTools(join4(cwd, "tools"));
1097
+ return { agents, entityTypes, roles, customTools };
1098
+ }
1099
+ async function loadAllAgents(dir) {
1100
+ if (!existsSync4(dir)) {
1101
+ return [];
1102
+ }
1103
+ const indexPath = join4(dir, "index.ts");
1104
+ if (existsSync4(indexPath)) {
1105
+ return loadFromIndex(indexPath);
1106
+ }
1107
+ return loadFromDirectory(dir);
1108
+ }
1109
+ async function loadAllEntityTypes(dir) {
1110
+ if (!existsSync4(dir)) {
1111
+ return [];
1112
+ }
1113
+ const indexPath = join4(dir, "index.ts");
1114
+ if (existsSync4(indexPath)) {
1115
+ return loadFromIndex(indexPath);
1116
+ }
1117
+ return loadFromDirectory(dir);
1118
+ }
1119
+ async function loadAllRoles(dir) {
1120
+ if (!existsSync4(dir)) {
1121
+ return [];
1122
+ }
1123
+ const indexPath = join4(dir, "index.ts");
1124
+ if (existsSync4(indexPath)) {
1125
+ return loadFromIndex(indexPath);
1126
+ }
1127
+ return loadFromDirectory(dir);
1128
+ }
1129
+ async function loadCustomTools(dir) {
1130
+ if (!existsSync4(dir)) {
1131
+ return [];
1132
+ }
1133
+ const indexPath = join4(dir, "index.ts");
1134
+ if (!existsSync4(indexPath)) {
1135
+ return [];
1428
1136
  }
1429
- };
1430
- async function loadConfig(cwd) {
1431
- const configPath = join5(cwd, "struere.config.ts");
1432
1137
  try {
1433
- const module = await import(configPath);
1434
- const config = module.default || module;
1435
- return {
1436
- ...defaultConfig,
1437
- ...config,
1438
- cors: {
1439
- ...defaultConfig.cors,
1440
- ...config.cors
1441
- },
1442
- logging: {
1443
- ...defaultConfig.logging,
1444
- ...config.logging
1445
- },
1446
- auth: {
1447
- ...defaultConfig.auth,
1448
- ...config.auth
1449
- }
1450
- };
1138
+ const module = await import(indexPath);
1139
+ if (Array.isArray(module.default)) {
1140
+ return module.default;
1141
+ }
1142
+ if (module.tools && Array.isArray(module.tools)) {
1143
+ return module.tools;
1144
+ }
1145
+ return [];
1451
1146
  } catch {
1452
- return defaultConfig;
1147
+ return [];
1453
1148
  }
1454
1149
  }
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");
1150
+ async function loadFromIndex(indexPath) {
1460
1151
  try {
1461
- const module = await import(`${agentPath}?t=${Date.now()}`);
1462
- const agent = module.default || module;
1463
- if (!agent.name) {
1464
- throw new Error("Agent must have a name");
1152
+ const module = await import(indexPath);
1153
+ if (Array.isArray(module.default)) {
1154
+ return module.default;
1465
1155
  }
1466
- if (!agent.version) {
1467
- throw new Error("Agent must have a version");
1468
- }
1469
- if (!agent.systemPrompt) {
1470
- throw new Error("Agent must have a systemPrompt");
1156
+ const items = [];
1157
+ for (const key of Object.keys(module)) {
1158
+ if (key === "default")
1159
+ continue;
1160
+ const value = module[key];
1161
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1162
+ items.push(value);
1163
+ }
1471
1164
  }
1472
- return agent;
1165
+ return items;
1473
1166
  } catch (error) {
1474
- if (error instanceof Error && error.message.includes("Cannot find module")) {
1475
- throw new Error(`Agent not found at ${agentPath}`);
1167
+ throw new Error(`Failed to load index at ${indexPath}: ${error instanceof Error ? error.message : String(error)}`);
1168
+ }
1169
+ }
1170
+ async function loadFromDirectory(dir) {
1171
+ const files = readdirSync(dir).filter((f) => f.endsWith(".ts") && f !== "index.ts" && !f.endsWith(".d.ts"));
1172
+ const items = [];
1173
+ for (const file of files) {
1174
+ const filePath = join4(dir, file);
1175
+ try {
1176
+ const module = await import(filePath);
1177
+ if (module.default) {
1178
+ items.push(module.default);
1179
+ }
1180
+ } catch (error) {
1181
+ throw new Error(`Failed to load ${file}: ${error instanceof Error ? error.message : String(error)}`);
1476
1182
  }
1477
- throw error;
1478
1183
  }
1184
+ return items;
1185
+ }
1186
+ function getResourceDirectories(cwd) {
1187
+ return {
1188
+ agents: join4(cwd, "agents"),
1189
+ entityTypes: join4(cwd, "entity-types"),
1190
+ roles: join4(cwd, "roles"),
1191
+ tools: join4(cwd, "tools")
1192
+ };
1193
+ }
1194
+
1195
+ // src/cli/utils/extractor.ts
1196
+ var BUILTIN_TOOLS = [
1197
+ "entity.create",
1198
+ "entity.get",
1199
+ "entity.query",
1200
+ "entity.update",
1201
+ "entity.delete",
1202
+ "entity.link",
1203
+ "entity.unlink",
1204
+ "event.emit",
1205
+ "event.query",
1206
+ "job.enqueue",
1207
+ "job.status"
1208
+ ];
1209
+ function extractSyncPayload(resources) {
1210
+ const customToolsMap = new Map;
1211
+ for (const tool of resources.customTools) {
1212
+ customToolsMap.set(tool.name, tool);
1213
+ }
1214
+ const agents = resources.agents.map((agent) => extractAgentPayload(agent, customToolsMap));
1215
+ const entityTypes = resources.entityTypes.map((et) => ({
1216
+ name: et.name,
1217
+ slug: et.slug,
1218
+ schema: et.schema,
1219
+ searchFields: et.searchFields,
1220
+ displayConfig: et.displayConfig
1221
+ }));
1222
+ const roles = resources.roles.map((role) => ({
1223
+ name: role.name,
1224
+ description: role.description,
1225
+ policies: role.policies.map((p) => ({
1226
+ resource: p.resource,
1227
+ actions: p.actions,
1228
+ effect: p.effect,
1229
+ priority: p.priority
1230
+ })),
1231
+ scopeRules: role.scopeRules?.map((sr) => ({
1232
+ entityType: sr.entityType,
1233
+ field: sr.field,
1234
+ operator: sr.operator,
1235
+ value: sr.value
1236
+ })),
1237
+ fieldMasks: role.fieldMasks?.map((fm) => ({
1238
+ entityType: fm.entityType,
1239
+ fieldPath: fm.fieldPath,
1240
+ maskType: fm.maskType,
1241
+ maskConfig: fm.maskConfig
1242
+ }))
1243
+ }));
1244
+ return { agents, entityTypes, roles };
1245
+ }
1246
+ function extractAgentPayload(agent, customToolsMap) {
1247
+ let systemPrompt;
1248
+ if (typeof agent.systemPrompt === "function") {
1249
+ const result = agent.systemPrompt();
1250
+ if (result instanceof Promise) {
1251
+ throw new Error("Async system prompts must be resolved before syncing");
1252
+ }
1253
+ systemPrompt = result;
1254
+ } else {
1255
+ systemPrompt = agent.systemPrompt;
1256
+ }
1257
+ const tools = (agent.tools || []).map((toolName) => {
1258
+ const isBuiltin = BUILTIN_TOOLS.includes(toolName);
1259
+ if (isBuiltin) {
1260
+ return {
1261
+ name: toolName,
1262
+ description: getBuiltinToolDescription(toolName),
1263
+ parameters: { type: "object", properties: {} },
1264
+ isBuiltin: true
1265
+ };
1266
+ }
1267
+ const customTool = customToolsMap.get(toolName);
1268
+ if (!customTool) {
1269
+ throw new Error(`Tool "${toolName}" not found in custom tools`);
1270
+ }
1271
+ return {
1272
+ name: customTool.name,
1273
+ description: customTool.description,
1274
+ parameters: customTool.parameters || { type: "object", properties: {} },
1275
+ handlerCode: extractHandlerCode(customTool.handler),
1276
+ isBuiltin: false
1277
+ };
1278
+ });
1279
+ return {
1280
+ name: agent.name,
1281
+ slug: agent.slug,
1282
+ version: agent.version,
1283
+ description: agent.description,
1284
+ systemPrompt,
1285
+ model: {
1286
+ provider: agent.model?.provider || "anthropic",
1287
+ name: agent.model?.name || "claude-sonnet-4-20250514",
1288
+ temperature: agent.model?.temperature,
1289
+ maxTokens: agent.model?.maxTokens
1290
+ },
1291
+ tools
1292
+ };
1293
+ }
1294
+ function getBuiltinToolDescription(name) {
1295
+ const descriptions = {
1296
+ "entity.create": "Create a new entity",
1297
+ "entity.get": "Get an entity by ID",
1298
+ "entity.query": "Query entities by type and filters",
1299
+ "entity.update": "Update an entity",
1300
+ "entity.delete": "Delete an entity",
1301
+ "entity.link": "Link two entities",
1302
+ "entity.unlink": "Unlink two entities",
1303
+ "event.emit": "Emit an event",
1304
+ "event.query": "Query events",
1305
+ "job.enqueue": "Schedule a background job",
1306
+ "job.status": "Get job status"
1307
+ };
1308
+ return descriptions[name] || name;
1309
+ }
1310
+ function extractHandlerCode(handler) {
1311
+ const code = handler.toString();
1312
+ const arrowMatch = code.match(/(?:async\s*)?\([^)]*\)\s*=>\s*\{?([\s\S]*)\}?$/);
1313
+ if (arrowMatch) {
1314
+ let body = arrowMatch[1].trim();
1315
+ if (body.startsWith("{") && body.endsWith("}")) {
1316
+ body = body.slice(1, -1).trim();
1317
+ }
1318
+ return body;
1319
+ }
1320
+ const funcMatch = code.match(/(?:async\s*)?function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
1321
+ if (funcMatch) {
1322
+ return funcMatch[1].trim();
1323
+ }
1324
+ return code;
1479
1325
  }
1480
1326
 
1481
1327
  // src/cli/commands/dev.ts
1482
- var devCommand = new Command3("dev").description("Sync agent to development environment").action(async () => {
1328
+ var devCommand = new Command3("dev").description("Sync all resources to development environment").action(async () => {
1483
1329
  const spinner = ora3();
1484
1330
  const cwd = process.cwd();
1485
1331
  console.log();
1486
1332
  console.log(chalk3.bold("Struere Dev"));
1487
1333
  console.log();
1488
- let project = loadProject(cwd);
1489
1334
  if (!hasProject(cwd)) {
1490
1335
  console.log(chalk3.yellow("No struere.json found"));
1491
1336
  console.log();
1492
- const setupResult = await interactiveSetup(cwd);
1493
- if (!setupResult) {
1494
- process.exit(0);
1495
- }
1496
- project = setupResult;
1337
+ console.log(chalk3.gray("Run"), chalk3.cyan("struere init"), chalk3.gray("to initialize this project"));
1338
+ console.log();
1339
+ process.exit(1);
1497
1340
  }
1498
- project = loadProject(cwd);
1341
+ const version = getProjectVersion(cwd);
1342
+ if (version === "1.0") {
1343
+ console.log(chalk3.yellow("This is a v1 agent-centric project."));
1344
+ console.log(chalk3.yellow("Please migrate to v2 structure or use an older CLI version."));
1345
+ console.log();
1346
+ process.exit(1);
1347
+ }
1348
+ const project = loadProjectV2(cwd);
1499
1349
  if (!project) {
1500
1350
  console.log(chalk3.red("Failed to load struere.json"));
1501
1351
  process.exit(1);
1502
1352
  }
1503
- console.log(chalk3.gray("Agent:"), chalk3.cyan(project.agent.name));
1353
+ console.log(chalk3.gray("Organization:"), chalk3.cyan(project.organization.name));
1504
1354
  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
1355
  let credentials = loadCredentials();
1517
1356
  const apiKey = getApiKey();
1518
1357
  if (!credentials && !apiKey) {
@@ -1524,11 +1363,20 @@ var devCommand = new Command3("dev").description("Sync agent to development envi
1524
1363
  process.exit(1);
1525
1364
  }
1526
1365
  }
1527
- spinner.start("Syncing to Convex");
1366
+ const claudeMdPath = join5(cwd, "CLAUDE.md");
1367
+ if (!existsSync5(claudeMdPath)) {
1368
+ writeFileSync4(claudeMdPath, getClaudeMDV2(project.organization.name));
1369
+ console.log(chalk3.green("\u2713"), "Created CLAUDE.md");
1370
+ }
1371
+ const isAuthError = (error) => {
1372
+ const message = error instanceof Error ? error.message : String(error);
1373
+ return message.includes("Unauthenticated") || message.includes("OIDC") || message.includes("token") || message.includes("expired");
1374
+ };
1528
1375
  const performSync = async () => {
1529
1376
  try {
1530
- const config = extractConfig(agent);
1531
- const result = await syncToConvex(project.agentId, config);
1377
+ const resources = await loadAllResources(cwd);
1378
+ const payload = extractSyncPayload(resources);
1379
+ const result = await syncOrganization(payload);
1532
1380
  if (!result.success) {
1533
1381
  throw new Error(result.error || "Sync failed");
1534
1382
  }
@@ -1537,10 +1385,16 @@ var devCommand = new Command3("dev").description("Sync agent to development envi
1537
1385
  throw error;
1538
1386
  }
1539
1387
  };
1540
- const isAuthError = (error) => {
1541
- const message = error instanceof Error ? error.message : String(error);
1542
- return message.includes("Unauthenticated") || message.includes("OIDC") || message.includes("token") || message.includes("expired");
1543
- };
1388
+ spinner.start("Loading resources");
1389
+ try {
1390
+ const resources = await loadAllResources(cwd);
1391
+ spinner.succeed(`Loaded ${resources.agents.length} agents, ${resources.entityTypes.length} entity types, ${resources.roles.length} roles`);
1392
+ } catch (error) {
1393
+ spinner.fail("Failed to load resources");
1394
+ console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
1395
+ process.exit(1);
1396
+ }
1397
+ spinner.start("Syncing to Convex");
1544
1398
  try {
1545
1399
  await performSync();
1546
1400
  spinner.succeed("Synced to development");
@@ -1570,13 +1424,18 @@ var devCommand = new Command3("dev").description("Sync agent to development envi
1570
1424
  process.exit(1);
1571
1425
  }
1572
1426
  }
1573
- const devUrl = `https://${project.agent.slug}-dev.struere.dev`;
1574
- console.log();
1575
- console.log(chalk3.green("Development URL:"), chalk3.cyan(devUrl));
1576
1427
  console.log();
1577
1428
  console.log(chalk3.gray("Watching for changes... Press Ctrl+C to stop"));
1578
1429
  console.log();
1579
- const watcher = chokidar.watch([join7(cwd, "src"), join7(cwd, "struere.config.ts")], {
1430
+ const dirs = getResourceDirectories(cwd);
1431
+ const watchPaths = [
1432
+ dirs.agents,
1433
+ dirs.entityTypes,
1434
+ dirs.roles,
1435
+ dirs.tools,
1436
+ join5(cwd, "struere.config.ts")
1437
+ ].filter((p) => existsSync5(p));
1438
+ const watcher = chokidar.watch(watchPaths, {
1580
1439
  ignoreInitial: true,
1581
1440
  ignored: /node_modules/
1582
1441
  });
@@ -1585,7 +1444,6 @@ var devCommand = new Command3("dev").description("Sync agent to development envi
1585
1444
  console.log(chalk3.gray(`Changed: ${relativePath}`));
1586
1445
  const syncSpinner = ora3("Syncing...").start();
1587
1446
  try {
1588
- agent = await loadAgent(cwd);
1589
1447
  await performSync();
1590
1448
  syncSpinner.succeed("Synced");
1591
1449
  } catch (error) {
@@ -1600,217 +1458,50 @@ var devCommand = new Command3("dev").description("Sync agent to development envi
1600
1458
  return;
1601
1459
  }
1602
1460
  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
- const { agents: existingAgents, error: listError } = await listAgents();
1640
- if (listError) {
1641
- spinner.fail("Failed to fetch agents");
1642
- console.log();
1643
- console.log(chalk3.gray("Run"), chalk3.cyan("struere login"), chalk3.gray("to re-authenticate"));
1644
- return null;
1645
- }
1646
- const agents = existingAgents.map((a) => ({ id: a._id, name: a.name, slug: a.slug }));
1647
- spinner.succeed(`Found ${agents.length} existing agent(s)`);
1648
- let selectedAgent = null;
1649
- if (agents.length === 0) {
1650
- console.log(chalk3.gray("No existing agents found. Creating a new one..."));
1651
- } else {
1652
- console.log();
1653
- const choices = [
1654
- { value: "link", label: "Link to an existing agent" },
1655
- { value: "create", label: "Create a new agent" },
1656
- { value: "cancel", label: "Cancel" }
1657
- ];
1658
- const action = await promptChoiceArrows("No agent configured. Would you like to:", choices);
1659
- if (action === "cancel") {
1660
- console.log();
1661
- console.log(chalk3.gray("Run"), chalk3.cyan("struere init"), chalk3.gray("when ready to set up"));
1662
- return null;
1663
- }
1664
- if (action === "link") {
1665
- console.log();
1666
- const agentChoices = agents.map((a) => ({ value: a.id, label: `${a.name} (${a.slug})` }));
1667
- const agentId = await promptChoiceArrows("Select an agent:", agentChoices);
1668
- selectedAgent = agents.find((a) => a.id === agentId) || null;
1669
- }
1670
- }
1671
- if (!selectedAgent) {
1672
- console.log();
1673
- const projectName = slugify2(basename2(cwd));
1674
- const name = await promptText2("Agent name:", projectName);
1675
- const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1676
- spinner.start("Creating agent");
1677
- const { agentId, error: createError } = await createAgent({
1678
- name: displayName,
1679
- slug: name,
1680
- description: `${displayName} Agent`
1681
- });
1682
- if (createError || !agentId) {
1683
- spinner.fail("Failed to create agent");
1684
- console.log();
1685
- console.log(chalk3.red("Error:"), createError || "Unknown error");
1686
- return null;
1687
- }
1688
- selectedAgent = { id: agentId, name: displayName, slug: name };
1689
- spinner.succeed(`Created agent "${name}"`);
1690
- }
1691
- if (!selectedAgent) {
1692
- return null;
1693
- }
1694
- const projectData = {
1695
- agentId: selectedAgent.id,
1696
- team: credentials.organization.slug,
1697
- agent: {
1698
- slug: selectedAgent.slug,
1699
- name: selectedAgent.name
1700
- }
1701
- };
1702
- saveProject(cwd, projectData);
1703
- console.log(chalk3.green("\u2713"), "Created struere.json");
1704
- if (!hasAgentFiles(cwd)) {
1705
- const scaffoldResult = scaffoldAgentFiles(cwd, selectedAgent.slug);
1706
- for (const file of scaffoldResult.createdFiles) {
1707
- console.log(chalk3.green("\u2713"), `Created ${file}`);
1708
- }
1709
- console.log();
1710
- spinner.start("Installing dependencies");
1711
- try {
1712
- const proc = Bun.spawn(["bun", "install"], {
1713
- cwd,
1714
- stdout: "pipe",
1715
- stderr: "pipe"
1716
- });
1717
- await proc.exited;
1718
- if (proc.exitCode === 0) {
1719
- spinner.succeed("Dependencies installed");
1720
- } else {
1721
- spinner.fail("Failed to install dependencies");
1722
- console.log(chalk3.yellow("Run"), chalk3.cyan("bun install"), chalk3.yellow("manually"));
1723
- }
1724
- } catch {
1725
- spinner.fail("Failed to install dependencies");
1726
- console.log(chalk3.yellow("Run"), chalk3.cyan("bun install"), chalk3.yellow("manually"));
1727
- }
1728
- }
1729
- console.log();
1730
- return projectData;
1731
- }
1732
- function slugify2(name) {
1733
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1734
- }
1735
- async function promptChoiceArrows(message, choices) {
1736
- return new Promise((resolve) => {
1737
- let selectedIndex = 0;
1738
- const render = () => {
1739
- process.stdout.write("\x1B[?25l");
1740
- process.stdout.write(`\x1B[${choices.length + 2}A`);
1741
- console.log(chalk3.gray(message));
1742
- console.log();
1743
- for (let i = 0;i < choices.length; i++) {
1744
- const prefix = i === selectedIndex ? chalk3.cyan("\u276F") : " ";
1745
- const label = i === selectedIndex ? chalk3.cyan(choices[i].label) : choices[i].label;
1746
- console.log(`${prefix} ${label}`);
1461
+ try {
1462
+ await performSync();
1463
+ retrySyncSpinner.succeed("Synced");
1464
+ } catch (retryError) {
1465
+ retrySyncSpinner.fail("Sync failed");
1466
+ console.log(chalk3.red("Error:"), retryError instanceof Error ? retryError.message : String(retryError));
1467
+ }
1468
+ } else {
1469
+ syncSpinner.fail("Sync failed");
1470
+ console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
1747
1471
  }
1748
- };
1749
- console.log(chalk3.gray(message));
1750
- console.log();
1751
- for (let i = 0;i < choices.length; i++) {
1752
- const prefix = i === selectedIndex ? chalk3.cyan("\u276F") : " ";
1753
- const label = i === selectedIndex ? chalk3.cyan(choices[i].label) : choices[i].label;
1754
- console.log(`${prefix} ${label}`);
1755
1472
  }
1756
- if (!process.stdin.isTTY) {
1757
- resolve(choices[0].value);
1758
- return;
1473
+ });
1474
+ watcher.on("add", async (path) => {
1475
+ const relativePath = path.replace(cwd, ".");
1476
+ console.log(chalk3.gray(`Added: ${relativePath}`));
1477
+ const syncSpinner = ora3("Syncing...").start();
1478
+ try {
1479
+ await performSync();
1480
+ syncSpinner.succeed("Synced");
1481
+ } catch (error) {
1482
+ syncSpinner.fail("Sync failed");
1483
+ console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
1759
1484
  }
1760
- process.stdin.setRawMode?.(true);
1761
- process.stdin.resume();
1762
- const onKeypress = (key) => {
1763
- const char = key.toString();
1764
- if (char === "\x1B[A" || char === "k") {
1765
- selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
1766
- render();
1767
- } else if (char === "\x1B[B" || char === "j") {
1768
- selectedIndex = (selectedIndex + 1) % choices.length;
1769
- render();
1770
- } else if (char === "\r" || char === `
1771
- `) {
1772
- process.stdin.removeListener("data", onKeypress);
1773
- process.stdin.setRawMode?.(false);
1774
- process.stdin.pause();
1775
- process.stdout.write("\x1B[?25h");
1776
- resolve(choices[selectedIndex].value);
1777
- } else if (char === "\x03") {
1778
- process.stdin.removeListener("data", onKeypress);
1779
- process.stdin.setRawMode?.(false);
1780
- process.stdout.write("\x1B[?25h");
1781
- process.exit(0);
1782
- }
1783
- };
1784
- process.stdin.on("data", onKeypress);
1785
1485
  });
1786
- }
1787
- async function promptText2(message, defaultValue) {
1788
- process.stdout.write(chalk3.gray(`${message} `));
1789
- process.stdout.write(chalk3.cyan(`(${defaultValue}) `));
1790
- const answer = await readLine2();
1791
- return answer || defaultValue;
1792
- }
1793
- function readLine2() {
1794
- return new Promise((resolve) => {
1795
- let buffer = "";
1796
- const onData = (chunk) => {
1797
- const str = chunk.toString();
1798
- buffer += str;
1799
- if (str.includes(`
1800
- `) || str.includes("\r")) {
1801
- process.stdin.removeListener("data", onData);
1802
- process.stdin.pause();
1803
- process.stdin.setRawMode?.(false);
1804
- resolve(buffer.replace(/[\r\n]/g, "").trim());
1805
- }
1806
- };
1807
- if (process.stdin.isTTY) {
1808
- process.stdin.setRawMode?.(false);
1486
+ watcher.on("unlink", async (path) => {
1487
+ const relativePath = path.replace(cwd, ".");
1488
+ console.log(chalk3.gray(`Removed: ${relativePath}`));
1489
+ const syncSpinner = ora3("Syncing...").start();
1490
+ try {
1491
+ await performSync();
1492
+ syncSpinner.succeed("Synced");
1493
+ } catch (error) {
1494
+ syncSpinner.fail("Sync failed");
1495
+ console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
1809
1496
  }
1810
- process.stdin.resume();
1811
- process.stdin.on("data", onData);
1812
1497
  });
1813
- }
1498
+ process.on("SIGINT", () => {
1499
+ console.log();
1500
+ watcher.close();
1501
+ console.log(chalk3.gray("Stopped"));
1502
+ process.exit(0);
1503
+ });
1504
+ });
1814
1505
 
1815
1506
  // src/cli/commands/build.ts
1816
1507
  import { Command as Command4 } from "commander";
@@ -1818,6 +1509,74 @@ import chalk4 from "chalk";
1818
1509
  import ora4 from "ora";
1819
1510
  import { join as join8 } from "path";
1820
1511
 
1512
+ // src/cli/utils/config.ts
1513
+ import { join as join6 } from "path";
1514
+ var defaultConfig = {
1515
+ port: 3000,
1516
+ host: "localhost",
1517
+ cors: {
1518
+ origins: ["http://localhost:3000"],
1519
+ credentials: true
1520
+ },
1521
+ logging: {
1522
+ level: "info",
1523
+ format: "pretty"
1524
+ },
1525
+ auth: {
1526
+ type: "none"
1527
+ }
1528
+ };
1529
+ async function loadConfig(cwd) {
1530
+ const configPath = join6(cwd, "struere.config.ts");
1531
+ try {
1532
+ const module = await import(configPath);
1533
+ const config = module.default || module;
1534
+ return {
1535
+ ...defaultConfig,
1536
+ ...config,
1537
+ cors: {
1538
+ ...defaultConfig.cors,
1539
+ ...config.cors
1540
+ },
1541
+ logging: {
1542
+ ...defaultConfig.logging,
1543
+ ...config.logging
1544
+ },
1545
+ auth: {
1546
+ ...defaultConfig.auth,
1547
+ ...config.auth
1548
+ }
1549
+ };
1550
+ } catch {
1551
+ return defaultConfig;
1552
+ }
1553
+ }
1554
+
1555
+ // src/cli/utils/agent.ts
1556
+ import { join as join7 } from "path";
1557
+ async function loadAgent(cwd) {
1558
+ const agentPath = join7(cwd, "src/agent.ts");
1559
+ try {
1560
+ const module = await import(`${agentPath}?t=${Date.now()}`);
1561
+ const agent = module.default || module;
1562
+ if (!agent.name) {
1563
+ throw new Error("Agent must have a name");
1564
+ }
1565
+ if (!agent.version) {
1566
+ throw new Error("Agent must have a version");
1567
+ }
1568
+ if (!agent.systemPrompt) {
1569
+ throw new Error("Agent must have a systemPrompt");
1570
+ }
1571
+ return agent;
1572
+ } catch (error) {
1573
+ if (error instanceof Error && error.message.includes("Cannot find module")) {
1574
+ throw new Error(`Agent not found at ${agentPath}`);
1575
+ }
1576
+ throw error;
1577
+ }
1578
+ }
1579
+
1821
1580
  // src/cli/utils/validate.ts
1822
1581
  function validateAgent(agent) {
1823
1582
  const errors = [];
@@ -2138,12 +1897,11 @@ function formatAssertionError(assertion, context) {
2138
1897
  import { Command as Command6 } from "commander";
2139
1898
  import chalk6 from "chalk";
2140
1899
  import ora6 from "ora";
2141
- var deployCommand = new Command6("deploy").description("Deploy agent to production").option("--dry-run", "Show what would be deployed without deploying").action(async (options) => {
2142
- const environment = "production";
1900
+ var deployCommand = new Command6("deploy").description("Deploy all agents to production").option("--dry-run", "Show what would be deployed without deploying").action(async (options) => {
2143
1901
  const spinner = ora6();
2144
1902
  const cwd = process.cwd();
2145
1903
  console.log();
2146
- console.log(chalk6.bold("Deploying Agent"));
1904
+ console.log(chalk6.bold("Deploying Agents"));
2147
1905
  console.log();
2148
1906
  if (!hasProject(cwd)) {
2149
1907
  console.log(chalk6.yellow("No struere.json found"));
@@ -2152,59 +1910,73 @@ var deployCommand = new Command6("deploy").description("Deploy agent to producti
2152
1910
  console.log();
2153
1911
  process.exit(1);
2154
1912
  }
2155
- const project = loadProject(cwd);
1913
+ const version = getProjectVersion(cwd);
1914
+ if (version === "1.0") {
1915
+ console.log(chalk6.yellow("This is a v1 agent-centric project."));
1916
+ console.log(chalk6.yellow("Please migrate to v2 structure or use an older CLI version."));
1917
+ console.log();
1918
+ process.exit(1);
1919
+ }
1920
+ const project = loadProjectV2(cwd);
2156
1921
  if (!project) {
2157
1922
  console.log(chalk6.red("Failed to load struere.json"));
2158
1923
  process.exit(1);
2159
1924
  }
2160
- console.log(chalk6.gray("Agent:"), chalk6.cyan(project.agent.name));
1925
+ console.log(chalk6.gray("Organization:"), chalk6.cyan(project.organization.name));
2161
1926
  console.log();
2162
- spinner.start("Loading configuration");
2163
- await loadConfig(cwd);
2164
- spinner.succeed("Configuration loaded");
2165
- spinner.start("Loading agent");
2166
- const agent = await loadAgent(cwd);
2167
- spinner.succeed(`Agent "${agent.name}" loaded`);
2168
- spinner.start("Validating agent");
2169
- const errors = validateAgent(agent);
2170
- if (errors.length > 0) {
2171
- spinner.fail("Validation failed");
1927
+ const credentials = loadCredentials();
1928
+ const apiKey = getApiKey();
1929
+ if (!credentials && !apiKey) {
1930
+ console.log(chalk6.red("Not authenticated"));
2172
1931
  console.log();
2173
- for (const error of errors) {
2174
- console.log(chalk6.red(" x"), error);
2175
- }
1932
+ console.log(chalk6.gray("Run"), chalk6.cyan("struere login"), chalk6.gray("to authenticate"));
1933
+ console.log(chalk6.gray("Or set"), chalk6.cyan("STRUERE_API_KEY"), chalk6.gray("environment variable"));
2176
1934
  console.log();
2177
1935
  process.exit(1);
2178
1936
  }
2179
- spinner.succeed("Agent validated");
1937
+ spinner.start("Loading resources");
1938
+ let resources;
1939
+ try {
1940
+ resources = await loadAllResources(cwd);
1941
+ spinner.succeed(`Loaded ${resources.agents.length} agents, ${resources.entityTypes.length} entity types, ${resources.roles.length} roles`);
1942
+ } catch (error) {
1943
+ spinner.fail("Failed to load resources");
1944
+ console.log(chalk6.red("Error:"), error instanceof Error ? error.message : String(error));
1945
+ process.exit(1);
1946
+ }
1947
+ if (resources.agents.length === 0) {
1948
+ console.log();
1949
+ console.log(chalk6.yellow("No agents found to deploy"));
1950
+ console.log();
1951
+ console.log(chalk6.gray("Run"), chalk6.cyan("struere add agent my-agent"), chalk6.gray("to create an agent"));
1952
+ console.log();
1953
+ return;
1954
+ }
2180
1955
  if (options.dryRun) {
2181
1956
  console.log();
2182
1957
  console.log(chalk6.yellow("Dry run mode - no changes will be made"));
2183
1958
  console.log();
2184
1959
  console.log("Would deploy:");
2185
- console.log(chalk6.gray(" -"), `Agent: ${chalk6.cyan(agent.name)}`);
2186
- console.log(chalk6.gray(" -"), `Version: ${chalk6.cyan(agent.version)}`);
2187
- console.log(chalk6.gray(" -"), `Environment: ${chalk6.cyan(environment)}`);
2188
- console.log(chalk6.gray(" -"), `Agent ID: ${chalk6.cyan(project.agentId)}`);
1960
+ for (const agent of resources.agents) {
1961
+ console.log(chalk6.gray(" -"), `${chalk6.cyan(agent.name)} (${agent.slug}) v${agent.version}`);
1962
+ }
2189
1963
  console.log();
2190
- return;
2191
- }
2192
- const credentials = loadCredentials();
2193
- const apiKey = getApiKey();
2194
- if (!credentials && !apiKey) {
2195
- spinner.fail("Not authenticated");
1964
+ console.log("Entity types:");
1965
+ for (const et of resources.entityTypes) {
1966
+ console.log(chalk6.gray(" -"), chalk6.cyan(et.name), `(${et.slug})`);
1967
+ }
2196
1968
  console.log();
2197
- console.log(chalk6.gray("Run"), chalk6.cyan("struere login"), chalk6.gray("to authenticate"));
2198
- console.log(chalk6.gray("Or set"), chalk6.cyan("STRUERE_API_KEY"), chalk6.gray("environment variable"));
1969
+ console.log("Roles:");
1970
+ for (const role of resources.roles) {
1971
+ console.log(chalk6.gray(" -"), chalk6.cyan(role.name));
1972
+ }
2199
1973
  console.log();
2200
- process.exit(1);
1974
+ return;
2201
1975
  }
2202
- spinner.start("Extracting agent configuration");
2203
- const config = extractConfig(agent);
2204
- spinner.succeed("Configuration extracted");
2205
1976
  spinner.start("Syncing to development");
2206
1977
  try {
2207
- const syncResult = await syncToConvex(project.agentId, config);
1978
+ const payload = extractSyncPayload(resources);
1979
+ const syncResult = await syncOrganization(payload);
2208
1980
  if (!syncResult.success) {
2209
1981
  throw new Error(syncResult.error || "Sync failed");
2210
1982
  }
@@ -2214,24 +1986,34 @@ var deployCommand = new Command6("deploy").description("Deploy agent to producti
2214
1986
  console.log(chalk6.red("Error:"), error instanceof Error ? error.message : String(error));
2215
1987
  process.exit(1);
2216
1988
  }
2217
- spinner.start(`Deploying to ${environment}`);
1989
+ spinner.start("Deploying to production");
2218
1990
  try {
2219
- const deployResult = await deployToProduction(project.agentId);
1991
+ const deployResult = await deployAllAgents();
2220
1992
  if (!deployResult.success) {
2221
1993
  throw new Error(deployResult.error || "Deployment failed");
2222
1994
  }
2223
- spinner.succeed(`Deployed to ${environment}`);
2224
- const prodUrl = `https://${project.agent.slug}.struere.dev`;
1995
+ spinner.succeed("Deployed to production");
2225
1996
  console.log();
2226
- console.log(chalk6.green("Success!"), "Agent deployed");
1997
+ console.log(chalk6.green("Success!"), "All agents deployed");
2227
1998
  console.log();
2228
- console.log("Deployment details:");
2229
- console.log(chalk6.gray(" -"), `Version: ${chalk6.cyan(agent.version)}`);
2230
- console.log(chalk6.gray(" -"), `Environment: ${chalk6.cyan(environment)}`);
2231
- console.log(chalk6.gray(" -"), `URL: ${chalk6.cyan(prodUrl)}`);
1999
+ if (deployResult.deployed && deployResult.deployed.length > 0) {
2000
+ console.log("Deployed agents:");
2001
+ for (const slug of deployResult.deployed) {
2002
+ const agent = resources.agents.find((a) => a.slug === slug);
2003
+ const prodUrl = `https://${slug}.struere.dev`;
2004
+ console.log(chalk6.gray(" -"), chalk6.cyan(agent?.name || slug), chalk6.gray(`\u2192 ${prodUrl}`));
2005
+ }
2006
+ }
2007
+ if (deployResult.skipped && deployResult.skipped.length > 0) {
2008
+ console.log();
2009
+ console.log(chalk6.yellow("Skipped (no development config):"));
2010
+ for (const slug of deployResult.skipped) {
2011
+ console.log(chalk6.gray(" -"), slug);
2012
+ }
2013
+ }
2232
2014
  console.log();
2233
- console.log(chalk6.gray("Test your agent:"));
2234
- console.log(chalk6.gray(" $"), chalk6.cyan(`curl -X POST ${prodUrl}/chat -H "Authorization: Bearer YOUR_API_KEY" -d '{"message": "Hello"}'`));
2015
+ console.log(chalk6.gray("Test your agents:"));
2016
+ console.log(chalk6.gray(" $"), chalk6.cyan(`curl -X POST https://<agent-slug>.struere.dev/chat -H "Authorization: Bearer YOUR_API_KEY" -d '{"message": "Hello"}'`));
2235
2017
  console.log();
2236
2018
  } catch (error) {
2237
2019
  spinner.fail("Deployment failed");
@@ -2469,10 +2251,223 @@ var whoamiCommand = new Command11("whoami").description("Show current logged in
2469
2251
  console.log();
2470
2252
  }
2471
2253
  });
2254
+
2255
+ // src/cli/commands/add.ts
2256
+ import { Command as Command12 } from "commander";
2257
+ import chalk12 from "chalk";
2258
+ 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) => {
2259
+ const cwd = process.cwd();
2260
+ console.log();
2261
+ if (!hasProject(cwd)) {
2262
+ console.log(chalk12.yellow("No struere.json found"));
2263
+ console.log();
2264
+ console.log(chalk12.gray("Run"), chalk12.cyan("struere init"), chalk12.gray("to initialize this project"));
2265
+ console.log();
2266
+ process.exit(1);
2267
+ }
2268
+ const version = getProjectVersion(cwd);
2269
+ if (version === "1.0") {
2270
+ console.log(chalk12.yellow("This is a v1 agent-centric project."));
2271
+ console.log(chalk12.yellow("The add command requires v2 structure."));
2272
+ console.log();
2273
+ process.exit(1);
2274
+ }
2275
+ const slug = slugify2(name);
2276
+ const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2277
+ let result;
2278
+ switch (type.toLowerCase()) {
2279
+ case "agent":
2280
+ result = scaffoldAgent(cwd, displayName, slug);
2281
+ if (result.createdFiles.length > 0) {
2282
+ console.log(chalk12.green("\u2713"), `Created agent "${displayName}"`);
2283
+ for (const file of result.createdFiles) {
2284
+ console.log(chalk12.gray(" \u2192"), file);
2285
+ }
2286
+ } else {
2287
+ console.log(chalk12.yellow("Agent already exists:"), `agents/${slug}.ts`);
2288
+ }
2289
+ break;
2290
+ case "entity-type":
2291
+ case "entitytype":
2292
+ case "type":
2293
+ result = scaffoldEntityType(cwd, displayName, slug);
2294
+ if (result.createdFiles.length > 0) {
2295
+ console.log(chalk12.green("\u2713"), `Created entity type "${displayName}"`);
2296
+ for (const file of result.createdFiles) {
2297
+ console.log(chalk12.gray(" \u2192"), file);
2298
+ }
2299
+ } else {
2300
+ console.log(chalk12.yellow("Entity type already exists:"), `entity-types/${slug}.ts`);
2301
+ }
2302
+ break;
2303
+ case "role":
2304
+ result = scaffoldRole(cwd, slug);
2305
+ if (result.createdFiles.length > 0) {
2306
+ console.log(chalk12.green("\u2713"), `Created role "${slug}"`);
2307
+ for (const file of result.createdFiles) {
2308
+ console.log(chalk12.gray(" \u2192"), file);
2309
+ }
2310
+ } else {
2311
+ console.log(chalk12.yellow("Role already exists:"), `roles/${slug}.ts`);
2312
+ }
2313
+ break;
2314
+ default:
2315
+ console.log(chalk12.red("Unknown resource type:"), type);
2316
+ console.log();
2317
+ console.log("Available types:");
2318
+ console.log(chalk12.gray(" -"), chalk12.cyan("agent"), "- Create an AI agent");
2319
+ console.log(chalk12.gray(" -"), chalk12.cyan("entity-type"), "- Create an entity type schema");
2320
+ console.log(chalk12.gray(" -"), chalk12.cyan("role"), "- Create a role with permissions");
2321
+ console.log();
2322
+ process.exit(1);
2323
+ }
2324
+ console.log();
2325
+ console.log(chalk12.gray("Run"), chalk12.cyan("struere dev"), chalk12.gray("to sync changes"));
2326
+ console.log();
2327
+ });
2328
+ function slugify2(name) {
2329
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
2330
+ }
2331
+
2332
+ // src/cli/commands/status.ts
2333
+ import { Command as Command13 } from "commander";
2334
+ import chalk13 from "chalk";
2335
+ import ora11 from "ora";
2336
+ var statusCommand = new Command13("status").description("Compare local vs remote state").action(async () => {
2337
+ const spinner = ora11();
2338
+ const cwd = process.cwd();
2339
+ console.log();
2340
+ console.log(chalk13.bold("Struere Status"));
2341
+ console.log();
2342
+ if (!hasProject(cwd)) {
2343
+ console.log(chalk13.yellow("No struere.json found"));
2344
+ console.log();
2345
+ console.log(chalk13.gray("Run"), chalk13.cyan("struere init"), chalk13.gray("to initialize this project"));
2346
+ console.log();
2347
+ process.exit(1);
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
+ const credentials = loadCredentials();
2364
+ const apiKey = getApiKey();
2365
+ if (!credentials && !apiKey) {
2366
+ console.log(chalk13.red("Not authenticated"));
2367
+ console.log();
2368
+ console.log(chalk13.gray("Run"), chalk13.cyan("struere login"), chalk13.gray("to authenticate"));
2369
+ console.log();
2370
+ process.exit(1);
2371
+ }
2372
+ spinner.start("Loading local resources");
2373
+ let localResources;
2374
+ try {
2375
+ localResources = await loadAllResources(cwd);
2376
+ spinner.succeed("Local resources loaded");
2377
+ } catch (error) {
2378
+ spinner.fail("Failed to load local resources");
2379
+ console.log(chalk13.red("Error:"), error instanceof Error ? error.message : String(error));
2380
+ process.exit(1);
2381
+ }
2382
+ spinner.start("Fetching remote state");
2383
+ const { state: remoteState, error: fetchError } = await getSyncState();
2384
+ if (fetchError || !remoteState) {
2385
+ spinner.fail("Failed to fetch remote state");
2386
+ console.log(chalk13.red("Error:"), fetchError || "Unknown error");
2387
+ process.exit(1);
2388
+ }
2389
+ spinner.succeed("Remote state fetched");
2390
+ console.log();
2391
+ const localAgentSlugs = new Set(localResources.agents.map((a) => a.slug));
2392
+ const remoteAgentSlugs = new Set(remoteState.agents.map((a) => a.slug));
2393
+ const localEntityTypeSlugs = new Set(localResources.entityTypes.map((et) => et.slug));
2394
+ const remoteEntityTypeSlugs = new Set(remoteState.entityTypes.map((et) => et.slug));
2395
+ const localRoleNames = new Set(localResources.roles.map((r) => r.name));
2396
+ const remoteRoleNames = new Set(remoteState.roles.map((r) => r.name));
2397
+ console.log(chalk13.bold("Agents"));
2398
+ console.log(chalk13.gray("\u2500".repeat(60)));
2399
+ if (localResources.agents.length === 0 && remoteState.agents.length === 0) {
2400
+ console.log(chalk13.gray(" No agents"));
2401
+ } else {
2402
+ for (const agent of localResources.agents) {
2403
+ const remote = remoteState.agents.find((a) => a.slug === agent.slug);
2404
+ if (remote) {
2405
+ const statusIcon = remote.hasProdConfig ? chalk13.green("\u25CF") : chalk13.yellow("\u25CB");
2406
+ console.log(` ${statusIcon} ${chalk13.cyan(agent.name)} (${agent.slug}) - v${agent.version}`);
2407
+ if (!remote.hasProdConfig) {
2408
+ console.log(chalk13.gray(" Not deployed to production"));
2409
+ }
2410
+ } else {
2411
+ console.log(` ${chalk13.blue("+")} ${chalk13.cyan(agent.name)} (${agent.slug}) - ${chalk13.blue("new")}`);
2412
+ }
2413
+ }
2414
+ for (const remote of remoteState.agents) {
2415
+ if (!localAgentSlugs.has(remote.slug)) {
2416
+ console.log(` ${chalk13.red("-")} ${remote.name} (${remote.slug}) - ${chalk13.red("will be deleted")}`);
2417
+ }
2418
+ }
2419
+ }
2420
+ console.log();
2421
+ console.log(chalk13.bold("Entity Types"));
2422
+ console.log(chalk13.gray("\u2500".repeat(60)));
2423
+ if (localResources.entityTypes.length === 0 && remoteState.entityTypes.length === 0) {
2424
+ console.log(chalk13.gray(" No entity types"));
2425
+ } else {
2426
+ for (const et of localResources.entityTypes) {
2427
+ const remote = remoteState.entityTypes.find((r) => r.slug === et.slug);
2428
+ if (remote) {
2429
+ console.log(` ${chalk13.green("\u25CF")} ${chalk13.cyan(et.name)} (${et.slug})`);
2430
+ } else {
2431
+ console.log(` ${chalk13.blue("+")} ${chalk13.cyan(et.name)} (${et.slug}) - ${chalk13.blue("new")}`);
2432
+ }
2433
+ }
2434
+ for (const remote of remoteState.entityTypes) {
2435
+ if (!localEntityTypeSlugs.has(remote.slug)) {
2436
+ console.log(` ${chalk13.red("-")} ${remote.name} (${remote.slug}) - ${chalk13.red("will be deleted")}`);
2437
+ }
2438
+ }
2439
+ }
2440
+ console.log();
2441
+ console.log(chalk13.bold("Roles"));
2442
+ console.log(chalk13.gray("\u2500".repeat(60)));
2443
+ if (localResources.roles.length === 0 && remoteState.roles.length === 0) {
2444
+ console.log(chalk13.gray(" No roles"));
2445
+ } else {
2446
+ for (const role of localResources.roles) {
2447
+ const remote = remoteState.roles.find((r) => r.name === role.name);
2448
+ if (remote) {
2449
+ console.log(` ${chalk13.green("\u25CF")} ${chalk13.cyan(role.name)} (${role.policies.length} policies)`);
2450
+ } else {
2451
+ console.log(` ${chalk13.blue("+")} ${chalk13.cyan(role.name)} - ${chalk13.blue("new")}`);
2452
+ }
2453
+ }
2454
+ for (const remote of remoteState.roles) {
2455
+ if (!localRoleNames.has(remote.name)) {
2456
+ console.log(` ${chalk13.red("-")} ${remote.name} - ${chalk13.red("will be deleted")}`);
2457
+ }
2458
+ }
2459
+ }
2460
+ console.log();
2461
+ console.log(chalk13.gray("Legend:"));
2462
+ console.log(chalk13.gray(" "), chalk13.green("\u25CF"), "Synced", chalk13.yellow("\u25CB"), "Not deployed", chalk13.blue("+"), "New", chalk13.red("-"), "Will be deleted");
2463
+ console.log();
2464
+ console.log(chalk13.gray("Run"), chalk13.cyan("struere dev"), chalk13.gray("to sync changes"));
2465
+ console.log();
2466
+ });
2472
2467
  // package.json
2473
2468
  var package_default = {
2474
2469
  name: "struere",
2475
- version: "0.3.10",
2470
+ version: "0.4.0",
2476
2471
  description: "Build, test, and deploy AI agents",
2477
2472
  keywords: [
2478
2473
  "ai",
@@ -2577,4 +2572,6 @@ program.addCommand(deployCommand);
2577
2572
  program.addCommand(validateCommand);
2578
2573
  program.addCommand(logsCommand);
2579
2574
  program.addCommand(stateCommand);
2575
+ program.addCommand(addCommand);
2576
+ program.addCommand(statusCommand);
2580
2577
  program.parse();