struere 0.12.0 → 0.12.2

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.
Files changed (39) hide show
  1. package/dist/bin/struere.js +605 -150
  2. package/dist/cli/commands/add.d.ts.map +1 -1
  3. package/dist/cli/commands/chat.d.ts.map +1 -1
  4. package/dist/cli/commands/dev.d.ts.map +1 -1
  5. package/dist/cli/commands/login.d.ts.map +1 -1
  6. package/dist/cli/commands/pull.d.ts.map +1 -1
  7. package/dist/cli/commands/status.d.ts.map +1 -1
  8. package/dist/cli/commands/sync.d.ts.map +1 -1
  9. package/dist/cli/commands/templates.d.ts.map +1 -1
  10. package/dist/cli/index.js +605 -150
  11. package/dist/cli/templates/index.d.ts +1 -0
  12. package/dist/cli/templates/index.d.ts.map +1 -1
  13. package/dist/cli/utils/convex.d.ts +78 -0
  14. package/dist/cli/utils/convex.d.ts.map +1 -1
  15. package/dist/cli/utils/credentials.d.ts +6 -0
  16. package/dist/cli/utils/credentials.d.ts.map +1 -1
  17. package/dist/cli/utils/entities.d.ts.map +1 -1
  18. package/dist/cli/utils/extractor.d.ts +27 -0
  19. package/dist/cli/utils/extractor.d.ts.map +1 -1
  20. package/dist/cli/utils/generator.d.ts +3 -2
  21. package/dist/cli/utils/generator.d.ts.map +1 -1
  22. package/dist/cli/utils/integrations.d.ts.map +1 -1
  23. package/dist/cli/utils/loader.d.ts +3 -1
  24. package/dist/cli/utils/loader.d.ts.map +1 -1
  25. package/dist/cli/utils/logs.d.ts.map +1 -1
  26. package/dist/cli/utils/scaffold.d.ts +1 -0
  27. package/dist/cli/utils/scaffold.d.ts.map +1 -1
  28. package/dist/cli/utils/whatsapp.d.ts +9 -0
  29. package/dist/cli/utils/whatsapp.d.ts.map +1 -1
  30. package/dist/define/index.d.ts +1 -0
  31. package/dist/define/index.d.ts.map +1 -1
  32. package/dist/define/router.d.ts +3 -0
  33. package/dist/define/router.d.ts.map +1 -0
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +40 -0
  37. package/dist/types.d.ts +31 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +1 -1
@@ -1,68 +1,43 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true,
9
+ configurable: true,
10
+ set: (newValue) => all[name] = () => newValue
11
+ });
12
+ };
13
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
3
14
  var __require = import.meta.require;
4
15
 
5
- // src/cli/index.ts
6
- import { program } from "commander";
7
-
8
- // src/cli/commands/init.ts
9
- import { Command as Command4 } from "commander";
10
- import chalk5 from "chalk";
11
- import ora5 from "ora";
12
- import { select } from "@inquirer/prompts";
13
- import { basename as basename2 } from "path";
14
-
15
- // src/cli/utils/credentials.ts
16
- import { homedir } from "os";
17
- import { join } from "path";
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
19
- var CONFIG_DIR = join(homedir(), ".struere");
20
- var CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
21
- function ensureConfigDir() {
22
- if (!existsSync(CONFIG_DIR)) {
23
- mkdirSync(CONFIG_DIR, { recursive: true });
24
- }
25
- }
26
- function saveCredentials(credentials) {
27
- ensureConfigDir();
28
- writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 384 });
29
- }
30
- function loadCredentials() {
31
- if (!existsSync(CREDENTIALS_FILE)) {
32
- return null;
33
- }
34
- try {
35
- const data = readFileSync(CREDENTIALS_FILE, "utf-8");
36
- return JSON.parse(data);
37
- } catch {
38
- return null;
39
- }
40
- }
41
- function clearCredentials() {
42
- if (existsSync(CREDENTIALS_FILE)) {
43
- unlinkSync(CREDENTIALS_FILE);
44
- }
45
- }
46
- function getApiKey() {
47
- const credentials = loadCredentials();
48
- if (credentials?.apiKey) {
49
- return credentials.apiKey;
50
- }
51
- return process.env.STRUERE_API_KEY || null;
52
- }
53
-
54
- // src/cli/commands/login.ts
55
- import { Command } from "commander";
56
- import chalk from "chalk";
57
- import ora from "ora";
58
-
59
16
  // src/cli/utils/config.ts
60
- var CONVEX_URL = process.env.STRUERE_CONVEX_URL || "https://rapid-wildebeest-172.convex.cloud";
61
17
  function getSiteUrl() {
62
18
  return CONVEX_URL.replace(".cloud", ".site");
63
19
  }
20
+ var CONVEX_URL;
21
+ var init_config = __esm(() => {
22
+ CONVEX_URL = process.env.STRUERE_CONVEX_URL || "https://rapid-wildebeest-172.convex.cloud";
23
+ });
64
24
 
65
25
  // src/cli/utils/convex.ts
26
+ var exports_convex = {};
27
+ __export(exports_convex, {
28
+ syncOrganization: () => syncOrganization,
29
+ runTool: () => runTool,
30
+ refreshToken: () => refreshToken,
31
+ listMyOrganizations: () => listMyOrganizations,
32
+ getUserInfo: () => getUserInfo,
33
+ getSyncState: () => getSyncState,
34
+ getSiteUrl: () => getSiteUrl,
35
+ getPullState: () => getPullState,
36
+ createOrganization: () => createOrganization,
37
+ compilePrompt: () => compilePrompt,
38
+ chatWithRouter: () => chatWithRouter,
39
+ chatWithAgent: () => chatWithAgent
40
+ });
66
41
  async function refreshToken() {
67
42
  const credentials = loadCredentials();
68
43
  if (!credentials?.sessionId)
@@ -81,6 +56,7 @@ async function refreshToken() {
81
56
  if (!data.token)
82
57
  return null;
83
58
  credentials.token = data.token;
59
+ credentials.expiresAt = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString();
84
60
  saveCredentials(credentials);
85
61
  return data.token;
86
62
  } catch {
@@ -209,6 +185,7 @@ async function syncViaHttp(apiKey, payload) {
209
185
  roles: payload.roles,
210
186
  evalSuites: payload.evalSuites,
211
187
  triggers: payload.triggers,
188
+ routers: payload.routers,
212
189
  fixtures: payload.fixtures
213
190
  }),
214
191
  signal: AbortSignal.timeout(30000)
@@ -660,6 +637,65 @@ async function chatWithAgent(options) {
660
637
  return { error: `Network error: ${err instanceof Error ? err.message : String(err)}` };
661
638
  }
662
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
+ }
663
699
  async function getPullState(organizationId, environment = "development") {
664
700
  const credentials = loadCredentials();
665
701
  const apiKey = getApiKey();
@@ -716,8 +752,100 @@ async function getPullState(organizationId, environment = "development") {
716
752
  }
717
753
  return { error: `Unexpected response: ${JSON.stringify(result)}` };
718
754
  }
755
+ var init_convex = __esm(() => {
756
+ init_credentials();
757
+ init_config();
758
+ });
759
+
760
+ // src/cli/utils/credentials.ts
761
+ import { homedir } from "os";
762
+ import { join } from "path";
763
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
764
+ function ensureConfigDir() {
765
+ if (!existsSync(CONFIG_DIR)) {
766
+ mkdirSync(CONFIG_DIR, { recursive: true });
767
+ }
768
+ }
769
+ function saveCredentials(credentials) {
770
+ ensureConfigDir();
771
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 384 });
772
+ }
773
+ function loadCredentials() {
774
+ if (!existsSync(CREDENTIALS_FILE)) {
775
+ return null;
776
+ }
777
+ try {
778
+ const data = readFileSync(CREDENTIALS_FILE, "utf-8");
779
+ return JSON.parse(data);
780
+ } catch {
781
+ return null;
782
+ }
783
+ }
784
+ function getJwtExpiry(token) {
785
+ try {
786
+ const parts = token.split(".");
787
+ if (parts.length !== 3)
788
+ return null;
789
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
790
+ if (typeof payload.exp === "number")
791
+ return new Date(payload.exp * 1000);
792
+ return null;
793
+ } catch {
794
+ return null;
795
+ }
796
+ }
797
+ async function getValidToken() {
798
+ const apiKey = getApiKey();
799
+ if (apiKey)
800
+ return { token: apiKey };
801
+ const credentials = loadCredentials();
802
+ if (!credentials?.token)
803
+ return { error: "Not authenticated. Run `struere login`." };
804
+ const expiry = getJwtExpiry(credentials.token);
805
+ const needsRefresh = !expiry || expiry.getTime() - Date.now() < 60000;
806
+ if (!needsRefresh)
807
+ return { token: credentials.token };
808
+ const { refreshToken: refreshToken2 } = await Promise.resolve().then(() => (init_convex(), exports_convex));
809
+ const refreshed = await refreshToken2();
810
+ if (refreshed)
811
+ return { token: refreshed };
812
+ return { error: "Session expired. Run `struere login`." };
813
+ }
814
+ function clearCredentials() {
815
+ if (existsSync(CREDENTIALS_FILE)) {
816
+ unlinkSync(CREDENTIALS_FILE);
817
+ }
818
+ }
819
+ function getApiKey() {
820
+ const credentials = loadCredentials();
821
+ if (credentials?.apiKey) {
822
+ return credentials.apiKey;
823
+ }
824
+ return process.env.STRUERE_API_KEY || null;
825
+ }
826
+ var CONFIG_DIR, CREDENTIALS_FILE;
827
+ var init_credentials = __esm(() => {
828
+ CONFIG_DIR = join(homedir(), ".struere");
829
+ CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
830
+ });
831
+
832
+ // src/cli/index.ts
833
+ import { program } from "commander";
834
+
835
+ // src/cli/commands/init.ts
836
+ init_credentials();
837
+ import { Command as Command4 } from "commander";
838
+ import chalk5 from "chalk";
839
+ import ora5 from "ora";
840
+ import { select } from "@inquirer/prompts";
841
+ import { basename as basename2 } from "path";
719
842
 
720
843
  // src/cli/commands/login.ts
844
+ init_convex();
845
+ init_credentials();
846
+ import { Command } from "commander";
847
+ import chalk from "chalk";
848
+ import ora from "ora";
721
849
  var AUTH_CALLBACK_PORT = 9876;
722
850
  var loginCommand = new Command("login").description("Log in to Struere").action(async () => {
723
851
  const spinner = ora();
@@ -726,10 +854,26 @@ var loginCommand = new Command("login").description("Log in to Struere").action(
726
854
  console.log();
727
855
  const existing = loadCredentials();
728
856
  if (existing) {
729
- console.log(chalk.yellow("Already logged in as"), chalk.cyan(existing.user.email));
730
- console.log(chalk.gray("Run"), chalk.cyan("struere logout"), chalk.gray("to log out first"));
857
+ const expiry = getJwtExpiry(existing.token);
858
+ const isValid = expiry && expiry.getTime() - Date.now() > 60000;
859
+ if (isValid) {
860
+ console.log(chalk.yellow("Already logged in as"), chalk.cyan(existing.user.email));
861
+ console.log(chalk.gray("Run"), chalk.cyan("struere logout"), chalk.gray("to log out first"));
862
+ console.log();
863
+ return;
864
+ }
865
+ spinner.start("Session expired, refreshing token");
866
+ const refreshed = await refreshToken();
867
+ if (refreshed) {
868
+ spinner.succeed("Token refreshed");
869
+ console.log(chalk.yellow("Already logged in as"), chalk.cyan(existing.user.email));
870
+ console.log();
871
+ return;
872
+ }
873
+ spinner.fail("Could not refresh session");
874
+ clearCredentials();
875
+ console.log(chalk.gray("Starting fresh login..."));
731
876
  console.log();
732
- return;
733
877
  }
734
878
  await browserLogin(spinner);
735
879
  });
@@ -812,7 +956,7 @@ async function browserLoginInternal(spinner) {
812
956
  email: user.email,
813
957
  name: user.name || user.email
814
958
  },
815
- expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
959
+ expiresAt: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString()
816
960
  };
817
961
  saveCredentials(credentials);
818
962
  spinner.succeed("Logged in successfully");
@@ -1260,6 +1404,26 @@ export default defineTrigger({
1260
1404
  })
1261
1405
  `;
1262
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
+ }
1263
1427
 
1264
1428
  // src/cli/utils/scaffold.ts
1265
1429
  function ensureDir(filePath) {
@@ -1285,6 +1449,7 @@ function scaffoldProject(cwd, options) {
1285
1449
  "tools",
1286
1450
  "evals",
1287
1451
  "triggers",
1452
+ "routers",
1288
1453
  "fixtures",
1289
1454
  ".struere"
1290
1455
  ];
@@ -1404,6 +1569,24 @@ function scaffoldTrigger(cwd, name, slug) {
1404
1569
  result.createdFiles.push(`triggers/${fileName}`);
1405
1570
  return result;
1406
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
+ }
1407
1590
  function scaffoldFixture(cwd, name, slug) {
1408
1591
  const result = {
1409
1592
  createdFiles: [],
@@ -1423,6 +1606,9 @@ function scaffoldFixture(cwd, name, slug) {
1423
1606
  return result;
1424
1607
  }
1425
1608
 
1609
+ // src/cli/commands/init.ts
1610
+ init_convex();
1611
+
1426
1612
  // src/cli/utils/plugin.ts
1427
1613
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1428
1614
  import { join as join4 } from "path";
@@ -1830,9 +2016,10 @@ async function loadAllResources(cwd) {
1830
2016
  const { suites: evalSuites, errors: evalErrors } = loadEvalSuites(join5(cwd, "evals"));
1831
2017
  errors.push(...evalErrors);
1832
2018
  const triggers = await loadTsDirectory(join5(cwd, "triggers"));
2019
+ const routers = await loadTsDirectory(join5(cwd, "routers"));
1833
2020
  const { fixtures, errors: fixtureErrors } = loadFixtures(join5(cwd, "fixtures"));
1834
2021
  errors.push(...fixtureErrors);
1835
- return { agents, entityTypes, roles, customTools, evalSuites, triggers, fixtures, errors };
2022
+ return { agents, entityTypes, roles, customTools, evalSuites, triggers, routers, fixtures, errors };
1836
2023
  }
1837
2024
  async function loadTsDirectory(dir) {
1838
2025
  if (!existsSync5(dir)) {
@@ -1919,6 +2106,7 @@ function getResourceDirectories(cwd) {
1919
2106
  tools: join5(cwd, "tools"),
1920
2107
  evals: join5(cwd, "evals"),
1921
2108
  triggers: join5(cwd, "triggers"),
2109
+ routers: join5(cwd, "routers"),
1922
2110
  fixtures: join5(cwd, "fixtures")
1923
2111
  };
1924
2112
  }
@@ -2057,6 +2245,7 @@ async function generateDocs(cwd, targets) {
2057
2245
  customTools: [],
2058
2246
  evalSuites: [],
2059
2247
  triggers: [],
2248
+ routers: [],
2060
2249
  fixtures: [],
2061
2250
  errors: []
2062
2251
  });
@@ -2104,6 +2293,7 @@ var docsCommand = new Command2("docs").description("Generate AI context files (C
2104
2293
  });
2105
2294
 
2106
2295
  // src/cli/utils/runtime.ts
2296
+ init_credentials();
2107
2297
  import ora3 from "ora";
2108
2298
  import chalk3 from "chalk";
2109
2299
  function isInteractive() {
@@ -2174,6 +2364,9 @@ function isOrgAccessError(error) {
2174
2364
  }
2175
2365
 
2176
2366
  // src/cli/commands/org.ts
2367
+ init_credentials();
2368
+ init_convex();
2369
+ init_convex();
2177
2370
  import { Command as Command3 } from "commander";
2178
2371
  import chalk4 from "chalk";
2179
2372
  import ora4 from "ora";
@@ -2475,6 +2668,7 @@ function slugify(name) {
2475
2668
  }
2476
2669
 
2477
2670
  // src/cli/commands/dev.ts
2671
+ init_credentials();
2478
2672
  import { Command as Command6 } from "commander";
2479
2673
  import chalk7 from "chalk";
2480
2674
  import ora6 from "ora";
@@ -2483,9 +2677,11 @@ import { join as join8 } from "path";
2483
2677
  import { existsSync as existsSync8 } from "fs";
2484
2678
 
2485
2679
  // src/cli/commands/sync.ts
2680
+ init_credentials();
2486
2681
  import { Command as Command5 } from "commander";
2487
2682
  import chalk6 from "chalk";
2488
2683
  import { confirm as confirm2 } from "@inquirer/prompts";
2684
+ init_convex();
2489
2685
 
2490
2686
  // src/cli/utils/extractor.ts
2491
2687
  var BUILTIN_TOOLS = [
@@ -2594,6 +2790,29 @@ function extractSyncPayload(resources) {
2594
2790
  schedule: t.schedule,
2595
2791
  retry: t.retry
2596
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;
2597
2816
  const fixtures = resources.fixtures.length > 0 ? resources.fixtures.map((f) => ({
2598
2817
  name: f.name,
2599
2818
  slug: f.slug,
@@ -2610,7 +2829,7 @@ function extractSyncPayload(resources) {
2610
2829
  metadata: r.metadata
2611
2830
  }))
2612
2831
  })) : undefined;
2613
- return { agents, entityTypes, roles, evalSuites, triggers, fixtures };
2832
+ return { agents, entityTypes, roles, evalSuites, triggers, routers, fixtures };
2614
2833
  }
2615
2834
  function extractAgentPayload(agent, customToolsMap) {
2616
2835
  let systemPrompt;
@@ -3039,6 +3258,7 @@ ${resources.errors.join(`
3039
3258
  entityTypes: payload.entityTypes,
3040
3259
  roles: payload.roles,
3041
3260
  triggers: payload.triggers,
3261
+ routers: payload.routers,
3042
3262
  organizationId,
3043
3263
  environment: "development"
3044
3264
  });
@@ -3072,7 +3292,8 @@ async function checkForDeletions(resources, organizationId, environment) {
3072
3292
  entityTypes: new Set(payload.entityTypes.map((et) => et.slug)),
3073
3293
  roles: new Set(payload.roles.map((r) => r.name)),
3074
3294
  evalSuites: new Set((payload.evalSuites || []).map((es) => es.slug)),
3075
- 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))
3076
3297
  };
3077
3298
  const deletions = [];
3078
3299
  const deletedAgents = remoteState.agents.filter((a) => !localSlugs.agents.has(a.slug)).map((a) => a.name);
@@ -3092,6 +3313,10 @@ async function checkForDeletions(resources, organizationId, environment) {
3092
3313
  const deletedTriggers = remoteTriggers.filter((t) => !localSlugs.triggers.has(t.slug)).map((t) => t.name);
3093
3314
  if (deletedTriggers.length > 0)
3094
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 });
3095
3320
  return deletions;
3096
3321
  }
3097
3322
  async function syncToEnvironment(cwd, organizationId, environment) {
@@ -3211,6 +3436,7 @@ var syncCommand = new Command5("sync").description("Sync resources to Convex and
3211
3436
  entityTypes: payload.entityTypes.map((et) => et.slug),
3212
3437
  roles: payload.roles.map((r) => r.name),
3213
3438
  triggers: (payload.triggers || []).map((t) => t.slug),
3439
+ routers: (payload.routers || []).map((r) => r.slug),
3214
3440
  deletions: deletions.map((d) => ({ type: d.type, names: d.deleted }))
3215
3441
  }));
3216
3442
  } else {
@@ -3220,6 +3446,7 @@ var syncCommand = new Command5("sync").description("Sync resources to Convex and
3220
3446
  console.log(chalk6.gray(" Data types:"), payload.entityTypes.map((et) => et.slug).join(", ") || "none");
3221
3447
  console.log(chalk6.gray(" Roles:"), payload.roles.map((r) => r.name).join(", ") || "none");
3222
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");
3223
3450
  if (deletions.length > 0) {
3224
3451
  console.log();
3225
3452
  console.log(chalk6.yellow.bold(" Would delete:"));
@@ -3384,7 +3611,7 @@ var devCommand = new Command6("dev").description("Watch files and sync to develo
3384
3611
  spinner.start("Loading resources");
3385
3612
  try {
3386
3613
  loadedResources = await loadAllResources(cwd);
3387
- 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`);
3388
3615
  for (const err of loadedResources.errors) {
3389
3616
  console.log(chalk7.red(" \u2716"), err);
3390
3617
  }
@@ -3479,6 +3706,7 @@ var devCommand = new Command6("dev").description("Watch files and sync to develo
3479
3706
  dirs.tools,
3480
3707
  dirs.evals,
3481
3708
  dirs.triggers,
3709
+ dirs.routers,
3482
3710
  dirs.fixtures
3483
3711
  ].filter((p) => existsSync8(p));
3484
3712
  const watcher = chokidar.watch(watchPaths, {
@@ -3544,10 +3772,12 @@ var devCommand = new Command6("dev").description("Watch files and sync to develo
3544
3772
  });
3545
3773
 
3546
3774
  // src/cli/commands/deploy.ts
3775
+ init_credentials();
3547
3776
  import { Command as Command7 } from "commander";
3548
3777
  import chalk8 from "chalk";
3549
3778
  import ora7 from "ora";
3550
3779
  import { confirm as confirm4 } from "@inquirer/prompts";
3780
+ init_convex();
3551
3781
  var deployCommand = new Command7("deploy").description("Deploy all resources to production").option("--dry-run", "Show what would be deployed without deploying").option("--force", "Skip destructive sync confirmation").option("--json", "Output results as JSON").action(async (options) => {
3552
3782
  const spinner = ora7();
3553
3783
  const cwd = process.cwd();
@@ -3859,6 +4089,7 @@ var deployCommand = new Command7("deploy").description("Deploy all resources to
3859
4089
  });
3860
4090
 
3861
4091
  // src/cli/commands/logout.ts
4092
+ init_credentials();
3862
4093
  import { Command as Command8 } from "commander";
3863
4094
  import chalk9 from "chalk";
3864
4095
  var logoutCommand = new Command8("logout").description("Log out of Struere").action(async () => {
@@ -3876,6 +4107,8 @@ var logoutCommand = new Command8("logout").description("Log out of Struere").act
3876
4107
  });
3877
4108
 
3878
4109
  // src/cli/commands/whoami.ts
4110
+ init_credentials();
4111
+ init_convex();
3879
4112
  import { Command as Command9 } from "commander";
3880
4113
  import chalk10 from "chalk";
3881
4114
  import ora8 from "ora";
@@ -3971,7 +4204,7 @@ var whoamiCommand = new Command9("whoami").description("Show current logged in u
3971
4204
  // src/cli/commands/add.ts
3972
4205
  import { Command as Command10 } from "commander";
3973
4206
  import chalk11 from "chalk";
3974
- 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) => {
3975
4208
  const cwd = process.cwd();
3976
4209
  console.log();
3977
4210
  if (!hasProject(cwd)) {
@@ -4049,6 +4282,17 @@ var addCommand = new Command10("add").description("Scaffold a new resource").arg
4049
4282
  console.log(chalk11.yellow("Trigger already exists:"), `triggers/${slug}.ts`);
4050
4283
  }
4051
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;
4052
4296
  case "fixture":
4053
4297
  result = scaffoldFixture(cwd, displayName, slug);
4054
4298
  if (result.createdFiles.length > 0) {
@@ -4071,6 +4315,7 @@ var addCommand = new Command10("add").description("Scaffold a new resource").arg
4071
4315
  console.log(chalk11.gray(" -"), chalk11.cyan("role"), "- Create a role with permissions");
4072
4316
  console.log(chalk11.gray(" -"), chalk11.cyan("eval"), "- Create an eval suite (YAML)");
4073
4317
  console.log(chalk11.gray(" -"), chalk11.cyan("trigger"), "- Create a data trigger");
4318
+ console.log(chalk11.gray(" -"), chalk11.cyan("router"), "- Create a message router");
4074
4319
  console.log(chalk11.gray(" -"), chalk11.cyan("fixture"), "- Create a test data fixture (YAML)");
4075
4320
  console.log();
4076
4321
  process.exit(1);
@@ -4084,9 +4329,11 @@ function slugify2(name) {
4084
4329
  }
4085
4330
 
4086
4331
  // src/cli/commands/status.ts
4332
+ init_credentials();
4087
4333
  import { Command as Command11 } from "commander";
4088
4334
  import chalk12 from "chalk";
4089
4335
  import ora9 from "ora";
4336
+ init_convex();
4090
4337
  var statusCommand = new Command11("status").description("Compare local vs remote state").option("--json", "Output raw JSON").action(async (opts) => {
4091
4338
  const spinner = ora9();
4092
4339
  const cwd = process.cwd();
@@ -4198,6 +4445,8 @@ var statusCommand = new Command11("status").description("Compare local vs remote
4198
4445
  const devEntityTypeSlugs = new Set(devState.entityTypes.map((et) => et.slug));
4199
4446
  const localRoleNames = new Set(localResources.roles.map((r) => r.name));
4200
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));
4201
4450
  if (opts.json) {
4202
4451
  const classify = (localItems, remoteItems, useSlug) => {
4203
4452
  const localKeys = new Set(localItems.map((i) => useSlug ? i.slug : i.name));
@@ -4211,7 +4460,8 @@ var statusCommand = new Command11("status").description("Compare local vs remote
4211
4460
  console.log(JSON.stringify({
4212
4461
  agents: classify(localResources.agents, devState.agents, true),
4213
4462
  entityTypes: classify(localResources.entityTypes, devState.entityTypes, true),
4214
- roles: classify(localResources.roles, devState.roles, false)
4463
+ roles: classify(localResources.roles, devState.roles, false),
4464
+ routers: classify(localResources.routers, devState.routers || [], true)
4215
4465
  }));
4216
4466
  return;
4217
4467
  }
@@ -4281,6 +4531,26 @@ var statusCommand = new Command11("status").description("Compare local vs remote
4281
4531
  }
4282
4532
  }
4283
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();
4284
4554
  console.log(chalk12.gray("Legend:"));
4285
4555
  console.log(chalk12.gray(" "), chalk12.green("\u25CF"), "Synced", chalk12.yellow("\u25CB"), "Not in production", chalk12.blue("+"), "New", chalk12.red("-"), "Will be deleted");
4286
4556
  console.log();
@@ -4290,11 +4560,13 @@ var statusCommand = new Command11("status").description("Compare local vs remote
4290
4560
  });
4291
4561
 
4292
4562
  // src/cli/commands/pull.ts
4563
+ init_credentials();
4293
4564
  import { Command as Command12 } from "commander";
4294
4565
  import chalk13 from "chalk";
4295
4566
  import ora10 from "ora";
4296
4567
  import { existsSync as existsSync9, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
4297
4568
  import { join as join9 } from "path";
4569
+ init_convex();
4298
4570
 
4299
4571
  // src/cli/utils/generator.ts
4300
4572
  var BUILTIN_TOOLS2 = [
@@ -4553,6 +4825,53 @@ ${parts.join(`,
4553
4825
  })
4554
4826
  `;
4555
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
+ }
4556
4875
  function generateIndexFile(type, slugs) {
4557
4876
  if (slugs.length === 0) {
4558
4877
  return "";
@@ -4687,6 +5006,7 @@ var pullCommand = new Command12("pull").description("Pull remote resources to lo
4687
5006
  ensureDir2(join9(cwd, "entity-types"));
4688
5007
  ensureDir2(join9(cwd, "roles"));
4689
5008
  ensureDir2(join9(cwd, "triggers"));
5009
+ ensureDir2(join9(cwd, "routers"));
4690
5010
  ensureDir2(join9(cwd, "tools"));
4691
5011
  const agentSlugs = [];
4692
5012
  for (const agent of state.agents) {
@@ -4714,6 +5034,12 @@ var pullCommand = new Command12("pull").description("Pull remote resources to lo
4714
5034
  const content = generateTriggerFile(trigger);
4715
5035
  writeOrSkip(`triggers/${trigger.slug}.ts`, content);
4716
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
+ }
4717
5043
  const customTools = collectCustomTools(state.agents);
4718
5044
  if (customTools.length > 0) {
4719
5045
  const content = generateToolsFile(customTools);
@@ -4739,6 +5065,11 @@ var pullCommand = new Command12("pull").description("Pull remote resources to lo
4739
5065
  if (content)
4740
5066
  writeOrSkip("triggers/index.ts", content);
4741
5067
  }
5068
+ if (routerSlugs.length > 0) {
5069
+ const content = generateIndexFile("routers", routerSlugs);
5070
+ if (content)
5071
+ writeOrSkip("routers/index.ts", content);
5072
+ }
4742
5073
  await installSkill(cwd);
4743
5074
  if (options.json) {
4744
5075
  console.log(JSON.stringify({
@@ -4777,21 +5108,20 @@ var pullCommand = new Command12("pull").description("Pull remote resources to lo
4777
5108
  });
4778
5109
 
4779
5110
  // src/cli/commands/entities.ts
5111
+ init_credentials();
4780
5112
  import { Command as Command13 } from "commander";
4781
5113
  import chalk15 from "chalk";
4782
5114
  import ora11 from "ora";
4783
5115
  import { input as input2, confirm as confirm5 } from "@inquirer/prompts";
4784
5116
 
4785
5117
  // src/cli/utils/entities.ts
4786
- function getToken() {
4787
- const credentials = loadCredentials();
4788
- const apiKey = getApiKey();
4789
- return apiKey || credentials?.token || null;
4790
- }
5118
+ init_credentials();
5119
+ init_config();
4791
5120
  async function convexQuery(path, args) {
4792
- const token = getToken();
4793
- if (!token)
4794
- return { error: "Not authenticated" };
5121
+ const result = await getValidToken();
5122
+ if ("error" in result)
5123
+ return { error: result.error };
5124
+ const { token } = result;
4795
5125
  const response = await fetch(`${CONVEX_URL}/api/query`, {
4796
5126
  method: "POST",
4797
5127
  headers: {
@@ -4818,9 +5148,10 @@ async function convexQuery(path, args) {
4818
5148
  return { error: `Unexpected response: ${text}` };
4819
5149
  }
4820
5150
  async function convexMutation(path, args) {
4821
- const token = getToken();
4822
- if (!token)
4823
- return { error: "Not authenticated" };
5151
+ const result = await getValidToken();
5152
+ if ("error" in result)
5153
+ return { error: result.error };
5154
+ const { token } = result;
4824
5155
  const response = await fetch(`${CONVEX_URL}/api/mutation`, {
4825
5156
  method: "POST",
4826
5157
  headers: {
@@ -5348,24 +5679,20 @@ entitiesCommand.command("search <type> <query>").description("Search records").o
5348
5679
  });
5349
5680
 
5350
5681
  // src/cli/commands/logs.ts
5682
+ init_credentials();
5351
5683
  import { Command as Command14 } from "commander";
5352
5684
  import chalk16 from "chalk";
5353
5685
  import ora12 from "ora";
5354
5686
 
5355
5687
  // src/cli/utils/logs.ts
5356
- function getToken2() {
5357
- const credentials = loadCredentials();
5358
- const apiKey = getApiKey();
5359
- return apiKey || credentials?.token || null;
5360
- }
5688
+ init_credentials();
5689
+ init_config();
5361
5690
  async function convexQuery2(path, args) {
5362
- let token = getToken2();
5363
- if (!token) {
5364
- token = await refreshToken();
5365
- if (!token)
5366
- return { error: "Not authenticated. Run `struere login`." };
5367
- }
5368
- let response = await fetch(`${CONVEX_URL}/api/query`, {
5691
+ const tokenResult = await getValidToken();
5692
+ if ("error" in tokenResult)
5693
+ return { error: tokenResult.error };
5694
+ const { token } = tokenResult;
5695
+ const response = await fetch(`${CONVEX_URL}/api/query`, {
5369
5696
  method: "POST",
5370
5697
  headers: {
5371
5698
  "Content-Type": "application/json",
@@ -5373,19 +5700,6 @@ async function convexQuery2(path, args) {
5373
5700
  },
5374
5701
  body: JSON.stringify({ path, args })
5375
5702
  });
5376
- if (response.status === 401 || response.status === 403) {
5377
- const refreshed = await refreshToken();
5378
- if (refreshed) {
5379
- response = await fetch(`${CONVEX_URL}/api/query`, {
5380
- method: "POST",
5381
- headers: {
5382
- "Content-Type": "application/json",
5383
- Authorization: `Bearer ${refreshed}`
5384
- },
5385
- body: JSON.stringify({ path, args })
5386
- });
5387
- }
5388
- }
5389
5703
  const text = await response.text();
5390
5704
  let json;
5391
5705
  try {
@@ -5396,21 +5710,12 @@ async function convexQuery2(path, args) {
5396
5710
  if (!response.ok) {
5397
5711
  const errorData = json.errorData;
5398
5712
  const msg = errorData?.message || json.message || json.errorMessage || text;
5399
- const msgStr = String(msg);
5400
- if (msgStr.includes("OIDC") || msgStr.includes("token")) {
5401
- return { error: "Session expired. Run `struere logout && struere login`." };
5402
- }
5403
- return { error: msgStr };
5713
+ return { error: String(msg) };
5404
5714
  }
5405
5715
  if (json.status === "success")
5406
5716
  return { data: json.value };
5407
- if (json.status === "error") {
5408
- const errMsg = String(json.errorMessage || "Unknown error");
5409
- if (errMsg.includes("OIDC") || errMsg.includes("token")) {
5410
- return { error: "Session expired. Run `struere logout && struere login`." };
5411
- }
5412
- return { error: errMsg };
5413
- }
5717
+ if (json.status === "error")
5718
+ return { error: String(json.errorMessage || "Unknown error") };
5414
5719
  return { error: `Unexpected response: ${text}` };
5415
5720
  }
5416
5721
  async function queryThreads(options) {
@@ -5721,14 +6026,18 @@ logsCommand.command("view <thread-id>").description("View conversation messages"
5721
6026
  });
5722
6027
 
5723
6028
  // src/cli/commands/eval.ts
6029
+ init_credentials();
5724
6030
  import { Command as Command15 } from "commander";
5725
6031
  import chalk17 from "chalk";
5726
6032
  import ora13 from "ora";
5727
6033
  import { join as join10 } from "path";
5728
6034
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
6035
+ init_convex();
5729
6036
 
5730
6037
  // src/cli/utils/evals.ts
5731
- function getToken3() {
6038
+ init_credentials();
6039
+ init_config();
6040
+ function getToken() {
5732
6041
  const credentials = loadCredentials();
5733
6042
  const apiKey = getApiKey();
5734
6043
  const token = apiKey || credentials?.token;
@@ -5737,7 +6046,7 @@ function getToken3() {
5737
6046
  return token;
5738
6047
  }
5739
6048
  async function convexQuery3(path, args) {
5740
- const token = getToken3();
6049
+ const token = getToken();
5741
6050
  const response = await fetch(`${CONVEX_URL}/api/query`, {
5742
6051
  method: "POST",
5743
6052
  headers: {
@@ -5762,7 +6071,7 @@ async function convexQuery3(path, args) {
5762
6071
  return json.value;
5763
6072
  }
5764
6073
  async function convexMutation2(path, args) {
5765
- const token = getToken3();
6074
+ const token = getToken();
5766
6075
  const response = await fetch(`${CONVEX_URL}/api/mutation`, {
5767
6076
  method: "POST",
5768
6077
  headers: {
@@ -6178,13 +6487,16 @@ var evalCommand = new Command15("eval").description("Eval suite management");
6178
6487
  evalCommand.addCommand(runCommand);
6179
6488
 
6180
6489
  // src/cli/commands/templates.ts
6490
+ init_credentials();
6181
6491
  import { Command as Command16 } from "commander";
6182
6492
  import chalk18 from "chalk";
6183
6493
  import { readFileSync as readFileSync5 } from "fs";
6184
6494
  import { confirm as confirm6 } from "@inquirer/prompts";
6185
6495
 
6186
6496
  // src/cli/utils/whatsapp.ts
6187
- function getToken4() {
6497
+ init_credentials();
6498
+ init_config();
6499
+ function getToken2() {
6188
6500
  const credentials = loadCredentials();
6189
6501
  const apiKey = getApiKey();
6190
6502
  return apiKey || credentials?.token || null;
@@ -6223,7 +6535,7 @@ async function httpPost(path, body) {
6223
6535
  }
6224
6536
  }
6225
6537
  async function convexAction(path, args) {
6226
- const token = getToken4();
6538
+ const token = getToken2();
6227
6539
  if (!token)
6228
6540
  return { error: "Not authenticated" };
6229
6541
  const response = await fetch(`${CONVEX_URL}/api/action`, {
@@ -6252,7 +6564,7 @@ async function convexAction(path, args) {
6252
6564
  return { error: `Unexpected response: ${text}` };
6253
6565
  }
6254
6566
  async function convexQuery4(path, args) {
6255
- const token = getToken4();
6567
+ const token = getToken2();
6256
6568
  if (!token)
6257
6569
  return { error: "Not authenticated" };
6258
6570
  const response = await fetch(`${CONVEX_URL}/api/query`, {
@@ -6335,6 +6647,16 @@ async function getTemplateStatus(connectionId, name) {
6335
6647
  name
6336
6648
  });
6337
6649
  }
6650
+ async function updateTemplate(connectionId, templateId, updates) {
6651
+ if (getApiKey()) {
6652
+ return httpPost("/v1/templates/update", { connectionId, templateId, ...updates });
6653
+ }
6654
+ return convexAction("whatsappActions:updateTemplate", {
6655
+ connectionId,
6656
+ templateId,
6657
+ ...updates
6658
+ });
6659
+ }
6338
6660
 
6339
6661
  // src/cli/commands/templates.ts
6340
6662
  async function ensureAuth3() {
@@ -6427,9 +6749,9 @@ function statusColor2(status) {
6427
6749
  }
6428
6750
  }
6429
6751
  var templatesCommand = new Command16("templates").description("Manage WhatsApp message templates");
6430
- templatesCommand.command("list").description("List all message templates").option("--connection <id>", "WhatsApp connection ID").option("--json", "Output raw JSON").action(async (opts) => {
6752
+ templatesCommand.command("list").description("List all message templates").option("--env <environment>", "Environment to find connection (development|production|eval)").option("--connection <id>", "WhatsApp connection ID").option("--json", "Output raw JSON").action(async (opts) => {
6431
6753
  await ensureAuth3();
6432
- const connectionId = await resolveConnectionId("development", opts.connection);
6754
+ const connectionId = await resolveConnectionId(opts.env ?? "production", opts.connection);
6433
6755
  const out = createOutput();
6434
6756
  out.start("Fetching templates");
6435
6757
  const { data, error } = await listTemplates(connectionId);
@@ -6464,9 +6786,9 @@ templatesCommand.command("list").description("List all message templates").optio
6464
6786
  })));
6465
6787
  console.log();
6466
6788
  });
6467
- templatesCommand.command("create <name>").description("Create a new message template").option("--connection <id>", "WhatsApp connection ID").option("--language <code>", "Language code", "en_US").option("--category <cat>", "Category (UTILITY|MARKETING|AUTHENTICATION)", "UTILITY").option("--components <json>", "Components as JSON string").option("--file <path>", "Read components from a JSON file").option("--allow-category-change", "Allow Meta to reassign category").option("--json", "Output raw JSON").action(async (name, opts) => {
6789
+ templatesCommand.command("create <name>").description("Create a new message template").option("--env <environment>", "Environment to find connection").option("--connection <id>", "WhatsApp connection ID").option("--language <code>", "Language code", "en_US").option("--category <cat>", "Category (UTILITY|MARKETING|AUTHENTICATION)", "UTILITY").option("--components <json>", "Components as JSON string").option("--file <path>", "Read components from a JSON file").option("--allow-category-change", "Allow Meta to reassign category").option("--json", "Output raw JSON").action(async (name, opts) => {
6468
6790
  await ensureAuth3();
6469
- const connectionId = await resolveConnectionId("development", opts.connection);
6791
+ const connectionId = await resolveConnectionId(opts.env ?? "production", opts.connection);
6470
6792
  let components;
6471
6793
  if (opts.file) {
6472
6794
  try {
@@ -6512,9 +6834,9 @@ templatesCommand.command("create <name>").description("Create a new message temp
6512
6834
  console.log();
6513
6835
  }
6514
6836
  });
6515
- templatesCommand.command("delete <name>").description("Delete a message template").option("--connection <id>", "WhatsApp connection ID").option("--yes", "Skip confirmation").action(async (name, opts) => {
6837
+ templatesCommand.command("delete <name>").description("Delete a message template").option("--env <environment>", "Environment to find connection").option("--connection <id>", "WhatsApp connection ID").option("--yes", "Skip confirmation").action(async (name, opts) => {
6516
6838
  await ensureAuth3();
6517
- const connectionId = await resolveConnectionId("development", opts.connection);
6839
+ const connectionId = await resolveConnectionId(opts.env ?? "production", opts.connection);
6518
6840
  if (!opts.yes && isInteractive()) {
6519
6841
  const confirmed = await confirm6({
6520
6842
  message: `Delete template "${name}"? This cannot be undone.`,
@@ -6536,9 +6858,9 @@ templatesCommand.command("delete <name>").description("Delete a message template
6536
6858
  out.succeed(`Template "${name}" deleted`);
6537
6859
  console.log();
6538
6860
  });
6539
- templatesCommand.command("status <name>").description("Check template approval status").option("--connection <id>", "WhatsApp connection ID").option("--json", "Output raw JSON").action(async (name, opts) => {
6861
+ templatesCommand.command("status <name>").description("Check template approval status").option("--env <environment>", "Environment to find connection").option("--connection <id>", "WhatsApp connection ID").option("--json", "Output raw JSON").action(async (name, opts) => {
6540
6862
  await ensureAuth3();
6541
- const connectionId = await resolveConnectionId("development", opts.connection);
6863
+ const connectionId = await resolveConnectionId(opts.env ?? "production", opts.connection);
6542
6864
  const out = createOutput();
6543
6865
  out.start(`Checking status for "${name}"`);
6544
6866
  const { data, error } = await getTemplateStatus(connectionId, name);
@@ -6564,6 +6886,8 @@ templatesCommand.command("status <name>").description("Check template approval s
6564
6886
  console.log(` ${chalk18.gray("Status:")} ${statusColor2(String(t.status ?? ""))}`);
6565
6887
  console.log(` ${chalk18.gray("Category:")} ${t.category}`);
6566
6888
  console.log(` ${chalk18.gray("Language:")} ${t.language}`);
6889
+ if (t.id)
6890
+ console.log(` ${chalk18.gray("ID:")} ${t.id}`);
6567
6891
  if (t.components) {
6568
6892
  console.log(` ${chalk18.gray("Components:")} ${JSON.stringify(t.components, null, 2).split(`
6569
6893
  `).join(`
@@ -6572,22 +6896,105 @@ templatesCommand.command("status <name>").description("Check template approval s
6572
6896
  console.log();
6573
6897
  }
6574
6898
  });
6899
+ templatesCommand.command("edit <name>").description("Edit a message template").option("--env <environment>", "Environment to find connection").option("--connection <id>", "WhatsApp connection ID").option("--components <json>", "New components as JSON string").option("--file <path>", "Read components from a JSON file").option("--category <cat>", "New category (UTILITY|MARKETING|AUTHENTICATION)").option("--language <code>", "Language code").option("--json", "Output raw JSON").action(async (name, opts) => {
6900
+ await ensureAuth3();
6901
+ const connectionId = await resolveConnectionId(opts.env ?? "production", opts.connection);
6902
+ const out = createOutput();
6903
+ out.start(`Fetching template "${name}"`);
6904
+ const { data: statusData, error: statusError } = await getTemplateStatus(connectionId, name);
6905
+ if (statusError) {
6906
+ out.fail("Failed to fetch template");
6907
+ out.error(statusError);
6908
+ process.exit(1);
6909
+ }
6910
+ const statusResult = statusData;
6911
+ const existing = statusResult?.data ?? [];
6912
+ const match = opts.language ? existing.find((t) => t.language === opts.language) : existing[0];
6913
+ if (!match) {
6914
+ out.fail(`Template "${name}" not found${opts.language ? ` for language ${opts.language}` : ""}`);
6915
+ process.exit(1);
6916
+ }
6917
+ const templateId = match.id;
6918
+ if (!templateId) {
6919
+ out.fail("Template ID not found in response");
6920
+ process.exit(1);
6921
+ }
6922
+ out.succeed(`Found template "${name}" (${match.language}, ID: ${templateId})`);
6923
+ let components;
6924
+ if (opts.file) {
6925
+ try {
6926
+ const fileContent = readFileSync5(opts.file, "utf-8");
6927
+ const parsed = JSON.parse(fileContent);
6928
+ components = Array.isArray(parsed) ? parsed : parsed.components ?? [parsed];
6929
+ } catch (err) {
6930
+ console.log(chalk18.red("Failed to read components file:"), err instanceof Error ? err.message : String(err));
6931
+ process.exit(1);
6932
+ }
6933
+ } else if (opts.components) {
6934
+ try {
6935
+ components = JSON.parse(opts.components);
6936
+ } catch {
6937
+ console.log(chalk18.red("Invalid JSON in --components"));
6938
+ process.exit(1);
6939
+ }
6940
+ }
6941
+ if (!components && !opts.category) {
6942
+ console.log();
6943
+ console.log(chalk18.gray("Current components:"));
6944
+ console.log(JSON.stringify(match.components, null, 2));
6945
+ console.log();
6946
+ console.log(chalk18.yellow("Provide --components <json>, --file <path>, or --category <cat> to update"));
6947
+ process.exit(1);
6948
+ }
6949
+ const updates = { name, language: opts.language ?? match.language };
6950
+ if (components)
6951
+ updates.components = components;
6952
+ if (opts.category)
6953
+ updates.category = opts.category.toUpperCase();
6954
+ if (!opts.json && isInteractive()) {
6955
+ console.log();
6956
+ if (components) {
6957
+ console.log(chalk18.gray("New components:"));
6958
+ console.log(JSON.stringify(components, null, 2));
6959
+ console.log();
6960
+ }
6961
+ const confirmed = await confirm6({
6962
+ message: `Update template "${name}"? This will resubmit for Meta approval.`,
6963
+ default: true
6964
+ });
6965
+ if (!confirmed) {
6966
+ console.log(chalk18.gray("Cancelled"));
6967
+ return;
6968
+ }
6969
+ }
6970
+ out.start(`Updating template "${name}"`);
6971
+ const { data, error } = await updateTemplate(connectionId, templateId, updates);
6972
+ if (error) {
6973
+ out.fail("Failed to update template");
6974
+ out.error(error);
6975
+ process.exit(1);
6976
+ }
6977
+ out.succeed(`Template "${name}" updated \u2014 resubmitted for approval`);
6978
+ if (opts.json) {
6979
+ console.log(JSON.stringify(data, null, 2));
6980
+ }
6981
+ console.log();
6982
+ });
6575
6983
 
6576
6984
  // src/cli/commands/integration.ts
6985
+ init_credentials();
6577
6986
  import { Command as Command17 } from "commander";
6578
6987
  import chalk19 from "chalk";
6579
6988
  import { confirm as confirm7 } from "@inquirer/prompts";
6580
6989
 
6581
6990
  // src/cli/utils/integrations.ts
6582
- function getToken5() {
6583
- const credentials = loadCredentials();
6584
- const apiKey = getApiKey();
6585
- return apiKey || credentials?.token || null;
6586
- }
6991
+ init_credentials();
6992
+ init_config();
6587
6993
  async function convexQuery5(path, args) {
6588
- const token = getToken5();
6589
- if (!token)
6590
- return { error: "Not authenticated" };
6994
+ const result = await getValidToken();
6995
+ if ("error" in result)
6996
+ return { error: result.error };
6997
+ const { token } = result;
6591
6998
  const response = await fetch(`${CONVEX_URL}/api/query`, {
6592
6999
  method: "POST",
6593
7000
  headers: {
@@ -6614,9 +7021,10 @@ async function convexQuery5(path, args) {
6614
7021
  return { error: `Unexpected response: ${text}` };
6615
7022
  }
6616
7023
  async function convexMutation3(path, args) {
6617
- const token = getToken5();
6618
- if (!token)
6619
- return { error: "Not authenticated" };
7024
+ const result = await getValidToken();
7025
+ if ("error" in result)
7026
+ return { error: result.error };
7027
+ const { token } = result;
6620
7028
  const response = await fetch(`${CONVEX_URL}/api/mutation`, {
6621
7029
  method: "POST",
6622
7030
  headers: {
@@ -6643,9 +7051,10 @@ async function convexMutation3(path, args) {
6643
7051
  return { error: `Unexpected response: ${text}` };
6644
7052
  }
6645
7053
  async function convexAction2(path, args) {
6646
- const token = getToken5();
6647
- if (!token)
6648
- return { error: "Not authenticated" };
7054
+ const result = await getValidToken();
7055
+ if ("error" in result)
7056
+ return { error: result.error };
7057
+ const { token } = result;
6649
7058
  const response = await fetch(`${CONVEX_URL}/api/action`, {
6650
7059
  method: "POST",
6651
7060
  headers: {
@@ -6966,12 +7375,15 @@ var integrationCommand = new Command17("integration").description("Manage integr
6966
7375
  });
6967
7376
 
6968
7377
  // src/cli/commands/triggers.ts
7378
+ init_credentials();
6969
7379
  import { Command as Command18 } from "commander";
6970
7380
  import chalk20 from "chalk";
6971
7381
  import ora14 from "ora";
6972
7382
 
6973
7383
  // src/cli/utils/triggers.ts
6974
- function getToken6() {
7384
+ init_credentials();
7385
+ init_config();
7386
+ function getToken3() {
6975
7387
  const credentials = loadCredentials();
6976
7388
  const apiKey = getApiKey();
6977
7389
  const token = apiKey || credentials?.token;
@@ -6980,7 +7392,7 @@ function getToken6() {
6980
7392
  return token;
6981
7393
  }
6982
7394
  async function convexQuery6(path, args) {
6983
- const token = getToken6();
7395
+ const token = getToken3();
6984
7396
  const response = await fetch(`${CONVEX_URL}/api/query`, {
6985
7397
  method: "POST",
6986
7398
  headers: {
@@ -7005,7 +7417,7 @@ async function convexQuery6(path, args) {
7005
7417
  return json.value;
7006
7418
  }
7007
7419
  async function convexMutation4(path, args) {
7008
- const token = getToken6();
7420
+ const token = getToken3();
7009
7421
  const response = await fetch(`${CONVEX_URL}/api/mutation`, {
7010
7422
  method: "POST",
7011
7423
  headers: {
@@ -7076,7 +7488,7 @@ async function getTriggerExecutionDetail(eventId) {
7076
7488
  return convexQuery6("triggers:getExecutionDetail", { eventId });
7077
7489
  }
7078
7490
  async function convexAction3(path, args) {
7079
- const token = getToken6();
7491
+ const token = getToken3();
7080
7492
  const response = await fetch(`${CONVEX_URL}/api/action`, {
7081
7493
  method: "POST",
7082
7494
  headers: {
@@ -7785,9 +8197,11 @@ triggersCommand.command("retry-event <event-id>").description("Retry a failed im
7785
8197
  });
7786
8198
 
7787
8199
  // src/cli/commands/compile-prompt.ts
8200
+ init_credentials();
7788
8201
  import { Command as Command19 } from "commander";
7789
8202
  import chalk21 from "chalk";
7790
8203
  import ora15 from "ora";
8204
+ init_convex();
7791
8205
  var compilePromptCommand = new Command19("compile-prompt").description("Compile and preview an agent's system prompt after template processing").argument("<agent-slug>", "Agent slug to compile prompt for").option("--env <env>", "Environment: development | production", "development").option("--message <msg>", "Sample message for template context").option("--channel <channel>", "Sample channel (whatsapp, widget, api, dashboard)").option("--param <key=value...>", "Custom thread param (repeatable)", (val, acc) => {
7792
8206
  acc.push(val);
7793
8207
  return acc;
@@ -7933,9 +8347,11 @@ var compilePromptCommand = new Command19("compile-prompt").description("Compile
7933
8347
  });
7934
8348
 
7935
8349
  // src/cli/commands/run-tool.ts
8350
+ init_credentials();
7936
8351
  import { Command as Command20 } from "commander";
7937
8352
  import chalk22 from "chalk";
7938
8353
  import ora16 from "ora";
8354
+ init_convex();
7939
8355
  var runToolCommand = new Command20("run-tool").description("Run a tool as it would execute during a real agent conversation").argument("<agent-slug>", "Agent slug").argument("<tool-name>", "Tool name (e.g., entity.query)").option("--env <environment>", "Environment: development | production | eval", "development").option("--args <json>", "Tool arguments as JSON string", "{}").option("--args-file <path>", "Read tool arguments from a JSON file").option("--json", "Output full JSON result").option("--confirm", "Skip production confirmation prompt").action(async (agentSlug, toolName, options) => {
7940
8356
  const spinner = ora16();
7941
8357
  const cwd = process.cwd();
@@ -8088,11 +8504,13 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8088
8504
  });
8089
8505
 
8090
8506
  // src/cli/commands/chat.ts
8507
+ init_credentials();
8091
8508
  import { Command as Command21 } from "commander";
8092
8509
  import chalk23 from "chalk";
8093
8510
  import ora17 from "ora";
8094
8511
  import readline from "readline";
8095
- 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) => {
8512
+ init_convex();
8513
+ 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) => {
8096
8514
  const spinner = ora17();
8097
8515
  const cwd = process.cwd();
8098
8516
  const nonInteractive = !isInteractive();
@@ -8144,6 +8562,15 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8144
8562
  console.log();
8145
8563
  }
8146
8564
  const environment = options.env;
8565
+ const isRouterMode = !!options.router;
8566
+ if (isRouterMode && !options.phone) {
8567
+ if (jsonMode) {
8568
+ console.log(JSON.stringify({ success: false, error: "--phone is required when using --router" }));
8569
+ } else {
8570
+ console.log(chalk23.red("--phone is required when using --router"));
8571
+ }
8572
+ process.exit(1);
8573
+ }
8147
8574
  if (environment === "production" && !nonInteractive && !options.confirm) {
8148
8575
  const confirmRl = readline.createInterface({ input: process.stdin, output: process.stdout });
8149
8576
  await new Promise((resolve) => {
@@ -8153,8 +8580,20 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8153
8580
  confirmRl.close();
8154
8581
  }
8155
8582
  const doChat = async (message, threadId2, signal) => {
8583
+ if (isRouterMode) {
8584
+ return chatWithRouter({
8585
+ routerSlug: slug,
8586
+ message,
8587
+ threadId: threadId2,
8588
+ phoneNumber: options.phone,
8589
+ environment,
8590
+ organizationId: project?.organization.id,
8591
+ channel: options.channel,
8592
+ signal
8593
+ });
8594
+ }
8156
8595
  return chatWithAgent({
8157
- slug: agentSlug,
8596
+ slug,
8158
8597
  message,
8159
8598
  threadId: threadId2,
8160
8599
  environment,
@@ -8206,12 +8645,22 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8206
8645
  }
8207
8646
  if (!jsonMode)
8208
8647
  spinner.succeed("Message sent");
8648
+ const routerResult = result;
8209
8649
  if (jsonMode) {
8210
- console.log(JSON.stringify({ message: result.message, threadId: result.threadId, usage: result.usage }, null, 2));
8650
+ const output = { message: result.message, threadId: result.threadId, usage: result.usage };
8651
+ if (routerResult.routedToAgent) {
8652
+ output.routedToAgent = routerResult.routedToAgent;
8653
+ output.routedToAgentSlug = routerResult.routedToAgentSlug;
8654
+ }
8655
+ console.log(JSON.stringify(output, null, 2));
8211
8656
  } else {
8212
8657
  console.log();
8213
8658
  console.log("\u2500".repeat(60));
8214
8659
  console.log();
8660
+ if (routerResult.routedToAgent) {
8661
+ console.log(chalk23.magenta(`Routed to: ${routerResult.routedToAgent} (${routerResult.routedToAgentSlug})`));
8662
+ console.log();
8663
+ }
8215
8664
  console.log(chalk23.green("Agent:"));
8216
8665
  console.log(result.message);
8217
8666
  console.log();
@@ -8227,7 +8676,8 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8227
8676
  }
8228
8677
  return;
8229
8678
  }
8230
- console.log(chalk23.bold(`Chat with ${chalk23.cyan(agentSlug)} (${environment})`));
8679
+ const headerLabel = isRouterMode ? `router ${chalk23.cyan(slug)}` : chalk23.cyan(slug);
8680
+ console.log(chalk23.bold(`Chat with ${headerLabel} (${environment})`));
8231
8681
  console.log(chalk23.dim("Type 'exit' to quit"));
8232
8682
  console.log();
8233
8683
  let threadId = options.thread;
@@ -8316,7 +8766,12 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8316
8766
  }
8317
8767
  spinner.stop();
8318
8768
  threadId = result.threadId;
8769
+ const interactiveRouterResult = result;
8319
8770
  console.log();
8771
+ if (interactiveRouterResult.routedToAgent) {
8772
+ console.log(chalk23.magenta(`Routed to: ${interactiveRouterResult.routedToAgent} (${interactiveRouterResult.routedToAgentSlug})`));
8773
+ console.log();
8774
+ }
8320
8775
  console.log(chalk23.green("Agent:"));
8321
8776
  console.log(result.message);
8322
8777
  console.log();
@@ -8342,7 +8797,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent").argume
8342
8797
  // package.json
8343
8798
  var package_default = {
8344
8799
  name: "struere",
8345
- version: "0.12.0",
8800
+ version: "0.12.2",
8346
8801
  description: "Build, test, and deploy AI agents",
8347
8802
  keywords: [
8348
8803
  "ai",