struere 0.4.2 → 0.4.4

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.
@@ -16904,8 +16904,11 @@ async function syncOrganization(payload) {
16904
16904
  const error = await response.text();
16905
16905
  return { success: false, error };
16906
16906
  }
16907
- const result = await response.json();
16908
- return result;
16907
+ const json = await response.json();
16908
+ if (json.status === "success" && json.value) {
16909
+ return json.value;
16910
+ }
16911
+ return { success: false, error: "Unexpected response format" };
16909
16912
  }
16910
16913
  async function getSyncState() {
16911
16914
  const credentials = loadCredentials();
@@ -16954,8 +16957,11 @@ async function deployAllAgents() {
16954
16957
  const error = await response.text();
16955
16958
  return { success: false, error };
16956
16959
  }
16957
- const result = await response.json();
16958
- return result;
16960
+ const json = await response.json();
16961
+ if (json.status === "success" && json.value) {
16962
+ return json.value;
16963
+ }
16964
+ return { success: false, error: "Unexpected response format" };
16959
16965
  }
16960
16966
 
16961
16967
  // src/cli/commands/login.ts
@@ -17325,10 +17331,10 @@ function getIndexTs(type) {
17325
17331
  function getToolsIndexTs() {
17326
17332
  return `import { defineTools } from 'struere'
17327
17333
 
17328
- export const tools = defineTools([
17334
+ export default defineTools([
17329
17335
  {
17330
17336
  name: 'get_current_time',
17331
- description: 'Get the current date and time',
17337
+ description: 'Get the current date and time in a specific timezone',
17332
17338
  parameters: {
17333
17339
  type: 'object',
17334
17340
  properties: {
@@ -17338,19 +17344,59 @@ export const tools = defineTools([
17338
17344
  },
17339
17345
  },
17340
17346
  },
17341
- handler: async (params) => {
17342
- const timezone = (params.timezone as string) || 'UTC'
17347
+ handler: async (args, context, fetch) => {
17348
+ const timezone = (args.timezone as string) || 'UTC'
17343
17349
  const now = new Date()
17344
17350
  return {
17345
17351
  timestamp: now.toISOString(),
17346
17352
  formatted: now.toLocaleString('en-US', { timeZone: timezone }),
17347
17353
  timezone,
17354
+ organizationId: context.organizationId,
17348
17355
  }
17349
17356
  },
17350
17357
  },
17351
- ])
17352
17358
 
17353
- export default tools
17359
+ {
17360
+ name: 'send_slack_message',
17361
+ description: 'Send a message to a Slack channel via webhook',
17362
+ parameters: {
17363
+ type: 'object',
17364
+ properties: {
17365
+ message: {
17366
+ type: 'string',
17367
+ description: 'The message to send',
17368
+ },
17369
+ channel: {
17370
+ type: 'string',
17371
+ description: 'Channel name (for logging purposes)',
17372
+ },
17373
+ },
17374
+ required: ['message'],
17375
+ },
17376
+ handler: async (args, context, fetch) => {
17377
+ const webhookUrl = process.env.SLACK_WEBHOOK_URL
17378
+ if (!webhookUrl) {
17379
+ return { success: false, error: 'SLACK_WEBHOOK_URL not configured' }
17380
+ }
17381
+
17382
+ const response = await fetch(webhookUrl, {
17383
+ method: 'POST',
17384
+ headers: { 'Content-Type': 'application/json' },
17385
+ body: JSON.stringify({
17386
+ text: args.message,
17387
+ username: 'Struere Agent',
17388
+ }),
17389
+ })
17390
+
17391
+ return {
17392
+ success: response.ok,
17393
+ status: response.status,
17394
+ actorId: context.actorId,
17395
+ actorType: context.actorType,
17396
+ }
17397
+ },
17398
+ },
17399
+ ])
17354
17400
  `;
17355
17401
  }
17356
17402
  function getStruereJsonV2(orgId, orgSlug, orgName) {
@@ -17384,123 +17430,430 @@ function getPackageJsonV2(name) {
17384
17430
  }, null, 2);
17385
17431
  }
17386
17432
  function getClaudeMDV2(orgName) {
17387
- return `# ${orgName} - Struere Project
17433
+ return `# ${orgName} - Struere Workspace
17388
17434
 
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.
17435
+ > **This is a workspace project**, not the Struere framework source code. You define agents, entity types, roles, and custom tools here. The CLI syncs them to Convex. Framework source: github.com/struere/struere
17436
+
17437
+ Struere is a framework for building production AI agents with Convex as the real-time backend. Agents can manage entities (business data), emit events, and schedule background jobs\u2014all with built-in RBAC permissions.
17438
+
17439
+ ## How It Works
17440
+
17441
+ \`\`\`
17442
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
17443
+ \u2502 Your Project (this folder) \u2502
17444
+ \u2502 \u251C\u2500\u2500 agents/*.ts \u2192 Agent configs synced to Convex \u2502
17445
+ \u2502 \u251C\u2500\u2500 entity-types/*.ts \u2192 Schema definitions synced to Convex \u2502
17446
+ \u2502 \u251C\u2500\u2500 roles/*.ts \u2192 RBAC policies synced to Convex \u2502
17447
+ \u2502 \u2514\u2500\u2500 tools/index.ts \u2192 Custom tools (handlers run on CF Worker)\u2502
17448
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
17449
+ \u2502 struere dev (watches & syncs)
17450
+ \u25BC
17451
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
17452
+ \u2502 Convex (Real-time Backend) \u2502
17453
+ \u2502 \u2022 Stores agent configs, entities, events, jobs \u2502
17454
+ \u2502 \u2022 Runs LLM calls (Anthropic/OpenAI) \u2502
17455
+ \u2502 \u2022 Enforces RBAC on every operation \u2502
17456
+ \u2502 \u2022 Executes custom tools via Cloudflare Worker \u2502
17457
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
17458
+ \u2502 HTTP API
17459
+ \u25BC
17460
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
17461
+ \u2502 Clients \u2502
17462
+ \u2502 \u2022 Dashboard (chat UI, entity browser) \u2502
17463
+ \u2502 \u2022 Your app (REST API with Bearer token) \u2502
17464
+ \u2502 \u2022 WhatsApp/Webhooks \u2502
17465
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
17466
+ \`\`\`
17390
17467
 
17391
17468
  ## Project Structure
17392
17469
 
17393
17470
  \`\`\`
17394
- agents/ # Agent definitions
17395
- \u251C\u2500\u2500 scheduler.ts # Example agent
17396
- \u2514\u2500\u2500 index.ts # Re-exports all agents
17471
+ agents/ # Agent definitions (synced to Convex)
17472
+ \u251C\u2500\u2500 my-agent.ts # One file per agent
17473
+ \u2514\u2500\u2500 index.ts # Re-exports (optional)
17397
17474
 
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
17475
+ entity-types/ # Data schemas (like DB tables)
17476
+ \u251C\u2500\u2500 customer.ts # Defines shape of "customer" entities
17477
+ \u2514\u2500\u2500 index.ts
17401
17478
 
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
17479
+ roles/ # RBAC: who can do what
17480
+ \u251C\u2500\u2500 admin.ts # Full access
17481
+ \u251C\u2500\u2500 support.ts # Limited access
17482
+ \u2514\u2500\u2500 index.ts
17405
17483
 
17406
- tools/ # Shared custom tools
17407
- \u2514\u2500\u2500 index.ts # Custom tool definitions
17484
+ tools/ # Custom tools shared by all agents
17485
+ \u2514\u2500\u2500 index.ts # defineTools([...])
17408
17486
 
17409
- struere.json # Organization configuration
17410
- struere.config.ts # Framework settings
17487
+ struere.json # Organization ID (don't edit)
17488
+ struere.config.ts # Local dev settings (port, CORS)
17411
17489
  \`\`\`
17412
17490
 
17413
- ## CLI Commands
17491
+ ## Configuration Files
17414
17492
 
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 |
17493
+ ### struere.json (auto-generated, don't edit)
17494
+ Links this project to your Convex organization:
17495
+ \`\`\`json
17496
+ {
17497
+ "version": "2.0",
17498
+ "organization": { "id": "org_xxx", "slug": "my-org", "name": "My Org" }
17499
+ }
17500
+ \`\`\`
17421
17501
 
17422
- ## Defining Resources
17502
+ ### struere.config.ts (optional local settings)
17503
+ \`\`\`typescript
17504
+ import { defineConfig } from 'struere'
17505
+ export default defineConfig({
17506
+ port: 3000, // Local dev server port
17507
+ logging: { level: 'debug' }
17508
+ })
17509
+ \`\`\`
17510
+
17511
+ ## CLI Commands
17512
+
17513
+ | Command | What it does |
17514
+ |---------|--------------|
17515
+ | \`struere dev\` | Watch files, sync to Convex on every save |
17516
+ | \`struere deploy\` | Copy dev config to production for all agents |
17517
+ | \`struere add agent <name>\` | Create agents/name.ts with template |
17518
+ | \`struere add entity-type <name>\` | Create entity-types/name.ts |
17519
+ | \`struere add role <name>\` | Create roles/name.ts |
17520
+ | \`struere status\` | Show what's synced vs local-only |
17423
17521
 
17424
- ### Agents (\`agents/*.ts\`)
17522
+ ## Defining Agents
17425
17523
 
17524
+ Create \`agents/support.ts\`:
17426
17525
  \`\`\`typescript
17427
17526
  import { defineAgent } from 'struere'
17428
17527
 
17429
17528
  export default defineAgent({
17430
- name: "Scheduler",
17431
- slug: "scheduler",
17529
+ name: "Support Agent",
17530
+ slug: "support",
17432
17531
  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"],
17532
+ model: {
17533
+ provider: "anthropic", // or "openai", "google"
17534
+ name: "claude-sonnet-4-20250514",
17535
+ temperature: 0.7,
17536
+ maxTokens: 4096,
17537
+ },
17538
+ systemPrompt: \\\`You are a support agent for {{organizationName}}.
17539
+ Current time: {{currentTime}}
17540
+
17541
+ Available customers:
17542
+ {{#each entityTypes}}
17543
+ - {{this.name}}: {{this.description}}
17544
+ {{/each}}
17545
+
17546
+ Use entity.query to look up customer info before responding.\\\`,
17547
+ tools: [
17548
+ "entity.query",
17549
+ "entity.get",
17550
+ "entity.update",
17551
+ "event.emit",
17552
+ "send_email", // custom tool from tools/index.ts
17553
+ ],
17436
17554
  })
17437
17555
  \`\`\`
17438
17556
 
17439
- ### Entity Types (\`entity-types/*.ts\`)
17557
+ ### System Prompt Variables
17558
+
17559
+ | Variable | Value |
17560
+ |----------|-------|
17561
+ | \`{{currentTime}}\` | ISO 8601 timestamp |
17562
+ | \`{{organizationName}}\` | Your org name |
17563
+ | \`{{agentName}}\` | This agent's name |
17564
+ | \`{{entityTypes}}\` | Array of all entity types (for #each loops) |
17565
+ | \`{{roles}}\` | Array of all roles |
17440
17566
 
17567
+ ## Defining Entity Types
17568
+
17569
+ Create \`entity-types/customer.ts\`:
17441
17570
  \`\`\`typescript
17442
17571
  import { defineEntityType } from 'struere'
17443
17572
 
17444
17573
  export default defineEntityType({
17445
- name: "Teacher",
17446
- slug: "teacher",
17574
+ name: "Customer",
17575
+ slug: "customer",
17447
17576
  schema: {
17448
17577
  type: "object",
17449
17578
  properties: {
17450
17579
  name: { type: "string" },
17451
17580
  email: { type: "string", format: "email" },
17452
- hourlyRate: { type: "number" },
17581
+ plan: { type: "string", enum: ["free", "pro", "enterprise"] },
17582
+ metadata: { type: "object" },
17453
17583
  },
17454
17584
  required: ["name", "email"],
17455
17585
  },
17456
- searchFields: ["name", "email"],
17586
+ searchFields: ["name", "email"], // Fields indexed for search
17457
17587
  })
17458
17588
  \`\`\`
17459
17589
 
17460
- ### Roles (\`roles/*.ts\`)
17590
+ Entities are stored as:
17591
+ \`\`\`json
17592
+ {
17593
+ "_id": "ent_abc123",
17594
+ "type": "customer",
17595
+ "data": { "name": "John", "email": "john@example.com", "plan": "pro" },
17596
+ "status": "active",
17597
+ "createdAt": 1706745600000
17598
+ }
17599
+ \`\`\`
17600
+
17601
+ ## Defining Roles (RBAC)
17461
17602
 
17603
+ Create \`roles/support.ts\`:
17462
17604
  \`\`\`typescript
17463
17605
  import { defineRole } from 'struere'
17464
17606
 
17465
17607
  export default defineRole({
17466
- name: "teacher",
17467
- description: "Tutors who conduct sessions",
17608
+ name: "support",
17609
+ description: "Support staff with limited access",
17610
+
17611
+ // What actions are allowed/denied
17468
17612
  policies: [
17469
- { resource: "session", actions: ["list", "read", "update"], effect: "allow", priority: 50 },
17613
+ { resource: "customer", actions: ["list", "read"], effect: "allow", priority: 50 },
17614
+ { resource: "customer", actions: ["delete"], effect: "deny", priority: 100 },
17470
17615
  { resource: "payment", actions: ["*"], effect: "deny", priority: 100 },
17471
17616
  ],
17617
+
17618
+ // Row-level security: only see assigned customers
17472
17619
  scopeRules: [
17473
- { entityType: "session", field: "data.teacherId", operator: "eq", value: "actor.userId" },
17620
+ {
17621
+ entityType: "customer",
17622
+ field: "data.assignedTo", // Field in entity data
17623
+ operator: "eq",
17624
+ value: "actor.userId" // Current user's ID
17625
+ },
17474
17626
  ],
17627
+
17628
+ // Column-level security: hide sensitive fields
17475
17629
  fieldMasks: [
17476
- { entityType: "session", fieldPath: "data.paymentId", maskType: "hide" },
17630
+ { entityType: "customer", fieldPath: "data.ssn", maskType: "hide" },
17631
+ { entityType: "customer", fieldPath: "data.creditCard", maskType: "redact" },
17477
17632
  ],
17478
17633
  })
17479
17634
  \`\`\`
17480
17635
 
17481
- ## Built-in Tools
17482
-
17483
- | Tool | Description |
17484
- |------|-------------|
17485
- | \`entity.create\` | Create a new entity |
17486
- | \`entity.get\` | Get entity by ID |
17487
- | \`entity.query\` | Query entities by type/filters |
17488
- | \`entity.update\` | Update entity data |
17489
- | \`entity.delete\` | Soft-delete entity |
17490
- | \`entity.link\` | Create entity relation |
17491
- | \`entity.unlink\` | Remove entity relation |
17492
- | \`event.emit\` | Emit custom event |
17493
- | \`event.query\` | Query events |
17494
- | \`job.enqueue\` | Schedule background job |
17495
- | \`job.status\` | Get job status |
17636
+ ### RBAC Enforcement
17637
+
17638
+ Every tool call goes through permission checks:
17639
+ 1. **Policy check**: Does this role allow the action on this resource?
17640
+ 2. **Scope filter**: Query results filtered to rows user can access
17641
+ 3. **Field mask**: Sensitive fields hidden/redacted in response
17642
+
17643
+ Deny policies override allow. Higher priority wins.
17644
+
17645
+ ## Defining Custom Tools
17646
+
17647
+ Edit \`tools/index.ts\`:
17648
+ \`\`\`typescript
17649
+ import { defineTools } from 'struere'
17650
+
17651
+ export default defineTools([
17652
+ {
17653
+ name: "send_email",
17654
+ description: "Send an email to a recipient",
17655
+ parameters: {
17656
+ type: "object",
17657
+ properties: {
17658
+ to: { type: "string", description: "Recipient email" },
17659
+ subject: { type: "string", description: "Email subject" },
17660
+ body: { type: "string", description: "Email body" },
17661
+ },
17662
+ required: ["to", "subject", "body"],
17663
+ },
17664
+ // Handler runs on Cloudflare Worker (sandboxed)
17665
+ handler: async (args, context, fetch) => {
17666
+ // context = { organizationId, actorId, actorType }
17667
+ // fetch = sandboxed fetch (limited domains)
17668
+
17669
+ const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
17670
+ method: "POST",
17671
+ headers: {
17672
+ "Authorization": \\\`Bearer \\\${process.env.SENDGRID_API_KEY}\\\`,
17673
+ "Content-Type": "application/json",
17674
+ },
17675
+ body: JSON.stringify({
17676
+ personalizations: [{ to: [{ email: args.to }] }],
17677
+ from: { email: "noreply@example.com" },
17678
+ subject: args.subject,
17679
+ content: [{ type: "text/plain", value: args.body }],
17680
+ }),
17681
+ })
17682
+ return { success: response.ok, status: response.status }
17683
+ },
17684
+ },
17685
+
17686
+ {
17687
+ name: "lookup_order",
17688
+ description: "Look up order by ID",
17689
+ parameters: {
17690
+ type: "object",
17691
+ properties: {
17692
+ orderId: { type: "string", description: "Order ID" },
17693
+ },
17694
+ required: ["orderId"],
17695
+ },
17696
+ handler: async (args, context, fetch) => {
17697
+ const res = await fetch(\\\`https://api.myshop.com/orders/\\\${args.orderId}\\\`, {
17698
+ headers: { "X-Org-Id": context.organizationId },
17699
+ })
17700
+ return await res.json()
17701
+ },
17702
+ },
17703
+ ])
17704
+ \`\`\`
17705
+
17706
+ ### Allowed Domains for Custom Tools
17707
+
17708
+ Custom tool handlers can only fetch from:
17709
+ - api.openai.com, api.anthropic.com
17710
+ - api.stripe.com, api.sendgrid.com, api.twilio.com
17711
+ - hooks.slack.com, discord.com, api.github.com
17712
+
17713
+ ## Built-in Tools Reference
17714
+
17715
+ ### Entity Tools
17716
+
17717
+ \`\`\`typescript
17718
+ // entity.create - Create new entity
17719
+ { type: "customer", data: { name: "John", email: "j@example.com" }, status: "active" }
17720
+
17721
+ // entity.get - Get by ID
17722
+ { id: "ent_abc123" }
17723
+
17724
+ // entity.query - Search/filter
17725
+ { type: "customer", filters: { "data.plan": "pro" }, status: "active", limit: 50 }
17726
+
17727
+ // entity.update - Partial update
17728
+ { id: "ent_abc123", data: { plan: "enterprise" } }
17729
+
17730
+ // entity.delete - Soft delete
17731
+ { id: "ent_abc123" }
17732
+
17733
+ // entity.link - Create relation
17734
+ { fromEntityId: "ent_abc", toEntityId: "ent_xyz", relationType: "assigned_to" }
17735
+
17736
+ // entity.unlink - Remove relation
17737
+ { fromEntityId: "ent_abc", toEntityId: "ent_xyz", relationType: "assigned_to" }
17738
+ \`\`\`
17739
+
17740
+ ### Event Tools
17741
+
17742
+ \`\`\`typescript
17743
+ // event.emit - Log an event
17744
+ { eventType: "support.ticket.resolved", entityId: "ent_abc", payload: { rating: 5 } }
17745
+
17746
+ // event.query - Query event history
17747
+ { eventType: "support.ticket.*", entityId: "ent_abc", limit: 20 }
17748
+ \`\`\`
17749
+
17750
+ Events are immutable audit logs. Use for analytics, debugging, compliance.
17751
+
17752
+ ### Job Tools
17753
+
17754
+ \`\`\`typescript
17755
+ // job.enqueue - Schedule background work
17756
+ {
17757
+ jobType: "send_reminder",
17758
+ payload: { customerId: "ent_abc", message: "Your trial ends soon" },
17759
+ runAt: 1706832000000 // Unix timestamp (optional, runs immediately if omitted)
17760
+ }
17761
+
17762
+ // job.status - Check job status
17763
+ { jobId: "job_xyz123" }
17764
+ // Returns: { status: "pending" | "running" | "completed" | "failed", result: {...} }
17765
+ \`\`\`
17766
+
17767
+ Jobs run asynchronously with retry logic. Use for: emails, notifications, data sync.
17768
+
17769
+ ## Invoking Agents (API)
17770
+
17771
+ ### Chat Endpoint
17772
+ \`\`\`bash
17773
+ curl -X POST https://your-convex-url.convex.cloud/v1/chat \\
17774
+ -H "Authorization: Bearer sk_live_xxx" \\
17775
+ -H "Content-Type: application/json" \\
17776
+ -d '{
17777
+ "agentId": "agent_abc123",
17778
+ "message": "What is the status of order #12345?",
17779
+ "threadId": "thread_xyz", // optional, creates new if omitted
17780
+ "metadata": { "customerId": "ent_cust_789" } // available in system prompt
17781
+ }'
17782
+ \`\`\`
17783
+
17784
+ ### Response
17785
+ \`\`\`json
17786
+ {
17787
+ "threadId": "thread_xyz",
17788
+ "message": "Order #12345 is currently being shipped...",
17789
+ "usage": { "inputTokens": 150, "outputTokens": 89 }
17790
+ }
17791
+ \`\`\`
17792
+
17793
+ ### By Slug (production)
17794
+ \`\`\`bash
17795
+ curl -X POST https://your-convex-url.convex.cloud/v1/agents/support/chat \\
17796
+ -H "Authorization: Bearer sk_live_xxx" \\
17797
+ -d '{"message": "Hello"}'
17798
+ \`\`\`
17496
17799
 
17497
17800
  ## Development Workflow
17498
17801
 
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
17802
+ 1. \`struere dev\` - Start watching
17803
+ 2. Edit files in agents/, entity-types/, roles/, tools/
17804
+ 3. Save \u2192 auto-syncs to Convex (you'll see "Synced" message)
17805
+ 4. Test via dashboard or curl
17806
+ 5. \`struere deploy\` - Push to production
17807
+
17808
+ ## Common Patterns
17809
+
17810
+ ### Customer Support Agent
17811
+ \`\`\`typescript
17812
+ // agents/support.ts
17813
+ export default defineAgent({
17814
+ name: "Support",
17815
+ slug: "support",
17816
+ version: "0.1.0",
17817
+ systemPrompt: \\\`You help customers with their orders and account issues.
17818
+
17819
+ When a customer asks about an order, use entity.query to find it first.
17820
+ Always be polite and helpful. If you can't help, offer to escalate.\\\`,
17821
+ model: { provider: "anthropic", name: "claude-sonnet-4-20250514" },
17822
+ tools: ["entity.query", "entity.get", "entity.update", "event.emit"],
17823
+ })
17824
+ \`\`\`
17825
+
17826
+ ### Scheduling Agent
17827
+ \`\`\`typescript
17828
+ // agents/scheduler.ts
17829
+ export default defineAgent({
17830
+ name: "Scheduler",
17831
+ slug: "scheduler",
17832
+ version: "0.1.0",
17833
+ systemPrompt: \\\`You help schedule appointments between teachers and students.
17834
+
17835
+ Check teacher availability before booking. Create session entities for confirmed bookings.
17836
+ Send confirmation via the send_notification custom tool.\\\`,
17837
+ model: { provider: "anthropic", name: "claude-sonnet-4-20250514" },
17838
+ tools: ["entity.create", "entity.query", "job.enqueue", "send_notification"],
17839
+ })
17840
+ \`\`\`
17841
+
17842
+ ### Data Entry Agent
17843
+ \`\`\`typescript
17844
+ // agents/data-entry.ts
17845
+ export default defineAgent({
17846
+ name: "Data Entry",
17847
+ slug: "data-entry",
17848
+ version: "0.1.0",
17849
+ systemPrompt: \\\`You help users create and update records in the system.
17850
+
17851
+ When creating entities, validate the data matches the schema.
17852
+ Always confirm what was created/updated.\\\`,
17853
+ model: { provider: "anthropic", name: "claude-sonnet-4-20250514" },
17854
+ tools: ["entity.create", "entity.update", "entity.query"],
17855
+ })
17856
+ \`\`\`
17504
17857
  `;
17505
17858
  }
17506
17859
 
@@ -18032,10 +18385,55 @@ var devCommand = new Command("dev").description("Sync all resources to developme
18032
18385
  const message = error instanceof Error ? error.message : String(error);
18033
18386
  return message.includes("Unauthenticated") || message.includes("OIDC") || message.includes("token") || message.includes("expired");
18034
18387
  };
18388
+ spinner.start("Checking remote state");
18389
+ const { state: remoteState, error: stateError } = await getSyncState();
18390
+ if (stateError) {
18391
+ if (isAuthError(new Error(stateError))) {
18392
+ spinner.fail("Session expired - re-authenticating...");
18393
+ clearCredentials();
18394
+ credentials = await performLogin();
18395
+ if (!credentials) {
18396
+ console.log(source_default.red("Authentication failed"));
18397
+ process.exit(1);
18398
+ }
18399
+ } else {
18400
+ spinner.fail("Failed to fetch remote state");
18401
+ console.log(source_default.red("Error:"), stateError);
18402
+ process.exit(1);
18403
+ }
18404
+ } else {
18405
+ spinner.succeed("Remote state fetched");
18406
+ }
18407
+ if (remoteState?.installedPacks && remoteState.installedPacks.length > 0) {
18408
+ console.log();
18409
+ console.log(source_default.cyan("Installed packs detected:"));
18410
+ for (const pack of remoteState.installedPacks) {
18411
+ console.log(source_default.gray(" \u2022"), `${pack.packId} v${pack.version}`, source_default.gray(`(${pack.entityTypeCount} types, ${pack.roleCount} roles)`));
18412
+ }
18413
+ console.log(source_default.gray(" Pack resources will be preserved during sync."));
18414
+ console.log();
18415
+ }
18416
+ if (remoteState?.agents && remoteState.agents.length > 0) {
18417
+ const localResources = await loadAllResources(cwd);
18418
+ const localSlugs = new Set(localResources.agents.map((a) => a.slug));
18419
+ const unmanagedAgents = remoteState.agents.filter((a) => !localSlugs.has(a.slug));
18420
+ if (unmanagedAgents.length > 0) {
18421
+ console.log(source_default.cyan("Existing agents not in local files:"));
18422
+ for (const agent of unmanagedAgents) {
18423
+ console.log(source_default.gray(" \u2022"), agent.name, source_default.gray(`(${agent.slug})`));
18424
+ }
18425
+ console.log(source_default.gray(" These agents will be preserved during sync."));
18426
+ console.log();
18427
+ }
18428
+ }
18035
18429
  const performSync = async () => {
18036
18430
  const resources = await loadAllResources(cwd);
18037
18431
  const payload = extractSyncPayload(resources);
18038
- const result = await syncOrganization(payload);
18432
+ const result = await syncOrganization({
18433
+ ...payload,
18434
+ preservePackResources: true,
18435
+ preserveUnmanagedAgents: true
18436
+ });
18039
18437
  if (!result.success) {
18040
18438
  throw new Error(result.error || "Sync failed");
18041
18439
  }
@@ -18091,7 +18489,9 @@ var devCommand = new Command("dev").description("Sync all resources to developme
18091
18489
  ].filter((p) => existsSync5(p));
18092
18490
  const watcher = import_chokidar.default.watch(watchPaths, {
18093
18491
  ignoreInitial: true,
18094
- ignored: /node_modules/
18492
+ ignored: /node_modules/,
18493
+ persistent: true,
18494
+ usePolling: false
18095
18495
  });
18096
18496
  watcher.on("change", async (path) => {
18097
18497
  const relativePath = path.replace(cwd, ".");
@@ -18147,10 +18547,15 @@ var devCommand = new Command("dev").description("Sync all resources to developme
18147
18547
  console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
18148
18548
  }
18149
18549
  });
18150
- process.on("SIGINT", () => {
18550
+ let isClosing = false;
18551
+ process.on("SIGINT", async () => {
18552
+ if (isClosing) {
18553
+ process.exit(0);
18554
+ }
18555
+ isClosing = true;
18151
18556
  console.log();
18152
- watcher.close();
18153
- console.log(source_default.gray("Stopped"));
18557
+ console.log(source_default.gray("Stopping..."));
18558
+ await watcher.close();
18154
18559
  process.exit(0);
18155
18560
  });
18156
18561
  });
@@ -19100,7 +19505,7 @@ var statusCommand = new Command("status").description("Compare local vs remote s
19100
19505
  // package.json
19101
19506
  var package_default = {
19102
19507
  name: "struere",
19103
- version: "0.4.2",
19508
+ version: "0.4.4",
19104
19509
  description: "Build, test, and deploy AI agents",
19105
19510
  keywords: [
19106
19511
  "ai",