struere 0.12.1 → 0.12.3

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.
@@ -35,6 +35,7 @@ __export(exports_convex, {
35
35
  getPullState: () => getPullState,
36
36
  createOrganization: () => createOrganization,
37
37
  compilePrompt: () => compilePrompt,
38
+ chatWithRouter: () => chatWithRouter,
38
39
  chatWithAgent: () => chatWithAgent
39
40
  });
40
41
  async function refreshToken() {
@@ -184,6 +185,7 @@ async function syncViaHttp(apiKey, payload) {
184
185
  roles: payload.roles,
185
186
  evalSuites: payload.evalSuites,
186
187
  triggers: payload.triggers,
188
+ routers: payload.routers,
187
189
  fixtures: payload.fixtures
188
190
  }),
189
191
  signal: AbortSignal.timeout(30000)
@@ -635,6 +637,65 @@ async function chatWithAgent(options) {
635
637
  return { error: `Network error: ${err instanceof Error ? err.message : String(err)}` };
636
638
  }
637
639
  }
640
+ async function chatWithRouter(options) {
641
+ const credentials = loadCredentials();
642
+ const apiKey = getApiKey();
643
+ if (credentials?.sessionId) {
644
+ await refreshToken();
645
+ }
646
+ const freshCredentials = loadCredentials();
647
+ const token = apiKey || freshCredentials?.token;
648
+ if (!token) {
649
+ return { error: "Not authenticated" };
650
+ }
651
+ try {
652
+ const response = await fetch(`${CONVEX_URL}/api/action`, {
653
+ method: "POST",
654
+ headers: {
655
+ "Content-Type": "application/json",
656
+ Authorization: `Bearer ${token}`
657
+ },
658
+ body: JSON.stringify({
659
+ path: "chat:sendByRouterSlug",
660
+ args: {
661
+ routerSlug: options.routerSlug,
662
+ message: options.message,
663
+ threadId: options.threadId,
664
+ phoneNumber: options.phoneNumber,
665
+ environment: options.environment,
666
+ channel: options.channel || "api"
667
+ }
668
+ }),
669
+ signal: options.signal || AbortSignal.timeout(120000)
670
+ });
671
+ const text = await response.text();
672
+ let json;
673
+ try {
674
+ json = JSON.parse(text);
675
+ } catch {
676
+ return { error: text || `HTTP ${response.status}` };
677
+ }
678
+ if (!response.ok) {
679
+ const msg = json.errorData?.message || json.errorMessage || text;
680
+ return { error: msg };
681
+ }
682
+ if (json.status === "success" && json.value) {
683
+ return { result: json.value };
684
+ }
685
+ if (json.status === "success" && json.value === null) {
686
+ return { error: `Router not found or no config for environment: ${options.environment}` };
687
+ }
688
+ if (json.status === "error") {
689
+ return { error: json.errorData?.message || json.errorMessage || "Unknown error from Convex" };
690
+ }
691
+ return { error: `Unexpected response: ${text}` };
692
+ } catch (err) {
693
+ if (err instanceof DOMException && err.name === "TimeoutError") {
694
+ return { error: "Request timed out after 120s" };
695
+ }
696
+ return { error: `Network error: ${err instanceof Error ? err.message : String(err)}` };
697
+ }
698
+ }
638
699
  async function getPullState(organizationId, environment = "development") {
639
700
  const credentials = loadCredentials();
640
701
  const apiKey = getApiKey();
@@ -1343,6 +1404,26 @@ export default defineTrigger({
1343
1404
  })
1344
1405
  `;
1345
1406
  }
1407
+ function getRouterTs(name, slug) {
1408
+ return `import { defineRouter } from 'struere'
1409
+
1410
+ export default defineRouter({
1411
+ name: "${name}",
1412
+ slug: "${slug}",
1413
+ mode: "rules",
1414
+ agents: [
1415
+ { slug: "agent-one", description: "Handles ..." },
1416
+ ],
1417
+ rules: [
1418
+ {
1419
+ conditions: [{ field: "channel", operator: "eq", value: "whatsapp" }],
1420
+ route: "agent-one",
1421
+ },
1422
+ ],
1423
+ fallback: "agent-one",
1424
+ })
1425
+ `;
1426
+ }
1346
1427
 
1347
1428
  // src/cli/utils/scaffold.ts
1348
1429
  function ensureDir(filePath) {
@@ -1368,6 +1449,7 @@ function scaffoldProject(cwd, options) {
1368
1449
  "tools",
1369
1450
  "evals",
1370
1451
  "triggers",
1452
+ "routers",
1371
1453
  "fixtures",
1372
1454
  ".struere"
1373
1455
  ];
@@ -1487,6 +1569,24 @@ function scaffoldTrigger(cwd, name, slug) {
1487
1569
  result.createdFiles.push(`triggers/${fileName}`);
1488
1570
  return result;
1489
1571
  }
1572
+ function scaffoldRouter(cwd, name, slug) {
1573
+ const result = {
1574
+ createdFiles: [],
1575
+ updatedFiles: []
1576
+ };
1577
+ const routersDir = join3(cwd, "routers");
1578
+ if (!existsSync3(routersDir)) {
1579
+ mkdirSync2(routersDir, { recursive: true });
1580
+ }
1581
+ const fileName = `${slug}.ts`;
1582
+ const filePath = join3(routersDir, fileName);
1583
+ if (existsSync3(filePath)) {
1584
+ return result;
1585
+ }
1586
+ writeFileSync2(filePath, getRouterTs(name, slug));
1587
+ result.createdFiles.push(`routers/${fileName}`);
1588
+ return result;
1589
+ }
1490
1590
  function scaffoldFixture(cwd, name, slug) {
1491
1591
  const result = {
1492
1592
  createdFiles: [],
@@ -1916,9 +2016,10 @@ async function loadAllResources(cwd) {
1916
2016
  const { suites: evalSuites, errors: evalErrors } = loadEvalSuites(join5(cwd, "evals"));
1917
2017
  errors.push(...evalErrors);
1918
2018
  const triggers = await loadTsDirectory(join5(cwd, "triggers"));
2019
+ const routers = await loadTsDirectory(join5(cwd, "routers"));
1919
2020
  const { fixtures, errors: fixtureErrors } = loadFixtures(join5(cwd, "fixtures"));
1920
2021
  errors.push(...fixtureErrors);
1921
- return { agents, entityTypes, roles, customTools, evalSuites, triggers, fixtures, errors };
2022
+ return { agents, entityTypes, roles, customTools, evalSuites, triggers, routers, fixtures, errors };
1922
2023
  }
1923
2024
  async function loadTsDirectory(dir) {
1924
2025
  if (!existsSync5(dir)) {
@@ -2005,6 +2106,7 @@ function getResourceDirectories(cwd) {
2005
2106
  tools: join5(cwd, "tools"),
2006
2107
  evals: join5(cwd, "evals"),
2007
2108
  triggers: join5(cwd, "triggers"),
2109
+ routers: join5(cwd, "routers"),
2008
2110
  fixtures: join5(cwd, "fixtures")
2009
2111
  };
2010
2112
  }
@@ -2143,6 +2245,7 @@ async function generateDocs(cwd, targets) {
2143
2245
  customTools: [],
2144
2246
  evalSuites: [],
2145
2247
  triggers: [],
2248
+ routers: [],
2146
2249
  fixtures: [],
2147
2250
  errors: []
2148
2251
  });
@@ -2687,6 +2790,29 @@ function extractSyncPayload(resources) {
2687
2790
  schedule: t.schedule,
2688
2791
  retry: t.retry
2689
2792
  })) : undefined;
2793
+ const routers = resources.routers.length > 0 ? resources.routers.map((r) => ({
2794
+ name: r.name,
2795
+ slug: r.slug,
2796
+ description: r.description,
2797
+ mode: r.mode,
2798
+ agents: r.agents.map((a) => ({
2799
+ slug: a.slug,
2800
+ description: a.description
2801
+ })),
2802
+ rules: r.rules?.map((rule) => ({
2803
+ conditions: rule.conditions.map((c) => ({
2804
+ field: c.field,
2805
+ operator: c.operator,
2806
+ value: c.value
2807
+ })),
2808
+ route: rule.route
2809
+ })),
2810
+ fallback: r.fallback,
2811
+ classifyModel: r.classifyModel,
2812
+ contextMessages: r.contextMessages,
2813
+ maxTransfers: r.maxTransfers,
2814
+ inactivityResetMs: r.inactivityResetMs
2815
+ })) : undefined;
2690
2816
  const fixtures = resources.fixtures.length > 0 ? resources.fixtures.map((f) => ({
2691
2817
  name: f.name,
2692
2818
  slug: f.slug,
@@ -2703,7 +2829,7 @@ function extractSyncPayload(resources) {
2703
2829
  metadata: r.metadata
2704
2830
  }))
2705
2831
  })) : undefined;
2706
- return { agents, entityTypes, roles, evalSuites, triggers, fixtures };
2832
+ return { agents, entityTypes, roles, evalSuites, triggers, routers, fixtures };
2707
2833
  }
2708
2834
  function extractAgentPayload(agent, customToolsMap) {
2709
2835
  let systemPrompt;
@@ -3132,6 +3258,7 @@ ${resources.errors.join(`
3132
3258
  entityTypes: payload.entityTypes,
3133
3259
  roles: payload.roles,
3134
3260
  triggers: payload.triggers,
3261
+ routers: payload.routers,
3135
3262
  organizationId,
3136
3263
  environment: "development"
3137
3264
  });
@@ -3165,7 +3292,8 @@ async function checkForDeletions(resources, organizationId, environment) {
3165
3292
  entityTypes: new Set(payload.entityTypes.map((et) => et.slug)),
3166
3293
  roles: new Set(payload.roles.map((r) => r.name)),
3167
3294
  evalSuites: new Set((payload.evalSuites || []).map((es) => es.slug)),
3168
- triggers: new Set((payload.triggers || []).map((t) => t.slug))
3295
+ triggers: new Set((payload.triggers || []).map((t) => t.slug)),
3296
+ routers: new Set((payload.routers || []).map((r) => r.slug))
3169
3297
  };
3170
3298
  const deletions = [];
3171
3299
  const deletedAgents = remoteState.agents.filter((a) => !localSlugs.agents.has(a.slug)).map((a) => a.name);
@@ -3185,6 +3313,10 @@ async function checkForDeletions(resources, organizationId, environment) {
3185
3313
  const deletedTriggers = remoteTriggers.filter((t) => !localSlugs.triggers.has(t.slug)).map((t) => t.name);
3186
3314
  if (deletedTriggers.length > 0)
3187
3315
  deletions.push({ type: "Triggers", remote: remoteTriggers.length, local: (payload.triggers || []).length, deleted: deletedTriggers });
3316
+ const remoteRouters = remoteState.routers || [];
3317
+ const deletedRouters = remoteRouters.filter((r) => !localSlugs.routers.has(r.slug)).map((r) => r.name);
3318
+ if (deletedRouters.length > 0)
3319
+ deletions.push({ type: "Routers", remote: remoteRouters.length, local: (payload.routers || []).length, deleted: deletedRouters });
3188
3320
  return deletions;
3189
3321
  }
3190
3322
  async function syncToEnvironment(cwd, organizationId, environment) {
@@ -3304,6 +3436,7 @@ var syncCommand = new Command5("sync").description("Sync resources to Convex and
3304
3436
  entityTypes: payload.entityTypes.map((et) => et.slug),
3305
3437
  roles: payload.roles.map((r) => r.name),
3306
3438
  triggers: (payload.triggers || []).map((t) => t.slug),
3439
+ routers: (payload.routers || []).map((r) => r.slug),
3307
3440
  deletions: deletions.map((d) => ({ type: d.type, names: d.deleted }))
3308
3441
  }));
3309
3442
  } else {
@@ -3313,6 +3446,7 @@ var syncCommand = new Command5("sync").description("Sync resources to Convex and
3313
3446
  console.log(chalk6.gray(" Data types:"), payload.entityTypes.map((et) => et.slug).join(", ") || "none");
3314
3447
  console.log(chalk6.gray(" Roles:"), payload.roles.map((r) => r.name).join(", ") || "none");
3315
3448
  console.log(chalk6.gray(" Triggers:"), (payload.triggers || []).map((t) => t.slug).join(", ") || "none");
3449
+ console.log(chalk6.gray(" Routers:"), (payload.routers || []).map((r) => r.slug).join(", ") || "none");
3316
3450
  if (deletions.length > 0) {
3317
3451
  console.log();
3318
3452
  console.log(chalk6.yellow.bold(" Would delete:"));
@@ -3477,7 +3611,7 @@ var devCommand = new Command6("dev").description("Watch files and sync to develo
3477
3611
  spinner.start("Loading resources");
3478
3612
  try {
3479
3613
  loadedResources = await loadAllResources(cwd);
3480
- spinner.succeed(`Loaded ${loadedResources.agents.length} agents, ${loadedResources.entityTypes.length} data types, ${loadedResources.roles.length} roles, ${loadedResources.customTools.length} custom tools, ${loadedResources.evalSuites.length} eval suites, ${loadedResources.triggers.length} triggers, ${loadedResources.fixtures.length} fixtures`);
3614
+ spinner.succeed(`Loaded ${loadedResources.agents.length} agents, ${loadedResources.entityTypes.length} data types, ${loadedResources.roles.length} roles, ${loadedResources.customTools.length} custom tools, ${loadedResources.evalSuites.length} eval suites, ${loadedResources.triggers.length} triggers, ${loadedResources.routers.length} routers, ${loadedResources.fixtures.length} fixtures`);
3481
3615
  for (const err of loadedResources.errors) {
3482
3616
  console.log(chalk7.red(" \u2716"), err);
3483
3617
  }
@@ -3572,6 +3706,7 @@ var devCommand = new Command6("dev").description("Watch files and sync to develo
3572
3706
  dirs.tools,
3573
3707
  dirs.evals,
3574
3708
  dirs.triggers,
3709
+ dirs.routers,
3575
3710
  dirs.fixtures
3576
3711
  ].filter((p) => existsSync8(p));
3577
3712
  const watcher = chokidar.watch(watchPaths, {
@@ -4069,7 +4204,7 @@ var whoamiCommand = new Command9("whoami").description("Show current logged in u
4069
4204
  // src/cli/commands/add.ts
4070
4205
  import { Command as Command10 } from "commander";
4071
4206
  import chalk11 from "chalk";
4072
- var addCommand = new Command10("add").description("Scaffold a new resource").argument("<type>", "Resource type: agent, data-type, role, eval, trigger, or fixture").argument("<name>", "Resource name").action(async (type, name) => {
4207
+ var addCommand = new Command10("add").description("Scaffold a new resource").argument("<type>", "Resource type: agent, data-type, role, eval, trigger, router, or fixture").argument("<name>", "Resource name").action(async (type, name) => {
4073
4208
  const cwd = process.cwd();
4074
4209
  console.log();
4075
4210
  if (!hasProject(cwd)) {
@@ -4147,6 +4282,17 @@ var addCommand = new Command10("add").description("Scaffold a new resource").arg
4147
4282
  console.log(chalk11.yellow("Trigger already exists:"), `triggers/${slug}.ts`);
4148
4283
  }
4149
4284
  break;
4285
+ case "router":
4286
+ result = scaffoldRouter(cwd, displayName, slug);
4287
+ if (result.createdFiles.length > 0) {
4288
+ console.log(chalk11.green("\u2713"), `Created router "${displayName}"`);
4289
+ for (const file of result.createdFiles) {
4290
+ console.log(chalk11.gray(" \u2192"), file);
4291
+ }
4292
+ } else {
4293
+ console.log(chalk11.yellow("Router already exists:"), `routers/${slug}.ts`);
4294
+ }
4295
+ break;
4150
4296
  case "fixture":
4151
4297
  result = scaffoldFixture(cwd, displayName, slug);
4152
4298
  if (result.createdFiles.length > 0) {
@@ -4169,6 +4315,7 @@ var addCommand = new Command10("add").description("Scaffold a new resource").arg
4169
4315
  console.log(chalk11.gray(" -"), chalk11.cyan("role"), "- Create a role with permissions");
4170
4316
  console.log(chalk11.gray(" -"), chalk11.cyan("eval"), "- Create an eval suite (YAML)");
4171
4317
  console.log(chalk11.gray(" -"), chalk11.cyan("trigger"), "- Create a data trigger");
4318
+ console.log(chalk11.gray(" -"), chalk11.cyan("router"), "- Create a message router");
4172
4319
  console.log(chalk11.gray(" -"), chalk11.cyan("fixture"), "- Create a test data fixture (YAML)");
4173
4320
  console.log();
4174
4321
  process.exit(1);
@@ -4298,6 +4445,8 @@ var statusCommand = new Command11("status").description("Compare local vs remote
4298
4445
  const devEntityTypeSlugs = new Set(devState.entityTypes.map((et) => et.slug));
4299
4446
  const localRoleNames = new Set(localResources.roles.map((r) => r.name));
4300
4447
  const devRoleNames = new Set(devState.roles.map((r) => r.name));
4448
+ const localRouterSlugs = new Set(localResources.routers.map((r) => r.slug));
4449
+ const devRouterSlugs = new Set((devState.routers || []).map((r) => r.slug));
4301
4450
  if (opts.json) {
4302
4451
  const classify = (localItems, remoteItems, useSlug) => {
4303
4452
  const localKeys = new Set(localItems.map((i) => useSlug ? i.slug : i.name));
@@ -4311,7 +4460,8 @@ var statusCommand = new Command11("status").description("Compare local vs remote
4311
4460
  console.log(JSON.stringify({
4312
4461
  agents: classify(localResources.agents, devState.agents, true),
4313
4462
  entityTypes: classify(localResources.entityTypes, devState.entityTypes, true),
4314
- roles: classify(localResources.roles, devState.roles, false)
4463
+ roles: classify(localResources.roles, devState.roles, false),
4464
+ routers: classify(localResources.routers, devState.routers || [], true)
4315
4465
  }));
4316
4466
  return;
4317
4467
  }
@@ -4381,6 +4531,26 @@ var statusCommand = new Command11("status").description("Compare local vs remote
4381
4531
  }
4382
4532
  }
4383
4533
  console.log();
4534
+ console.log(chalk12.bold("Routers"));
4535
+ console.log(chalk12.gray("\u2500".repeat(60)));
4536
+ if (localResources.routers.length === 0 && (devState.routers || []).length === 0) {
4537
+ console.log(chalk12.gray(" No routers"));
4538
+ } else {
4539
+ for (const router of localResources.routers) {
4540
+ const remote = (devState.routers || []).find((r) => r.slug === router.slug);
4541
+ if (remote) {
4542
+ console.log(` ${chalk12.green("\u25CF")} ${chalk12.cyan(router.name)} (${router.slug}) - ${router.mode}`);
4543
+ } else {
4544
+ console.log(` ${chalk12.blue("+")} ${chalk12.cyan(router.name)} (${router.slug}) - ${chalk12.blue("new")}`);
4545
+ }
4546
+ }
4547
+ for (const remote of devState.routers || []) {
4548
+ if (!localRouterSlugs.has(remote.slug)) {
4549
+ console.log(` ${chalk12.red("-")} ${remote.name} (${remote.slug}) - ${chalk12.red("will be deleted")}`);
4550
+ }
4551
+ }
4552
+ }
4553
+ console.log();
4384
4554
  console.log(chalk12.gray("Legend:"));
4385
4555
  console.log(chalk12.gray(" "), chalk12.green("\u25CF"), "Synced", chalk12.yellow("\u25CB"), "Not in production", chalk12.blue("+"), "New", chalk12.red("-"), "Will be deleted");
4386
4556
  console.log();
@@ -4655,6 +4825,53 @@ ${parts.join(`,
4655
4825
  })
4656
4826
  `;
4657
4827
  }
4828
+ function generateRouterFile(router) {
4829
+ const agentLines = router.agents.map((a) => {
4830
+ const aParts = [
4831
+ ` slug: "${a.slug}"`,
4832
+ ` description: "${a.description}"`
4833
+ ];
4834
+ return ` {
4835
+ ${aParts.join(`,
4836
+ `)},
4837
+ }`;
4838
+ });
4839
+ const parts = [
4840
+ ` name: "${router.name}"`,
4841
+ ` slug: "${router.slug}"`
4842
+ ];
4843
+ if (router.description) {
4844
+ parts.push(` description: "${router.description}"`);
4845
+ }
4846
+ parts.push(` mode: "${router.mode}"`);
4847
+ parts.push(` agents: [
4848
+ ${agentLines.join(`,
4849
+ `)},
4850
+ ]`);
4851
+ if (router.rules && router.rules.length > 0) {
4852
+ parts.push(` rules: ${stringifyValue(router.rules, 2)}`);
4853
+ }
4854
+ parts.push(` fallback: "${router.fallback}"`);
4855
+ if (router.classifyModel) {
4856
+ parts.push(` classifyModel: ${stringifyValue(router.classifyModel, 2)}`);
4857
+ }
4858
+ if (router.contextMessages !== undefined) {
4859
+ parts.push(` contextMessages: ${router.contextMessages}`);
4860
+ }
4861
+ if (router.maxTransfers !== undefined) {
4862
+ parts.push(` maxTransfers: ${router.maxTransfers}`);
4863
+ }
4864
+ if (router.inactivityResetMs !== undefined) {
4865
+ parts.push(` inactivityResetMs: ${router.inactivityResetMs}`);
4866
+ }
4867
+ return `import { defineRouter } from 'struere'
4868
+
4869
+ export default defineRouter({
4870
+ ${parts.join(`,
4871
+ `)},
4872
+ })
4873
+ `;
4874
+ }
4658
4875
  function generateIndexFile(type, slugs) {
4659
4876
  if (slugs.length === 0) {
4660
4877
  return "";
@@ -4789,6 +5006,7 @@ var pullCommand = new Command12("pull").description("Pull remote resources to lo
4789
5006
  ensureDir2(join9(cwd, "entity-types"));
4790
5007
  ensureDir2(join9(cwd, "roles"));
4791
5008
  ensureDir2(join9(cwd, "triggers"));
5009
+ ensureDir2(join9(cwd, "routers"));
4792
5010
  ensureDir2(join9(cwd, "tools"));
4793
5011
  const agentSlugs = [];
4794
5012
  for (const agent of state.agents) {
@@ -4816,6 +5034,12 @@ var pullCommand = new Command12("pull").description("Pull remote resources to lo
4816
5034
  const content = generateTriggerFile(trigger);
4817
5035
  writeOrSkip(`triggers/${trigger.slug}.ts`, content);
4818
5036
  }
5037
+ const routerSlugs = [];
5038
+ for (const router of state.routers || []) {
5039
+ routerSlugs.push(router.slug);
5040
+ const content = generateRouterFile(router);
5041
+ writeOrSkip(`routers/${router.slug}.ts`, content);
5042
+ }
4819
5043
  const customTools = collectCustomTools(state.agents);
4820
5044
  if (customTools.length > 0) {
4821
5045
  const content = generateToolsFile(customTools);
@@ -4841,6 +5065,11 @@ var pullCommand = new Command12("pull").description("Pull remote resources to lo
4841
5065
  if (content)
4842
5066
  writeOrSkip("triggers/index.ts", content);
4843
5067
  }
5068
+ if (routerSlugs.length > 0) {
5069
+ const content = generateIndexFile("routers", routerSlugs);
5070
+ if (content)
5071
+ writeOrSkip("routers/index.ts", content);
5072
+ }
4844
5073
  await installSkill(cwd);
4845
5074
  if (options.json) {
4846
5075
  console.log(JSON.stringify({
@@ -6418,6 +6647,53 @@ async function getTemplateStatus(connectionId, name) {
6418
6647
  name
6419
6648
  });
6420
6649
  }
6650
+ async function convexMutation3(path, args) {
6651
+ const token = getToken2();
6652
+ if (!token)
6653
+ return { error: "Not authenticated" };
6654
+ const response = await fetch(`${CONVEX_URL}/api/mutation`, {
6655
+ method: "POST",
6656
+ headers: {
6657
+ "Content-Type": "application/json",
6658
+ Authorization: `Bearer ${token}`
6659
+ },
6660
+ body: JSON.stringify({ path, args })
6661
+ });
6662
+ const text = await response.text();
6663
+ let json;
6664
+ try {
6665
+ json = JSON.parse(text);
6666
+ } catch {
6667
+ return { error: text || `HTTP ${response.status}` };
6668
+ }
6669
+ if (!response.ok) {
6670
+ const msg = json.errorData?.message || json.message || json.errorMessage || text;
6671
+ return { error: String(msg) };
6672
+ }
6673
+ if (json.status === "success")
6674
+ return { data: json.value };
6675
+ if (json.status === "error")
6676
+ return { error: String(json.errorMessage || "Unknown error") };
6677
+ return { error: `Unexpected response: ${text}` };
6678
+ }
6679
+ async function listRouters(env) {
6680
+ return convexQuery4("routers:list", { environment: env });
6681
+ }
6682
+ async function setConnectionRouter(connectionId, routerId) {
6683
+ return convexMutation3("whatsapp:setPhoneAgent", {
6684
+ connectionId,
6685
+ routerId
6686
+ });
6687
+ }
6688
+ async function setConnectionAgent(connectionId, agentId) {
6689
+ return convexMutation3("whatsapp:setPhoneAgent", {
6690
+ connectionId,
6691
+ agentId
6692
+ });
6693
+ }
6694
+ async function resolveAgentBySlug(slug) {
6695
+ return convexQuery4("agents:getBySlug", { slug });
6696
+ }
6421
6697
  async function updateTemplate(connectionId, templateId, updates) {
6422
6698
  if (getApiKey()) {
6423
6699
  return httpPost("/v1/templates/update", { connectionId, templateId, ...updates });
@@ -6791,7 +7067,7 @@ async function convexQuery5(path, args) {
6791
7067
  return { error: String(json.errorMessage || "Unknown error") };
6792
7068
  return { error: `Unexpected response: ${text}` };
6793
7069
  }
6794
- async function convexMutation3(path, args) {
7070
+ async function convexMutation4(path, args) {
6795
7071
  const result = await getValidToken();
6796
7072
  if ("error" in result)
6797
7073
  return { error: result.error };
@@ -6855,19 +7131,19 @@ async function getIntegrationConfig(provider, env) {
6855
7131
  return convexQuery5("integrations:getConfig", { provider, environment: env });
6856
7132
  }
6857
7133
  async function updateIntegrationConfig(provider, env, config) {
6858
- return convexMutation3("integrations:updateConfig", { provider, environment: env, config });
7134
+ return convexMutation4("integrations:updateConfig", { provider, environment: env, config });
6859
7135
  }
6860
7136
  async function testIntegrationConnection(provider, env) {
6861
7137
  return convexAction2("integrations:testConnection", { provider, environment: env });
6862
7138
  }
6863
7139
  async function deleteIntegrationConfig(provider, env) {
6864
- return convexMutation3("integrations:deleteConfig", { provider, environment: env });
7140
+ return convexMutation4("integrations:deleteConfig", { provider, environment: env });
6865
7141
  }
6866
7142
  async function listIntegrationConfigs(env) {
6867
7143
  return convexQuery5("integrations:listConfigs", { environment: env });
6868
7144
  }
6869
7145
  async function setIntegrationStatus(provider, env, status) {
6870
- return convexMutation3("integrations:setConfigStatus", { provider, environment: env, status });
7146
+ return convexMutation4("integrations:setConfigStatus", { provider, environment: env, status });
6871
7147
  }
6872
7148
 
6873
7149
  // src/cli/commands/integration.ts
@@ -7187,7 +7463,7 @@ async function convexQuery6(path, args) {
7187
7463
  }
7188
7464
  return json.value;
7189
7465
  }
7190
- async function convexMutation4(path, args) {
7466
+ async function convexMutation5(path, args) {
7191
7467
  const token = getToken3();
7192
7468
  const response = await fetch(`${CONVEX_URL}/api/mutation`, {
7193
7469
  method: "POST",
@@ -7239,14 +7515,14 @@ async function getLastRunStatuses(environment) {
7239
7515
  return convexQuery6("triggers:getLastRunStatuses", { environment });
7240
7516
  }
7241
7517
  async function cancelTriggerRun(runId, environment) {
7242
- return convexMutation4("triggers:cancelRun", { runId, environment });
7518
+ return convexMutation5("triggers:cancelRun", { runId, environment });
7243
7519
  }
7244
7520
  async function retryTriggerRun(runId, environment) {
7245
- return convexMutation4("triggers:retryRun", { runId, environment });
7521
+ return convexMutation5("triggers:retryRun", { runId, environment });
7246
7522
  }
7247
7523
  async function toggleTrigger(slug, enabled, environment) {
7248
7524
  const path = enabled ? "triggers:enable" : "triggers:disable";
7249
- return convexMutation4(path, { slug, environment });
7525
+ return convexMutation5(path, { slug, environment });
7250
7526
  }
7251
7527
  async function listTriggerExecutions(options) {
7252
7528
  return convexQuery6("triggers:listExecutions", {
@@ -8281,7 +8557,7 @@ import chalk23 from "chalk";
8281
8557
  import ora17 from "ora";
8282
8558
  import readline from "readline";
8283
8559
  init_convex();
8284
- var chatCommand = new Command21("chat").description("Chat with an agent").argument("<agent-slug>", "Agent slug").option("--env <environment>", "Environment: development | production | eval", "development").option("--thread <id>", "Continue an existing thread").option("--message <msg>", "Single message mode (send and exit)").option("--json", "Output JSON").option("--channel <channel>", "Channel identifier", "api").option("-v, --verbose", "Show detailed response info").option("--confirm", "Skip production warning prompt").action(async (agentSlug, options) => {
8560
+ var chatCommand = new Command21("chat").description("Chat with an agent or via a router").argument("<slug>", "Agent slug (or router slug when --router is used)").option("--env <environment>", "Environment: development | production | eval", "development").option("--thread <id>", "Continue an existing thread").option("--message <msg>", "Single message mode (send and exit)").option("--json", "Output JSON").option("--channel <channel>", "Channel identifier", "api").option("-v, --verbose", "Show detailed response info").option("--confirm", "Skip production warning prompt").option("--router", "Chat via a router instead of directly with an agent").option("--phone <number>", "Sender phone number for routing rules").action(async (slug, options) => {
8285
8561
  const spinner = ora17();
8286
8562
  const cwd = process.cwd();
8287
8563
  const nonInteractive = !isInteractive();
@@ -8333,6 +8609,15 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8333
8609
  console.log();
8334
8610
  }
8335
8611
  const environment = options.env;
8612
+ const isRouterMode = !!options.router;
8613
+ if (isRouterMode && !options.phone) {
8614
+ if (jsonMode) {
8615
+ console.log(JSON.stringify({ success: false, error: "--phone is required when using --router" }));
8616
+ } else {
8617
+ console.log(chalk23.red("--phone is required when using --router"));
8618
+ }
8619
+ process.exit(1);
8620
+ }
8336
8621
  if (environment === "production" && !nonInteractive && !options.confirm) {
8337
8622
  const confirmRl = readline.createInterface({ input: process.stdin, output: process.stdout });
8338
8623
  await new Promise((resolve) => {
@@ -8342,8 +8627,20 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8342
8627
  confirmRl.close();
8343
8628
  }
8344
8629
  const doChat = async (message, threadId2, signal) => {
8630
+ if (isRouterMode) {
8631
+ return chatWithRouter({
8632
+ routerSlug: slug,
8633
+ message,
8634
+ threadId: threadId2,
8635
+ phoneNumber: options.phone,
8636
+ environment,
8637
+ organizationId: project?.organization.id,
8638
+ channel: options.channel,
8639
+ signal
8640
+ });
8641
+ }
8345
8642
  return chatWithAgent({
8346
- slug: agentSlug,
8643
+ slug,
8347
8644
  message,
8348
8645
  threadId: threadId2,
8349
8646
  environment,
@@ -8395,12 +8692,22 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8395
8692
  }
8396
8693
  if (!jsonMode)
8397
8694
  spinner.succeed("Message sent");
8695
+ const routerResult = result;
8398
8696
  if (jsonMode) {
8399
- console.log(JSON.stringify({ message: result.message, threadId: result.threadId, usage: result.usage }, null, 2));
8697
+ const output = { message: result.message, threadId: result.threadId, usage: result.usage };
8698
+ if (routerResult.routedToAgent) {
8699
+ output.routedToAgent = routerResult.routedToAgent;
8700
+ output.routedToAgentSlug = routerResult.routedToAgentSlug;
8701
+ }
8702
+ console.log(JSON.stringify(output, null, 2));
8400
8703
  } else {
8401
8704
  console.log();
8402
8705
  console.log("\u2500".repeat(60));
8403
8706
  console.log();
8707
+ if (routerResult.routedToAgent) {
8708
+ console.log(chalk23.magenta(`Routed to: ${routerResult.routedToAgent} (${routerResult.routedToAgentSlug})`));
8709
+ console.log();
8710
+ }
8404
8711
  console.log(chalk23.green("Agent:"));
8405
8712
  console.log(result.message);
8406
8713
  console.log();
@@ -8416,7 +8723,8 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8416
8723
  }
8417
8724
  return;
8418
8725
  }
8419
- console.log(chalk23.bold(`Chat with ${chalk23.cyan(agentSlug)} (${environment})`));
8726
+ const headerLabel = isRouterMode ? `router ${chalk23.cyan(slug)}` : chalk23.cyan(slug);
8727
+ console.log(chalk23.bold(`Chat with ${headerLabel} (${environment})`));
8420
8728
  console.log(chalk23.dim("Type 'exit' to quit"));
8421
8729
  console.log();
8422
8730
  let threadId = options.thread;
@@ -8505,7 +8813,12 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8505
8813
  }
8506
8814
  spinner.stop();
8507
8815
  threadId = result.threadId;
8816
+ const interactiveRouterResult = result;
8508
8817
  console.log();
8818
+ if (interactiveRouterResult.routedToAgent) {
8819
+ console.log(chalk23.magenta(`Routed to: ${interactiveRouterResult.routedToAgent} (${interactiveRouterResult.routedToAgentSlug})`));
8820
+ console.log();
8821
+ }
8509
8822
  console.log(chalk23.green("Agent:"));
8510
8823
  console.log(result.message);
8511
8824
  console.log();
@@ -8528,10 +8841,207 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8528
8841
  process.exit(0);
8529
8842
  });
8530
8843
  });
8844
+
8845
+ // src/cli/commands/whatsapp.ts
8846
+ init_credentials();
8847
+ import { Command as Command22 } from "commander";
8848
+ import chalk24 from "chalk";
8849
+ async function ensureAuth6() {
8850
+ const cwd = process.cwd();
8851
+ const nonInteractive = !isInteractive();
8852
+ if (!hasProject(cwd)) {
8853
+ if (nonInteractive) {
8854
+ console.error(chalk24.red("No struere.json found. Run struere init first."));
8855
+ process.exit(1);
8856
+ }
8857
+ console.log(chalk24.yellow("No struere.json found - initializing project..."));
8858
+ console.log();
8859
+ const success = await runInit(cwd);
8860
+ if (!success) {
8861
+ process.exit(1);
8862
+ }
8863
+ console.log();
8864
+ }
8865
+ let credentials = loadCredentials();
8866
+ const apiKey = getApiKey();
8867
+ if (!credentials && !apiKey) {
8868
+ if (nonInteractive) {
8869
+ console.error(chalk24.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
8870
+ process.exit(1);
8871
+ }
8872
+ console.log(chalk24.yellow("Not logged in - authenticating..."));
8873
+ console.log();
8874
+ credentials = await performLogin();
8875
+ if (!credentials) {
8876
+ console.log(chalk24.red("Authentication failed"));
8877
+ process.exit(1);
8878
+ }
8879
+ console.log();
8880
+ }
8881
+ return true;
8882
+ }
8883
+ async function resolveConnection(env, identifier, out) {
8884
+ out.start("Fetching WhatsApp connections");
8885
+ const { data, error } = await listWhatsAppConnections(env);
8886
+ if (error || !data) {
8887
+ out.fail("Failed to fetch connections");
8888
+ out.error(error ?? "Unknown error");
8889
+ return null;
8890
+ }
8891
+ const connections = data;
8892
+ const match = connections.find((c) => {
8893
+ if (c._id === identifier)
8894
+ return true;
8895
+ if (c._id.endsWith(identifier))
8896
+ return true;
8897
+ if (c.label && c.label.toLowerCase() === identifier.toLowerCase())
8898
+ return true;
8899
+ if (c.phoneNumber && c.phoneNumber === identifier.replace(/^\+/, ""))
8900
+ return true;
8901
+ if (c.phoneNumber && `+${c.phoneNumber}` === identifier)
8902
+ return true;
8903
+ return false;
8904
+ });
8905
+ if (!match) {
8906
+ out.fail(`Connection not found: ${identifier}`);
8907
+ out.info('Run "struere whatsapp list" to see available connections');
8908
+ return null;
8909
+ }
8910
+ out.succeed(`Found connection: ${match.label || (match.phoneNumber ? `+${match.phoneNumber}` : match._id)}`);
8911
+ return match;
8912
+ }
8913
+ function connectionLabel(conn) {
8914
+ return conn.label || (conn.phoneNumber ? `+${conn.phoneNumber}` : conn._id.slice(-12));
8915
+ }
8916
+ function statusColor5(status) {
8917
+ switch (status) {
8918
+ case "connected":
8919
+ return chalk24.green(status);
8920
+ case "pending_setup":
8921
+ return chalk24.yellow("pending");
8922
+ case "disconnected":
8923
+ return chalk24.red(status);
8924
+ default:
8925
+ return chalk24.gray(status);
8926
+ }
8927
+ }
8928
+ function assignmentLabel(conn) {
8929
+ if (conn.routerName)
8930
+ return `Router: ${conn.routerName}`;
8931
+ if (conn.agentName)
8932
+ return `Agent: ${conn.agentName}`;
8933
+ if (conn.routerId)
8934
+ return `Router: ${conn.routerId.slice(-8)}`;
8935
+ if (conn.agentId)
8936
+ return `Agent: ${conn.agentId.slice(-8)}`;
8937
+ return chalk24.gray("none");
8938
+ }
8939
+ var whatsappCommand = new Command22("whatsapp").description("Manage WhatsApp connections and routing");
8940
+ whatsappCommand.command("list").description("List WhatsApp connections with routing assignments").option("--env <environment>", "Environment (development|production)", "production").option("--json", "Output raw JSON").action(async (opts) => {
8941
+ await ensureAuth6();
8942
+ const env = opts.env;
8943
+ const out = createOutput();
8944
+ out.start("Fetching WhatsApp connections");
8945
+ const { data, error } = await listWhatsAppConnections(env);
8946
+ if (error || !data) {
8947
+ out.fail("Failed to fetch connections");
8948
+ out.error(error ?? "Unknown error");
8949
+ process.exit(1);
8950
+ }
8951
+ const connections = data;
8952
+ out.succeed(`Found ${connections.length} connections`);
8953
+ if (opts.json) {
8954
+ console.log(JSON.stringify(connections, null, 2));
8955
+ return;
8956
+ }
8957
+ console.log();
8958
+ if (connections.length === 0) {
8959
+ console.log(chalk24.gray(" No WhatsApp connections found"));
8960
+ console.log();
8961
+ return;
8962
+ }
8963
+ renderTable([
8964
+ { key: "label", label: "Label", width: 20 },
8965
+ { key: "phone", label: "Phone", width: 18 },
8966
+ { key: "status", label: "Status", width: 14 },
8967
+ { key: "assignment", label: "Assignment", width: 30 },
8968
+ { key: "id", label: "ID", width: 16 }
8969
+ ], connections.map((c) => ({
8970
+ label: c.label || chalk24.gray("-"),
8971
+ phone: c.phoneNumber ? `+${c.phoneNumber}` : chalk24.gray("-"),
8972
+ status: statusColor5(c.status),
8973
+ assignment: assignmentLabel(c),
8974
+ id: c._id.slice(-12)
8975
+ })));
8976
+ console.log();
8977
+ });
8978
+ whatsappCommand.command("set-router <connection> <router-slug>").description("Assign a router to a WhatsApp connection").option("--env <environment>", "Environment (development|production)", "production").action(async (connection, routerSlug, opts) => {
8979
+ await ensureAuth6();
8980
+ const env = opts.env;
8981
+ const out = createOutput();
8982
+ const conn = await resolveConnection(env, connection, out);
8983
+ if (!conn)
8984
+ process.exit(1);
8985
+ out.start(`Looking up router "${routerSlug}"`);
8986
+ const { data: routersData, error: routersError } = await listRouters(env);
8987
+ if (routersError || !routersData) {
8988
+ out.fail("Failed to fetch routers");
8989
+ out.error(routersError ?? "Unknown error");
8990
+ process.exit(1);
8991
+ }
8992
+ const routers = routersData;
8993
+ const router = routers.find((r) => r.slug === routerSlug);
8994
+ if (!router) {
8995
+ out.fail(`Router not found: ${routerSlug}`);
8996
+ if (routers.length > 0) {
8997
+ out.info(`Available routers: ${routers.map((r) => r.slug).join(", ")}`);
8998
+ } else {
8999
+ out.info("No routers found in this environment");
9000
+ }
9001
+ process.exit(1);
9002
+ }
9003
+ out.succeed(`Found router: ${router.name}`);
9004
+ out.start("Assigning router to connection");
9005
+ const { error: setError } = await setConnectionRouter(conn._id, router._id);
9006
+ if (setError) {
9007
+ out.fail("Failed to assign router");
9008
+ out.error(setError);
9009
+ process.exit(1);
9010
+ }
9011
+ out.succeed(`Connection ${connectionLabel(conn)} now routes via ${router.name} (${routerSlug})`);
9012
+ console.log();
9013
+ });
9014
+ whatsappCommand.command("set-agent <connection> <agent-slug>").description("Assign an agent directly to a WhatsApp connection").option("--env <environment>", "Environment (development|production)", "production").action(async (connection, agentSlug, opts) => {
9015
+ await ensureAuth6();
9016
+ const env = opts.env;
9017
+ const out = createOutput();
9018
+ const conn = await resolveConnection(env, connection, out);
9019
+ if (!conn)
9020
+ process.exit(1);
9021
+ out.start(`Looking up agent "${agentSlug}"`);
9022
+ const { data: agentData, error: agentError } = await resolveAgentBySlug(agentSlug);
9023
+ if (agentError || !agentData) {
9024
+ out.fail(`Agent not found: ${agentSlug}`);
9025
+ if (agentError)
9026
+ out.error(agentError);
9027
+ process.exit(1);
9028
+ }
9029
+ const agent = agentData;
9030
+ out.succeed(`Found agent: ${agent.name}`);
9031
+ out.start("Assigning agent to connection");
9032
+ const { error: setError } = await setConnectionAgent(conn._id, agent._id);
9033
+ if (setError) {
9034
+ out.fail("Failed to assign agent");
9035
+ out.error(setError);
9036
+ process.exit(1);
9037
+ }
9038
+ out.succeed(`Connection ${connectionLabel(conn)} now assigned to agent ${agent.name} (${agentSlug})`);
9039
+ console.log();
9040
+ });
8531
9041
  // package.json
8532
9042
  var package_default = {
8533
9043
  name: "struere",
8534
- version: "0.12.1",
9044
+ version: "0.12.3",
8535
9045
  description: "Build, test, and deploy AI agents",
8536
9046
  keywords: [
8537
9047
  "ai",
@@ -8652,4 +9162,5 @@ program.addCommand(triggersCommand);
8652
9162
  program.addCommand(compilePromptCommand);
8653
9163
  program.addCommand(runToolCommand);
8654
9164
  program.addCommand(chatCommand);
9165
+ program.addCommand(whatsappCommand);
8655
9166
  program.parse();