struere 0.3.6 → 0.3.8

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.
@@ -17480,6 +17480,332 @@ function getEnvLocal(deploymentUrl) {
17480
17480
  return `STRUERE_DEPLOYMENT_URL=${deploymentUrl}
17481
17481
  `;
17482
17482
  }
17483
+ function getClaudeMd(name) {
17484
+ const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
17485
+ return `# ${displayName} Agent
17486
+
17487
+ 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.
17488
+
17489
+ ## Project Structure
17490
+
17491
+ \`\`\`
17492
+ src/
17493
+ \u251C\u2500\u2500 agent.ts # Agent definition (system prompt, model, tools)
17494
+ \u251C\u2500\u2500 context.ts # Dynamic context injection per request
17495
+ \u251C\u2500\u2500 tools.ts # Custom tool definitions
17496
+ \u2514\u2500\u2500 workflows/ # Multi-step workflow definitions
17497
+ tests/
17498
+ \u2514\u2500\u2500 *.test.yaml # YAML-based conversation tests
17499
+ struere.json # Project configuration (agentId, team, slug)
17500
+ struere.config.ts # Framework settings (port, CORS, logging)
17501
+ \`\`\`
17502
+
17503
+ ## Agent Definition
17504
+
17505
+ Define your agent in \`src/agent.ts\`:
17506
+
17507
+ \`\`\`typescript
17508
+ import { defineAgent } from 'struere'
17509
+ import { tools } from './tools'
17510
+ import { context } from './context'
17511
+
17512
+ export default defineAgent({
17513
+ name: 'my-agent',
17514
+ version: '0.1.0',
17515
+ description: 'My AI Agent',
17516
+ model: {
17517
+ provider: 'anthropic',
17518
+ name: 'claude-sonnet-4-20250514',
17519
+ temperature: 0.7,
17520
+ maxTokens: 4096,
17521
+ },
17522
+ systemPrompt: \\\`You are a helpful assistant.
17523
+
17524
+ Current time: {{datetime}}
17525
+ Customer: {{entity.get({"id": "{{thread.metadata.customerId}}"})}}\\\`,
17526
+ tools,
17527
+ context,
17528
+ state: {
17529
+ storage: 'memory',
17530
+ ttl: 3600,
17531
+ },
17532
+ })
17533
+ \`\`\`
17534
+
17535
+ ## System Prompt Templates
17536
+
17537
+ System prompts support dynamic \`{{...}}\` templates that are resolved at runtime before the LLM call.
17538
+
17539
+ ### Available Variables
17540
+
17541
+ | Variable | Description |
17542
+ |----------|-------------|
17543
+ | \`{{organizationId}}\` | Current organization ID |
17544
+ | \`{{userId}}\` | Current user ID |
17545
+ | \`{{threadId}}\` | Conversation thread ID |
17546
+ | \`{{agentId}}\` | Agent ID |
17547
+ | \`{{agent.name}}\` | Agent name |
17548
+ | \`{{agent.slug}}\` | Agent slug |
17549
+ | \`{{thread.metadata.X}}\` | Thread metadata field X |
17550
+ | \`{{message}}\` | Current user message |
17551
+ | \`{{timestamp}}\` | Unix timestamp (ms) |
17552
+ | \`{{datetime}}\` | ISO 8601 datetime |
17553
+
17554
+ ### Function Calls
17555
+
17556
+ Call any agent tool directly in the system prompt:
17557
+
17558
+ \`\`\`
17559
+ {{entity.get({"id": "ent_123"})}}
17560
+ {{entity.query({"type": "customer", "limit": 5})}}
17561
+ {{event.query({"entityId": "ent_123", "limit": 10})}}
17562
+ \`\`\`
17563
+
17564
+ ### Nested Templates
17565
+
17566
+ Variables can be used inside function arguments:
17567
+
17568
+ \`\`\`
17569
+ {{entity.get({"id": "{{thread.metadata.customerId}}"})}}
17570
+ \`\`\`
17571
+
17572
+ ### Error Handling
17573
+
17574
+ Failed templates are replaced with inline errors:
17575
+ \`\`\`
17576
+ [TEMPLATE_ERROR: variableName not found]
17577
+ [TEMPLATE_ERROR: toolName - error message]
17578
+ \`\`\`
17579
+
17580
+ ## Custom Tools
17581
+
17582
+ Define tools in \`src/tools.ts\`:
17583
+
17584
+ \`\`\`typescript
17585
+ import { defineTools } from 'struere'
17586
+
17587
+ export const tools = defineTools([
17588
+ {
17589
+ name: 'search_products',
17590
+ description: 'Search the product catalog',
17591
+ parameters: {
17592
+ type: 'object',
17593
+ properties: {
17594
+ query: { type: 'string', description: 'Search query' },
17595
+ limit: { type: 'number', description: 'Max results' },
17596
+ },
17597
+ required: ['query'],
17598
+ },
17599
+ handler: async (params) => {
17600
+ const results = await searchProducts(params.query, params.limit ?? 10)
17601
+ return { products: results }
17602
+ },
17603
+ },
17604
+ ])
17605
+ \`\`\`
17606
+
17607
+ Custom tool handlers are executed in a sandboxed Cloudflare Worker environment. They can make HTTP requests to allowlisted domains:
17608
+ - api.openai.com, api.anthropic.com, api.stripe.com
17609
+ - api.sendgrid.com, api.twilio.com, hooks.slack.com
17610
+ - discord.com, api.github.com
17611
+
17612
+ ## Built-in Tools
17613
+
17614
+ Agents have access to these built-in tools for data management:
17615
+
17616
+ ### Entity Tools
17617
+
17618
+ | Tool | Description |
17619
+ |------|-------------|
17620
+ | \`entity.create\` | Create a new entity |
17621
+ | \`entity.get\` | Get entity by ID |
17622
+ | \`entity.query\` | Query entities by type/filters |
17623
+ | \`entity.update\` | Update entity data |
17624
+ | \`entity.delete\` | Soft-delete entity |
17625
+ | \`entity.link\` | Create entity relation |
17626
+ | \`entity.unlink\` | Remove entity relation |
17627
+
17628
+ Example entity operations:
17629
+ \`\`\`json
17630
+ // entity.create
17631
+ { "type": "customer", "data": { "name": "John", "email": "john@example.com" } }
17632
+
17633
+ // entity.query
17634
+ { "type": "customer", "filters": { "status": "active" }, "limit": 10 }
17635
+
17636
+ // entity.update
17637
+ { "id": "ent_123", "data": { "status": "vip" } }
17638
+ \`\`\`
17639
+
17640
+ ### Event Tools
17641
+
17642
+ | Tool | Description |
17643
+ |------|-------------|
17644
+ | \`event.emit\` | Emit a custom event |
17645
+ | \`event.query\` | Query event history |
17646
+
17647
+ Example event operations:
17648
+ \`\`\`json
17649
+ // event.emit
17650
+ { "entityId": "ent_123", "eventType": "order.placed", "payload": { "amount": 99.99 } }
17651
+
17652
+ // event.query
17653
+ { "entityId": "ent_123", "eventType": "order.*", "limit": 20 }
17654
+ \`\`\`
17655
+
17656
+ ### Job Tools
17657
+
17658
+ | Tool | Description |
17659
+ |------|-------------|
17660
+ | \`job.enqueue\` | Schedule a background job |
17661
+ | \`job.status\` | Get job status |
17662
+
17663
+ Example job operations:
17664
+ \`\`\`json
17665
+ // job.enqueue
17666
+ { "jobType": "send_email", "payload": { "to": "user@example.com" }, "scheduledFor": 1706745600000 }
17667
+
17668
+ // job.status
17669
+ { "id": "job_abc123" }
17670
+ \`\`\`
17671
+
17672
+ ## Context Function
17673
+
17674
+ Inject dynamic context per request in \`src/context.ts\`:
17675
+
17676
+ \`\`\`typescript
17677
+ import { defineContext } from 'struere'
17678
+
17679
+ export const context = defineContext(async (request) => {
17680
+ const { conversationId, userId, channel, state } = request
17681
+
17682
+ const userProfile = await fetchUserProfile(userId)
17683
+
17684
+ return {
17685
+ additionalContext: \\\`
17686
+ User: \${userProfile.name} (\${userProfile.tier} tier)
17687
+ Conversation: \${conversationId}
17688
+ Channel: \${channel}
17689
+ \\\`,
17690
+ variables: {
17691
+ userId,
17692
+ userTier: userProfile.tier,
17693
+ timestamp: new Date().toISOString(),
17694
+ },
17695
+ }
17696
+ })
17697
+ \`\`\`
17698
+
17699
+ ## Testing
17700
+
17701
+ Write YAML-based conversation tests in \`tests/\`:
17702
+
17703
+ \`\`\`yaml
17704
+ name: Order flow test
17705
+ description: Test the complete order flow
17706
+
17707
+ conversation:
17708
+ - role: user
17709
+ content: I want to order a pizza
17710
+ - role: assistant
17711
+ assertions:
17712
+ - type: contains
17713
+ value: size
17714
+ - type: toolCalled
17715
+ value: get_menu
17716
+
17717
+ - role: user
17718
+ content: Large pepperoni please
17719
+ - role: assistant
17720
+ assertions:
17721
+ - type: toolCalled
17722
+ value: entity.create
17723
+ \`\`\`
17724
+
17725
+ ### Assertion Types
17726
+
17727
+ | Type | Description |
17728
+ |------|-------------|
17729
+ | \`contains\` | Response contains substring |
17730
+ | \`matches\` | Response matches regex |
17731
+ | \`toolCalled\` | Specific tool was called |
17732
+ | \`noToolCalled\` | No tools were called |
17733
+
17734
+ Run tests with:
17735
+ \`\`\`bash
17736
+ bun run test
17737
+ \`\`\`
17738
+
17739
+ ## CLI Commands
17740
+
17741
+ | Command | Description |
17742
+ |---------|-------------|
17743
+ | \`struere dev\` | Start development mode (live sync to Convex) |
17744
+ | \`struere build\` | Validate agent configuration |
17745
+ | \`struere deploy\` | Deploy agent to production |
17746
+ | \`struere test\` | Run YAML conversation tests |
17747
+ | \`struere logs\` | View recent execution logs |
17748
+ | \`struere state\` | Inspect conversation thread state |
17749
+
17750
+ ## Thread Metadata
17751
+
17752
+ Set thread metadata when creating conversations to provide context:
17753
+
17754
+ \`\`\`typescript
17755
+ // Via API
17756
+ POST /v1/chat
17757
+ {
17758
+ "agentId": "agent_123",
17759
+ "message": "Hello",
17760
+ "metadata": {
17761
+ "customerId": "ent_customer_456",
17762
+ "channel": "web",
17763
+ "language": "en"
17764
+ }
17765
+ }
17766
+ \`\`\`
17767
+
17768
+ Access in system prompt:
17769
+ \`\`\`
17770
+ Customer: {{entity.get({"id": "{{thread.metadata.customerId}}"})}}
17771
+ Channel: {{thread.metadata.channel}}
17772
+ \`\`\`
17773
+
17774
+ ## Development Workflow
17775
+
17776
+ 1. **Edit agent configuration** in \`src/agent.ts\`
17777
+ 2. **Run \`bun run dev\`** to sync changes to Convex
17778
+ 3. **Test via API** or dashboard chat interface
17779
+ 4. **Write tests** in \`tests/*.test.yaml\`
17780
+ 5. **Deploy** with \`bun run deploy\`
17781
+
17782
+ ## API Endpoints
17783
+
17784
+ | Endpoint | Method | Description |
17785
+ |----------|--------|-------------|
17786
+ | \`/v1/chat\` | POST | Chat by agent ID |
17787
+ | \`/v1/agents/:slug/chat\` | POST | Chat by agent slug |
17788
+
17789
+ Authentication: Bearer token (API key from dashboard)
17790
+
17791
+ \`\`\`bash
17792
+ curl -X POST https://your-deployment.convex.cloud/v1/chat \\
17793
+ -H "Authorization: Bearer sk_live_..." \\
17794
+ -H "Content-Type: application/json" \\
17795
+ -d '{"agentId": "...", "message": "Hello"}'
17796
+ \`\`\`
17797
+
17798
+ ## Best Practices
17799
+
17800
+ 1. **System Prompts**: Use templates for dynamic data instead of hardcoding
17801
+ 2. **Tools**: Keep tool handlers focused and stateless
17802
+ 3. **Entities**: Model your domain data as entity types
17803
+ 4. **Events**: Emit events for audit trails and analytics
17804
+ 5. **Jobs**: Use jobs for async operations (emails, notifications)
17805
+ 6. **Testing**: Write tests for critical conversation flows
17806
+ 7. **Context**: Use context for user-specific personalization
17807
+ `;
17808
+ }
17483
17809
 
17484
17810
  // src/cli/utils/scaffold.ts
17485
17811
  function ensureDir(filePath) {
@@ -17519,7 +17845,8 @@ function scaffoldAgentFiles(cwd, projectName) {
17519
17845
  "src/tools.ts": getToolsTs(),
17520
17846
  "src/workflows/.gitkeep": "",
17521
17847
  "tests/basic.test.yaml": getBasicTestYaml(),
17522
- ".env.example": getEnvExample()
17848
+ ".env.example": getEnvExample(),
17849
+ ".claude.md": getClaudeMd(projectName)
17523
17850
  };
17524
17851
  for (const [relativePath, content] of Object.entries(files)) {
17525
17852
  const fullPath = join3(cwd, relativePath);
@@ -17797,6 +18124,7 @@ function readLine() {
17797
18124
  // src/cli/commands/dev.ts
17798
18125
  var import_chokidar = __toESM(require_chokidar(), 1);
17799
18126
  import { join as join7, basename as basename2 } from "path";
18127
+ import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
17800
18128
 
17801
18129
  // src/cli/utils/config.ts
17802
18130
  import { join as join5 } from "path";
@@ -17896,14 +18224,21 @@ var devCommand = new Command("dev").description("Sync agent to development envir
17896
18224
  spinner.start("Loading agent");
17897
18225
  let agent = await loadAgent(cwd);
17898
18226
  spinner.succeed(`Agent "${agent.name}" loaded`);
17899
- const credentials = loadCredentials();
18227
+ const claudeMdPath = join7(cwd, ".claude.md");
18228
+ if (!existsSync5(claudeMdPath)) {
18229
+ writeFileSync4(claudeMdPath, getClaudeMd(project.agent.slug));
18230
+ console.log(source_default.green("\u2713"), "Created .claude.md");
18231
+ }
18232
+ let credentials = loadCredentials();
17900
18233
  const apiKey = getApiKey();
17901
18234
  if (!credentials && !apiKey) {
17902
- spinner.fail("Not logged in");
17903
- console.log();
17904
- console.log(source_default.gray("Run"), source_default.cyan("struere login"), source_default.gray("to authenticate"));
18235
+ console.log(source_default.gray("Authentication required"));
17905
18236
  console.log();
17906
- process.exit(1);
18237
+ credentials = await performLogin();
18238
+ if (!credentials) {
18239
+ console.log(source_default.red("Authentication failed"));
18240
+ process.exit(1);
18241
+ }
17907
18242
  }
17908
18243
  spinner.start("Syncing to Convex");
17909
18244
  const performSync = async () => {
@@ -17918,13 +18253,38 @@ var devCommand = new Command("dev").description("Sync agent to development envir
17918
18253
  throw error;
17919
18254
  }
17920
18255
  };
18256
+ const isAuthError = (error) => {
18257
+ const message = error instanceof Error ? error.message : String(error);
18258
+ return message.includes("Unauthenticated") || message.includes("OIDC") || message.includes("token") || message.includes("expired");
18259
+ };
17921
18260
  try {
17922
18261
  await performSync();
17923
18262
  spinner.succeed("Synced to development");
17924
18263
  } catch (error) {
17925
- spinner.fail("Sync failed");
17926
- console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
17927
- process.exit(1);
18264
+ if (isAuthError(error)) {
18265
+ spinner.fail("Session expired");
18266
+ console.log();
18267
+ console.log(source_default.gray("Re-authenticating..."));
18268
+ clearCredentials();
18269
+ credentials = await performLogin();
18270
+ if (!credentials) {
18271
+ console.log(source_default.red("Authentication failed"));
18272
+ process.exit(1);
18273
+ }
18274
+ spinner.start("Syncing to Convex");
18275
+ try {
18276
+ await performSync();
18277
+ spinner.succeed("Synced to development");
18278
+ } catch (retryError) {
18279
+ spinner.fail("Sync failed");
18280
+ console.log(source_default.red("Error:"), retryError instanceof Error ? retryError.message : String(retryError));
18281
+ process.exit(1);
18282
+ }
18283
+ } else {
18284
+ spinner.fail("Sync failed");
18285
+ console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
18286
+ process.exit(1);
18287
+ }
17928
18288
  }
17929
18289
  const devUrl = `https://${project.agent.slug}-dev.struere.dev`;
17930
18290
  console.log();
@@ -17945,8 +18305,28 @@ var devCommand = new Command("dev").description("Sync agent to development envir
17945
18305
  await performSync();
17946
18306
  syncSpinner.succeed("Synced");
17947
18307
  } catch (error) {
17948
- syncSpinner.fail("Sync failed");
17949
- console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
18308
+ if (isAuthError(error)) {
18309
+ syncSpinner.fail("Session expired");
18310
+ console.log();
18311
+ console.log(source_default.gray("Re-authenticating..."));
18312
+ clearCredentials();
18313
+ const newCredentials = await performLogin();
18314
+ if (!newCredentials) {
18315
+ console.log(source_default.red("Authentication failed"));
18316
+ return;
18317
+ }
18318
+ const retrySyncSpinner = ora("Syncing...").start();
18319
+ try {
18320
+ await performSync();
18321
+ retrySyncSpinner.succeed("Synced");
18322
+ } catch (retryError) {
18323
+ retrySyncSpinner.fail("Sync failed");
18324
+ console.log(source_default.red("Error:"), retryError instanceof Error ? retryError.message : String(retryError));
18325
+ }
18326
+ } else {
18327
+ syncSpinner.fail("Sync failed");
18328
+ console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
18329
+ }
17950
18330
  }
17951
18331
  });
17952
18332
  process.on("SIGINT", () => {
@@ -18768,9 +19148,17 @@ var whoamiCommand = new Command("whoami").description("Show current logged in us
18768
19148
  // package.json
18769
19149
  var package_default = {
18770
19150
  name: "struere",
18771
- version: "0.3.6",
19151
+ version: "0.3.8",
18772
19152
  description: "Build, test, and deploy AI agents",
18773
- keywords: ["ai", "agents", "llm", "anthropic", "openai", "framework", "cli"],
19153
+ keywords: [
19154
+ "ai",
19155
+ "agents",
19156
+ "llm",
19157
+ "anthropic",
19158
+ "openai",
19159
+ "framework",
19160
+ "cli"
19161
+ ],
18774
19162
  author: "struere",
18775
19163
  license: "MIT",
18776
19164
  publishConfig: {
@@ -18797,7 +19185,9 @@ var package_default = {
18797
19185
  types: "./dist/index.d.ts"
18798
19186
  }
18799
19187
  },
18800
- files: ["dist"],
19188
+ files: [
19189
+ "dist"
19190
+ ],
18801
19191
  scripts: {
18802
19192
  build: "bun build ./src/cli/index.ts --outdir ./dist/cli --target bun --external commander --external chalk --external ora --external chokidar --external yaml && bun build ./src/index.ts --outdir ./dist --target node && bun build ./src/bin/struere.ts --outdir ./dist/bin --target bun && tsc --emitDeclarationOnly && chmod +x ./dist/bin/struere.js",
18803
19193
  dev: "tsc --watch",
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAanC,eAAO,MAAM,UAAU,SA8GnB,CAAA"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAenC,eAAO,MAAM,UAAU,SAuKnB,CAAA"}
package/dist/cli/index.js CHANGED
@@ -819,6 +819,332 @@ function getEnvLocal(deploymentUrl) {
819
819
  return `STRUERE_DEPLOYMENT_URL=${deploymentUrl}
820
820
  `;
821
821
  }
822
+ function getClaudeMd(name) {
823
+ const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
824
+ return `# ${displayName} Agent
825
+
826
+ 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.
827
+
828
+ ## Project Structure
829
+
830
+ \`\`\`
831
+ src/
832
+ \u251C\u2500\u2500 agent.ts # Agent definition (system prompt, model, tools)
833
+ \u251C\u2500\u2500 context.ts # Dynamic context injection per request
834
+ \u251C\u2500\u2500 tools.ts # Custom tool definitions
835
+ \u2514\u2500\u2500 workflows/ # Multi-step workflow definitions
836
+ tests/
837
+ \u2514\u2500\u2500 *.test.yaml # YAML-based conversation tests
838
+ struere.json # Project configuration (agentId, team, slug)
839
+ struere.config.ts # Framework settings (port, CORS, logging)
840
+ \`\`\`
841
+
842
+ ## Agent Definition
843
+
844
+ Define your agent in \`src/agent.ts\`:
845
+
846
+ \`\`\`typescript
847
+ import { defineAgent } from 'struere'
848
+ import { tools } from './tools'
849
+ import { context } from './context'
850
+
851
+ export default defineAgent({
852
+ name: 'my-agent',
853
+ version: '0.1.0',
854
+ description: 'My AI Agent',
855
+ model: {
856
+ provider: 'anthropic',
857
+ name: 'claude-sonnet-4-20250514',
858
+ temperature: 0.7,
859
+ maxTokens: 4096,
860
+ },
861
+ systemPrompt: \\\`You are a helpful assistant.
862
+
863
+ Current time: {{datetime}}
864
+ Customer: {{entity.get({"id": "{{thread.metadata.customerId}}"})}}\\\`,
865
+ tools,
866
+ context,
867
+ state: {
868
+ storage: 'memory',
869
+ ttl: 3600,
870
+ },
871
+ })
872
+ \`\`\`
873
+
874
+ ## System Prompt Templates
875
+
876
+ System prompts support dynamic \`{{...}}\` templates that are resolved at runtime before the LLM call.
877
+
878
+ ### Available Variables
879
+
880
+ | Variable | Description |
881
+ |----------|-------------|
882
+ | \`{{organizationId}}\` | Current organization ID |
883
+ | \`{{userId}}\` | Current user ID |
884
+ | \`{{threadId}}\` | Conversation thread ID |
885
+ | \`{{agentId}}\` | Agent ID |
886
+ | \`{{agent.name}}\` | Agent name |
887
+ | \`{{agent.slug}}\` | Agent slug |
888
+ | \`{{thread.metadata.X}}\` | Thread metadata field X |
889
+ | \`{{message}}\` | Current user message |
890
+ | \`{{timestamp}}\` | Unix timestamp (ms) |
891
+ | \`{{datetime}}\` | ISO 8601 datetime |
892
+
893
+ ### Function Calls
894
+
895
+ Call any agent tool directly in the system prompt:
896
+
897
+ \`\`\`
898
+ {{entity.get({"id": "ent_123"})}}
899
+ {{entity.query({"type": "customer", "limit": 5})}}
900
+ {{event.query({"entityId": "ent_123", "limit": 10})}}
901
+ \`\`\`
902
+
903
+ ### Nested Templates
904
+
905
+ Variables can be used inside function arguments:
906
+
907
+ \`\`\`
908
+ {{entity.get({"id": "{{thread.metadata.customerId}}"})}}
909
+ \`\`\`
910
+
911
+ ### Error Handling
912
+
913
+ Failed templates are replaced with inline errors:
914
+ \`\`\`
915
+ [TEMPLATE_ERROR: variableName not found]
916
+ [TEMPLATE_ERROR: toolName - error message]
917
+ \`\`\`
918
+
919
+ ## Custom Tools
920
+
921
+ Define tools in \`src/tools.ts\`:
922
+
923
+ \`\`\`typescript
924
+ import { defineTools } from 'struere'
925
+
926
+ export const tools = defineTools([
927
+ {
928
+ name: 'search_products',
929
+ description: 'Search the product catalog',
930
+ parameters: {
931
+ type: 'object',
932
+ properties: {
933
+ query: { type: 'string', description: 'Search query' },
934
+ limit: { type: 'number', description: 'Max results' },
935
+ },
936
+ required: ['query'],
937
+ },
938
+ handler: async (params) => {
939
+ const results = await searchProducts(params.query, params.limit ?? 10)
940
+ return { products: results }
941
+ },
942
+ },
943
+ ])
944
+ \`\`\`
945
+
946
+ Custom tool handlers are executed in a sandboxed Cloudflare Worker environment. They can make HTTP requests to allowlisted domains:
947
+ - api.openai.com, api.anthropic.com, api.stripe.com
948
+ - api.sendgrid.com, api.twilio.com, hooks.slack.com
949
+ - discord.com, api.github.com
950
+
951
+ ## Built-in Tools
952
+
953
+ Agents have access to these built-in tools for data management:
954
+
955
+ ### Entity Tools
956
+
957
+ | Tool | Description |
958
+ |------|-------------|
959
+ | \`entity.create\` | Create a new entity |
960
+ | \`entity.get\` | Get entity by ID |
961
+ | \`entity.query\` | Query entities by type/filters |
962
+ | \`entity.update\` | Update entity data |
963
+ | \`entity.delete\` | Soft-delete entity |
964
+ | \`entity.link\` | Create entity relation |
965
+ | \`entity.unlink\` | Remove entity relation |
966
+
967
+ Example entity operations:
968
+ \`\`\`json
969
+ // entity.create
970
+ { "type": "customer", "data": { "name": "John", "email": "john@example.com" } }
971
+
972
+ // entity.query
973
+ { "type": "customer", "filters": { "status": "active" }, "limit": 10 }
974
+
975
+ // entity.update
976
+ { "id": "ent_123", "data": { "status": "vip" } }
977
+ \`\`\`
978
+
979
+ ### Event Tools
980
+
981
+ | Tool | Description |
982
+ |------|-------------|
983
+ | \`event.emit\` | Emit a custom event |
984
+ | \`event.query\` | Query event history |
985
+
986
+ Example event operations:
987
+ \`\`\`json
988
+ // event.emit
989
+ { "entityId": "ent_123", "eventType": "order.placed", "payload": { "amount": 99.99 } }
990
+
991
+ // event.query
992
+ { "entityId": "ent_123", "eventType": "order.*", "limit": 20 }
993
+ \`\`\`
994
+
995
+ ### Job Tools
996
+
997
+ | Tool | Description |
998
+ |------|-------------|
999
+ | \`job.enqueue\` | Schedule a background job |
1000
+ | \`job.status\` | Get job status |
1001
+
1002
+ Example job operations:
1003
+ \`\`\`json
1004
+ // job.enqueue
1005
+ { "jobType": "send_email", "payload": { "to": "user@example.com" }, "scheduledFor": 1706745600000 }
1006
+
1007
+ // job.status
1008
+ { "id": "job_abc123" }
1009
+ \`\`\`
1010
+
1011
+ ## Context Function
1012
+
1013
+ Inject dynamic context per request in \`src/context.ts\`:
1014
+
1015
+ \`\`\`typescript
1016
+ import { defineContext } from 'struere'
1017
+
1018
+ export const context = defineContext(async (request) => {
1019
+ const { conversationId, userId, channel, state } = request
1020
+
1021
+ const userProfile = await fetchUserProfile(userId)
1022
+
1023
+ return {
1024
+ additionalContext: \\\`
1025
+ User: \${userProfile.name} (\${userProfile.tier} tier)
1026
+ Conversation: \${conversationId}
1027
+ Channel: \${channel}
1028
+ \\\`,
1029
+ variables: {
1030
+ userId,
1031
+ userTier: userProfile.tier,
1032
+ timestamp: new Date().toISOString(),
1033
+ },
1034
+ }
1035
+ })
1036
+ \`\`\`
1037
+
1038
+ ## Testing
1039
+
1040
+ Write YAML-based conversation tests in \`tests/\`:
1041
+
1042
+ \`\`\`yaml
1043
+ name: Order flow test
1044
+ description: Test the complete order flow
1045
+
1046
+ conversation:
1047
+ - role: user
1048
+ content: I want to order a pizza
1049
+ - role: assistant
1050
+ assertions:
1051
+ - type: contains
1052
+ value: size
1053
+ - type: toolCalled
1054
+ value: get_menu
1055
+
1056
+ - role: user
1057
+ content: Large pepperoni please
1058
+ - role: assistant
1059
+ assertions:
1060
+ - type: toolCalled
1061
+ value: entity.create
1062
+ \`\`\`
1063
+
1064
+ ### Assertion Types
1065
+
1066
+ | Type | Description |
1067
+ |------|-------------|
1068
+ | \`contains\` | Response contains substring |
1069
+ | \`matches\` | Response matches regex |
1070
+ | \`toolCalled\` | Specific tool was called |
1071
+ | \`noToolCalled\` | No tools were called |
1072
+
1073
+ Run tests with:
1074
+ \`\`\`bash
1075
+ bun run test
1076
+ \`\`\`
1077
+
1078
+ ## CLI Commands
1079
+
1080
+ | Command | Description |
1081
+ |---------|-------------|
1082
+ | \`struere dev\` | Start development mode (live sync to Convex) |
1083
+ | \`struere build\` | Validate agent configuration |
1084
+ | \`struere deploy\` | Deploy agent to production |
1085
+ | \`struere test\` | Run YAML conversation tests |
1086
+ | \`struere logs\` | View recent execution logs |
1087
+ | \`struere state\` | Inspect conversation thread state |
1088
+
1089
+ ## Thread Metadata
1090
+
1091
+ Set thread metadata when creating conversations to provide context:
1092
+
1093
+ \`\`\`typescript
1094
+ // Via API
1095
+ POST /v1/chat
1096
+ {
1097
+ "agentId": "agent_123",
1098
+ "message": "Hello",
1099
+ "metadata": {
1100
+ "customerId": "ent_customer_456",
1101
+ "channel": "web",
1102
+ "language": "en"
1103
+ }
1104
+ }
1105
+ \`\`\`
1106
+
1107
+ Access in system prompt:
1108
+ \`\`\`
1109
+ Customer: {{entity.get({"id": "{{thread.metadata.customerId}}"})}}
1110
+ Channel: {{thread.metadata.channel}}
1111
+ \`\`\`
1112
+
1113
+ ## Development Workflow
1114
+
1115
+ 1. **Edit agent configuration** in \`src/agent.ts\`
1116
+ 2. **Run \`bun run dev\`** to sync changes to Convex
1117
+ 3. **Test via API** or dashboard chat interface
1118
+ 4. **Write tests** in \`tests/*.test.yaml\`
1119
+ 5. **Deploy** with \`bun run deploy\`
1120
+
1121
+ ## API Endpoints
1122
+
1123
+ | Endpoint | Method | Description |
1124
+ |----------|--------|-------------|
1125
+ | \`/v1/chat\` | POST | Chat by agent ID |
1126
+ | \`/v1/agents/:slug/chat\` | POST | Chat by agent slug |
1127
+
1128
+ Authentication: Bearer token (API key from dashboard)
1129
+
1130
+ \`\`\`bash
1131
+ curl -X POST https://your-deployment.convex.cloud/v1/chat \\
1132
+ -H "Authorization: Bearer sk_live_..." \\
1133
+ -H "Content-Type: application/json" \\
1134
+ -d '{"agentId": "...", "message": "Hello"}'
1135
+ \`\`\`
1136
+
1137
+ ## Best Practices
1138
+
1139
+ 1. **System Prompts**: Use templates for dynamic data instead of hardcoding
1140
+ 2. **Tools**: Keep tool handlers focused and stateless
1141
+ 3. **Entities**: Model your domain data as entity types
1142
+ 4. **Events**: Emit events for audit trails and analytics
1143
+ 5. **Jobs**: Use jobs for async operations (emails, notifications)
1144
+ 6. **Testing**: Write tests for critical conversation flows
1145
+ 7. **Context**: Use context for user-specific personalization
1146
+ `;
1147
+ }
822
1148
 
823
1149
  // src/cli/utils/scaffold.ts
824
1150
  function ensureDir(filePath) {
@@ -858,7 +1184,8 @@ function scaffoldAgentFiles(cwd, projectName) {
858
1184
  "src/tools.ts": getToolsTs(),
859
1185
  "src/workflows/.gitkeep": "",
860
1186
  "tests/basic.test.yaml": getBasicTestYaml(),
861
- ".env.example": getEnvExample()
1187
+ ".env.example": getEnvExample(),
1188
+ ".claude.md": getClaudeMd(projectName)
862
1189
  };
863
1190
  for (const [relativePath, content] of Object.entries(files)) {
864
1191
  const fullPath = join3(cwd, relativePath);
@@ -1139,6 +1466,7 @@ import chalk3 from "chalk";
1139
1466
  import ora3 from "ora";
1140
1467
  import chokidar from "chokidar";
1141
1468
  import { join as join7, basename as basename2 } from "path";
1469
+ import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
1142
1470
 
1143
1471
  // src/cli/utils/config.ts
1144
1472
  import { join as join5 } from "path";
@@ -1238,14 +1566,21 @@ var devCommand = new Command3("dev").description("Sync agent to development envi
1238
1566
  spinner.start("Loading agent");
1239
1567
  let agent = await loadAgent(cwd);
1240
1568
  spinner.succeed(`Agent "${agent.name}" loaded`);
1241
- const credentials = loadCredentials();
1569
+ const claudeMdPath = join7(cwd, ".claude.md");
1570
+ if (!existsSync5(claudeMdPath)) {
1571
+ writeFileSync4(claudeMdPath, getClaudeMd(project.agent.slug));
1572
+ console.log(chalk3.green("\u2713"), "Created .claude.md");
1573
+ }
1574
+ let credentials = loadCredentials();
1242
1575
  const apiKey = getApiKey();
1243
1576
  if (!credentials && !apiKey) {
1244
- spinner.fail("Not logged in");
1245
- console.log();
1246
- console.log(chalk3.gray("Run"), chalk3.cyan("struere login"), chalk3.gray("to authenticate"));
1577
+ console.log(chalk3.gray("Authentication required"));
1247
1578
  console.log();
1248
- process.exit(1);
1579
+ credentials = await performLogin();
1580
+ if (!credentials) {
1581
+ console.log(chalk3.red("Authentication failed"));
1582
+ process.exit(1);
1583
+ }
1249
1584
  }
1250
1585
  spinner.start("Syncing to Convex");
1251
1586
  const performSync = async () => {
@@ -1260,13 +1595,38 @@ var devCommand = new Command3("dev").description("Sync agent to development envi
1260
1595
  throw error;
1261
1596
  }
1262
1597
  };
1598
+ const isAuthError = (error) => {
1599
+ const message = error instanceof Error ? error.message : String(error);
1600
+ return message.includes("Unauthenticated") || message.includes("OIDC") || message.includes("token") || message.includes("expired");
1601
+ };
1263
1602
  try {
1264
1603
  await performSync();
1265
1604
  spinner.succeed("Synced to development");
1266
1605
  } catch (error) {
1267
- spinner.fail("Sync failed");
1268
- console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
1269
- process.exit(1);
1606
+ if (isAuthError(error)) {
1607
+ spinner.fail("Session expired");
1608
+ console.log();
1609
+ console.log(chalk3.gray("Re-authenticating..."));
1610
+ clearCredentials();
1611
+ credentials = await performLogin();
1612
+ if (!credentials) {
1613
+ console.log(chalk3.red("Authentication failed"));
1614
+ process.exit(1);
1615
+ }
1616
+ spinner.start("Syncing to Convex");
1617
+ try {
1618
+ await performSync();
1619
+ spinner.succeed("Synced to development");
1620
+ } catch (retryError) {
1621
+ spinner.fail("Sync failed");
1622
+ console.log(chalk3.red("Error:"), retryError instanceof Error ? retryError.message : String(retryError));
1623
+ process.exit(1);
1624
+ }
1625
+ } else {
1626
+ spinner.fail("Sync failed");
1627
+ console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
1628
+ process.exit(1);
1629
+ }
1270
1630
  }
1271
1631
  const devUrl = `https://${project.agent.slug}-dev.struere.dev`;
1272
1632
  console.log();
@@ -1287,8 +1647,28 @@ var devCommand = new Command3("dev").description("Sync agent to development envi
1287
1647
  await performSync();
1288
1648
  syncSpinner.succeed("Synced");
1289
1649
  } catch (error) {
1290
- syncSpinner.fail("Sync failed");
1291
- console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
1650
+ if (isAuthError(error)) {
1651
+ syncSpinner.fail("Session expired");
1652
+ console.log();
1653
+ console.log(chalk3.gray("Re-authenticating..."));
1654
+ clearCredentials();
1655
+ const newCredentials = await performLogin();
1656
+ if (!newCredentials) {
1657
+ console.log(chalk3.red("Authentication failed"));
1658
+ return;
1659
+ }
1660
+ const retrySyncSpinner = ora3("Syncing...").start();
1661
+ try {
1662
+ await performSync();
1663
+ retrySyncSpinner.succeed("Synced");
1664
+ } catch (retryError) {
1665
+ retrySyncSpinner.fail("Sync failed");
1666
+ console.log(chalk3.red("Error:"), retryError instanceof Error ? retryError.message : String(retryError));
1667
+ }
1668
+ } else {
1669
+ syncSpinner.fail("Sync failed");
1670
+ console.log(chalk3.red("Error:"), error instanceof Error ? error.message : String(error));
1671
+ }
1292
1672
  }
1293
1673
  });
1294
1674
  process.on("SIGINT", () => {
@@ -2133,9 +2513,17 @@ var whoamiCommand = new Command11("whoami").description("Show current logged in
2133
2513
  // package.json
2134
2514
  var package_default = {
2135
2515
  name: "struere",
2136
- version: "0.3.6",
2516
+ version: "0.3.8",
2137
2517
  description: "Build, test, and deploy AI agents",
2138
- keywords: ["ai", "agents", "llm", "anthropic", "openai", "framework", "cli"],
2518
+ keywords: [
2519
+ "ai",
2520
+ "agents",
2521
+ "llm",
2522
+ "anthropic",
2523
+ "openai",
2524
+ "framework",
2525
+ "cli"
2526
+ ],
2139
2527
  author: "struere",
2140
2528
  license: "MIT",
2141
2529
  publishConfig: {
@@ -2162,7 +2550,9 @@ var package_default = {
2162
2550
  types: "./dist/index.d.ts"
2163
2551
  }
2164
2552
  },
2165
- files: ["dist"],
2553
+ files: [
2554
+ "dist"
2555
+ ],
2166
2556
  scripts: {
2167
2557
  build: "bun build ./src/cli/index.ts --outdir ./dist/cli --target bun --external commander --external chalk --external ora --external chokidar --external yaml && bun build ./src/index.ts --outdir ./dist --target node && bun build ./src/bin/struere.ts --outdir ./dist/bin --target bun && tsc --emitDeclarationOnly && chmod +x ./dist/bin/struere.js",
2168
2558
  dev: "tsc --watch",
@@ -9,4 +9,5 @@ export declare function getEnvExample(): string;
9
9
  export declare function getGitignore(): string;
10
10
  export declare function getStruereJson(agentId: string, team: string, slug: string, name: string): string;
11
11
  export declare function getEnvLocal(deploymentUrl: string): string;
12
+ export declare function getClaudeMd(name: string): string;
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/templates/index.ts"],"names":[],"mappings":"AAAA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuBnD;AAED,wBAAgB,WAAW,IAAI,MAAM,CAsBpC;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAgBzC;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoC/C;AAED,wBAAgB,YAAY,IAAI,MAAM,CAkBrC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAoDnC;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAmBzC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAatC;AAED,wBAAgB,YAAY,IAAI,MAAM,CAgBrC;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAahG;AAED,wBAAgB,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAGzD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/templates/index.ts"],"names":[],"mappings":"AAAA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuBnD;AAED,wBAAgB,WAAW,IAAI,MAAM,CAsBpC;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAgBzC;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoC/C;AAED,wBAAgB,YAAY,IAAI,MAAM,CAkBrC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAoDnC;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAmBzC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAatC;AAED,wBAAgB,YAAY,IAAI,MAAM,CAgBrC;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAahG;AAED,wBAAgB,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyUhD"}
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/scaffold.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,YAAY,EAAE,MAAM,EAAE,CAAA;CACvB;AAeD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc,CAexF;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc,CA+BrF;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,cAAc,CA8BnF;AAsBD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAElD"}
1
+ {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/scaffold.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,YAAY,EAAE,MAAM,EAAE,CAAA;CACvB;AAeD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc,CAexF;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc,CAgCrF;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,cAAc,CA+BnF;AAsBD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAElD"}
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "struere",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Build, test, and deploy AI agents",
5
- "keywords": ["ai", "agents", "llm", "anthropic", "openai", "framework", "cli"],
5
+ "keywords": [
6
+ "ai",
7
+ "agents",
8
+ "llm",
9
+ "anthropic",
10
+ "openai",
11
+ "framework",
12
+ "cli"
13
+ ],
6
14
  "author": "struere",
7
15
  "license": "MIT",
8
16
  "publishConfig": {
@@ -29,7 +37,9 @@
29
37
  "types": "./dist/index.d.ts"
30
38
  }
31
39
  },
32
- "files": ["dist"],
40
+ "files": [
41
+ "dist"
42
+ ],
33
43
  "scripts": {
34
44
  "build": "bun build ./src/cli/index.ts --outdir ./dist/cli --target bun --external commander --external chalk --external ora --external chokidar --external yaml && bun build ./src/index.ts --outdir ./dist --target node && bun build ./src/bin/struere.ts --outdir ./dist/bin --target bun && tsc --emitDeclarationOnly && chmod +x ./dist/bin/struere.js",
35
45
  "dev": "tsc --watch",