struere 0.3.11 → 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.
@@ -16674,8 +16674,7 @@ function ora(options) {
16674
16674
  }
16675
16675
 
16676
16676
  // src/cli/commands/init.ts
16677
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
16678
- import { join as join4, basename } from "path";
16677
+ import { basename } from "path";
16679
16678
 
16680
16679
  // src/cli/utils/credentials.ts
16681
16680
  import { homedir } from "os";
@@ -16723,110 +16722,6 @@ function getApiKey() {
16723
16722
 
16724
16723
  // src/cli/utils/convex.ts
16725
16724
  var CONVEX_URL = process.env.STRUERE_CONVEX_URL || "https://rapid-wildebeest-172.convex.cloud";
16726
- async function syncToConvex(agentId, config) {
16727
- const credentials = loadCredentials();
16728
- const apiKey = getApiKey();
16729
- const token = apiKey || credentials?.token;
16730
- if (!token) {
16731
- return { success: false, error: "Not authenticated" };
16732
- }
16733
- const response = await fetch(`${CONVEX_URL}/api/mutation`, {
16734
- method: "POST",
16735
- headers: {
16736
- "Content-Type": "application/json",
16737
- Authorization: `Bearer ${token}`
16738
- },
16739
- body: JSON.stringify({
16740
- path: "agents:syncDevelopment",
16741
- args: {
16742
- agentId,
16743
- config
16744
- }
16745
- })
16746
- });
16747
- if (!response.ok) {
16748
- const error = await response.text();
16749
- return { success: false, error };
16750
- }
16751
- const result = await response.json();
16752
- return { success: result.success ?? true };
16753
- }
16754
- async function deployToProduction(agentId) {
16755
- const credentials = loadCredentials();
16756
- const apiKey = getApiKey();
16757
- const token = apiKey || credentials?.token;
16758
- if (!token) {
16759
- return { success: false, error: "Not authenticated" };
16760
- }
16761
- const response = await fetch(`${CONVEX_URL}/api/mutation`, {
16762
- method: "POST",
16763
- headers: {
16764
- "Content-Type": "application/json",
16765
- Authorization: `Bearer ${token}`
16766
- },
16767
- body: JSON.stringify({
16768
- path: "agents:deploy",
16769
- args: { agentId }
16770
- })
16771
- });
16772
- if (!response.ok) {
16773
- const error = await response.text();
16774
- return { success: false, error };
16775
- }
16776
- const result = await response.json();
16777
- return { success: result.success ?? true, configId: result.configId };
16778
- }
16779
- async function listAgents() {
16780
- const credentials = loadCredentials();
16781
- const apiKey = getApiKey();
16782
- const token = apiKey || credentials?.token;
16783
- if (!token) {
16784
- return { agents: [], error: "Not authenticated" };
16785
- }
16786
- const response = await fetch(`${CONVEX_URL}/api/query`, {
16787
- method: "POST",
16788
- headers: {
16789
- "Content-Type": "application/json",
16790
- Authorization: `Bearer ${token}`
16791
- },
16792
- body: JSON.stringify({
16793
- path: "agents:list",
16794
- args: {}
16795
- })
16796
- });
16797
- if (!response.ok) {
16798
- const error = await response.text();
16799
- return { agents: [], error };
16800
- }
16801
- const result = await response.json();
16802
- const agents = Array.isArray(result) ? result : result?.value || [];
16803
- return { agents };
16804
- }
16805
- async function createAgent(data) {
16806
- const credentials = loadCredentials();
16807
- const apiKey = getApiKey();
16808
- const token = apiKey || credentials?.token;
16809
- if (!token) {
16810
- return { error: "Not authenticated" };
16811
- }
16812
- const response = await fetch(`${CONVEX_URL}/api/mutation`, {
16813
- method: "POST",
16814
- headers: {
16815
- "Content-Type": "application/json",
16816
- Authorization: `Bearer ${token}`
16817
- },
16818
- body: JSON.stringify({
16819
- path: "agents:create",
16820
- args: data
16821
- })
16822
- });
16823
- if (!response.ok) {
16824
- const error = await response.text();
16825
- return { error };
16826
- }
16827
- const agentId = await response.json();
16828
- return { agentId };
16829
- }
16830
16725
  async function getUserInfo(token) {
16831
16726
  const ensureResponse = await fetch(`${CONVEX_URL}/api/mutation`, {
16832
16727
  method: "POST",
@@ -16898,73 +16793,6 @@ async function getUserInfo(token) {
16898
16793
  }
16899
16794
  };
16900
16795
  }
16901
- function extractConfig(agent) {
16902
- const BUILTIN_TOOLS = [
16903
- "entity.create",
16904
- "entity.get",
16905
- "entity.query",
16906
- "entity.update",
16907
- "entity.delete",
16908
- "entity.link",
16909
- "entity.unlink",
16910
- "event.emit",
16911
- "event.query",
16912
- "job.enqueue",
16913
- "job.status"
16914
- ];
16915
- let systemPrompt;
16916
- if (typeof agent.systemPrompt === "function") {
16917
- const result = agent.systemPrompt();
16918
- if (result instanceof Promise) {
16919
- throw new Error("Async system prompts must be resolved before syncing");
16920
- }
16921
- systemPrompt = result;
16922
- } else {
16923
- systemPrompt = agent.systemPrompt;
16924
- }
16925
- const tools = (agent.tools || []).map((tool) => {
16926
- const isBuiltin = BUILTIN_TOOLS.includes(tool.name);
16927
- let handlerCode;
16928
- if (!isBuiltin && tool.handler) {
16929
- handlerCode = extractHandlerCode(tool.handler);
16930
- }
16931
- return {
16932
- name: tool.name,
16933
- description: tool.description,
16934
- parameters: tool.parameters || { type: "object", properties: {} },
16935
- handlerCode,
16936
- isBuiltin
16937
- };
16938
- });
16939
- return {
16940
- name: agent.name,
16941
- version: agent.version || "0.0.1",
16942
- systemPrompt,
16943
- model: {
16944
- provider: agent.model?.provider || "anthropic",
16945
- name: agent.model?.name || "claude-sonnet-4-20250514",
16946
- temperature: agent.model?.temperature,
16947
- maxTokens: agent.model?.maxTokens
16948
- },
16949
- tools
16950
- };
16951
- }
16952
- function extractHandlerCode(handler) {
16953
- const code = handler.toString();
16954
- const arrowMatch = code.match(/(?:async\s*)?\([^)]*\)\s*=>\s*\{?([\s\S]*)\}?$/);
16955
- if (arrowMatch) {
16956
- let body = arrowMatch[1].trim();
16957
- if (body.startsWith("{") && body.endsWith("}")) {
16958
- body = body.slice(1, -1).trim();
16959
- }
16960
- return body;
16961
- }
16962
- const funcMatch = code.match(/(?:async\s*)?function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
16963
- if (funcMatch) {
16964
- return funcMatch[1].trim();
16965
- }
16966
- return code;
16967
- }
16968
16796
  async function getRecentExecutions(limit = 100) {
16969
16797
  const credentials = loadCredentials();
16970
16798
  const apiKey = getApiKey();
@@ -17054,6 +16882,81 @@ async function runTestConversation(agentId, message, threadId) {
17054
16882
  threadId: result.threadId
17055
16883
  };
17056
16884
  }
16885
+ async function syncOrganization(payload) {
16886
+ const credentials = loadCredentials();
16887
+ const apiKey = getApiKey();
16888
+ const token = apiKey || credentials?.token;
16889
+ if (!token) {
16890
+ return { success: false, error: "Not authenticated" };
16891
+ }
16892
+ const response = await fetch(`${CONVEX_URL}/api/mutation`, {
16893
+ method: "POST",
16894
+ headers: {
16895
+ "Content-Type": "application/json",
16896
+ Authorization: `Bearer ${token}`
16897
+ },
16898
+ body: JSON.stringify({
16899
+ path: "sync:syncOrganization",
16900
+ args: payload
16901
+ })
16902
+ });
16903
+ if (!response.ok) {
16904
+ const error = await response.text();
16905
+ return { success: false, error };
16906
+ }
16907
+ const result = await response.json();
16908
+ return result;
16909
+ }
16910
+ async function getSyncState() {
16911
+ const credentials = loadCredentials();
16912
+ const apiKey = getApiKey();
16913
+ const token = apiKey || credentials?.token;
16914
+ if (!token) {
16915
+ return { error: "Not authenticated" };
16916
+ }
16917
+ const response = await fetch(`${CONVEX_URL}/api/query`, {
16918
+ method: "POST",
16919
+ headers: {
16920
+ "Content-Type": "application/json",
16921
+ Authorization: `Bearer ${token}`
16922
+ },
16923
+ body: JSON.stringify({
16924
+ path: "sync:getSyncState",
16925
+ args: {}
16926
+ })
16927
+ });
16928
+ if (!response.ok) {
16929
+ const error = await response.text();
16930
+ return { error };
16931
+ }
16932
+ const result = await response.json();
16933
+ return { state: result.value };
16934
+ }
16935
+ async function deployAllAgents() {
16936
+ const credentials = loadCredentials();
16937
+ const apiKey = getApiKey();
16938
+ const token = apiKey || credentials?.token;
16939
+ if (!token) {
16940
+ return { success: false, error: "Not authenticated" };
16941
+ }
16942
+ const response = await fetch(`${CONVEX_URL}/api/mutation`, {
16943
+ method: "POST",
16944
+ headers: {
16945
+ "Content-Type": "application/json",
16946
+ Authorization: `Bearer ${token}`
16947
+ },
16948
+ body: JSON.stringify({
16949
+ path: "sync:deployAllAgents",
16950
+ args: {}
16951
+ })
16952
+ });
16953
+ if (!response.ok) {
16954
+ const error = await response.text();
16955
+ return { success: false, error };
16956
+ }
16957
+ const result = await response.json();
16958
+ return result;
16959
+ }
17057
16960
 
17058
16961
  // src/cli/commands/login.ts
17059
16962
  var AUTH_CALLBACK_PORT = 9876;
@@ -17240,40 +17143,50 @@ function loadProject(cwd) {
17240
17143
  return null;
17241
17144
  }
17242
17145
  }
17243
- function saveProject(cwd, project) {
17146
+ function loadProjectV2(cwd) {
17244
17147
  const projectPath = join2(cwd, PROJECT_FILE);
17245
- writeFileSync2(projectPath, JSON.stringify(project, null, 2) + `
17246
- `);
17148
+ if (!existsSync2(projectPath)) {
17149
+ return null;
17150
+ }
17151
+ try {
17152
+ const data = readFileSync2(projectPath, "utf-8");
17153
+ const parsed = JSON.parse(data);
17154
+ if (parsed.version === "2.0") {
17155
+ return parsed;
17156
+ }
17157
+ return null;
17158
+ } catch {
17159
+ return null;
17160
+ }
17247
17161
  }
17248
17162
  function hasProject(cwd) {
17249
17163
  return existsSync2(join2(cwd, PROJECT_FILE));
17250
17164
  }
17165
+ function getProjectVersion(cwd) {
17166
+ const projectPath = join2(cwd, PROJECT_FILE);
17167
+ if (!existsSync2(projectPath)) {
17168
+ return null;
17169
+ }
17170
+ try {
17171
+ const data = readFileSync2(projectPath, "utf-8");
17172
+ const parsed = JSON.parse(data);
17173
+ if (parsed.version === "2.0") {
17174
+ return "2.0";
17175
+ }
17176
+ if (parsed.agentId) {
17177
+ return "1.0";
17178
+ }
17179
+ return null;
17180
+ } catch {
17181
+ return null;
17182
+ }
17183
+ }
17251
17184
 
17252
17185
  // src/cli/utils/scaffold.ts
17253
17186
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync3, appendFileSync } from "fs";
17254
17187
  import { join as join3, dirname } from "path";
17255
17188
 
17256
17189
  // src/cli/templates/index.ts
17257
- function getPackageJson(name) {
17258
- return JSON.stringify({
17259
- name,
17260
- version: "0.1.0",
17261
- type: "module",
17262
- scripts: {
17263
- dev: "struere dev",
17264
- build: "struere build",
17265
- test: "struere test",
17266
- deploy: "struere deploy"
17267
- },
17268
- dependencies: {
17269
- struere: "^0.3.0"
17270
- },
17271
- devDependencies: {
17272
- "bun-types": "^1.0.0",
17273
- typescript: "^5.3.0"
17274
- }
17275
- }, null, 2);
17276
- }
17277
17190
  function getTsConfig() {
17278
17191
  return JSON.stringify({
17279
17192
  compilerOptions: {
@@ -17310,36 +17223,106 @@ export default defineConfig({
17310
17223
  })
17311
17224
  `;
17312
17225
  }
17313
- function getAgentTs(name) {
17314
- const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
17315
- return `import { defineAgent } from 'struere'
17316
- import { tools } from './tools'
17317
-
17318
- export default defineAgent({
17319
- name: '${name}',
17320
- version: '0.1.0',
17321
- description: '${displayName} Agent',
17322
- model: {
17323
- provider: 'anthropic',
17324
- name: 'claude-sonnet-4-20250514',
17325
- temperature: 0.7,
17326
- maxTokens: 4096,
17327
- },
17328
- systemPrompt: \`You are ${displayName}, a helpful AI assistant.
17226
+ function getEnvExample() {
17227
+ return `# Anthropic API Key (default provider)
17228
+ ANTHROPIC_API_KEY=your_api_key_here
17329
17229
 
17330
- Current time: {{datetime}}
17230
+ # Optional: OpenAI API Key (if using OpenAI models)
17231
+ # OPENAI_API_KEY=your_openai_api_key
17331
17232
 
17332
- Your capabilities:
17333
- - Answer questions accurately and helpfully
17334
- - Use available tools when appropriate
17335
- - Maintain conversation context
17233
+ # Optional: Google AI API Key (if using Gemini models)
17234
+ # GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key
17235
+
17236
+ # Optional: Custom Convex URL
17237
+ # STRUERE_CONVEX_URL=https://struere.convex.cloud
17238
+ `;
17239
+ }
17240
+ function getGitignore() {
17241
+ return `node_modules/
17242
+ dist/
17243
+ .env
17244
+ .env.local
17245
+ .env.*.local
17246
+ .idea/
17247
+ .vscode/
17248
+ *.swp
17249
+ *.swo
17250
+ .DS_Store
17251
+ Thumbs.db
17252
+ *.log
17253
+ logs/
17254
+ .vercel/
17255
+ `;
17256
+ }
17257
+ function getEntityTypeTs(name, slug) {
17258
+ const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
17259
+ return `import { defineEntityType } from 'struere'
17260
+
17261
+ export default defineEntityType({
17262
+ name: "${displayName}",
17263
+ slug: "${slug}",
17264
+ schema: {
17265
+ type: "object",
17266
+ properties: {
17267
+ name: { type: "string", description: "Name" },
17268
+ email: { type: "string", format: "email", description: "Email address" },
17269
+ status: { type: "string", enum: ["active", "inactive"], description: "Status" },
17270
+ },
17271
+ required: ["name"],
17272
+ },
17273
+ searchFields: ["name", "email"],
17274
+ })
17275
+ `;
17276
+ }
17277
+ function getRoleTs(name) {
17278
+ return `import { defineRole } from 'struere'
17279
+
17280
+ export default defineRole({
17281
+ name: "${name}",
17282
+ description: "${name.charAt(0).toUpperCase() + name.slice(1)} role",
17283
+ policies: [
17284
+ { resource: "*", actions: ["list", "read"], effect: "allow", priority: 50 },
17285
+ ],
17286
+ scopeRules: [],
17287
+ fieldMasks: [],
17288
+ })
17289
+ `;
17290
+ }
17291
+ function getAgentTsV2(name, slug) {
17292
+ const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
17293
+ return `import { defineAgent } from 'struere'
17294
+
17295
+ export default defineAgent({
17296
+ name: "${displayName}",
17297
+ slug: "${slug}",
17298
+ version: "0.1.0",
17299
+ description: "${displayName} Agent",
17300
+ model: {
17301
+ provider: "anthropic",
17302
+ name: "claude-sonnet-4-20250514",
17303
+ temperature: 0.7,
17304
+ maxTokens: 4096,
17305
+ },
17306
+ systemPrompt: \`You are ${displayName}, a helpful AI assistant.
17307
+
17308
+ Current time: {{datetime}}
17309
+
17310
+ Your capabilities:
17311
+ - Answer questions accurately and helpfully
17312
+ - Use available tools when appropriate
17313
+ - Maintain conversation context
17336
17314
 
17337
17315
  Always be concise, accurate, and helpful.\`,
17338
- tools,
17316
+ tools: ["entity.query", "entity.get", "event.emit"],
17339
17317
  })
17340
17318
  `;
17341
17319
  }
17342
- function getToolsTs() {
17320
+ function getIndexTs(type) {
17321
+ return `// Export all ${type} from this directory
17322
+ // Example: export { default as myAgent } from './my-agent'
17323
+ `;
17324
+ }
17325
+ function getToolsIndexTs() {
17343
17326
  return `import { defineTools } from 'struere'
17344
17327
 
17345
17328
  export const tools = defineTools([
@@ -17365,225 +17348,137 @@ export const tools = defineTools([
17365
17348
  }
17366
17349
  },
17367
17350
  },
17368
- {
17369
- name: 'calculate',
17370
- description: 'Perform a mathematical calculation',
17371
- parameters: {
17372
- type: 'object',
17373
- properties: {
17374
- expression: {
17375
- type: 'string',
17376
- description: 'Mathematical expression to evaluate (e.g., "2 + 2")',
17377
- },
17378
- },
17379
- required: ['expression'],
17380
- },
17381
- handler: async (params) => {
17382
- const expression = params.expression as string
17383
- const sanitized = expression.replace(/[^0-9+*/().\\s-]/g, '')
17384
- try {
17385
- const result = new Function(\`return \${sanitized}\`)()
17386
- return { expression, result }
17387
- } catch {
17388
- return { expression, error: 'Invalid expression' }
17389
- }
17390
- },
17391
- },
17392
17351
  ])
17393
- `;
17394
- }
17395
- function getBasicTestYaml() {
17396
- return `name: Basic conversation test
17397
- description: Verify the agent responds correctly to basic queries
17398
-
17399
- conversation:
17400
- - role: user
17401
- content: Hello, what can you do?
17402
- - role: assistant
17403
- assertions:
17404
- - type: contains
17405
- value: help
17406
-
17407
- - role: user
17408
- content: What time is it?
17409
- - role: assistant
17410
- assertions:
17411
- - type: toolCalled
17412
- value: get_current_time
17413
- `;
17414
- }
17415
- function getEnvExample() {
17416
- return `# Anthropic API Key (default provider)
17417
- ANTHROPIC_API_KEY=your_api_key_here
17418
-
17419
- # Optional: OpenAI API Key (if using OpenAI models)
17420
- # OPENAI_API_KEY=your_openai_api_key
17421
-
17422
- # Optional: Google AI API Key (if using Gemini models)
17423
- # GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key
17424
17352
 
17425
- # Optional: Custom Convex URL
17426
- # STRUERE_CONVEX_URL=https://struere.convex.cloud
17427
- `;
17428
- }
17429
- function getGitignore() {
17430
- return `node_modules/
17431
- dist/
17432
- .env
17433
- .env.local
17434
- .env.*.local
17435
- .idea/
17436
- .vscode/
17437
- *.swp
17438
- *.swo
17439
- .DS_Store
17440
- Thumbs.db
17441
- *.log
17442
- logs/
17443
- .vercel/
17353
+ export default tools
17444
17354
  `;
17445
17355
  }
17446
- function getStruereJson(agentId, team, slug, name) {
17356
+ function getStruereJsonV2(orgId, orgSlug, orgName) {
17447
17357
  return JSON.stringify({
17448
- agentId,
17449
- team,
17450
- agent: {
17451
- slug,
17452
- name
17358
+ version: "2.0",
17359
+ organization: {
17360
+ id: orgId,
17361
+ slug: orgSlug,
17362
+ name: orgName
17453
17363
  }
17454
17364
  }, null, 2);
17455
17365
  }
17456
- function getEnvLocal(deploymentUrl) {
17457
- return `STRUERE_DEPLOYMENT_URL=${deploymentUrl}
17458
- `;
17366
+ function getPackageJsonV2(name) {
17367
+ return JSON.stringify({
17368
+ name,
17369
+ version: "0.1.0",
17370
+ type: "module",
17371
+ scripts: {
17372
+ dev: "struere dev",
17373
+ build: "struere build",
17374
+ deploy: "struere deploy",
17375
+ status: "struere status"
17376
+ },
17377
+ dependencies: {
17378
+ struere: "^0.4.0"
17379
+ },
17380
+ devDependencies: {
17381
+ "bun-types": "^1.0.0",
17382
+ typescript: "^5.3.0"
17383
+ }
17384
+ }, null, 2);
17459
17385
  }
17460
- function getClaudeMD(name) {
17461
- const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
17462
- return `# ${displayName} Agent
17386
+ function getClaudeMDV2(orgName) {
17387
+ return `# ${orgName} - Struere Project
17463
17388
 
17464
- 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.
17389
+ 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.
17465
17390
 
17466
17391
  ## Project Structure
17467
17392
 
17468
17393
  \`\`\`
17469
- src/
17470
- \u251C\u2500\u2500 agent.ts # Agent definition (system prompt, model, tools)
17471
- \u251C\u2500\u2500 tools.ts # Custom tool definitions
17472
- \u2514\u2500\u2500 workflows/ # Multi-step workflow definitions
17473
- tests/
17474
- \u2514\u2500\u2500 *.test.yaml # YAML-based conversation tests
17475
- struere.json # Project configuration (agentId, team, slug)
17476
- struere.config.ts # Framework settings (port, CORS, logging)
17477
- \`\`\`
17394
+ agents/ # Agent definitions
17395
+ \u251C\u2500\u2500 scheduler.ts # Example agent
17396
+ \u2514\u2500\u2500 index.ts # Re-exports all agents
17478
17397
 
17479
- ## Agent Definition
17398
+ entity-types/ # Entity type schemas
17399
+ \u251C\u2500\u2500 teacher.ts # Example entity type
17400
+ \u2514\u2500\u2500 index.ts # Re-exports all entity types
17480
17401
 
17481
- Define your agent in \`src/agent.ts\`:
17402
+ roles/ # Role + permission definitions
17403
+ \u251C\u2500\u2500 admin.ts # Example role with policies
17404
+ \u2514\u2500\u2500 index.ts # Re-exports all roles
17482
17405
 
17483
- \`\`\`typescript
17484
- import { defineAgent } from 'struere'
17485
- import { tools } from './tools'
17486
-
17487
- export default defineAgent({
17488
- name: 'my-agent',
17489
- version: '0.1.0',
17490
- description: 'My AI Agent',
17491
- model: {
17492
- provider: 'anthropic',
17493
- name: 'claude-sonnet-4-20250514',
17494
- temperature: 0.7,
17495
- maxTokens: 4096,
17496
- },
17497
- systemPrompt: \\\`You are a helpful assistant.
17406
+ tools/ # Shared custom tools
17407
+ \u2514\u2500\u2500 index.ts # Custom tool definitions
17498
17408
 
17499
- Current time: {{datetime}}
17500
- Customer: {{entity.get({"id": "{{thread.metadata.customerId}}"})}}\\\`,
17501
- tools,
17502
- })
17409
+ struere.json # Organization configuration
17410
+ struere.config.ts # Framework settings
17503
17411
  \`\`\`
17504
17412
 
17505
- ## System Prompt Templates
17506
-
17507
- System prompts support dynamic \`{{...}}\` templates that are resolved at runtime before the LLM call.
17508
-
17509
- ### Available Variables
17510
-
17511
- | Variable | Description |
17512
- |----------|-------------|
17513
- | \`{{organizationId}}\` | Current organization ID |
17514
- | \`{{userId}}\` | Current user ID |
17515
- | \`{{threadId}}\` | Conversation thread ID |
17516
- | \`{{agentId}}\` | Agent ID |
17517
- | \`{{agent.name}}\` | Agent name |
17518
- | \`{{agent.slug}}\` | Agent slug |
17519
- | \`{{thread.metadata.X}}\` | Thread metadata field X |
17520
- | \`{{message}}\` | Current user message |
17521
- | \`{{timestamp}}\` | Unix timestamp (ms) |
17522
- | \`{{datetime}}\` | ISO 8601 datetime |
17523
-
17524
- ### Function Calls
17525
-
17526
- Call any agent tool directly in the system prompt:
17527
-
17528
- \`\`\`
17529
- {{entity.get({"id": "ent_123"})}}
17530
- {{entity.query({"type": "customer", "limit": 5})}}
17531
- {{event.query({"entityId": "ent_123", "limit": 10})}}
17532
- \`\`\`
17413
+ ## CLI Commands
17533
17414
 
17534
- ### Nested Templates
17415
+ | Command | Description |
17416
+ |---------|-------------|
17417
+ | \`struere dev\` | Watch and sync all resources to Convex |
17418
+ | \`struere deploy\` | Deploy all agents to production |
17419
+ | \`struere add <type> <name>\` | Scaffold new agent/entity-type/role |
17420
+ | \`struere status\` | Compare local vs remote state |
17535
17421
 
17536
- Variables can be used inside function arguments:
17422
+ ## Defining Resources
17537
17423
 
17538
- \`\`\`
17539
- {{entity.get({"id": "{{thread.metadata.customerId}}"})}}
17540
- \`\`\`
17424
+ ### Agents (\`agents/*.ts\`)
17541
17425
 
17542
- ### Error Handling
17426
+ \`\`\`typescript
17427
+ import { defineAgent } from 'struere'
17543
17428
 
17544
- Failed templates are replaced with inline errors:
17545
- \`\`\`
17546
- [TEMPLATE_ERROR: variableName not found]
17547
- [TEMPLATE_ERROR: toolName - error message]
17429
+ export default defineAgent({
17430
+ name: "Scheduler",
17431
+ slug: "scheduler",
17432
+ version: "0.1.0",
17433
+ systemPrompt: "You are a scheduling assistant...",
17434
+ model: { provider: "anthropic", name: "claude-sonnet-4-20250514" },
17435
+ tools: ["entity.create", "entity.query", "event.emit"],
17436
+ })
17548
17437
  \`\`\`
17549
17438
 
17550
- ## Custom Tools
17551
-
17552
- Define tools in \`src/tools.ts\`:
17439
+ ### Entity Types (\`entity-types/*.ts\`)
17553
17440
 
17554
17441
  \`\`\`typescript
17555
- import { defineTools } from 'struere'
17556
-
17557
- export const tools = defineTools([
17558
- {
17559
- name: 'search_products',
17560
- description: 'Search the product catalog',
17561
- parameters: {
17562
- type: 'object',
17563
- properties: {
17564
- query: { type: 'string', description: 'Search query' },
17565
- limit: { type: 'number', description: 'Max results' },
17566
- },
17567
- required: ['query'],
17568
- },
17569
- handler: async (params) => {
17570
- const results = await searchProducts(params.query, params.limit ?? 10)
17571
- return { products: results }
17442
+ import { defineEntityType } from 'struere'
17443
+
17444
+ export default defineEntityType({
17445
+ name: "Teacher",
17446
+ slug: "teacher",
17447
+ schema: {
17448
+ type: "object",
17449
+ properties: {
17450
+ name: { type: "string" },
17451
+ email: { type: "string", format: "email" },
17452
+ hourlyRate: { type: "number" },
17572
17453
  },
17454
+ required: ["name", "email"],
17573
17455
  },
17574
- ])
17456
+ searchFields: ["name", "email"],
17457
+ })
17575
17458
  \`\`\`
17576
17459
 
17577
- Custom tool handlers are executed in a sandboxed Cloudflare Worker environment. They can make HTTP requests to allowlisted domains:
17578
- - api.openai.com, api.anthropic.com, api.stripe.com
17579
- - api.sendgrid.com, api.twilio.com, hooks.slack.com
17580
- - discord.com, api.github.com
17581
-
17582
- ## Built-in Tools
17460
+ ### Roles (\`roles/*.ts\`)
17583
17461
 
17584
- Agents have access to these built-in tools for data management:
17462
+ \`\`\`typescript
17463
+ import { defineRole } from 'struere'
17464
+
17465
+ export default defineRole({
17466
+ name: "teacher",
17467
+ description: "Tutors who conduct sessions",
17468
+ policies: [
17469
+ { resource: "session", actions: ["list", "read", "update"], effect: "allow", priority: 50 },
17470
+ { resource: "payment", actions: ["*"], effect: "deny", priority: 100 },
17471
+ ],
17472
+ scopeRules: [
17473
+ { entityType: "session", field: "data.teacherId", operator: "eq", value: "actor.userId" },
17474
+ ],
17475
+ fieldMasks: [
17476
+ { entityType: "session", fieldPath: "data.paymentId", maskType: "hide" },
17477
+ ],
17478
+ })
17479
+ \`\`\`
17585
17480
 
17586
- ### Entity Tools
17481
+ ## Built-in Tools
17587
17482
 
17588
17483
  | Tool | Description |
17589
17484
  |------|-------------|
@@ -17594,159 +17489,18 @@ Agents have access to these built-in tools for data management:
17594
17489
  | \`entity.delete\` | Soft-delete entity |
17595
17490
  | \`entity.link\` | Create entity relation |
17596
17491
  | \`entity.unlink\` | Remove entity relation |
17597
-
17598
- Example entity operations:
17599
- \`\`\`json
17600
- // entity.create
17601
- { "type": "customer", "data": { "name": "John", "email": "john@example.com" } }
17602
-
17603
- // entity.query
17604
- { "type": "customer", "filters": { "status": "active" }, "limit": 10 }
17605
-
17606
- // entity.update
17607
- { "id": "ent_123", "data": { "status": "vip" } }
17608
- \`\`\`
17609
-
17610
- ### Event Tools
17611
-
17612
- | Tool | Description |
17613
- |------|-------------|
17614
- | \`event.emit\` | Emit a custom event |
17615
- | \`event.query\` | Query event history |
17616
-
17617
- Example event operations:
17618
- \`\`\`json
17619
- // event.emit
17620
- { "entityId": "ent_123", "eventType": "order.placed", "payload": { "amount": 99.99 } }
17621
-
17622
- // event.query
17623
- { "entityId": "ent_123", "eventType": "order.*", "limit": 20 }
17624
- \`\`\`
17625
-
17626
- ### Job Tools
17627
-
17628
- | Tool | Description |
17629
- |------|-------------|
17630
- | \`job.enqueue\` | Schedule a background job |
17492
+ | \`event.emit\` | Emit custom event |
17493
+ | \`event.query\` | Query events |
17494
+ | \`job.enqueue\` | Schedule background job |
17631
17495
  | \`job.status\` | Get job status |
17632
17496
 
17633
- Example job operations:
17634
- \`\`\`json
17635
- // job.enqueue
17636
- { "jobType": "send_email", "payload": { "to": "user@example.com" }, "scheduledFor": 1706745600000 }
17637
-
17638
- // job.status
17639
- { "id": "job_abc123" }
17640
- \`\`\`
17641
-
17642
- ## Testing
17643
-
17644
- Write YAML-based conversation tests in \`tests/\`:
17645
-
17646
- \`\`\`yaml
17647
- name: Order flow test
17648
- description: Test the complete order flow
17649
-
17650
- conversation:
17651
- - role: user
17652
- content: I want to order a pizza
17653
- - role: assistant
17654
- assertions:
17655
- - type: contains
17656
- value: size
17657
- - type: toolCalled
17658
- value: get_menu
17659
-
17660
- - role: user
17661
- content: Large pepperoni please
17662
- - role: assistant
17663
- assertions:
17664
- - type: toolCalled
17665
- value: entity.create
17666
- \`\`\`
17667
-
17668
- ### Assertion Types
17669
-
17670
- | Type | Description |
17671
- |------|-------------|
17672
- | \`contains\` | Response contains substring |
17673
- | \`matches\` | Response matches regex |
17674
- | \`toolCalled\` | Specific tool was called |
17675
- | \`noToolCalled\` | No tools were called |
17676
-
17677
- Run tests with:
17678
- \`\`\`bash
17679
- bun run test
17680
- \`\`\`
17681
-
17682
- ## CLI Commands
17683
-
17684
- | Command | Description |
17685
- |---------|-------------|
17686
- | \`struere dev\` | Start development mode (live sync to Convex) |
17687
- | \`struere build\` | Validate agent configuration |
17688
- | \`struere deploy\` | Deploy agent to production |
17689
- | \`struere test\` | Run YAML conversation tests |
17690
- | \`struere logs\` | View recent execution logs |
17691
- | \`struere state\` | Inspect conversation thread state |
17692
-
17693
- ## Thread Metadata
17694
-
17695
- Set thread metadata when creating conversations to provide context:
17696
-
17697
- \`\`\`typescript
17698
- // Via API
17699
- POST /v1/chat
17700
- {
17701
- "agentId": "agent_123",
17702
- "message": "Hello",
17703
- "metadata": {
17704
- "customerId": "ent_customer_456",
17705
- "channel": "web",
17706
- "language": "en"
17707
- }
17708
- }
17709
- \`\`\`
17710
-
17711
- Access in system prompt:
17712
- \`\`\`
17713
- Customer: {{entity.get({"id": "{{thread.metadata.customerId}}"})}}
17714
- Channel: {{thread.metadata.channel}}
17715
- \`\`\`
17716
-
17717
17497
  ## Development Workflow
17718
17498
 
17719
- 1. **Edit agent configuration** in \`src/agent.ts\`
17720
- 2. **Run \`bun run dev\`** to sync changes to Convex
17721
- 3. **Test via API** or dashboard chat interface
17722
- 4. **Write tests** in \`tests/*.test.yaml\`
17723
- 5. **Deploy** with \`bun run deploy\`
17724
-
17725
- ## API Endpoints
17726
-
17727
- | Endpoint | Method | Description |
17728
- |----------|--------|-------------|
17729
- | \`/v1/chat\` | POST | Chat by agent ID |
17730
- | \`/v1/agents/:slug/chat\` | POST | Chat by agent slug |
17731
-
17732
- Authentication: Bearer token (API key from dashboard)
17733
-
17734
- \`\`\`bash
17735
- curl -X POST https://your-deployment.convex.cloud/v1/chat \\
17736
- -H "Authorization: Bearer sk_live_..." \\
17737
- -H "Content-Type: application/json" \\
17738
- -d '{"agentId": "...", "message": "Hello"}'
17739
- \`\`\`
17740
-
17741
- ## Best Practices
17742
-
17743
- 1. **System Prompts**: Use templates for dynamic data instead of hardcoding
17744
- 2. **Tools**: Keep tool handlers focused and stateless
17745
- 3. **Entities**: Model your domain data as entity types
17746
- 4. **Events**: Emit events for audit trails and analytics
17747
- 5. **Jobs**: Use jobs for async operations (emails, notifications)
17748
- 6. **Testing**: Write tests for critical conversation flows
17749
- 7. **Thread Metadata**: Use metadata for user-specific personalization
17499
+ 1. Run \`struere dev\` to start watching for changes
17500
+ 2. Edit agents, entity types, or roles
17501
+ 3. Changes are automatically synced to Convex
17502
+ 4. Test via API or dashboard
17503
+ 5. Run \`struere deploy\` when ready for production
17750
17504
  `;
17751
17505
  }
17752
17506
 
@@ -17762,33 +17516,35 @@ function writeFile(cwd, relativePath, content) {
17762
17516
  ensureDir(fullPath);
17763
17517
  writeFileSync3(fullPath, content);
17764
17518
  }
17765
- function writeProjectConfig(cwd, options) {
17766
- const result = {
17767
- createdFiles: [],
17768
- updatedFiles: []
17769
- };
17770
- writeFile(cwd, "struere.json", getStruereJson(options.agentId, options.team, options.agentSlug, options.agentName));
17771
- result.createdFiles.push("struere.json");
17772
- writeFile(cwd, ".env.local", getEnvLocal(options.deploymentUrl));
17773
- result.createdFiles.push(".env.local");
17774
- updateGitignore(cwd, result);
17775
- return result;
17776
- }
17777
- function scaffoldAgentFiles(cwd, projectName) {
17519
+ function scaffoldProjectV2(cwd, options) {
17778
17520
  const result = {
17779
17521
  createdFiles: [],
17780
17522
  updatedFiles: []
17781
17523
  };
17524
+ const directories = [
17525
+ "agents",
17526
+ "entity-types",
17527
+ "roles",
17528
+ "tools"
17529
+ ];
17530
+ for (const dir of directories) {
17531
+ const dirPath = join3(cwd, dir);
17532
+ if (!existsSync3(dirPath)) {
17533
+ mkdirSync2(dirPath, { recursive: true });
17534
+ }
17535
+ }
17782
17536
  const files = {
17783
- "package.json": getPackageJson(projectName),
17537
+ "struere.json": getStruereJsonV2(options.orgId, options.orgSlug, options.orgName),
17538
+ "package.json": getPackageJsonV2(options.projectName),
17784
17539
  "tsconfig.json": getTsConfig(),
17785
17540
  "struere.config.ts": getStruereConfig(),
17786
- "src/agent.ts": getAgentTs(projectName),
17787
- "src/tools.ts": getToolsTs(),
17788
- "src/workflows/.gitkeep": "",
17789
- "tests/basic.test.yaml": getBasicTestYaml(),
17790
17541
  ".env.example": getEnvExample(),
17791
- "CLAUDE.md": getClaudeMD(projectName)
17542
+ ".gitignore": getGitignore(),
17543
+ "CLAUDE.md": getClaudeMDV2(options.orgName),
17544
+ "agents/index.ts": getIndexTs("agents"),
17545
+ "entity-types/index.ts": getIndexTs("entity-types"),
17546
+ "roles/index.ts": getIndexTs("roles"),
17547
+ "tools/index.ts": getToolsIndexTs()
17792
17548
  };
17793
17549
  for (const [relativePath, content] of Object.entries(files)) {
17794
17550
  const fullPath = join3(cwd, relativePath);
@@ -17798,57 +17554,85 @@ function scaffoldAgentFiles(cwd, projectName) {
17798
17554
  writeFile(cwd, relativePath, content);
17799
17555
  result.createdFiles.push(relativePath);
17800
17556
  }
17801
- updateGitignore(cwd, result);
17802
17557
  return result;
17803
17558
  }
17804
- function updateGitignore(cwd, result) {
17805
- const gitignorePath = join3(cwd, ".gitignore");
17806
- const linesToAdd = [".env.local"];
17807
- if (existsSync3(gitignorePath)) {
17808
- const content = readFileSync3(gitignorePath, "utf-8");
17809
- const lines = content.split(`
17810
- `);
17811
- const missingLines = linesToAdd.filter((line) => !lines.some((l) => l.trim() === line));
17812
- if (missingLines.length > 0) {
17813
- const toAppend = `
17814
- ` + missingLines.join(`
17815
- `) + `
17816
- `;
17817
- appendFileSync(gitignorePath, toAppend);
17818
- result.updatedFiles.push(".gitignore");
17819
- }
17820
- } else {
17821
- writeFile(cwd, ".gitignore", getGitignore());
17822
- result.createdFiles.push(".gitignore");
17559
+ function scaffoldAgent(cwd, name, slug) {
17560
+ const result = {
17561
+ createdFiles: [],
17562
+ updatedFiles: []
17563
+ };
17564
+ const agentsDir = join3(cwd, "agents");
17565
+ if (!existsSync3(agentsDir)) {
17566
+ mkdirSync2(agentsDir, { recursive: true });
17567
+ }
17568
+ const fileName = `${slug}.ts`;
17569
+ const filePath = join3(agentsDir, fileName);
17570
+ if (existsSync3(filePath)) {
17571
+ return result;
17823
17572
  }
17573
+ writeFileSync3(filePath, getAgentTsV2(name, slug));
17574
+ result.createdFiles.push(`agents/${fileName}`);
17575
+ return result;
17824
17576
  }
17825
- function hasAgentFiles(cwd) {
17826
- return existsSync3(join3(cwd, "src", "agent.ts"));
17577
+ function scaffoldEntityType(cwd, name, slug) {
17578
+ const result = {
17579
+ createdFiles: [],
17580
+ updatedFiles: []
17581
+ };
17582
+ const entityTypesDir = join3(cwd, "entity-types");
17583
+ if (!existsSync3(entityTypesDir)) {
17584
+ mkdirSync2(entityTypesDir, { recursive: true });
17585
+ }
17586
+ const fileName = `${slug}.ts`;
17587
+ const filePath = join3(entityTypesDir, fileName);
17588
+ if (existsSync3(filePath)) {
17589
+ return result;
17590
+ }
17591
+ writeFileSync3(filePath, getEntityTypeTs(name, slug));
17592
+ result.createdFiles.push(`entity-types/${fileName}`);
17593
+ return result;
17594
+ }
17595
+ function scaffoldRole(cwd, name) {
17596
+ const result = {
17597
+ createdFiles: [],
17598
+ updatedFiles: []
17599
+ };
17600
+ const rolesDir = join3(cwd, "roles");
17601
+ if (!existsSync3(rolesDir)) {
17602
+ mkdirSync2(rolesDir, { recursive: true });
17603
+ }
17604
+ const fileName = `${name}.ts`;
17605
+ const filePath = join3(rolesDir, fileName);
17606
+ if (existsSync3(filePath)) {
17607
+ return result;
17608
+ }
17609
+ writeFileSync3(filePath, getRoleTs(name));
17610
+ result.createdFiles.push(`roles/${fileName}`);
17611
+ return result;
17827
17612
  }
17828
17613
 
17829
17614
  // src/cli/commands/init.ts
17830
- var initCommand = new Command("init").description("Initialize a new Struere project").argument("[project-name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").action(async (projectNameArg, options) => {
17615
+ var initCommand = new Command("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) => {
17831
17616
  const cwd = process.cwd();
17832
17617
  const spinner = ora();
17833
17618
  console.log();
17834
17619
  console.log(source_default.bold("Struere CLI"));
17835
17620
  console.log();
17836
17621
  if (hasProject(cwd)) {
17837
- const existingProject = loadProject(cwd);
17838
- if (existingProject) {
17839
- console.log(source_default.yellow("This project is already initialized."));
17622
+ const version = getProjectVersion(cwd);
17623
+ if (version === "2.0") {
17624
+ console.log(source_default.yellow("This project is already initialized (v2.0)."));
17840
17625
  console.log();
17841
- console.log(source_default.gray(" Agent:"), source_default.cyan(existingProject.agent.name));
17842
- console.log(source_default.gray(" ID:"), source_default.gray(existingProject.agentId));
17843
- console.log(source_default.gray(" Team:"), source_default.cyan(existingProject.team));
17626
+ console.log(source_default.gray("Run"), source_default.cyan("struere dev"), source_default.gray("to start development"));
17844
17627
  console.log();
17845
- const shouldRelink = await promptYesNo("Would you like to relink to a different agent?");
17846
- if (!shouldRelink) {
17847
- console.log();
17848
- console.log(source_default.gray("Run"), source_default.cyan("struere dev"), source_default.gray("to start development"));
17849
- console.log();
17850
- return;
17851
- }
17628
+ return;
17629
+ } else if (version === "1.0") {
17630
+ console.log(source_default.yellow("This is a v1 agent-centric project."));
17631
+ console.log(source_default.yellow("The new CLI uses an organization-centric structure."));
17632
+ console.log();
17633
+ console.log(source_default.gray("Please create a new project directory for the v2 structure."));
17634
+ console.log();
17635
+ return;
17852
17636
  }
17853
17637
  }
17854
17638
  let credentials = loadCredentials();
@@ -17862,102 +17646,36 @@ var initCommand = new Command("init").description("Initialize a new Struere proj
17862
17646
  }
17863
17647
  } else {
17864
17648
  console.log(source_default.green("\u2713"), "Logged in as", source_default.cyan(credentials.user.name || credentials.user.email));
17649
+ console.log(source_default.gray(" Organization:"), source_default.cyan(credentials.organization.name));
17865
17650
  console.log();
17866
17651
  }
17652
+ if (!options.yes) {
17653
+ const confirmed = await promptYesNo(`Initialize project for organization "${credentials.organization.name}"?`);
17654
+ if (!confirmed) {
17655
+ console.log();
17656
+ console.log(source_default.gray("Cancelled"));
17657
+ return;
17658
+ }
17659
+ }
17867
17660
  let projectName = projectNameArg;
17868
17661
  if (!projectName) {
17869
- projectName = await deriveProjectName(cwd);
17662
+ projectName = slugify(basename(cwd));
17870
17663
  if (!options.yes) {
17871
- const confirmed = await promptText("Agent name:", projectName);
17664
+ const confirmed = await promptText("Project name:", projectName);
17872
17665
  projectName = confirmed || projectName;
17873
17666
  }
17874
17667
  }
17875
17668
  projectName = slugify(projectName);
17876
- spinner.start("Fetching agents");
17877
- const { agents: existingAgents, error: listError } = await listAgents();
17878
- if (listError) {
17879
- spinner.fail("Failed to fetch agents");
17880
- console.log();
17881
- console.log(source_default.gray("Run"), source_default.cyan("struere login"), source_default.gray("to re-authenticate"));
17882
- process.exit(1);
17883
- }
17884
- const agents = existingAgents.map((a) => ({ id: a._id, name: a.name, slug: a.slug }));
17885
- spinner.succeed(`Found ${agents.length} existing agent(s)`);
17886
- let selectedAgent = null;
17887
- let deploymentUrl = "";
17888
- if (agents.length > 0 && !options.yes) {
17889
- console.log();
17890
- const choice = await promptChoice("Create new agent or link existing?", [
17891
- { value: "new", label: "Create new agent" },
17892
- ...agents.map((a) => ({ value: a.id, label: `${a.name} (${a.slug})` }))
17893
- ]);
17894
- if (choice !== "new") {
17895
- selectedAgent = agents.find((a) => a.id === choice) || null;
17896
- }
17897
- }
17898
- if (!selectedAgent) {
17899
- const displayName2 = projectName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
17900
- spinner.start("Creating agent");
17901
- const { agentId, error: createError } = await createAgent({
17902
- name: displayName2,
17903
- slug: projectName,
17904
- description: `${displayName2} Agent`
17905
- });
17906
- if (createError || !agentId) {
17907
- spinner.fail("Failed to create agent");
17908
- console.log();
17909
- console.log(source_default.red("Error:"), createError || "Unknown error");
17910
- process.exit(1);
17911
- }
17912
- selectedAgent = { id: agentId, name: displayName2, slug: projectName };
17913
- deploymentUrl = `https://${projectName}-dev.struere.dev`;
17914
- spinner.succeed(`Created agent "${projectName}"`);
17915
- } else {
17916
- deploymentUrl = `https://${selectedAgent.slug}-dev.struere.dev`;
17917
- console.log();
17918
- console.log(source_default.green("\u2713"), `Linked to "${selectedAgent.name}"`);
17919
- }
17920
- saveProject(cwd, {
17921
- agentId: selectedAgent.id,
17922
- team: credentials.organization.slug,
17923
- agent: {
17924
- slug: selectedAgent.slug,
17925
- name: selectedAgent.name
17926
- }
17927
- });
17928
- console.log(source_default.green("\u2713"), "Created struere.json");
17929
- const configResult = writeProjectConfig(cwd, {
17669
+ spinner.start("Creating project structure");
17670
+ const scaffoldResult = scaffoldProjectV2(cwd, {
17930
17671
  projectName,
17931
- agentId: selectedAgent.id,
17932
- team: credentials.organization.slug,
17933
- agentSlug: selectedAgent.slug,
17934
- agentName: selectedAgent.name,
17935
- deploymentUrl
17672
+ orgId: credentials.organization.id,
17673
+ orgSlug: credentials.organization.slug,
17674
+ orgName: credentials.organization.name
17936
17675
  });
17937
- for (const file of configResult.createdFiles) {
17938
- if (file !== "struere.json") {
17939
- console.log(source_default.green("\u2713"), `Created ${file}`);
17940
- }
17941
- }
17942
- for (const file of configResult.updatedFiles) {
17943
- console.log(source_default.green("\u2713"), `Updated ${file}`);
17944
- }
17945
- if (!hasAgentFiles(cwd)) {
17946
- let shouldScaffold = options.yes;
17947
- if (!options.yes) {
17948
- console.log();
17949
- shouldScaffold = await promptYesNo("Scaffold starter files?");
17950
- }
17951
- if (shouldScaffold) {
17952
- const scaffoldResult = scaffoldAgentFiles(cwd, projectName);
17953
- console.log();
17954
- for (const file of scaffoldResult.createdFiles) {
17955
- console.log(source_default.green("\u2713"), `Created ${file}`);
17956
- }
17957
- for (const file of scaffoldResult.updatedFiles) {
17958
- console.log(source_default.green("\u2713"), `Updated ${file}`);
17959
- }
17960
- }
17676
+ spinner.succeed("Project structure created");
17677
+ for (const file of scaffoldResult.createdFiles) {
17678
+ console.log(source_default.green("\u2713"), `Created ${file}`);
17961
17679
  }
17962
17680
  console.log();
17963
17681
  spinner.start("Installing dependencies");
@@ -17972,46 +17690,20 @@ var initCommand = new Command("init").description("Initialize a new Struere proj
17972
17690
  spinner.warn("Could not install dependencies automatically");
17973
17691
  console.log(source_default.gray(" Run"), source_default.cyan("bun install"), source_default.gray("manually"));
17974
17692
  }
17975
- spinner.start("Syncing initial config to Convex");
17976
- const displayName = projectName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
17977
- const defaultConfig = {
17978
- name: displayName,
17979
- version: "0.1.0",
17980
- systemPrompt: `You are ${displayName}, a helpful AI assistant. You help users with their questions and tasks.`,
17981
- model: {
17982
- provider: "anthropic",
17983
- name: "claude-sonnet-4-20250514",
17984
- temperature: 0.7,
17985
- maxTokens: 4096
17986
- },
17987
- tools: []
17988
- };
17989
- const syncResult = await syncToConvex(selectedAgent.id, defaultConfig);
17990
- if (syncResult.success) {
17991
- spinner.succeed("Initial config synced");
17992
- } else {
17993
- spinner.warn("Could not sync initial config");
17994
- console.log(source_default.gray(" Run"), source_default.cyan("struere dev"), source_default.gray("to sync manually"));
17995
- }
17996
17693
  console.log();
17997
17694
  console.log(source_default.green("Success!"), "Project initialized");
17998
17695
  console.log();
17999
- console.log(source_default.gray("Next steps:"));
18000
- console.log(source_default.gray(" $"), source_default.cyan("struere dev"));
17696
+ console.log(source_default.gray("Project structure:"));
17697
+ console.log(source_default.gray(" agents/ "), source_default.cyan("Agent definitions"));
17698
+ console.log(source_default.gray(" entity-types/ "), source_default.cyan("Entity type schemas"));
17699
+ console.log(source_default.gray(" roles/ "), source_default.cyan("Role + permission definitions"));
17700
+ console.log(source_default.gray(" tools/ "), source_default.cyan("Shared custom tools"));
18001
17701
  console.log();
18002
- });
18003
- async function deriveProjectName(cwd) {
18004
- const packageJsonPath = join4(cwd, "package.json");
18005
- if (existsSync4(packageJsonPath)) {
18006
- try {
18007
- const pkg = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
18008
- if (pkg.name && typeof pkg.name === "string") {
18009
- return slugify(pkg.name);
18010
- }
18011
- } catch {}
18012
- }
18013
- return slugify(basename(cwd));
18014
- }
17702
+ console.log(source_default.gray("Next steps:"));
17703
+ console.log(source_default.gray(" 1."), source_default.cyan("struere add agent my-agent"), source_default.gray("- Create an agent"));
17704
+ console.log(source_default.gray(" 2."), source_default.cyan("struere dev"), source_default.gray("- Start development"));
17705
+ console.log();
17706
+ });
18015
17707
  function slugify(name) {
18016
17708
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
18017
17709
  }
@@ -18026,22 +17718,6 @@ async function promptText(message, defaultValue) {
18026
17718
  const answer = await readLine();
18027
17719
  return answer || defaultValue;
18028
17720
  }
18029
- async function promptChoice(message, choices) {
18030
- console.log(source_default.gray(message));
18031
- console.log();
18032
- for (let i = 0;i < choices.length; i++) {
18033
- const prefix = i === 0 ? source_default.cyan("\u276F") : source_default.gray(" ");
18034
- console.log(`${prefix} ${i + 1}. ${choices[i].label}`);
18035
- }
18036
- console.log();
18037
- process.stdout.write(source_default.gray("Enter choice (1-" + choices.length + "): "));
18038
- const answer = await readLine();
18039
- const num = parseInt(answer, 10);
18040
- if (num >= 1 && num <= choices.length) {
18041
- return choices[num - 1].value;
18042
- }
18043
- return choices[0].value;
18044
- }
18045
17721
  function readLine() {
18046
17722
  return new Promise((resolve) => {
18047
17723
  let buffer = "";
@@ -18065,112 +17741,275 @@ function readLine() {
18065
17741
 
18066
17742
  // src/cli/commands/dev.ts
18067
17743
  var import_chokidar = __toESM(require_chokidar(), 1);
18068
- import { join as join7, basename as basename2 } from "path";
17744
+ import { join as join5 } from "path";
18069
17745
  import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
18070
17746
 
18071
- // src/cli/utils/config.ts
18072
- import { join as join5 } from "path";
18073
- var defaultConfig = {
18074
- port: 3000,
18075
- host: "localhost",
18076
- cors: {
18077
- origins: ["http://localhost:3000"],
18078
- credentials: true
18079
- },
18080
- logging: {
18081
- level: "info",
18082
- format: "pretty"
18083
- },
18084
- auth: {
18085
- type: "none"
17747
+ // src/cli/utils/loader.ts
17748
+ import { existsSync as existsSync4, readdirSync } from "fs";
17749
+ import { join as join4 } from "path";
17750
+ async function loadAllResources(cwd) {
17751
+ const agents = await loadAllAgents(join4(cwd, "agents"));
17752
+ const entityTypes = await loadAllEntityTypes(join4(cwd, "entity-types"));
17753
+ const roles = await loadAllRoles(join4(cwd, "roles"));
17754
+ const customTools = await loadCustomTools(join4(cwd, "tools"));
17755
+ return { agents, entityTypes, roles, customTools };
17756
+ }
17757
+ async function loadAllAgents(dir) {
17758
+ if (!existsSync4(dir)) {
17759
+ return [];
17760
+ }
17761
+ const indexPath = join4(dir, "index.ts");
17762
+ if (existsSync4(indexPath)) {
17763
+ return loadFromIndex(indexPath);
17764
+ }
17765
+ return loadFromDirectory(dir);
17766
+ }
17767
+ async function loadAllEntityTypes(dir) {
17768
+ if (!existsSync4(dir)) {
17769
+ return [];
17770
+ }
17771
+ const indexPath = join4(dir, "index.ts");
17772
+ if (existsSync4(indexPath)) {
17773
+ return loadFromIndex(indexPath);
17774
+ }
17775
+ return loadFromDirectory(dir);
17776
+ }
17777
+ async function loadAllRoles(dir) {
17778
+ if (!existsSync4(dir)) {
17779
+ return [];
17780
+ }
17781
+ const indexPath = join4(dir, "index.ts");
17782
+ if (existsSync4(indexPath)) {
17783
+ return loadFromIndex(indexPath);
17784
+ }
17785
+ return loadFromDirectory(dir);
17786
+ }
17787
+ async function loadCustomTools(dir) {
17788
+ if (!existsSync4(dir)) {
17789
+ return [];
17790
+ }
17791
+ const indexPath = join4(dir, "index.ts");
17792
+ if (!existsSync4(indexPath)) {
17793
+ return [];
18086
17794
  }
18087
- };
18088
- async function loadConfig(cwd) {
18089
- const configPath = join5(cwd, "struere.config.ts");
18090
17795
  try {
18091
- const module = await import(configPath);
18092
- const config = module.default || module;
18093
- return {
18094
- ...defaultConfig,
18095
- ...config,
18096
- cors: {
18097
- ...defaultConfig.cors,
18098
- ...config.cors
18099
- },
18100
- logging: {
18101
- ...defaultConfig.logging,
18102
- ...config.logging
18103
- },
18104
- auth: {
18105
- ...defaultConfig.auth,
18106
- ...config.auth
18107
- }
18108
- };
17796
+ const module = await import(indexPath);
17797
+ if (Array.isArray(module.default)) {
17798
+ return module.default;
17799
+ }
17800
+ if (module.tools && Array.isArray(module.tools)) {
17801
+ return module.tools;
17802
+ }
17803
+ return [];
18109
17804
  } catch {
18110
- return defaultConfig;
17805
+ return [];
18111
17806
  }
18112
17807
  }
18113
-
18114
- // src/cli/utils/agent.ts
18115
- import { join as join6 } from "path";
18116
- async function loadAgent(cwd) {
18117
- const agentPath = join6(cwd, "src/agent.ts");
17808
+ async function loadFromIndex(indexPath) {
18118
17809
  try {
18119
- const module = await import(`${agentPath}?t=${Date.now()}`);
18120
- const agent = module.default || module;
18121
- if (!agent.name) {
18122
- throw new Error("Agent must have a name");
18123
- }
18124
- if (!agent.version) {
18125
- throw new Error("Agent must have a version");
17810
+ const module = await import(indexPath);
17811
+ if (Array.isArray(module.default)) {
17812
+ return module.default;
18126
17813
  }
18127
- if (!agent.systemPrompt) {
18128
- throw new Error("Agent must have a systemPrompt");
17814
+ const items = [];
17815
+ for (const key of Object.keys(module)) {
17816
+ if (key === "default")
17817
+ continue;
17818
+ const value = module[key];
17819
+ if (value && typeof value === "object" && !Array.isArray(value)) {
17820
+ items.push(value);
17821
+ }
18129
17822
  }
18130
- return agent;
17823
+ return items;
18131
17824
  } catch (error) {
18132
- if (error instanceof Error && error.message.includes("Cannot find module")) {
18133
- throw new Error(`Agent not found at ${agentPath}`);
17825
+ throw new Error(`Failed to load index at ${indexPath}: ${error instanceof Error ? error.message : String(error)}`);
17826
+ }
17827
+ }
17828
+ async function loadFromDirectory(dir) {
17829
+ const files = readdirSync(dir).filter((f) => f.endsWith(".ts") && f !== "index.ts" && !f.endsWith(".d.ts"));
17830
+ const items = [];
17831
+ for (const file of files) {
17832
+ const filePath = join4(dir, file);
17833
+ try {
17834
+ const module = await import(filePath);
17835
+ if (module.default) {
17836
+ items.push(module.default);
17837
+ }
17838
+ } catch (error) {
17839
+ throw new Error(`Failed to load ${file}: ${error instanceof Error ? error.message : String(error)}`);
18134
17840
  }
18135
- throw error;
18136
17841
  }
17842
+ return items;
17843
+ }
17844
+ function getResourceDirectories(cwd) {
17845
+ return {
17846
+ agents: join4(cwd, "agents"),
17847
+ entityTypes: join4(cwd, "entity-types"),
17848
+ roles: join4(cwd, "roles"),
17849
+ tools: join4(cwd, "tools")
17850
+ };
17851
+ }
17852
+
17853
+ // src/cli/utils/extractor.ts
17854
+ var BUILTIN_TOOLS = [
17855
+ "entity.create",
17856
+ "entity.get",
17857
+ "entity.query",
17858
+ "entity.update",
17859
+ "entity.delete",
17860
+ "entity.link",
17861
+ "entity.unlink",
17862
+ "event.emit",
17863
+ "event.query",
17864
+ "job.enqueue",
17865
+ "job.status"
17866
+ ];
17867
+ function extractSyncPayload(resources) {
17868
+ const customToolsMap = new Map;
17869
+ for (const tool of resources.customTools) {
17870
+ customToolsMap.set(tool.name, tool);
17871
+ }
17872
+ const agents = resources.agents.map((agent) => extractAgentPayload(agent, customToolsMap));
17873
+ const entityTypes = resources.entityTypes.map((et) => ({
17874
+ name: et.name,
17875
+ slug: et.slug,
17876
+ schema: et.schema,
17877
+ searchFields: et.searchFields,
17878
+ displayConfig: et.displayConfig
17879
+ }));
17880
+ const roles = resources.roles.map((role) => ({
17881
+ name: role.name,
17882
+ description: role.description,
17883
+ policies: role.policies.map((p) => ({
17884
+ resource: p.resource,
17885
+ actions: p.actions,
17886
+ effect: p.effect,
17887
+ priority: p.priority
17888
+ })),
17889
+ scopeRules: role.scopeRules?.map((sr) => ({
17890
+ entityType: sr.entityType,
17891
+ field: sr.field,
17892
+ operator: sr.operator,
17893
+ value: sr.value
17894
+ })),
17895
+ fieldMasks: role.fieldMasks?.map((fm) => ({
17896
+ entityType: fm.entityType,
17897
+ fieldPath: fm.fieldPath,
17898
+ maskType: fm.maskType,
17899
+ maskConfig: fm.maskConfig
17900
+ }))
17901
+ }));
17902
+ return { agents, entityTypes, roles };
17903
+ }
17904
+ function extractAgentPayload(agent, customToolsMap) {
17905
+ let systemPrompt;
17906
+ if (typeof agent.systemPrompt === "function") {
17907
+ const result = agent.systemPrompt();
17908
+ if (result instanceof Promise) {
17909
+ throw new Error("Async system prompts must be resolved before syncing");
17910
+ }
17911
+ systemPrompt = result;
17912
+ } else {
17913
+ systemPrompt = agent.systemPrompt;
17914
+ }
17915
+ const tools = (agent.tools || []).map((toolName) => {
17916
+ const isBuiltin = BUILTIN_TOOLS.includes(toolName);
17917
+ if (isBuiltin) {
17918
+ return {
17919
+ name: toolName,
17920
+ description: getBuiltinToolDescription(toolName),
17921
+ parameters: { type: "object", properties: {} },
17922
+ isBuiltin: true
17923
+ };
17924
+ }
17925
+ const customTool = customToolsMap.get(toolName);
17926
+ if (!customTool) {
17927
+ throw new Error(`Tool "${toolName}" not found in custom tools`);
17928
+ }
17929
+ return {
17930
+ name: customTool.name,
17931
+ description: customTool.description,
17932
+ parameters: customTool.parameters || { type: "object", properties: {} },
17933
+ handlerCode: extractHandlerCode(customTool.handler),
17934
+ isBuiltin: false
17935
+ };
17936
+ });
17937
+ return {
17938
+ name: agent.name,
17939
+ slug: agent.slug,
17940
+ version: agent.version,
17941
+ description: agent.description,
17942
+ systemPrompt,
17943
+ model: {
17944
+ provider: agent.model?.provider || "anthropic",
17945
+ name: agent.model?.name || "claude-sonnet-4-20250514",
17946
+ temperature: agent.model?.temperature,
17947
+ maxTokens: agent.model?.maxTokens
17948
+ },
17949
+ tools
17950
+ };
17951
+ }
17952
+ function getBuiltinToolDescription(name) {
17953
+ const descriptions = {
17954
+ "entity.create": "Create a new entity",
17955
+ "entity.get": "Get an entity by ID",
17956
+ "entity.query": "Query entities by type and filters",
17957
+ "entity.update": "Update an entity",
17958
+ "entity.delete": "Delete an entity",
17959
+ "entity.link": "Link two entities",
17960
+ "entity.unlink": "Unlink two entities",
17961
+ "event.emit": "Emit an event",
17962
+ "event.query": "Query events",
17963
+ "job.enqueue": "Schedule a background job",
17964
+ "job.status": "Get job status"
17965
+ };
17966
+ return descriptions[name] || name;
17967
+ }
17968
+ function extractHandlerCode(handler) {
17969
+ const code = handler.toString();
17970
+ const arrowMatch = code.match(/(?:async\s*)?\([^)]*\)\s*=>\s*\{?([\s\S]*)\}?$/);
17971
+ if (arrowMatch) {
17972
+ let body = arrowMatch[1].trim();
17973
+ if (body.startsWith("{") && body.endsWith("}")) {
17974
+ body = body.slice(1, -1).trim();
17975
+ }
17976
+ return body;
17977
+ }
17978
+ const funcMatch = code.match(/(?:async\s*)?function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
17979
+ if (funcMatch) {
17980
+ return funcMatch[1].trim();
17981
+ }
17982
+ return code;
18137
17983
  }
18138
17984
 
18139
17985
  // src/cli/commands/dev.ts
18140
- var devCommand = new Command("dev").description("Sync agent to development environment").action(async () => {
17986
+ var devCommand = new Command("dev").description("Sync all resources to development environment").action(async () => {
18141
17987
  const spinner = ora();
18142
17988
  const cwd = process.cwd();
18143
17989
  console.log();
18144
17990
  console.log(source_default.bold("Struere Dev"));
18145
17991
  console.log();
18146
- let project = loadProject(cwd);
18147
17992
  if (!hasProject(cwd)) {
18148
17993
  console.log(source_default.yellow("No struere.json found"));
18149
17994
  console.log();
18150
- const setupResult = await interactiveSetup(cwd);
18151
- if (!setupResult) {
18152
- process.exit(0);
18153
- }
18154
- project = setupResult;
17995
+ console.log(source_default.gray("Run"), source_default.cyan("struere init"), source_default.gray("to initialize this project"));
17996
+ console.log();
17997
+ process.exit(1);
17998
+ }
17999
+ const version = getProjectVersion(cwd);
18000
+ if (version === "1.0") {
18001
+ console.log(source_default.yellow("This is a v1 agent-centric project."));
18002
+ console.log(source_default.yellow("Please migrate to v2 structure or use an older CLI version."));
18003
+ console.log();
18004
+ process.exit(1);
18155
18005
  }
18156
- project = loadProject(cwd);
18006
+ const project = loadProjectV2(cwd);
18157
18007
  if (!project) {
18158
18008
  console.log(source_default.red("Failed to load struere.json"));
18159
18009
  process.exit(1);
18160
18010
  }
18161
- console.log(source_default.gray("Agent:"), source_default.cyan(project.agent.name));
18011
+ console.log(source_default.gray("Organization:"), source_default.cyan(project.organization.name));
18162
18012
  console.log();
18163
- spinner.start("Loading configuration");
18164
- await loadConfig(cwd);
18165
- spinner.succeed("Configuration loaded");
18166
- spinner.start("Loading agent");
18167
- let agent = await loadAgent(cwd);
18168
- spinner.succeed(`Agent "${agent.name}" loaded`);
18169
- const claudeMdPath = join7(cwd, "CLAUDE.md");
18170
- if (!existsSync5(claudeMdPath)) {
18171
- writeFileSync4(claudeMdPath, getClaudeMD(project.agent.slug));
18172
- console.log(source_default.green("\u2713"), "Created CLAUDE.md");
18173
- }
18174
18013
  let credentials = loadCredentials();
18175
18014
  const apiKey = getApiKey();
18176
18015
  if (!credentials && !apiKey) {
@@ -18182,11 +18021,20 @@ var devCommand = new Command("dev").description("Sync agent to development envir
18182
18021
  process.exit(1);
18183
18022
  }
18184
18023
  }
18185
- spinner.start("Syncing to Convex");
18024
+ const claudeMdPath = join5(cwd, "CLAUDE.md");
18025
+ if (!existsSync5(claudeMdPath)) {
18026
+ writeFileSync4(claudeMdPath, getClaudeMDV2(project.organization.name));
18027
+ console.log(source_default.green("\u2713"), "Created CLAUDE.md");
18028
+ }
18029
+ const isAuthError = (error) => {
18030
+ const message = error instanceof Error ? error.message : String(error);
18031
+ return message.includes("Unauthenticated") || message.includes("OIDC") || message.includes("token") || message.includes("expired");
18032
+ };
18186
18033
  const performSync = async () => {
18187
18034
  try {
18188
- const config = extractConfig(agent);
18189
- const result = await syncToConvex(project.agentId, config);
18035
+ const resources = await loadAllResources(cwd);
18036
+ const payload = extractSyncPayload(resources);
18037
+ const result = await syncOrganization(payload);
18190
18038
  if (!result.success) {
18191
18039
  throw new Error(result.error || "Sync failed");
18192
18040
  }
@@ -18195,10 +18043,16 @@ var devCommand = new Command("dev").description("Sync agent to development envir
18195
18043
  throw error;
18196
18044
  }
18197
18045
  };
18198
- const isAuthError = (error) => {
18199
- const message = error instanceof Error ? error.message : String(error);
18200
- return message.includes("Unauthenticated") || message.includes("OIDC") || message.includes("token") || message.includes("expired");
18201
- };
18046
+ spinner.start("Loading resources");
18047
+ try {
18048
+ const resources = await loadAllResources(cwd);
18049
+ spinner.succeed(`Loaded ${resources.agents.length} agents, ${resources.entityTypes.length} entity types, ${resources.roles.length} roles`);
18050
+ } catch (error) {
18051
+ spinner.fail("Failed to load resources");
18052
+ console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
18053
+ process.exit(1);
18054
+ }
18055
+ spinner.start("Syncing to Convex");
18202
18056
  try {
18203
18057
  await performSync();
18204
18058
  spinner.succeed("Synced to development");
@@ -18228,13 +18082,18 @@ var devCommand = new Command("dev").description("Sync agent to development envir
18228
18082
  process.exit(1);
18229
18083
  }
18230
18084
  }
18231
- const devUrl = `https://${project.agent.slug}-dev.struere.dev`;
18232
- console.log();
18233
- console.log(source_default.green("Development URL:"), source_default.cyan(devUrl));
18234
18085
  console.log();
18235
18086
  console.log(source_default.gray("Watching for changes... Press Ctrl+C to stop"));
18236
18087
  console.log();
18237
- const watcher = import_chokidar.default.watch([join7(cwd, "src"), join7(cwd, "struere.config.ts")], {
18088
+ const dirs = getResourceDirectories(cwd);
18089
+ const watchPaths = [
18090
+ dirs.agents,
18091
+ dirs.entityTypes,
18092
+ dirs.roles,
18093
+ dirs.tools,
18094
+ join5(cwd, "struere.config.ts")
18095
+ ].filter((p) => existsSync5(p));
18096
+ const watcher = import_chokidar.default.watch(watchPaths, {
18238
18097
  ignoreInitial: true,
18239
18098
  ignored: /node_modules/
18240
18099
  });
@@ -18243,7 +18102,6 @@ var devCommand = new Command("dev").description("Sync agent to development envir
18243
18102
  console.log(source_default.gray(`Changed: ${relativePath}`));
18244
18103
  const syncSpinner = ora("Syncing...").start();
18245
18104
  try {
18246
- agent = await loadAgent(cwd);
18247
18105
  await performSync();
18248
18106
  syncSpinner.succeed("Synced");
18249
18107
  } catch (error) {
@@ -18270,249 +18128,110 @@ var devCommand = new Command("dev").description("Sync agent to development envir
18270
18128
  console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
18271
18129
  }
18272
18130
  }
18273
- });
18274
- process.on("SIGINT", () => {
18275
- console.log();
18276
- watcher.close();
18277
- console.log(source_default.gray("Stopped"));
18278
- process.exit(0);
18279
- });
18280
- });
18281
- async function interactiveSetup(cwd) {
18282
- const spinner = ora();
18283
- let credentials = loadCredentials();
18284
- if (!credentials) {
18285
- console.log(source_default.gray("Authentication required"));
18286
- console.log();
18287
- credentials = await performLogin();
18288
- if (!credentials) {
18289
- console.log(source_default.red("Authentication failed"));
18290
- return null;
18291
- }
18292
- } else {
18293
- console.log(source_default.green("\u2713"), "Logged in as", source_default.cyan(credentials.user.name));
18294
- console.log();
18295
- }
18296
- spinner.start("Fetching agents");
18297
- let { agents: existingAgents, error: listError } = await listAgents();
18298
- if (listError) {
18299
- const isAuthError = listError.includes("Unauthenticated") || listError.includes("OIDC") || listError.includes("token") || listError.includes("expired");
18300
- if (isAuthError) {
18301
- spinner.fail("Session expired");
18302
- console.log();
18303
- console.log(source_default.gray("Re-authenticating..."));
18304
- clearCredentials();
18305
- credentials = await performLogin();
18306
- if (!credentials) {
18307
- console.log(source_default.red("Authentication failed"));
18308
- return null;
18309
- }
18310
- spinner.start("Fetching agents");
18311
- const retryResult = await listAgents();
18312
- existingAgents = retryResult.agents;
18313
- listError = retryResult.error;
18314
- }
18315
- if (listError) {
18316
- spinner.fail("Failed to fetch agents");
18317
- console.log();
18318
- console.log(source_default.red("Error:"), listError);
18319
- return null;
18320
- }
18321
- }
18322
- const agents = existingAgents.map((a) => ({ id: a._id, name: a.name, slug: a.slug }));
18323
- spinner.succeed(`Found ${agents.length} existing agent(s)`);
18324
- let selectedAgent = null;
18325
- if (agents.length === 0) {
18326
- console.log(source_default.gray("No existing agents found. Creating a new one..."));
18327
- } else {
18328
- console.log();
18329
- const choices = [
18330
- { value: "link", label: "Link to an existing agent" },
18331
- { value: "create", label: "Create a new agent" },
18332
- { value: "cancel", label: "Cancel" }
18333
- ];
18334
- const action = await promptChoiceArrows("No agent configured. Would you like to:", choices);
18335
- if (action === "cancel") {
18336
- console.log();
18337
- console.log(source_default.gray("Run"), source_default.cyan("struere init"), source_default.gray("when ready to set up"));
18338
- return null;
18339
- }
18340
- if (action === "link") {
18341
- console.log();
18342
- const agentChoices = agents.map((a) => ({ value: a.id, label: `${a.name} (${a.slug})` }));
18343
- const agentId = await promptChoiceArrows("Select an agent:", agentChoices);
18344
- selectedAgent = agents.find((a) => a.id === agentId) || null;
18345
- }
18346
- }
18347
- if (!selectedAgent) {
18348
- console.log();
18349
- const projectName = slugify2(basename2(cwd));
18350
- const name = await promptText2("Agent name:", projectName);
18351
- const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
18352
- spinner.start("Creating agent");
18353
- let { agentId, error: createError } = await createAgent({
18354
- name: displayName,
18355
- slug: name,
18356
- description: `${displayName} Agent`
18357
- });
18358
- if (createError) {
18359
- const isAuthError = createError.includes("Unauthenticated") || createError.includes("OIDC") || createError.includes("token") || createError.includes("expired");
18360
- if (isAuthError) {
18361
- spinner.fail("Session expired");
18362
- console.log();
18363
- console.log(source_default.gray("Re-authenticating..."));
18364
- clearCredentials();
18365
- credentials = await performLogin();
18366
- if (!credentials) {
18367
- console.log(source_default.red("Authentication failed"));
18368
- return null;
18369
- }
18370
- spinner.start("Creating agent");
18371
- const retryResult = await createAgent({
18372
- name: displayName,
18373
- slug: name,
18374
- description: `${displayName} Agent`
18375
- });
18376
- agentId = retryResult.agentId;
18377
- createError = retryResult.error;
18378
- }
18379
- }
18380
- if (createError || !agentId) {
18381
- spinner.fail("Failed to create agent");
18382
- console.log();
18383
- console.log(source_default.red("Error:"), createError || "Unknown error");
18384
- return null;
18385
- }
18386
- selectedAgent = { id: agentId, name: displayName, slug: name };
18387
- spinner.succeed(`Created agent "${name}"`);
18388
- }
18389
- if (!selectedAgent) {
18390
- return null;
18391
- }
18392
- const projectData = {
18393
- agentId: selectedAgent.id,
18394
- team: credentials.organization.slug,
18395
- agent: {
18396
- slug: selectedAgent.slug,
18397
- name: selectedAgent.name
18398
- }
18399
- };
18400
- saveProject(cwd, projectData);
18401
- console.log(source_default.green("\u2713"), "Created struere.json");
18402
- if (!hasAgentFiles(cwd)) {
18403
- const scaffoldResult = scaffoldAgentFiles(cwd, selectedAgent.slug);
18404
- for (const file of scaffoldResult.createdFiles) {
18405
- console.log(source_default.green("\u2713"), `Created ${file}`);
18406
- }
18407
- console.log();
18408
- spinner.start("Installing dependencies");
18409
- try {
18410
- const proc = Bun.spawn(["bun", "install"], {
18411
- cwd,
18412
- stdout: "pipe",
18413
- stderr: "pipe"
18414
- });
18415
- await proc.exited;
18416
- if (proc.exitCode === 0) {
18417
- spinner.succeed("Dependencies installed");
18418
- } else {
18419
- spinner.fail("Failed to install dependencies");
18420
- console.log(source_default.yellow("Run"), source_default.cyan("bun install"), source_default.yellow("manually"));
18421
- }
18422
- } catch {
18423
- spinner.fail("Failed to install dependencies");
18424
- console.log(source_default.yellow("Run"), source_default.cyan("bun install"), source_default.yellow("manually"));
18425
- }
18426
- }
18427
- console.log();
18428
- return projectData;
18429
- }
18430
- function slugify2(name) {
18431
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
18432
- }
18433
- async function promptChoiceArrows(message, choices) {
18434
- return new Promise((resolve) => {
18435
- let selectedIndex = 0;
18436
- const render = () => {
18437
- process.stdout.write("\x1B[?25l");
18438
- process.stdout.write(`\x1B[${choices.length + 2}A`);
18439
- console.log(source_default.gray(message));
18440
- console.log();
18441
- for (let i = 0;i < choices.length; i++) {
18442
- const prefix = i === selectedIndex ? source_default.cyan("\u276F") : " ";
18443
- const label = i === selectedIndex ? source_default.cyan(choices[i].label) : choices[i].label;
18444
- console.log(`${prefix} ${label}`);
18445
- }
18446
- };
18447
- console.log(source_default.gray(message));
18448
- console.log();
18449
- for (let i = 0;i < choices.length; i++) {
18450
- const prefix = i === selectedIndex ? source_default.cyan("\u276F") : " ";
18451
- const label = i === selectedIndex ? source_default.cyan(choices[i].label) : choices[i].label;
18452
- console.log(`${prefix} ${label}`);
18453
- }
18454
- if (!process.stdin.isTTY) {
18455
- resolve(choices[0].value);
18456
- return;
18131
+ });
18132
+ watcher.on("add", async (path) => {
18133
+ const relativePath = path.replace(cwd, ".");
18134
+ console.log(source_default.gray(`Added: ${relativePath}`));
18135
+ const syncSpinner = ora("Syncing...").start();
18136
+ try {
18137
+ await performSync();
18138
+ syncSpinner.succeed("Synced");
18139
+ } catch (error) {
18140
+ syncSpinner.fail("Sync failed");
18141
+ console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
18457
18142
  }
18458
- process.stdin.setRawMode?.(true);
18459
- process.stdin.resume();
18460
- const onKeypress = (key) => {
18461
- const char = key.toString();
18462
- if (char === "\x1B[A" || char === "k") {
18463
- selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
18464
- render();
18465
- } else if (char === "\x1B[B" || char === "j") {
18466
- selectedIndex = (selectedIndex + 1) % choices.length;
18467
- render();
18468
- } else if (char === "\r" || char === `
18469
- `) {
18470
- process.stdin.removeListener("data", onKeypress);
18471
- process.stdin.setRawMode?.(false);
18472
- process.stdin.pause();
18473
- process.stdout.write("\x1B[?25h");
18474
- resolve(choices[selectedIndex].value);
18475
- } else if (char === "\x03") {
18476
- process.stdin.removeListener("data", onKeypress);
18477
- process.stdin.setRawMode?.(false);
18478
- process.stdout.write("\x1B[?25h");
18479
- process.exit(0);
18480
- }
18481
- };
18482
- process.stdin.on("data", onKeypress);
18483
18143
  });
18484
- }
18485
- async function promptText2(message, defaultValue) {
18486
- process.stdout.write(source_default.gray(`${message} `));
18487
- process.stdout.write(source_default.cyan(`(${defaultValue}) `));
18488
- const answer = await readLine2();
18489
- return answer || defaultValue;
18490
- }
18491
- function readLine2() {
18492
- return new Promise((resolve) => {
18493
- let buffer = "";
18494
- const onData = (chunk) => {
18495
- const str = chunk.toString();
18496
- buffer += str;
18497
- if (str.includes(`
18498
- `) || str.includes("\r")) {
18499
- process.stdin.removeListener("data", onData);
18500
- process.stdin.pause();
18501
- process.stdin.setRawMode?.(false);
18502
- resolve(buffer.replace(/[\r\n]/g, "").trim());
18503
- }
18504
- };
18505
- if (process.stdin.isTTY) {
18506
- process.stdin.setRawMode?.(false);
18144
+ watcher.on("unlink", async (path) => {
18145
+ const relativePath = path.replace(cwd, ".");
18146
+ console.log(source_default.gray(`Removed: ${relativePath}`));
18147
+ const syncSpinner = ora("Syncing...").start();
18148
+ try {
18149
+ await performSync();
18150
+ syncSpinner.succeed("Synced");
18151
+ } catch (error) {
18152
+ syncSpinner.fail("Sync failed");
18153
+ console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
18507
18154
  }
18508
- process.stdin.resume();
18509
- process.stdin.on("data", onData);
18510
18155
  });
18511
- }
18156
+ process.on("SIGINT", () => {
18157
+ console.log();
18158
+ watcher.close();
18159
+ console.log(source_default.gray("Stopped"));
18160
+ process.exit(0);
18161
+ });
18162
+ });
18512
18163
 
18513
18164
  // src/cli/commands/build.ts
18514
18165
  import { join as join8 } from "path";
18515
18166
 
18167
+ // src/cli/utils/config.ts
18168
+ import { join as join6 } from "path";
18169
+ var defaultConfig = {
18170
+ port: 3000,
18171
+ host: "localhost",
18172
+ cors: {
18173
+ origins: ["http://localhost:3000"],
18174
+ credentials: true
18175
+ },
18176
+ logging: {
18177
+ level: "info",
18178
+ format: "pretty"
18179
+ },
18180
+ auth: {
18181
+ type: "none"
18182
+ }
18183
+ };
18184
+ async function loadConfig(cwd) {
18185
+ const configPath = join6(cwd, "struere.config.ts");
18186
+ try {
18187
+ const module = await import(configPath);
18188
+ const config = module.default || module;
18189
+ return {
18190
+ ...defaultConfig,
18191
+ ...config,
18192
+ cors: {
18193
+ ...defaultConfig.cors,
18194
+ ...config.cors
18195
+ },
18196
+ logging: {
18197
+ ...defaultConfig.logging,
18198
+ ...config.logging
18199
+ },
18200
+ auth: {
18201
+ ...defaultConfig.auth,
18202
+ ...config.auth
18203
+ }
18204
+ };
18205
+ } catch {
18206
+ return defaultConfig;
18207
+ }
18208
+ }
18209
+
18210
+ // src/cli/utils/agent.ts
18211
+ import { join as join7 } from "path";
18212
+ async function loadAgent(cwd) {
18213
+ const agentPath = join7(cwd, "src/agent.ts");
18214
+ try {
18215
+ const module = await import(`${agentPath}?t=${Date.now()}`);
18216
+ const agent = module.default || module;
18217
+ if (!agent.name) {
18218
+ throw new Error("Agent must have a name");
18219
+ }
18220
+ if (!agent.version) {
18221
+ throw new Error("Agent must have a version");
18222
+ }
18223
+ if (!agent.systemPrompt) {
18224
+ throw new Error("Agent must have a systemPrompt");
18225
+ }
18226
+ return agent;
18227
+ } catch (error) {
18228
+ if (error instanceof Error && error.message.includes("Cannot find module")) {
18229
+ throw new Error(`Agent not found at ${agentPath}`);
18230
+ }
18231
+ throw error;
18232
+ }
18233
+ }
18234
+
18516
18235
  // src/cli/utils/validate.ts
18517
18236
  function validateAgent(agent) {
18518
18237
  const errors = [];
@@ -18827,12 +18546,11 @@ function formatAssertionError(assertion, context) {
18827
18546
  }
18828
18547
 
18829
18548
  // src/cli/commands/deploy.ts
18830
- var deployCommand = new Command("deploy").description("Deploy agent to production").option("--dry-run", "Show what would be deployed without deploying").action(async (options) => {
18831
- const environment = "production";
18549
+ var deployCommand = new Command("deploy").description("Deploy all agents to production").option("--dry-run", "Show what would be deployed without deploying").action(async (options) => {
18832
18550
  const spinner = ora();
18833
18551
  const cwd = process.cwd();
18834
18552
  console.log();
18835
- console.log(source_default.bold("Deploying Agent"));
18553
+ console.log(source_default.bold("Deploying Agents"));
18836
18554
  console.log();
18837
18555
  if (!hasProject(cwd)) {
18838
18556
  console.log(source_default.yellow("No struere.json found"));
@@ -18841,59 +18559,73 @@ var deployCommand = new Command("deploy").description("Deploy agent to productio
18841
18559
  console.log();
18842
18560
  process.exit(1);
18843
18561
  }
18844
- const project = loadProject(cwd);
18562
+ const version = getProjectVersion(cwd);
18563
+ if (version === "1.0") {
18564
+ console.log(source_default.yellow("This is a v1 agent-centric project."));
18565
+ console.log(source_default.yellow("Please migrate to v2 structure or use an older CLI version."));
18566
+ console.log();
18567
+ process.exit(1);
18568
+ }
18569
+ const project = loadProjectV2(cwd);
18845
18570
  if (!project) {
18846
18571
  console.log(source_default.red("Failed to load struere.json"));
18847
18572
  process.exit(1);
18848
18573
  }
18849
- console.log(source_default.gray("Agent:"), source_default.cyan(project.agent.name));
18574
+ console.log(source_default.gray("Organization:"), source_default.cyan(project.organization.name));
18850
18575
  console.log();
18851
- spinner.start("Loading configuration");
18852
- await loadConfig(cwd);
18853
- spinner.succeed("Configuration loaded");
18854
- spinner.start("Loading agent");
18855
- const agent = await loadAgent(cwd);
18856
- spinner.succeed(`Agent "${agent.name}" loaded`);
18857
- spinner.start("Validating agent");
18858
- const errors = validateAgent(agent);
18859
- if (errors.length > 0) {
18860
- spinner.fail("Validation failed");
18576
+ const credentials = loadCredentials();
18577
+ const apiKey = getApiKey();
18578
+ if (!credentials && !apiKey) {
18579
+ console.log(source_default.red("Not authenticated"));
18861
18580
  console.log();
18862
- for (const error of errors) {
18863
- console.log(source_default.red(" x"), error);
18864
- }
18581
+ console.log(source_default.gray("Run"), source_default.cyan("struere login"), source_default.gray("to authenticate"));
18582
+ console.log(source_default.gray("Or set"), source_default.cyan("STRUERE_API_KEY"), source_default.gray("environment variable"));
18865
18583
  console.log();
18866
18584
  process.exit(1);
18867
18585
  }
18868
- spinner.succeed("Agent validated");
18586
+ spinner.start("Loading resources");
18587
+ let resources;
18588
+ try {
18589
+ resources = await loadAllResources(cwd);
18590
+ spinner.succeed(`Loaded ${resources.agents.length} agents, ${resources.entityTypes.length} entity types, ${resources.roles.length} roles`);
18591
+ } catch (error) {
18592
+ spinner.fail("Failed to load resources");
18593
+ console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
18594
+ process.exit(1);
18595
+ }
18596
+ if (resources.agents.length === 0) {
18597
+ console.log();
18598
+ console.log(source_default.yellow("No agents found to deploy"));
18599
+ console.log();
18600
+ console.log(source_default.gray("Run"), source_default.cyan("struere add agent my-agent"), source_default.gray("to create an agent"));
18601
+ console.log();
18602
+ return;
18603
+ }
18869
18604
  if (options.dryRun) {
18870
18605
  console.log();
18871
18606
  console.log(source_default.yellow("Dry run mode - no changes will be made"));
18872
18607
  console.log();
18873
18608
  console.log("Would deploy:");
18874
- console.log(source_default.gray(" -"), `Agent: ${source_default.cyan(agent.name)}`);
18875
- console.log(source_default.gray(" -"), `Version: ${source_default.cyan(agent.version)}`);
18876
- console.log(source_default.gray(" -"), `Environment: ${source_default.cyan(environment)}`);
18877
- console.log(source_default.gray(" -"), `Agent ID: ${source_default.cyan(project.agentId)}`);
18609
+ for (const agent of resources.agents) {
18610
+ console.log(source_default.gray(" -"), `${source_default.cyan(agent.name)} (${agent.slug}) v${agent.version}`);
18611
+ }
18878
18612
  console.log();
18879
- return;
18880
- }
18881
- const credentials = loadCredentials();
18882
- const apiKey = getApiKey();
18883
- if (!credentials && !apiKey) {
18884
- spinner.fail("Not authenticated");
18613
+ console.log("Entity types:");
18614
+ for (const et of resources.entityTypes) {
18615
+ console.log(source_default.gray(" -"), source_default.cyan(et.name), `(${et.slug})`);
18616
+ }
18885
18617
  console.log();
18886
- console.log(source_default.gray("Run"), source_default.cyan("struere login"), source_default.gray("to authenticate"));
18887
- console.log(source_default.gray("Or set"), source_default.cyan("STRUERE_API_KEY"), source_default.gray("environment variable"));
18618
+ console.log("Roles:");
18619
+ for (const role of resources.roles) {
18620
+ console.log(source_default.gray(" -"), source_default.cyan(role.name));
18621
+ }
18888
18622
  console.log();
18889
- process.exit(1);
18623
+ return;
18890
18624
  }
18891
- spinner.start("Extracting agent configuration");
18892
- const config = extractConfig(agent);
18893
- spinner.succeed("Configuration extracted");
18894
18625
  spinner.start("Syncing to development");
18895
18626
  try {
18896
- const syncResult = await syncToConvex(project.agentId, config);
18627
+ const payload = extractSyncPayload(resources);
18628
+ const syncResult = await syncOrganization(payload);
18897
18629
  if (!syncResult.success) {
18898
18630
  throw new Error(syncResult.error || "Sync failed");
18899
18631
  }
@@ -18903,24 +18635,34 @@ var deployCommand = new Command("deploy").description("Deploy agent to productio
18903
18635
  console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
18904
18636
  process.exit(1);
18905
18637
  }
18906
- spinner.start(`Deploying to ${environment}`);
18638
+ spinner.start("Deploying to production");
18907
18639
  try {
18908
- const deployResult = await deployToProduction(project.agentId);
18640
+ const deployResult = await deployAllAgents();
18909
18641
  if (!deployResult.success) {
18910
18642
  throw new Error(deployResult.error || "Deployment failed");
18911
18643
  }
18912
- spinner.succeed(`Deployed to ${environment}`);
18913
- const prodUrl = `https://${project.agent.slug}.struere.dev`;
18644
+ spinner.succeed("Deployed to production");
18914
18645
  console.log();
18915
- console.log(source_default.green("Success!"), "Agent deployed");
18646
+ console.log(source_default.green("Success!"), "All agents deployed");
18916
18647
  console.log();
18917
- console.log("Deployment details:");
18918
- console.log(source_default.gray(" -"), `Version: ${source_default.cyan(agent.version)}`);
18919
- console.log(source_default.gray(" -"), `Environment: ${source_default.cyan(environment)}`);
18920
- console.log(source_default.gray(" -"), `URL: ${source_default.cyan(prodUrl)}`);
18648
+ if (deployResult.deployed && deployResult.deployed.length > 0) {
18649
+ console.log("Deployed agents:");
18650
+ for (const slug of deployResult.deployed) {
18651
+ const agent = resources.agents.find((a) => a.slug === slug);
18652
+ const prodUrl = `https://${slug}.struere.dev`;
18653
+ console.log(source_default.gray(" -"), source_default.cyan(agent?.name || slug), source_default.gray(`\u2192 ${prodUrl}`));
18654
+ }
18655
+ }
18656
+ if (deployResult.skipped && deployResult.skipped.length > 0) {
18657
+ console.log();
18658
+ console.log(source_default.yellow("Skipped (no development config):"));
18659
+ for (const slug of deployResult.skipped) {
18660
+ console.log(source_default.gray(" -"), slug);
18661
+ }
18662
+ }
18921
18663
  console.log();
18922
- console.log(source_default.gray("Test your agent:"));
18923
- console.log(source_default.gray(" $"), source_default.cyan(`curl -X POST ${prodUrl}/chat -H "Authorization: Bearer YOUR_API_KEY" -d '{"message": "Hello"}'`));
18664
+ console.log(source_default.gray("Test your agents:"));
18665
+ console.log(source_default.gray(" $"), source_default.cyan(`curl -X POST https://<agent-slug>.struere.dev/chat -H "Authorization: Bearer YOUR_API_KEY" -d '{"message": "Hello"}'`));
18924
18666
  console.log();
18925
18667
  } catch (error) {
18926
18668
  spinner.fail("Deployment failed");
@@ -19144,10 +18886,218 @@ var whoamiCommand = new Command("whoami").description("Show current logged in us
19144
18886
  console.log();
19145
18887
  }
19146
18888
  });
18889
+
18890
+ // src/cli/commands/add.ts
18891
+ var addCommand = new Command("add").description("Scaffold a new resource").argument("<type>", "Resource type: agent, entity-type, or role").argument("<name>", "Resource name").action(async (type, name) => {
18892
+ const cwd = process.cwd();
18893
+ console.log();
18894
+ if (!hasProject(cwd)) {
18895
+ console.log(source_default.yellow("No struere.json found"));
18896
+ console.log();
18897
+ console.log(source_default.gray("Run"), source_default.cyan("struere init"), source_default.gray("to initialize this project"));
18898
+ console.log();
18899
+ process.exit(1);
18900
+ }
18901
+ const version = getProjectVersion(cwd);
18902
+ if (version === "1.0") {
18903
+ console.log(source_default.yellow("This is a v1 agent-centric project."));
18904
+ console.log(source_default.yellow("The add command requires v2 structure."));
18905
+ console.log();
18906
+ process.exit(1);
18907
+ }
18908
+ const slug = slugify2(name);
18909
+ const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
18910
+ let result;
18911
+ switch (type.toLowerCase()) {
18912
+ case "agent":
18913
+ result = scaffoldAgent(cwd, displayName, slug);
18914
+ if (result.createdFiles.length > 0) {
18915
+ console.log(source_default.green("\u2713"), `Created agent "${displayName}"`);
18916
+ for (const file of result.createdFiles) {
18917
+ console.log(source_default.gray(" \u2192"), file);
18918
+ }
18919
+ } else {
18920
+ console.log(source_default.yellow("Agent already exists:"), `agents/${slug}.ts`);
18921
+ }
18922
+ break;
18923
+ case "entity-type":
18924
+ case "entitytype":
18925
+ case "type":
18926
+ result = scaffoldEntityType(cwd, displayName, slug);
18927
+ if (result.createdFiles.length > 0) {
18928
+ console.log(source_default.green("\u2713"), `Created entity type "${displayName}"`);
18929
+ for (const file of result.createdFiles) {
18930
+ console.log(source_default.gray(" \u2192"), file);
18931
+ }
18932
+ } else {
18933
+ console.log(source_default.yellow("Entity type already exists:"), `entity-types/${slug}.ts`);
18934
+ }
18935
+ break;
18936
+ case "role":
18937
+ result = scaffoldRole(cwd, slug);
18938
+ if (result.createdFiles.length > 0) {
18939
+ console.log(source_default.green("\u2713"), `Created role "${slug}"`);
18940
+ for (const file of result.createdFiles) {
18941
+ console.log(source_default.gray(" \u2192"), file);
18942
+ }
18943
+ } else {
18944
+ console.log(source_default.yellow("Role already exists:"), `roles/${slug}.ts`);
18945
+ }
18946
+ break;
18947
+ default:
18948
+ console.log(source_default.red("Unknown resource type:"), type);
18949
+ console.log();
18950
+ console.log("Available types:");
18951
+ console.log(source_default.gray(" -"), source_default.cyan("agent"), "- Create an AI agent");
18952
+ console.log(source_default.gray(" -"), source_default.cyan("entity-type"), "- Create an entity type schema");
18953
+ console.log(source_default.gray(" -"), source_default.cyan("role"), "- Create a role with permissions");
18954
+ console.log();
18955
+ process.exit(1);
18956
+ }
18957
+ console.log();
18958
+ console.log(source_default.gray("Run"), source_default.cyan("struere dev"), source_default.gray("to sync changes"));
18959
+ console.log();
18960
+ });
18961
+ function slugify2(name) {
18962
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
18963
+ }
18964
+
18965
+ // src/cli/commands/status.ts
18966
+ var statusCommand = new Command("status").description("Compare local vs remote state").action(async () => {
18967
+ const spinner = ora();
18968
+ const cwd = process.cwd();
18969
+ console.log();
18970
+ console.log(source_default.bold("Struere Status"));
18971
+ console.log();
18972
+ if (!hasProject(cwd)) {
18973
+ console.log(source_default.yellow("No struere.json found"));
18974
+ console.log();
18975
+ console.log(source_default.gray("Run"), source_default.cyan("struere init"), source_default.gray("to initialize this project"));
18976
+ console.log();
18977
+ process.exit(1);
18978
+ }
18979
+ const version = getProjectVersion(cwd);
18980
+ if (version === "1.0") {
18981
+ console.log(source_default.yellow("This is a v1 agent-centric project."));
18982
+ console.log(source_default.yellow("The status command requires v2 structure."));
18983
+ console.log();
18984
+ process.exit(1);
18985
+ }
18986
+ const project = loadProjectV2(cwd);
18987
+ if (!project) {
18988
+ console.log(source_default.red("Failed to load struere.json"));
18989
+ process.exit(1);
18990
+ }
18991
+ console.log(source_default.gray("Organization:"), source_default.cyan(project.organization.name));
18992
+ console.log();
18993
+ const credentials = loadCredentials();
18994
+ const apiKey = getApiKey();
18995
+ if (!credentials && !apiKey) {
18996
+ console.log(source_default.red("Not authenticated"));
18997
+ console.log();
18998
+ console.log(source_default.gray("Run"), source_default.cyan("struere login"), source_default.gray("to authenticate"));
18999
+ console.log();
19000
+ process.exit(1);
19001
+ }
19002
+ spinner.start("Loading local resources");
19003
+ let localResources;
19004
+ try {
19005
+ localResources = await loadAllResources(cwd);
19006
+ spinner.succeed("Local resources loaded");
19007
+ } catch (error) {
19008
+ spinner.fail("Failed to load local resources");
19009
+ console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
19010
+ process.exit(1);
19011
+ }
19012
+ spinner.start("Fetching remote state");
19013
+ const { state: remoteState, error: fetchError } = await getSyncState();
19014
+ if (fetchError || !remoteState) {
19015
+ spinner.fail("Failed to fetch remote state");
19016
+ console.log(source_default.red("Error:"), fetchError || "Unknown error");
19017
+ process.exit(1);
19018
+ }
19019
+ spinner.succeed("Remote state fetched");
19020
+ console.log();
19021
+ const localAgentSlugs = new Set(localResources.agents.map((a) => a.slug));
19022
+ const remoteAgentSlugs = new Set(remoteState.agents.map((a) => a.slug));
19023
+ const localEntityTypeSlugs = new Set(localResources.entityTypes.map((et) => et.slug));
19024
+ const remoteEntityTypeSlugs = new Set(remoteState.entityTypes.map((et) => et.slug));
19025
+ const localRoleNames = new Set(localResources.roles.map((r) => r.name));
19026
+ const remoteRoleNames = new Set(remoteState.roles.map((r) => r.name));
19027
+ console.log(source_default.bold("Agents"));
19028
+ console.log(source_default.gray("\u2500".repeat(60)));
19029
+ if (localResources.agents.length === 0 && remoteState.agents.length === 0) {
19030
+ console.log(source_default.gray(" No agents"));
19031
+ } else {
19032
+ for (const agent of localResources.agents) {
19033
+ const remote = remoteState.agents.find((a) => a.slug === agent.slug);
19034
+ if (remote) {
19035
+ const statusIcon = remote.hasProdConfig ? source_default.green("\u25CF") : source_default.yellow("\u25CB");
19036
+ console.log(` ${statusIcon} ${source_default.cyan(agent.name)} (${agent.slug}) - v${agent.version}`);
19037
+ if (!remote.hasProdConfig) {
19038
+ console.log(source_default.gray(" Not deployed to production"));
19039
+ }
19040
+ } else {
19041
+ console.log(` ${source_default.blue("+")} ${source_default.cyan(agent.name)} (${agent.slug}) - ${source_default.blue("new")}`);
19042
+ }
19043
+ }
19044
+ for (const remote of remoteState.agents) {
19045
+ if (!localAgentSlugs.has(remote.slug)) {
19046
+ console.log(` ${source_default.red("-")} ${remote.name} (${remote.slug}) - ${source_default.red("will be deleted")}`);
19047
+ }
19048
+ }
19049
+ }
19050
+ console.log();
19051
+ console.log(source_default.bold("Entity Types"));
19052
+ console.log(source_default.gray("\u2500".repeat(60)));
19053
+ if (localResources.entityTypes.length === 0 && remoteState.entityTypes.length === 0) {
19054
+ console.log(source_default.gray(" No entity types"));
19055
+ } else {
19056
+ for (const et of localResources.entityTypes) {
19057
+ const remote = remoteState.entityTypes.find((r) => r.slug === et.slug);
19058
+ if (remote) {
19059
+ console.log(` ${source_default.green("\u25CF")} ${source_default.cyan(et.name)} (${et.slug})`);
19060
+ } else {
19061
+ console.log(` ${source_default.blue("+")} ${source_default.cyan(et.name)} (${et.slug}) - ${source_default.blue("new")}`);
19062
+ }
19063
+ }
19064
+ for (const remote of remoteState.entityTypes) {
19065
+ if (!localEntityTypeSlugs.has(remote.slug)) {
19066
+ console.log(` ${source_default.red("-")} ${remote.name} (${remote.slug}) - ${source_default.red("will be deleted")}`);
19067
+ }
19068
+ }
19069
+ }
19070
+ console.log();
19071
+ console.log(source_default.bold("Roles"));
19072
+ console.log(source_default.gray("\u2500".repeat(60)));
19073
+ if (localResources.roles.length === 0 && remoteState.roles.length === 0) {
19074
+ console.log(source_default.gray(" No roles"));
19075
+ } else {
19076
+ for (const role of localResources.roles) {
19077
+ const remote = remoteState.roles.find((r) => r.name === role.name);
19078
+ if (remote) {
19079
+ console.log(` ${source_default.green("\u25CF")} ${source_default.cyan(role.name)} (${role.policies.length} policies)`);
19080
+ } else {
19081
+ console.log(` ${source_default.blue("+")} ${source_default.cyan(role.name)} - ${source_default.blue("new")}`);
19082
+ }
19083
+ }
19084
+ for (const remote of remoteState.roles) {
19085
+ if (!localRoleNames.has(remote.name)) {
19086
+ console.log(` ${source_default.red("-")} ${remote.name} - ${source_default.red("will be deleted")}`);
19087
+ }
19088
+ }
19089
+ }
19090
+ console.log();
19091
+ console.log(source_default.gray("Legend:"));
19092
+ console.log(source_default.gray(" "), source_default.green("\u25CF"), "Synced", source_default.yellow("\u25CB"), "Not deployed", source_default.blue("+"), "New", source_default.red("-"), "Will be deleted");
19093
+ console.log();
19094
+ console.log(source_default.gray("Run"), source_default.cyan("struere dev"), source_default.gray("to sync changes"));
19095
+ console.log();
19096
+ });
19147
19097
  // package.json
19148
19098
  var package_default = {
19149
19099
  name: "struere",
19150
- version: "0.3.11",
19100
+ version: "0.4.0",
19151
19101
  description: "Build, test, and deploy AI agents",
19152
19102
  keywords: [
19153
19103
  "ai",
@@ -19252,4 +19202,6 @@ program.addCommand(deployCommand);
19252
19202
  program.addCommand(validateCommand);
19253
19203
  program.addCommand(logsCommand);
19254
19204
  program.addCommand(stateCommand);
19205
+ program.addCommand(addCommand);
19206
+ program.addCommand(statusCommand);
19255
19207
  program.parse();