struere 0.12.9 → 0.13.1

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.
@@ -218,7 +218,19 @@ async function syncViaHttp(apiKey, payload) {
218
218
  return { success: false, error: text || `HTTP ${response.status}` };
219
219
  }
220
220
  if (!response.ok) {
221
- const msg = json.error || text;
221
+ const parts = [];
222
+ if (json.error)
223
+ parts.push(String(json.error));
224
+ if (json.message)
225
+ parts.push(String(json.message));
226
+ if (json.details)
227
+ parts.push(String(json.details));
228
+ if (Array.isArray(json.errors)) {
229
+ for (const e of json.errors) {
230
+ parts.push(typeof e === "string" ? e : JSON.stringify(e));
231
+ }
232
+ }
233
+ const msg = parts.length > 0 ? parts.join(" \u2014 ") : `HTTP ${response.status}: ${text}`;
222
234
  return { success: false, error: msg };
223
235
  }
224
236
  return json;
@@ -268,15 +280,16 @@ async function _syncOrganization(payload) {
268
280
  return { success: false, error: text || `HTTP ${response.status}` };
269
281
  }
270
282
  const extractError = () => {
283
+ const code = typeof json.errorData === "object" && json.errorData?.code ? `[${json.errorData.code}] ` : "";
271
284
  if (typeof json.errorData === "string")
272
- return json.errorData;
285
+ return `${code}${json.errorData}`;
273
286
  if (json.errorData?.message)
274
- return json.errorData.message;
287
+ return `${code}${json.errorData.message}`;
275
288
  if (json.errorMessage)
276
- return json.errorMessage;
289
+ return `${code}${json.errorMessage}`;
277
290
  if (json.message)
278
- return json.message;
279
- return text;
291
+ return `${code}${json.message}`;
292
+ return `${code}${text}`;
280
293
  };
281
294
  if (!response.ok) {
282
295
  return { success: false, error: extractError() };
@@ -3164,10 +3177,11 @@ function validateResources(payload, resources) {
3164
3177
  }
3165
3178
  }
3166
3179
  }
3180
+ const BUILTIN_ENTITY_TYPES = new Set(["payment"]);
3167
3181
  const entityTypeSlugs = new Set(payload.entityTypes.map((et) => et.slug));
3168
3182
  if (payload.triggers) {
3169
3183
  for (const trigger of payload.triggers) {
3170
- if (trigger.entityType && !entityTypeSlugs.has(trigger.entityType)) {
3184
+ if (trigger.entityType && !entityTypeSlugs.has(trigger.entityType) && !BUILTIN_ENTITY_TYPES.has(trigger.entityType)) {
3171
3185
  errors.push(`Trigger "${trigger.name}" references entity type "${trigger.entityType}" but no entity type with that slug exists`);
3172
3186
  }
3173
3187
  }
@@ -3688,7 +3702,7 @@ import chalk8 from "chalk";
3688
3702
  import ora7 from "ora";
3689
3703
  import { confirm as confirm4 } from "@inquirer/prompts";
3690
3704
  init_convex();
3691
- 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) => {
3705
+ 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").option("-v, --verbose", "Show detailed resource information").action(async (options) => {
3692
3706
  const spinner = ora7();
3693
3707
  const cwd = process.cwd();
3694
3708
  const nonInteractive = !isInteractive();
@@ -3777,6 +3791,24 @@ var deployCommand = new Command7("deploy").description("Deploy all resources to
3777
3791
  }
3778
3792
  process.exit(1);
3779
3793
  }
3794
+ if (options.verbose && !jsonMode) {
3795
+ console.log();
3796
+ if (resources.agents.length > 0)
3797
+ console.log(chalk8.gray(`Loaded ${resources.agents.length} agents: ${resources.agents.map((a) => a.slug).join(", ")}`));
3798
+ if (resources.entityTypes.length > 0)
3799
+ console.log(chalk8.gray(`Loaded ${resources.entityTypes.length} data types: ${resources.entityTypes.map((et) => et.slug).join(", ")}`));
3800
+ if (resources.roles.length > 0)
3801
+ console.log(chalk8.gray(`Loaded ${resources.roles.length} roles: ${resources.roles.map((r) => r.name).join(", ")}`));
3802
+ if (resources.triggers.length > 0)
3803
+ console.log(chalk8.gray(`Loaded ${resources.triggers.length} triggers: ${resources.triggers.map((t) => t.slug).join(", ")}`));
3804
+ if (resources.routers.length > 0)
3805
+ console.log(chalk8.gray(`Loaded ${resources.routers.length} routers: ${resources.routers.map((r) => r.slug).join(", ")}`));
3806
+ if (resources.evalSuites.length > 0)
3807
+ console.log(chalk8.gray(`Loaded ${resources.evalSuites.length} eval suites: ${resources.evalSuites.map((es) => es.suite).join(", ")}`));
3808
+ if (resources.customTools.length > 0)
3809
+ console.log(chalk8.gray(`Loaded ${resources.customTools.length} custom tools: ${resources.customTools.map((t) => t.name).join(", ")}`));
3810
+ console.log();
3811
+ }
3780
3812
  if (resources.agents.length === 0) {
3781
3813
  if (jsonMode) {
3782
3814
  console.log(JSON.stringify({ success: false, error: "No agents found to deploy" }));
@@ -3820,8 +3852,10 @@ var deployCommand = new Command7("deploy").description("Deploy all resources to
3820
3852
  if (!jsonMode)
3821
3853
  spinner.stop();
3822
3854
  } catch {
3823
- if (!jsonMode)
3855
+ if (!jsonMode) {
3824
3856
  spinner.stop();
3857
+ console.log(chalk8.yellow("Could not check remote state for deletions \u2014 proceeding without deletion warnings"));
3858
+ }
3825
3859
  }
3826
3860
  if (options.dryRun) {
3827
3861
  if (jsonMode) {
@@ -3891,25 +3925,30 @@ var deployCommand = new Command7("deploy").description("Deploy all resources to
3891
3925
  }
3892
3926
  if (!jsonMode)
3893
3927
  spinner.start("Deploying to production");
3928
+ let syncResult;
3929
+ const startTime = Date.now();
3894
3930
  try {
3895
- const syncResult = await syncOrganization({
3931
+ syncResult = await syncOrganization({
3896
3932
  agents: payload.agents,
3897
3933
  tools: payload.tools,
3898
3934
  entityTypes: payload.entityTypes,
3899
3935
  roles: payload.roles,
3900
3936
  triggers: payload.triggers,
3937
+ routers: payload.routers,
3901
3938
  organizationId: project.organization.id,
3902
3939
  environment: "production"
3903
3940
  });
3904
3941
  if (!syncResult.success) {
3905
3942
  throw new Error(syncResult.error || "Deploy failed");
3906
3943
  }
3944
+ const elapsed = Date.now() - startTime;
3907
3945
  if (!jsonMode)
3908
- spinner.succeed("Deployed to production");
3946
+ spinner.succeed(`Deployed to production in ${(elapsed / 1000).toFixed(1)}s`);
3909
3947
  if (jsonMode) {
3910
3948
  console.log(JSON.stringify({
3911
3949
  success: true,
3912
3950
  environment: "production",
3951
+ durationMs: elapsed,
3913
3952
  agents: {
3914
3953
  created: syncResult.agents?.created || [],
3915
3954
  updated: syncResult.agents?.updated || [],
@@ -3924,26 +3963,71 @@ var deployCommand = new Command7("deploy").description("Deploy all resources to
3924
3963
  created: syncResult.roles?.created || [],
3925
3964
  updated: syncResult.roles?.updated || [],
3926
3965
  deleted: syncResult.roles?.deleted || []
3966
+ },
3967
+ triggers: {
3968
+ created: syncResult.triggers?.created || [],
3969
+ updated: syncResult.triggers?.updated || [],
3970
+ deleted: syncResult.triggers?.deleted || []
3971
+ },
3972
+ routers: {
3973
+ created: syncResult.routers?.created || [],
3974
+ updated: syncResult.routers?.updated || [],
3975
+ deleted: syncResult.routers?.deleted || []
3976
+ },
3977
+ evalSuites: {
3978
+ created: syncResult.evalSuites?.created || [],
3979
+ updated: syncResult.evalSuites?.updated || [],
3980
+ deleted: syncResult.evalSuites?.deleted || [],
3981
+ skipped: syncResult.evalSuites?.skipped || []
3927
3982
  }
3928
3983
  }));
3929
3984
  } else {
3930
3985
  console.log();
3931
3986
  console.log(chalk8.green("Success!"), "All resources deployed to production");
3932
3987
  console.log();
3933
- if (syncResult.agents?.created && syncResult.agents.created.length > 0) {
3934
- console.log("New agents:");
3935
- for (const slug of syncResult.agents.created) {
3936
- const agent = resources.agents.find((a) => a.slug === slug);
3937
- console.log(chalk8.gray(" -"), chalk8.cyan(agent?.name || slug));
3988
+ if (options.verbose) {
3989
+ const types = [
3990
+ { label: "agent", data: syncResult.agents },
3991
+ { label: "entity type", data: syncResult.entityTypes },
3992
+ { label: "role", data: syncResult.roles },
3993
+ { label: "trigger", data: syncResult.triggers },
3994
+ { label: "router", data: syncResult.routers },
3995
+ { label: "eval suite", data: syncResult.evalSuites }
3996
+ ];
3997
+ for (const { label, data } of types) {
3998
+ if (!data)
3999
+ continue;
4000
+ for (const name of data.created) {
4001
+ console.log(chalk8.green(`Created ${label}: ${name}`));
4002
+ }
4003
+ for (const name of data.updated) {
4004
+ console.log(chalk8.blue(`Updated ${label}: ${name}`));
4005
+ }
4006
+ for (const name of data.deleted) {
4007
+ console.log(chalk8.red(`Deleted ${label}: ${name}`));
4008
+ }
3938
4009
  }
3939
- }
3940
- if (syncResult.agents?.updated && syncResult.agents.updated.length > 0) {
3941
- console.log("Updated agents:");
3942
- for (const slug of syncResult.agents.updated) {
3943
- const agent = resources.agents.find((a) => a.slug === slug);
3944
- console.log(chalk8.gray(" -"), chalk8.cyan(agent?.name || slug));
4010
+ console.log();
4011
+ } else {
4012
+ if (syncResult.agents?.created && syncResult.agents.created.length > 0) {
4013
+ console.log("New agents:");
4014
+ for (const slug of syncResult.agents.created) {
4015
+ const agent = resources.agents.find((a) => a.slug === slug);
4016
+ console.log(chalk8.gray(" -"), chalk8.cyan(agent?.name || slug));
4017
+ }
4018
+ }
4019
+ if (syncResult.agents?.updated && syncResult.agents.updated.length > 0) {
4020
+ console.log("Updated agents:");
4021
+ for (const slug of syncResult.agents.updated) {
4022
+ const agent = resources.agents.find((a) => a.slug === slug);
4023
+ console.log(chalk8.gray(" -"), chalk8.cyan(agent?.name || slug));
4024
+ }
3945
4025
  }
3946
4026
  }
4027
+ if (syncResult.evalSuites?.skipped && syncResult.evalSuites.skipped.length > 0) {
4028
+ console.log(chalk8.yellow(`Skipped eval suites (agent not found): ${syncResult.evalSuites.skipped.join(", ")}`));
4029
+ console.log();
4030
+ }
3947
4031
  console.log();
3948
4032
  console.log(chalk8.gray("Test your agents:"));
3949
4033
  console.log(chalk8.gray(" $"), chalk8.cyan(`curl -X POST ${getSiteUrl()}/v1/agents/<agent-slug>/chat -H "Authorization: Bearer YOUR_API_KEY" -d '{"message": "Hello"}'`));
@@ -3971,6 +4055,22 @@ var deployCommand = new Command7("deploy").description("Deploy all resources to
3971
4055
  spinner.fail("Deployment failed");
3972
4056
  console.log();
3973
4057
  console.log(chalk8.red("Error:"), error instanceof Error ? error.message : String(error));
4058
+ const errMsg = error instanceof Error ? error.message : String(error);
4059
+ if (errMsg.includes("ArgumentValidationError")) {
4060
+ const fieldMatch = errMsg.match(/Argument "([^"]+)"/);
4061
+ if (fieldMatch) {
4062
+ console.log(chalk8.yellow(`Hint: field "${fieldMatch[1]}" failed validation. Run with --dry-run to inspect your payload.`));
4063
+ } else {
4064
+ console.log(chalk8.yellow("Hint: a field failed validation. Run with --dry-run to inspect your payload."));
4065
+ }
4066
+ }
4067
+ if (syncResult) {
4068
+ const agentCount = (syncResult.agents?.created?.length || 0) + (syncResult.agents?.updated?.length || 0);
4069
+ const entityTypeCount = (syncResult.entityTypes?.created?.length || 0) + (syncResult.entityTypes?.updated?.length || 0);
4070
+ if (agentCount > 0 || entityTypeCount > 0) {
4071
+ console.log(chalk8.yellow(`Partial sync: ${agentCount} agent(s), ${entityTypeCount} entity type(s) synced before failure`));
4072
+ }
4073
+ }
3974
4074
  console.log();
3975
4075
  }
3976
4076
  process.exit(1);
@@ -6688,9 +6788,9 @@ function statusColor2(status) {
6688
6788
  }
6689
6789
  }
6690
6790
  var templatesCommand = new Command16("templates").description("Manage WhatsApp message templates");
6691
- 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) => {
6791
+ templatesCommand.command("list").description("List all message templates").option("--env <environment>", "Environment to find connection (development|production|eval)").option("--json", "Output raw JSON").action(async (opts) => {
6692
6792
  await ensureAuth3();
6693
- const connectionId = await resolveConnectionId(opts.env ?? "production", opts.connection);
6793
+ const connectionId = await resolveConnectionId(opts.env ?? "production");
6694
6794
  const out = createOutput();
6695
6795
  out.start("Fetching templates");
6696
6796
  const { data, error } = await listTemplates(connectionId);
@@ -7318,6 +7418,7 @@ init_credentials();
7318
7418
  import { Command as Command18 } from "commander";
7319
7419
  import chalk20 from "chalk";
7320
7420
  import ora14 from "ora";
7421
+ init_convex();
7321
7422
 
7322
7423
  // src/cli/utils/triggers.ts
7323
7424
  init_credentials();
@@ -7457,6 +7558,20 @@ async function retryImmediateExecution(eventId, environment) {
7457
7558
  }
7458
7559
 
7459
7560
  // src/cli/commands/triggers.ts
7561
+ async function withTriggerAuthRetry(fn) {
7562
+ try {
7563
+ return await fn();
7564
+ } catch (err) {
7565
+ const msg = err instanceof Error ? err.message : String(err);
7566
+ if (msg.includes("Unauthenticated") || msg.includes("OIDC") || msg.includes("token") || msg.includes("expired")) {
7567
+ const refreshed = await refreshToken();
7568
+ if (!refreshed)
7569
+ throw err;
7570
+ return fn();
7571
+ }
7572
+ throw err;
7573
+ }
7574
+ }
7460
7575
  async function ensureAuth5() {
7461
7576
  const cwd = process.cwd();
7462
7577
  const nonInteractive = !isInteractive();
@@ -7883,11 +7998,11 @@ triggersCommand.command("logs [slug]").description("View trigger execution histo
7883
7998
  const spinner = ora14();
7884
7999
  try {
7885
8000
  spinner.start("Fetching execution logs");
7886
- const executions = await listTriggerExecutions({
8001
+ const executions = await withTriggerAuthRetry(() => listTriggerExecutions({
7887
8002
  environment: opts.env,
7888
8003
  triggerSlug: slug,
7889
8004
  limit: parseInt(opts.limit, 10)
7890
- });
8005
+ }));
7891
8006
  spinner.succeed(`Found ${executions.length} executions`);
7892
8007
  if (opts.json) {
7893
8008
  console.log(JSON.stringify(executions, null, 2));
@@ -7946,15 +8061,20 @@ triggersCommand.command("log <identifier>").description("View detailed trigger e
7946
8061
  await ensureAuth5();
7947
8062
  const spinner = ora14();
7948
8063
  try {
8064
+ const nth = parseInt(opts.nth, 10);
8065
+ if (isNaN(nth) || nth < 1) {
8066
+ console.error(chalk20.red("Error:"), "--nth must be a positive integer (e.g., --nth 1 for most recent)");
8067
+ process.exit(1);
8068
+ }
7949
8069
  let eventId = identifier;
7950
- const isConvexId = identifier.includes("_") || identifier.length > 30;
8070
+ const isConvexId = /^[0-9a-zA-Z]{20,}$/.test(identifier);
7951
8071
  if (!isConvexId) {
7952
8072
  spinner.start("Resolving trigger slug to latest execution");
7953
- const executions = await listTriggerExecutions({
8073
+ const executions = await withTriggerAuthRetry(() => listTriggerExecutions({
7954
8074
  environment: opts.env,
7955
8075
  triggerSlug: identifier,
7956
- limit: parseInt(opts.nth, 10)
7957
- });
8076
+ limit: nth
8077
+ }));
7958
8078
  if (!executions.length) {
7959
8079
  spinner.fail(`No executions found for trigger "${identifier}" in ${opts.env}`);
7960
8080
  if (opts.json) {
@@ -7962,16 +8082,16 @@ triggersCommand.command("log <identifier>").description("View detailed trigger e
7962
8082
  }
7963
8083
  process.exit(1);
7964
8084
  }
7965
- const idx = parseInt(opts.nth, 10) - 1;
8085
+ const idx = nth - 1;
7966
8086
  if (idx >= executions.length) {
7967
- spinner.fail(`Only ${executions.length} executions found, cannot get #${opts.nth}`);
8087
+ spinner.fail(`Only ${executions.length} executions found, cannot get #${nth}`);
7968
8088
  process.exit(1);
7969
8089
  }
7970
8090
  eventId = executions[idx]._id;
7971
8091
  spinner.succeed(`Found execution for "${identifier}"`);
7972
8092
  }
7973
8093
  spinner.start("Fetching execution detail");
7974
- const event = await getTriggerExecutionDetail(eventId, opts.env);
8094
+ const event = await withTriggerAuthRetry(() => getTriggerExecutionDetail(eventId, opts.env));
7975
8095
  if (!event) {
7976
8096
  spinner.fail("Execution not found");
7977
8097
  if (opts.json) {
@@ -8251,27 +8371,182 @@ triggersCommand.command("fire <slug>").description("Manually fire a trigger").op
8251
8371
  }
8252
8372
  });
8253
8373
 
8254
- // src/cli/commands/compile-prompt.ts
8374
+ // src/cli/commands/threads.ts
8255
8375
  init_credentials();
8256
8376
  import { Command as Command19 } from "commander";
8257
8377
  import chalk21 from "chalk";
8258
8378
  import ora15 from "ora";
8379
+
8380
+ // src/cli/utils/threads.ts
8381
+ init_credentials();
8382
+ init_config();
8259
8383
  init_convex();
8260
- 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 | eval", "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) => {
8261
- acc.push(val);
8262
- return acc;
8263
- }, []).option("--json", "Output full JSON (raw + compiled + context)").option("--raw", "Show raw uncompiled template instead of compiled").action(async (agentSlug, options) => {
8264
- const spinner = ora15();
8384
+ function getToken4() {
8385
+ const credentials = loadCredentials();
8386
+ const apiKey = getApiKey();
8387
+ const token = apiKey || credentials?.token;
8388
+ if (!token)
8389
+ throw new Error("Not authenticated");
8390
+ return token;
8391
+ }
8392
+ async function threadsApiCall(path, body) {
8393
+ const credentials = loadCredentials();
8394
+ const apiKey = getApiKey();
8395
+ if (apiKey && !credentials?.token) {
8396
+ const siteUrl2 = getSiteUrl();
8397
+ const response2 = await fetch(`${siteUrl2}${path}`, {
8398
+ method: "POST",
8399
+ headers: {
8400
+ "Content-Type": "application/json",
8401
+ Authorization: `Bearer ${apiKey}`
8402
+ },
8403
+ body: JSON.stringify(body),
8404
+ signal: AbortSignal.timeout(30000)
8405
+ });
8406
+ const text2 = await response2.text();
8407
+ let json2;
8408
+ try {
8409
+ json2 = JSON.parse(text2);
8410
+ } catch {
8411
+ throw new Error(text2 || `HTTP ${response2.status}`);
8412
+ }
8413
+ if (!response2.ok) {
8414
+ throw new Error(json2.error || text2);
8415
+ }
8416
+ return json2;
8417
+ }
8418
+ if (credentials?.sessionId) {
8419
+ await refreshToken();
8420
+ }
8421
+ const freshCredentials = loadCredentials();
8422
+ const token = apiKey || freshCredentials?.token;
8423
+ if (!token) {
8424
+ throw new Error("Not authenticated");
8425
+ }
8426
+ const siteUrl = getSiteUrl();
8427
+ const response = await fetch(`${siteUrl}${path}`, {
8428
+ method: "POST",
8429
+ headers: {
8430
+ "Content-Type": "application/json",
8431
+ Authorization: `Bearer ${token}`
8432
+ },
8433
+ body: JSON.stringify(body),
8434
+ signal: AbortSignal.timeout(30000)
8435
+ });
8436
+ const text = await response.text();
8437
+ let json;
8438
+ try {
8439
+ json = JSON.parse(text);
8440
+ } catch {
8441
+ throw new Error(text || `HTTP ${response.status}`);
8442
+ }
8443
+ if (!response.ok) {
8444
+ throw new Error(json.error || text);
8445
+ }
8446
+ return json;
8447
+ }
8448
+ async function convexQuery7(path, args) {
8449
+ const token = getToken4();
8450
+ const response = await fetch(`${CONVEX_URL}/api/query`, {
8451
+ method: "POST",
8452
+ headers: {
8453
+ "Content-Type": "application/json",
8454
+ Authorization: `Bearer ${token}`
8455
+ },
8456
+ body: JSON.stringify({ path, args })
8457
+ });
8458
+ const text = await response.text();
8459
+ let json;
8460
+ try {
8461
+ json = JSON.parse(text);
8462
+ } catch {
8463
+ throw new Error(text || `HTTP ${response.status}`);
8464
+ }
8465
+ if (!response.ok) {
8466
+ throw new Error(json.errorData?.message || json.errorMessage || text);
8467
+ }
8468
+ if (json.status === "error") {
8469
+ throw new Error(json.errorMessage || "Unknown error from Convex");
8470
+ }
8471
+ return json.value;
8472
+ }
8473
+ async function listThreads(options) {
8474
+ const apiKey = getApiKey();
8475
+ const credentials = loadCredentials();
8476
+ if (apiKey && !credentials?.token) {
8477
+ const result = await threadsApiCall("/v1/threads/list", {
8478
+ channel: options.channel,
8479
+ limit: options.limit
8480
+ });
8481
+ return result.data;
8482
+ }
8483
+ return convexQuery7("threads:listWithPreviews", {
8484
+ environment: options.environment,
8485
+ channel: options.channel,
8486
+ limit: options.limit
8487
+ });
8488
+ }
8489
+ async function getThreadWithMessages(options) {
8490
+ return convexQuery7("threads:getWithMessages", {
8491
+ id: options.threadId
8492
+ });
8493
+ }
8494
+ async function archiveThread(options) {
8495
+ const apiKey = getApiKey();
8496
+ const credentials = loadCredentials();
8497
+ if (apiKey && !credentials?.token) {
8498
+ return threadsApiCall("/v1/threads/archive", {
8499
+ threadId: options.threadId
8500
+ });
8501
+ }
8502
+ const token = getToken4();
8503
+ const siteUrl = getSiteUrl();
8504
+ const response = await fetch(`${siteUrl}/v1/threads/archive`, {
8505
+ method: "POST",
8506
+ headers: {
8507
+ "Content-Type": "application/json",
8508
+ Authorization: `Bearer ${token}`
8509
+ },
8510
+ body: JSON.stringify({ threadId: options.threadId }),
8511
+ signal: AbortSignal.timeout(30000)
8512
+ });
8513
+ const text = await response.text();
8514
+ let json;
8515
+ try {
8516
+ json = JSON.parse(text);
8517
+ } catch {
8518
+ throw new Error(text || `HTTP ${response.status}`);
8519
+ }
8520
+ if (!response.ok) {
8521
+ throw new Error(json.error || text);
8522
+ }
8523
+ return { success: true };
8524
+ }
8525
+ async function findThreadByPhone(options) {
8526
+ const threads = await listThreads({
8527
+ environment: options.environment,
8528
+ organizationId: options.organizationId,
8529
+ limit: 100
8530
+ });
8531
+ const normalize = (p) => p.replace(/\D/g, "");
8532
+ const normalizedInput = normalize(options.phone);
8533
+ return threads.find((t) => {
8534
+ const phoneNumber = t.channelParams?.phoneNumber;
8535
+ if (phoneNumber && normalize(phoneNumber) === normalizedInput)
8536
+ return true;
8537
+ if (t.externalId && t.externalId.includes(normalizedInput))
8538
+ return true;
8539
+ return false;
8540
+ }) ?? null;
8541
+ }
8542
+
8543
+ // src/cli/commands/threads.ts
8544
+ async function ensureAuth6() {
8265
8545
  const cwd = process.cwd();
8266
8546
  const nonInteractive = !isInteractive();
8267
- const jsonMode = !!options.json;
8268
8547
  if (!hasProject(cwd)) {
8269
8548
  if (nonInteractive) {
8270
- if (jsonMode) {
8271
- console.log(JSON.stringify({ success: false, error: "No struere.json found" }));
8272
- } else {
8273
- console.log(chalk21.red("No struere.json found. Run struere init first."));
8274
- }
8549
+ console.error(chalk21.red("No struere.json found. Run struere init first."));
8275
8550
  process.exit(1);
8276
8551
  }
8277
8552
  console.log(chalk21.yellow("No struere.json found - initializing project..."));
@@ -8282,24 +8557,11 @@ var compilePromptCommand = new Command19("compile-prompt").description("Compile
8282
8557
  }
8283
8558
  console.log();
8284
8559
  }
8285
- const project = loadProject(cwd);
8286
- if (!project) {
8287
- if (jsonMode) {
8288
- console.log(JSON.stringify({ success: false, error: "Failed to load struere.json" }));
8289
- } else {
8290
- console.log(chalk21.red("Failed to load struere.json"));
8291
- }
8292
- process.exit(1);
8293
- }
8294
8560
  let credentials = loadCredentials();
8295
8561
  const apiKey = getApiKey();
8296
8562
  if (!credentials && !apiKey) {
8297
8563
  if (nonInteractive) {
8298
- if (jsonMode) {
8299
- console.log(JSON.stringify({ success: false, error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
8300
- } else {
8301
- console.log(chalk21.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
8302
- }
8564
+ console.error(chalk21.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
8303
8565
  process.exit(1);
8304
8566
  }
8305
8567
  console.log(chalk21.yellow("Not logged in - authenticating..."));
@@ -8311,54 +8573,350 @@ var compilePromptCommand = new Command19("compile-prompt").description("Compile
8311
8573
  }
8312
8574
  console.log();
8313
8575
  }
8314
- const threadMetadata = {};
8315
- for (const param of options.param) {
8316
- const eqIndex = param.indexOf("=");
8317
- if (eqIndex === -1) {
8318
- if (jsonMode) {
8319
- console.log(JSON.stringify({ success: false, error: `Invalid param format: ${param}. Use key=value.` }));
8320
- } else {
8321
- console.log(chalk21.red(`Invalid param format: ${param}. Use key=value.`));
8322
- }
8323
- process.exit(1);
8324
- }
8325
- const key = param.slice(0, eqIndex);
8326
- const value = param.slice(eqIndex + 1);
8327
- threadMetadata[key] = value;
8328
- }
8329
- const environment = options.env;
8330
- if (!jsonMode) {
8331
- spinner.start(`Compiling prompt for ${chalk21.cyan(agentSlug)} (${environment})`);
8576
+ return true;
8577
+ }
8578
+ function relativeTime3(ts) {
8579
+ const diff = Date.now() - ts;
8580
+ const seconds = Math.floor(diff / 1000);
8581
+ if (seconds < 60)
8582
+ return `${seconds}s ago`;
8583
+ const minutes = Math.floor(seconds / 60);
8584
+ if (minutes < 60)
8585
+ return `${minutes}m ago`;
8586
+ const hours = Math.floor(minutes / 60);
8587
+ if (hours < 24)
8588
+ return `${hours}h ago`;
8589
+ const days = Math.floor(hours / 24);
8590
+ return `${days}d ago`;
8591
+ }
8592
+ function channelColor(channel) {
8593
+ switch (channel) {
8594
+ case "whatsapp":
8595
+ return chalk21.green(channel);
8596
+ case "api":
8597
+ return chalk21.blue(channel);
8598
+ case "widget":
8599
+ return chalk21.magenta(channel);
8600
+ case "dashboard":
8601
+ return chalk21.cyan(channel);
8602
+ default:
8603
+ return chalk21.gray(channel ?? "-");
8604
+ }
8605
+ }
8606
+ function roleColor(role) {
8607
+ switch (role) {
8608
+ case "user":
8609
+ return chalk21.cyan(role);
8610
+ case "assistant":
8611
+ return chalk21.green(role);
8612
+ case "system":
8613
+ return chalk21.yellow(role);
8614
+ case "tool":
8615
+ return chalk21.magenta(role);
8616
+ default:
8617
+ return chalk21.gray(role);
8332
8618
  }
8333
- const doCompile = async () => {
8334
- return compilePrompt({
8335
- slug: agentSlug,
8336
- environment,
8337
- organizationId: project?.organization.id,
8338
- message: options.message,
8339
- channel: options.channel,
8340
- threadMetadata: Object.keys(threadMetadata).length > 0 ? threadMetadata : undefined
8619
+ }
8620
+ var threadsCommand = new Command19("threads").description("Manage conversation threads");
8621
+ threadsCommand.command("list", { isDefault: true }).description("List conversation threads").option("--env <environment>", "Environment (development|production|eval)", "development").option("--channel <channel>", "Filter by channel (whatsapp|api|widget|dashboard)").option("--limit <n>", "Maximum results", "25").option("--json", "Output raw JSON").action(async (opts) => {
8622
+ await ensureAuth6();
8623
+ const spinner = ora15();
8624
+ try {
8625
+ spinner.start("Fetching threads");
8626
+ const threads = await listThreads({
8627
+ environment: opts.env,
8628
+ channel: opts.channel,
8629
+ limit: parseInt(opts.limit, 10)
8341
8630
  });
8342
- };
8343
- const { result, error } = await doCompile();
8344
- if (error) {
8345
- if (jsonMode) {
8346
- console.log(JSON.stringify({ success: false, error }));
8347
- } else {
8348
- spinner.fail("Failed to compile prompt");
8349
- console.log(chalk21.red("Error:"), error);
8631
+ spinner.succeed(`Found ${threads.length} threads`);
8632
+ if (opts.json) {
8633
+ console.log(JSON.stringify(threads, null, 2));
8634
+ return;
8350
8635
  }
8351
- process.exit(1);
8352
- }
8353
- if (!result) {
8354
- if (jsonMode) {
8355
- console.log(JSON.stringify({ success: false, error: "No result returned" }));
8636
+ console.log();
8637
+ renderTable([
8638
+ { key: "id", label: "ID", width: 36 },
8639
+ { key: "channel", label: "Channel", width: 10 },
8640
+ { key: "participant", label: "Participant", width: 20 },
8641
+ { key: "agent", label: "Agent", width: 18 },
8642
+ { key: "lastMessage", label: "Last Message", width: 40 },
8643
+ { key: "time", label: "Time", width: 10 }
8644
+ ], threads.map((t) => ({
8645
+ id: t._id,
8646
+ channel: channelColor(t.channel),
8647
+ participant: t.participantName ?? "Unknown",
8648
+ agent: t.agentName ?? "Unknown",
8649
+ lastMessage: t.lastMessage ? t.lastMessage.content.slice(0, 40) : chalk21.gray("-"),
8650
+ time: relativeTime3(t.updatedAt ?? t.createdAt)
8651
+ })));
8652
+ console.log();
8653
+ } catch (err) {
8654
+ const message = err instanceof Error ? err.message : String(err);
8655
+ spinner.fail("Failed to fetch threads");
8656
+ if (opts.json) {
8657
+ console.log(JSON.stringify({ success: false, error: message }));
8356
8658
  } else {
8357
- spinner.fail("No result returned");
8659
+ console.log(chalk21.red("Error:"), message);
8358
8660
  }
8359
8661
  process.exit(1);
8360
8662
  }
8361
- if (!jsonMode)
8663
+ });
8664
+ threadsCommand.command("view <id>").description("View thread details and messages").option("--env <environment>", "Environment", "development").option("--json", "Output raw JSON").action(async (id, opts) => {
8665
+ await ensureAuth6();
8666
+ const spinner = ora15();
8667
+ try {
8668
+ spinner.start("Fetching thread");
8669
+ const thread = await getThreadWithMessages({
8670
+ threadId: id,
8671
+ environment: opts.env
8672
+ });
8673
+ if (!thread) {
8674
+ spinner.fail("Thread not found");
8675
+ if (opts.json) {
8676
+ console.log(JSON.stringify({ success: false, error: "Thread not found" }));
8677
+ }
8678
+ process.exit(1);
8679
+ }
8680
+ spinner.succeed("Thread loaded");
8681
+ if (opts.json) {
8682
+ console.log(JSON.stringify(thread, null, 2));
8683
+ return;
8684
+ }
8685
+ console.log();
8686
+ console.log(` ${chalk21.gray("ID:")} ${chalk21.cyan(thread._id)}`);
8687
+ console.log(` ${chalk21.gray("Channel:")} ${channelColor(thread.channel)}`);
8688
+ if (thread.externalId) {
8689
+ console.log(` ${chalk21.gray("External:")} ${chalk21.cyan(thread.externalId)}`);
8690
+ }
8691
+ console.log(` ${chalk21.gray("Agent:")} ${chalk21.cyan(thread.agentId)}`);
8692
+ if (thread.currentAgentId) {
8693
+ console.log(` ${chalk21.gray("Current:")} ${chalk21.cyan(thread.currentAgentId)}`);
8694
+ }
8695
+ console.log(` ${chalk21.gray("Created:")} ${chalk21.cyan(new Date(thread.createdAt).toISOString())}`);
8696
+ console.log(` ${chalk21.gray("Updated:")} ${chalk21.cyan(new Date(thread.updatedAt).toISOString())}`);
8697
+ if (thread.messages?.length) {
8698
+ console.log();
8699
+ console.log(chalk21.bold("Messages"));
8700
+ console.log(chalk21.gray("\u2500".repeat(60)));
8701
+ for (const msg of thread.messages) {
8702
+ if (msg.toolCalls?.length)
8703
+ continue;
8704
+ const time = new Date(msg.createdAt).toLocaleTimeString();
8705
+ const role = roleColor(msg.role);
8706
+ const content = msg.content.length > 200 ? msg.content.slice(0, 200) + "..." : msg.content;
8707
+ console.log(` ${chalk21.gray(time)} ${role}: ${content}`);
8708
+ }
8709
+ }
8710
+ console.log();
8711
+ } catch (err) {
8712
+ const message = err instanceof Error ? err.message : String(err);
8713
+ spinner.fail("Failed to fetch thread");
8714
+ if (opts.json) {
8715
+ console.log(JSON.stringify({ success: false, error: message }));
8716
+ } else {
8717
+ console.log(chalk21.red("Error:"), message);
8718
+ }
8719
+ process.exit(1);
8720
+ }
8721
+ });
8722
+ threadsCommand.command("archive <id>").description("Archive a thread (frees its externalId)").option("--env <environment>", "Environment", "development").option("--confirm", "Skip production confirmation").option("--json", "Output raw JSON").action(async (id, opts) => {
8723
+ await ensureAuth6();
8724
+ const spinner = ora15();
8725
+ const environment = opts.env;
8726
+ if (environment === "production" && !opts.confirm && isInteractive()) {
8727
+ const readline = await import("readline");
8728
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8729
+ await new Promise((resolve) => {
8730
+ rl.question(chalk21.yellow(`WARNING: Archiving thread in PRODUCTION environment.
8731
+ Press Enter to continue or Ctrl+C to cancel: `), resolve);
8732
+ });
8733
+ rl.close();
8734
+ }
8735
+ try {
8736
+ spinner.start("Archiving thread...");
8737
+ const result = await archiveThread({
8738
+ threadId: id,
8739
+ environment
8740
+ });
8741
+ spinner.succeed(chalk21.green(`Thread ${id} archived`));
8742
+ if (opts.json) {
8743
+ console.log(JSON.stringify({ success: true, threadId: id }));
8744
+ }
8745
+ } catch (err) {
8746
+ const message = err instanceof Error ? err.message : String(err);
8747
+ spinner.fail("Failed to archive thread");
8748
+ if (opts.json) {
8749
+ console.log(JSON.stringify({ success: false, error: message }));
8750
+ } else {
8751
+ console.log(chalk21.red("Error:"), message);
8752
+ }
8753
+ process.exit(1);
8754
+ }
8755
+ });
8756
+ threadsCommand.command("reset").description("Archive a thread by phone number").requiredOption("--phone <number>", "Phone number to find and archive").option("--env <environment>", "Environment", "production").option("--confirm", "Skip production confirmation").option("--json", "Output raw JSON").action(async (opts) => {
8757
+ await ensureAuth6();
8758
+ const spinner = ora15();
8759
+ const environment = opts.env;
8760
+ if (environment === "production" && !opts.confirm && isInteractive()) {
8761
+ const readline = await import("readline");
8762
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8763
+ await new Promise((resolve) => {
8764
+ rl.question(chalk21.yellow(`WARNING: Resetting thread in PRODUCTION environment.
8765
+ Press Enter to continue or Ctrl+C to cancel: `), resolve);
8766
+ });
8767
+ rl.close();
8768
+ }
8769
+ try {
8770
+ spinner.start(`Finding thread for phone ${opts.phone}...`);
8771
+ const thread = await findThreadByPhone({
8772
+ phone: opts.phone,
8773
+ environment
8774
+ });
8775
+ if (!thread) {
8776
+ spinner.fail(`No thread found for phone number ${opts.phone}`);
8777
+ if (opts.json) {
8778
+ console.log(JSON.stringify({ success: false, error: "Thread not found for phone number" }));
8779
+ }
8780
+ process.exit(1);
8781
+ }
8782
+ spinner.text = `Archiving thread ${thread._id}...`;
8783
+ await archiveThread({
8784
+ threadId: thread._id,
8785
+ environment
8786
+ });
8787
+ spinner.succeed(chalk21.green(`Thread ${thread._id} archived (phone: ${opts.phone})`));
8788
+ if (opts.json) {
8789
+ console.log(JSON.stringify({ success: true, threadId: thread._id, phone: opts.phone }));
8790
+ }
8791
+ } catch (err) {
8792
+ const message = err instanceof Error ? err.message : String(err);
8793
+ spinner.fail("Failed to reset thread");
8794
+ if (opts.json) {
8795
+ console.log(JSON.stringify({ success: false, error: message }));
8796
+ } else {
8797
+ console.log(chalk21.red("Error:"), message);
8798
+ }
8799
+ process.exit(1);
8800
+ }
8801
+ });
8802
+
8803
+ // src/cli/commands/compile-prompt.ts
8804
+ init_credentials();
8805
+ import { Command as Command20 } from "commander";
8806
+ import chalk22 from "chalk";
8807
+ import ora16 from "ora";
8808
+ init_convex();
8809
+ var compilePromptCommand = new Command20("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 | eval", "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) => {
8810
+ acc.push(val);
8811
+ return acc;
8812
+ }, []).option("--phone <number>", "Shorthand for --param phoneNumber=<number>").option("--json", "Output full JSON (raw + compiled + context)").option("--raw", "Show raw uncompiled template instead of compiled").action(async (agentSlug, options) => {
8813
+ const spinner = ora16();
8814
+ const cwd = process.cwd();
8815
+ const nonInteractive = !isInteractive();
8816
+ const jsonMode = !!options.json;
8817
+ if (!hasProject(cwd)) {
8818
+ if (nonInteractive) {
8819
+ if (jsonMode) {
8820
+ console.log(JSON.stringify({ success: false, error: "No struere.json found" }));
8821
+ } else {
8822
+ console.log(chalk22.red("No struere.json found. Run struere init first."));
8823
+ }
8824
+ process.exit(1);
8825
+ }
8826
+ console.log(chalk22.yellow("No struere.json found - initializing project..."));
8827
+ console.log();
8828
+ const success = await runInit(cwd);
8829
+ if (!success) {
8830
+ process.exit(1);
8831
+ }
8832
+ console.log();
8833
+ }
8834
+ const project = loadProject(cwd);
8835
+ if (!project) {
8836
+ if (jsonMode) {
8837
+ console.log(JSON.stringify({ success: false, error: "Failed to load struere.json" }));
8838
+ } else {
8839
+ console.log(chalk22.red("Failed to load struere.json"));
8840
+ }
8841
+ process.exit(1);
8842
+ }
8843
+ let credentials = loadCredentials();
8844
+ const apiKey = getApiKey();
8845
+ if (!credentials && !apiKey) {
8846
+ if (nonInteractive) {
8847
+ if (jsonMode) {
8848
+ console.log(JSON.stringify({ success: false, error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
8849
+ } else {
8850
+ console.log(chalk22.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
8851
+ }
8852
+ process.exit(1);
8853
+ }
8854
+ console.log(chalk22.yellow("Not logged in - authenticating..."));
8855
+ console.log();
8856
+ credentials = await performLogin();
8857
+ if (!credentials) {
8858
+ console.log(chalk22.red("Authentication failed"));
8859
+ process.exit(1);
8860
+ }
8861
+ console.log();
8862
+ }
8863
+ const threadMetadata = {};
8864
+ for (const param of options.param) {
8865
+ const eqIndex = param.indexOf("=");
8866
+ if (eqIndex === -1) {
8867
+ if (jsonMode) {
8868
+ console.log(JSON.stringify({ success: false, error: `Invalid param format: ${param}. Use key=value.` }));
8869
+ } else {
8870
+ console.log(chalk22.red(`Invalid param format: ${param}. Use key=value.`));
8871
+ }
8872
+ process.exit(1);
8873
+ }
8874
+ const key = param.slice(0, eqIndex);
8875
+ const value = param.slice(eqIndex + 1);
8876
+ let parsedValue = value;
8877
+ try {
8878
+ parsedValue = JSON.parse(value);
8879
+ } catch {
8880
+ parsedValue = value;
8881
+ }
8882
+ threadMetadata[key] = parsedValue;
8883
+ }
8884
+ if (options.phone) {
8885
+ threadMetadata["phoneNumber"] = options.phone;
8886
+ }
8887
+ const environment = options.env;
8888
+ if (!jsonMode) {
8889
+ spinner.start(`Compiling prompt for ${chalk22.cyan(agentSlug)} (${environment})`);
8890
+ }
8891
+ const doCompile = async () => {
8892
+ return compilePrompt({
8893
+ slug: agentSlug,
8894
+ environment,
8895
+ organizationId: project?.organization.id,
8896
+ message: options.message,
8897
+ channel: options.channel,
8898
+ threadMetadata: Object.keys(threadMetadata).length > 0 ? threadMetadata : undefined
8899
+ });
8900
+ };
8901
+ const { result, error } = await doCompile();
8902
+ if (error) {
8903
+ if (jsonMode) {
8904
+ console.log(JSON.stringify({ success: false, error }));
8905
+ } else {
8906
+ spinner.fail("Failed to compile prompt");
8907
+ console.log(chalk22.red("Error:"), error);
8908
+ }
8909
+ process.exit(1);
8910
+ }
8911
+ if (!result) {
8912
+ if (jsonMode) {
8913
+ console.log(JSON.stringify({ success: false, error: "No result returned" }));
8914
+ } else {
8915
+ spinner.fail("No result returned");
8916
+ }
8917
+ process.exit(1);
8918
+ }
8919
+ if (!jsonMode)
8362
8920
  spinner.succeed("Compiled prompt");
8363
8921
  if (jsonMode) {
8364
8922
  console.log(JSON.stringify({
@@ -8369,27 +8927,33 @@ var compilePromptCommand = new Command19("compile-prompt").description("Compile
8369
8927
  }, null, 2));
8370
8928
  } else if (options.raw) {
8371
8929
  console.log();
8372
- console.log(chalk21.bold("Raw System Prompt"));
8373
- console.log(chalk21.gray("\u2500".repeat(60)));
8930
+ console.log(chalk22.bold("Raw System Prompt"));
8931
+ console.log(chalk22.gray("\u2500".repeat(60)));
8374
8932
  console.log(result.raw);
8375
- console.log(chalk21.gray("\u2500".repeat(60)));
8933
+ console.log(chalk22.gray("\u2500".repeat(60)));
8934
+ if (Object.keys(threadMetadata).length > 0) {
8935
+ console.log(chalk22.gray("Params:"), Object.entries(threadMetadata).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", "));
8936
+ }
8376
8937
  } else {
8377
8938
  console.log();
8378
- console.log(chalk21.bold("Compiled System Prompt"));
8379
- console.log(chalk21.gray("\u2500".repeat(60)));
8939
+ console.log(chalk22.bold("Compiled System Prompt"));
8940
+ console.log(chalk22.gray("\u2500".repeat(60)));
8380
8941
  console.log(result.compiled);
8381
- console.log(chalk21.gray("\u2500".repeat(60)));
8942
+ console.log(chalk22.gray("\u2500".repeat(60)));
8943
+ if (Object.keys(threadMetadata).length > 0) {
8944
+ console.log(chalk22.gray("Params:"), Object.entries(threadMetadata).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", "));
8945
+ }
8382
8946
  }
8383
8947
  });
8384
8948
 
8385
8949
  // src/cli/commands/run-tool.ts
8386
8950
  init_credentials();
8387
- import { Command as Command20 } from "commander";
8388
- import chalk22 from "chalk";
8389
- import ora16 from "ora";
8951
+ import { Command as Command21 } from "commander";
8952
+ import chalk23 from "chalk";
8953
+ import ora17 from "ora";
8390
8954
  init_convex();
8391
- var runToolCommand = new Command20("run-tool").description("Run a tool as it would execute during a real agent conversation").argument("<tool-name>", "Tool name (e.g., entity.query, run_scrapers)").option("--agent <slug>", "Agent slug (optional \u2014 omit to run without agent context)").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 (toolName, options) => {
8392
- const spinner = ora16();
8955
+ var runToolCommand = new Command21("run-tool").description("Run a tool as it would execute during a real agent conversation").argument("<tool-name>", "Tool name (e.g., entity.query, run_scrapers)").option("--agent <slug>", "Agent slug (optional \u2014 omit to run without agent context)").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 (toolName, options) => {
8956
+ const spinner = ora17();
8393
8957
  const cwd = process.cwd();
8394
8958
  const nonInteractive = !isInteractive();
8395
8959
  const jsonMode = !!options.json;
@@ -8398,11 +8962,11 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8398
8962
  if (jsonMode) {
8399
8963
  console.log(JSON.stringify({ success: false, error: "No struere.json found" }));
8400
8964
  } else {
8401
- console.log(chalk22.red("No struere.json found. Run struere init first."));
8965
+ console.log(chalk23.red("No struere.json found. Run struere init first."));
8402
8966
  }
8403
8967
  process.exit(1);
8404
8968
  }
8405
- console.log(chalk22.yellow("No struere.json found - initializing project..."));
8969
+ console.log(chalk23.yellow("No struere.json found - initializing project..."));
8406
8970
  console.log();
8407
8971
  const success = await runInit(cwd);
8408
8972
  if (!success) {
@@ -8415,7 +8979,7 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8415
8979
  if (jsonMode) {
8416
8980
  console.log(JSON.stringify({ success: false, error: "Failed to load struere.json" }));
8417
8981
  } else {
8418
- console.log(chalk22.red("Failed to load struere.json"));
8982
+ console.log(chalk23.red("Failed to load struere.json"));
8419
8983
  }
8420
8984
  process.exit(1);
8421
8985
  }
@@ -8426,15 +8990,15 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8426
8990
  if (jsonMode) {
8427
8991
  console.log(JSON.stringify({ success: false, error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
8428
8992
  } else {
8429
- console.log(chalk22.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
8993
+ console.log(chalk23.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
8430
8994
  }
8431
8995
  process.exit(1);
8432
8996
  }
8433
- console.log(chalk22.yellow("Not logged in - authenticating..."));
8997
+ console.log(chalk23.yellow("Not logged in - authenticating..."));
8434
8998
  console.log();
8435
8999
  credentials = await performLogin();
8436
9000
  if (!credentials) {
8437
- console.log(chalk22.red("Authentication failed"));
9001
+ console.log(chalk23.red("Authentication failed"));
8438
9002
  process.exit(1);
8439
9003
  }
8440
9004
  console.log();
@@ -8452,26 +9016,26 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8452
9016
  if (jsonMode) {
8453
9017
  console.log(JSON.stringify({ success: false, error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}` }));
8454
9018
  } else {
8455
- console.log(chalk22.red(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`));
9019
+ console.log(chalk23.red(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`));
8456
9020
  }
8457
9021
  process.exit(1);
8458
9022
  }
8459
9023
  const environment = options.env;
8460
9024
  if (!options.agent && !toolName.includes(".") && !jsonMode) {
8461
- console.log(chalk22.dim(`Tip: Running without agent context. Use --agent <slug> to run with a specific agent.`));
9025
+ console.log(chalk23.dim(`Tip: Running without agent context. Use --agent <slug> to run with a specific agent.`));
8462
9026
  }
8463
9027
  if (environment === "production" && !options.confirm && !nonInteractive) {
8464
9028
  const readline = await import("readline");
8465
9029
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8466
9030
  await new Promise((resolve) => {
8467
- rl.question(chalk22.yellow(`WARNING: Running tool against PRODUCTION environment.
9031
+ rl.question(chalk23.yellow(`WARNING: Running tool against PRODUCTION environment.
8468
9032
  This will execute real operations with real data.
8469
9033
  Press Enter to continue or Ctrl+C to cancel: `), resolve);
8470
9034
  });
8471
9035
  rl.close();
8472
9036
  }
8473
9037
  if (!jsonMode) {
8474
- spinner.start(`Running ${chalk22.cyan(toolName)}${options.agent ? ` on ${chalk22.cyan(options.agent)}` : ""} (${environment})`);
9038
+ spinner.start(`Running ${chalk23.cyan(toolName)}${options.agent ? ` on ${chalk23.cyan(options.agent)}` : ""} (${environment})`);
8475
9039
  }
8476
9040
  const doRunTool = async () => {
8477
9041
  return runTool({
@@ -8488,7 +9052,7 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8488
9052
  console.log(JSON.stringify({ success: false, error }));
8489
9053
  } else {
8490
9054
  spinner.fail("Failed to run tool");
8491
- console.log(chalk22.red("Error:"), error);
9055
+ console.log(chalk23.red("Error:"), error);
8492
9056
  }
8493
9057
  process.exit(1);
8494
9058
  }
@@ -8504,45 +9068,51 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8504
9068
  if (jsonMode) {
8505
9069
  console.log(JSON.stringify({ success: false, error: `${result.errorType}: ${result.message}`, result }));
8506
9070
  } else {
8507
- spinner.fail(chalk22.red(`${result.errorType}: ${result.message}`));
9071
+ spinner.fail(chalk23.red(`${result.errorType}: ${result.message}`));
8508
9072
  }
8509
9073
  process.exit(1);
8510
9074
  }
8511
9075
  if (!jsonMode) {
8512
- spinner.succeed(`Ran ${chalk22.cyan(toolName)}${result.agent ? ` on ${chalk22.cyan(result.agent.slug)}` : ""} (${result.environment}) in ${result.durationMs}ms`);
9076
+ spinner.succeed(`Ran ${chalk23.cyan(toolName)}${result.agent ? ` on ${chalk23.cyan(result.agent.slug)}` : ""} (${result.environment}) in ${result.durationMs}ms`);
8513
9077
  }
8514
9078
  if (jsonMode) {
8515
9079
  console.log(JSON.stringify(result, null, 2));
8516
9080
  } else {
8517
9081
  console.log();
8518
- console.log(chalk22.dim("\u2500".repeat(50)));
9082
+ console.log(chalk23.dim("\u2500".repeat(50)));
8519
9083
  console.log(JSON.stringify(result.result, null, 2));
8520
- console.log(chalk22.dim("\u2500".repeat(50)));
9084
+ console.log(chalk23.dim("\u2500".repeat(50)));
8521
9085
  console.log();
8522
- console.log(chalk22.dim(`Identity: ${result.identity.actorType} (${result.identity.identityMode} mode)`));
9086
+ console.log(chalk23.dim(`Identity: ${result.identity.actorType} (${result.identity.identityMode} mode)`));
8523
9087
  }
8524
9088
  });
8525
9089
 
8526
9090
  // src/cli/commands/chat.ts
8527
9091
  init_credentials();
8528
- import { Command as Command21 } from "commander";
8529
- import chalk23 from "chalk";
8530
- import ora17 from "ora";
9092
+ import { Command as Command22 } from "commander";
9093
+ import chalk24 from "chalk";
9094
+ import ora18 from "ora";
8531
9095
  import readline from "readline";
8532
9096
  init_convex();
8533
9097
  function printExecutionMeta(meta) {
8534
- console.log(chalk23.dim(`Model: ${meta.model} | Duration: ${meta.durationMs}ms`));
9098
+ console.log(chalk24.dim(`Model: ${meta.model} | Duration: ${meta.durationMs}ms`));
8535
9099
  if (meta.toolCallSummary.length > 0) {
8536
- console.log(chalk23.dim("Tool calls:"));
9100
+ console.log(chalk24.dim("Tool calls:"));
8537
9101
  for (const tc of meta.toolCallSummary) {
8538
- const status = tc.status === "success" ? chalk23.green("ok") : chalk23.red(tc.status);
8539
- const err = tc.errorMessage ? chalk23.red(` \u2014 ${tc.errorMessage}`) : "";
8540
- console.log(chalk23.dim(` ${tc.name} ${status} ${tc.durationMs}ms${err}`));
9102
+ const status = tc.status === "success" ? chalk24.green("ok") : chalk24.red(tc.status);
9103
+ const err = tc.errorMessage ? chalk24.red(` \u2014 ${tc.errorMessage}`) : "";
9104
+ console.log(chalk24.dim(` ${tc.name} ${status} ${tc.durationMs}ms${err}`));
8541
9105
  }
8542
9106
  }
9107
+ if (meta.permissionDenialCount > 0) {
9108
+ console.log(chalk24.dim(`Permission denials: ${chalk24.red(String(meta.permissionDenialCount))}`));
9109
+ }
9110
+ if (meta.errorCount > 0) {
9111
+ console.log(chalk24.dim(`Tool errors: ${chalk24.yellow(String(meta.errorCount))}`));
9112
+ }
8543
9113
  }
8544
- 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 execution metadata (model, duration, tool calls)").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) => {
8545
- const spinner = ora17();
9114
+ var chatCommand = new Command22("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 execution metadata (model, duration, tool calls)").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").option("--follow", "Continue in interactive mode after sending a message").action(async (slug, options) => {
9115
+ const spinner = ora18();
8546
9116
  const cwd = process.cwd();
8547
9117
  const nonInteractive = !isInteractive();
8548
9118
  const jsonMode = !!options.json;
@@ -8551,11 +9121,11 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8551
9121
  if (jsonMode) {
8552
9122
  console.log(JSON.stringify({ success: false, error: "No struere.json found" }));
8553
9123
  } else {
8554
- console.log(chalk23.red("No struere.json found. Run struere init first."));
9124
+ console.log(chalk24.red("No struere.json found. Run struere init first."));
8555
9125
  }
8556
9126
  process.exit(1);
8557
9127
  }
8558
- console.log(chalk23.yellow("No struere.json found - initializing project..."));
9128
+ console.log(chalk24.yellow("No struere.json found - initializing project..."));
8559
9129
  console.log();
8560
9130
  const success = await runInit(cwd);
8561
9131
  if (!success) {
@@ -8568,7 +9138,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8568
9138
  if (jsonMode) {
8569
9139
  console.log(JSON.stringify({ success: false, error: "Failed to load struere.json" }));
8570
9140
  } else {
8571
- console.log(chalk23.red("Failed to load struere.json"));
9141
+ console.log(chalk24.red("Failed to load struere.json"));
8572
9142
  }
8573
9143
  process.exit(1);
8574
9144
  }
@@ -8579,15 +9149,15 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8579
9149
  if (jsonMode) {
8580
9150
  console.log(JSON.stringify({ success: false, error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
8581
9151
  } else {
8582
- console.log(chalk23.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
9152
+ console.log(chalk24.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
8583
9153
  }
8584
9154
  process.exit(1);
8585
9155
  }
8586
- console.log(chalk23.yellow("Not logged in - authenticating..."));
9156
+ console.log(chalk24.yellow("Not logged in - authenticating..."));
8587
9157
  console.log();
8588
9158
  credentials = await performLogin();
8589
9159
  if (!credentials) {
8590
- console.log(chalk23.red("Authentication failed"));
9160
+ console.log(chalk24.red("Authentication failed"));
8591
9161
  process.exit(1);
8592
9162
  }
8593
9163
  console.log();
@@ -8598,14 +9168,14 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8598
9168
  if (jsonMode) {
8599
9169
  console.log(JSON.stringify({ success: false, error: "--phone is required when using --router" }));
8600
9170
  } else {
8601
- console.log(chalk23.red("--phone is required when using --router"));
9171
+ console.log(chalk24.red("--phone is required when using --router"));
8602
9172
  }
8603
9173
  process.exit(1);
8604
9174
  }
8605
9175
  if (environment === "production" && !nonInteractive && !options.confirm) {
8606
9176
  const confirmRl = readline.createInterface({ input: process.stdin, output: process.stdout });
8607
9177
  await new Promise((resolve) => {
8608
- confirmRl.question(chalk23.yellow(`WARNING: Chatting with agent in PRODUCTION environment.
9178
+ confirmRl.question(chalk24.yellow(`WARNING: Chatting with agent in PRODUCTION environment.
8609
9179
  Press Enter to continue or Ctrl+C to cancel: `), resolve);
8610
9180
  });
8611
9181
  confirmRl.close();
@@ -8633,6 +9203,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8633
9203
  signal
8634
9204
  });
8635
9205
  };
9206
+ let threadId = options.thread;
8636
9207
  if (options.message) {
8637
9208
  if (!jsonMode) {
8638
9209
  spinner.start("Sending message...");
@@ -8647,7 +9218,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8647
9218
  if (jsonMode) {
8648
9219
  console.log(JSON.stringify({ success: false, error: "Authentication failed" }));
8649
9220
  } else {
8650
- console.log(chalk23.red("Authentication failed"));
9221
+ console.log(chalk24.red("Authentication failed"));
8651
9222
  }
8652
9223
  process.exit(1);
8653
9224
  }
@@ -8662,7 +9233,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8662
9233
  console.log(JSON.stringify({ success: false, error }));
8663
9234
  } else {
8664
9235
  spinner.fail("Failed to send message");
8665
- console.log(chalk23.red("Error:"), error);
9236
+ console.log(chalk24.red("Error:"), error);
8666
9237
  }
8667
9238
  process.exit(1);
8668
9239
  }
@@ -8689,36 +9260,39 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8689
9260
  console.log("\u2500".repeat(60));
8690
9261
  console.log();
8691
9262
  if (routerResult.routedToAgent) {
8692
- console.log(chalk23.green(`${routerResult.routedToAgent}`) + chalk23.dim(` (${routerResult.routedToAgentSlug})`) + chalk23.magenta(" \u2190 routed") + chalk23.dim(":"));
9263
+ console.log(chalk24.green(`${routerResult.routedToAgent}`) + chalk24.dim(` (${routerResult.routedToAgentSlug})`) + chalk24.magenta(" \u2190 routed") + chalk24.dim(":"));
8693
9264
  } else {
8694
- console.log(chalk23.green("Agent:"));
9265
+ console.log(chalk24.green("Agent:"));
8695
9266
  }
8696
9267
  console.log(result.message);
8697
9268
  console.log();
8698
9269
  if (options.verbose) {
8699
- console.log(chalk23.dim(`Thread: ${result.threadId}`));
8700
- console.log(chalk23.dim(`Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out (${result.usage.totalTokens} total)`));
9270
+ console.log(chalk24.dim(`Thread: ${result.threadId}`));
9271
+ console.log(chalk24.dim(`Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out (${result.usage.totalTokens} total)`));
8701
9272
  if (result._executionMeta) {
8702
9273
  printExecutionMeta(result._executionMeta);
8703
9274
  }
8704
9275
  } else {
8705
- console.log(chalk23.dim(`Thread: ${result.threadId} | Tokens: ${result.usage.totalTokens}`));
9276
+ console.log(chalk24.dim(`Thread: ${result.threadId} | Tokens: ${result.usage.totalTokens}`));
8706
9277
  }
8707
9278
  console.log();
8708
9279
  console.log("\u2500".repeat(60));
8709
9280
  }
8710
- return;
9281
+ if (options.follow && isInteractive()) {
9282
+ threadId = result.threadId;
9283
+ } else {
9284
+ return;
9285
+ }
8711
9286
  }
8712
- const headerLabel = isRouterMode ? `router ${chalk23.cyan(slug)}` : chalk23.cyan(slug);
8713
- console.log(chalk23.bold(`Chat with ${headerLabel} (${environment})`));
8714
- console.log(chalk23.dim("Type 'exit' to quit"));
9287
+ const headerLabel = isRouterMode ? `router ${chalk24.cyan(slug)}` : chalk24.cyan(slug);
9288
+ console.log(chalk24.bold(`Chat with ${headerLabel} (${environment})`));
9289
+ console.log(chalk24.dim("Type 'exit' to quit"));
8715
9290
  console.log();
8716
- let threadId = options.thread;
8717
9291
  let processing = false;
8718
9292
  let generation = 0;
8719
9293
  let currentAbort = null;
8720
9294
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8721
- rl.setPrompt(chalk23.cyan("You: "));
9295
+ rl.setPrompt(chalk24.cyan("You: "));
8722
9296
  rl.prompt();
8723
9297
  rl.on("SIGINT", () => {
8724
9298
  if (processing) {
@@ -8730,7 +9304,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8730
9304
  spinner.stop();
8731
9305
  processing = false;
8732
9306
  console.log();
8733
- console.log(chalk23.yellow("Cancelled"));
9307
+ console.log(chalk24.yellow("Cancelled"));
8734
9308
  console.log();
8735
9309
  rl.resume();
8736
9310
  rl.prompt();
@@ -8768,7 +9342,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8768
9342
  if (thisGeneration !== generation)
8769
9343
  return;
8770
9344
  if (!credentials) {
8771
- console.log(chalk23.red("Authentication failed"));
9345
+ console.log(chalk24.red("Authentication failed"));
8772
9346
  rl.close();
8773
9347
  return;
8774
9348
  }
@@ -8782,7 +9356,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8782
9356
  }
8783
9357
  if (error) {
8784
9358
  spinner.fail("");
8785
- console.log(chalk23.red("Error:"), error);
9359
+ console.log(chalk24.red("Error:"), error);
8786
9360
  processing = false;
8787
9361
  currentAbort = null;
8788
9362
  rl.resume();
@@ -8802,20 +9376,20 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8802
9376
  const interactiveRouterResult = result;
8803
9377
  console.log();
8804
9378
  if (interactiveRouterResult.routedToAgent) {
8805
- console.log(chalk23.green(`${interactiveRouterResult.routedToAgent}`) + chalk23.dim(` (${interactiveRouterResult.routedToAgentSlug})`) + chalk23.magenta(" \u2190 routed") + chalk23.dim(":"));
9379
+ console.log(chalk24.green(`${interactiveRouterResult.routedToAgent}`) + chalk24.dim(` (${interactiveRouterResult.routedToAgentSlug})`) + chalk24.magenta(" \u2190 routed") + chalk24.dim(":"));
8806
9380
  } else {
8807
- console.log(chalk23.green("Agent:"));
9381
+ console.log(chalk24.green("Agent:"));
8808
9382
  }
8809
9383
  console.log(result.message);
8810
9384
  console.log();
8811
9385
  if (options.verbose) {
8812
- console.log(chalk23.dim(`Thread: ${result.threadId}`));
8813
- console.log(chalk23.dim(`Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out (${result.usage.totalTokens} total)`));
9386
+ console.log(chalk24.dim(`Thread: ${result.threadId}`));
9387
+ console.log(chalk24.dim(`Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out (${result.usage.totalTokens} total)`));
8814
9388
  if (result._executionMeta) {
8815
9389
  printExecutionMeta(result._executionMeta);
8816
9390
  }
8817
9391
  } else {
8818
- console.log(chalk23.dim(`Tokens: ${result.usage.totalTokens}`));
9392
+ console.log(chalk24.dim(`Thread: ${result.threadId} | Tokens: ${result.usage.totalTokens}`));
8819
9393
  }
8820
9394
  console.log();
8821
9395
  processing = false;
@@ -8825,24 +9399,24 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8825
9399
  });
8826
9400
  rl.on("close", () => {
8827
9401
  console.log();
8828
- console.log(chalk23.dim("Goodbye!"));
9402
+ console.log(chalk24.dim("Goodbye!"));
8829
9403
  process.exit(0);
8830
9404
  });
8831
9405
  });
8832
9406
 
8833
9407
  // src/cli/commands/whatsapp.ts
8834
9408
  init_credentials();
8835
- import { Command as Command22 } from "commander";
8836
- import chalk24 from "chalk";
8837
- async function ensureAuth6() {
9409
+ import { Command as Command23 } from "commander";
9410
+ import chalk25 from "chalk";
9411
+ async function ensureAuth7() {
8838
9412
  const cwd = process.cwd();
8839
9413
  const nonInteractive = !isInteractive();
8840
9414
  if (!hasProject(cwd)) {
8841
9415
  if (nonInteractive) {
8842
- console.error(chalk24.red("No struere.json found. Run struere init first."));
9416
+ console.error(chalk25.red("No struere.json found. Run struere init first."));
8843
9417
  process.exit(1);
8844
9418
  }
8845
- console.log(chalk24.yellow("No struere.json found - initializing project..."));
9419
+ console.log(chalk25.yellow("No struere.json found - initializing project..."));
8846
9420
  console.log();
8847
9421
  const success = await runInit(cwd);
8848
9422
  if (!success) {
@@ -8854,14 +9428,14 @@ async function ensureAuth6() {
8854
9428
  const apiKey = getApiKey();
8855
9429
  if (!credentials && !apiKey) {
8856
9430
  if (nonInteractive) {
8857
- console.error(chalk24.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
9431
+ console.error(chalk25.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
8858
9432
  process.exit(1);
8859
9433
  }
8860
- console.log(chalk24.yellow("Not logged in - authenticating..."));
9434
+ console.log(chalk25.yellow("Not logged in - authenticating..."));
8861
9435
  console.log();
8862
9436
  credentials = await performLogin();
8863
9437
  if (!credentials) {
8864
- console.log(chalk24.red("Authentication failed"));
9438
+ console.log(chalk25.red("Authentication failed"));
8865
9439
  process.exit(1);
8866
9440
  }
8867
9441
  console.log();
@@ -8904,13 +9478,13 @@ function connectionLabel(conn) {
8904
9478
  function statusColor5(status) {
8905
9479
  switch (status) {
8906
9480
  case "connected":
8907
- return chalk24.green(status);
9481
+ return chalk25.green(status);
8908
9482
  case "pending_setup":
8909
- return chalk24.yellow("pending");
9483
+ return chalk25.yellow("pending");
8910
9484
  case "disconnected":
8911
- return chalk24.red(status);
9485
+ return chalk25.red(status);
8912
9486
  default:
8913
- return chalk24.gray(status);
9487
+ return chalk25.gray(status);
8914
9488
  }
8915
9489
  }
8916
9490
  function assignmentLabel(conn) {
@@ -8922,11 +9496,11 @@ function assignmentLabel(conn) {
8922
9496
  return `Router: ${conn.routerId.slice(-8)}`;
8923
9497
  if (conn.agentId)
8924
9498
  return `Agent: ${conn.agentId.slice(-8)}`;
8925
- return chalk24.gray("none");
9499
+ return chalk25.gray("none");
8926
9500
  }
8927
- var whatsappCommand = new Command22("whatsapp").description("Manage WhatsApp connections and routing");
9501
+ var whatsappCommand = new Command23("whatsapp").description("Manage WhatsApp connections and routing");
8928
9502
  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) => {
8929
- await ensureAuth6();
9503
+ await ensureAuth7();
8930
9504
  const env = opts.env;
8931
9505
  const out = createOutput();
8932
9506
  out.start("Fetching WhatsApp connections");
@@ -8944,7 +9518,7 @@ whatsappCommand.command("list").description("List WhatsApp connections with rout
8944
9518
  }
8945
9519
  console.log();
8946
9520
  if (connections.length === 0) {
8947
- console.log(chalk24.gray(" No WhatsApp connections found"));
9521
+ console.log(chalk25.gray(" No WhatsApp connections found"));
8948
9522
  console.log();
8949
9523
  return;
8950
9524
  }
@@ -8955,8 +9529,8 @@ whatsappCommand.command("list").description("List WhatsApp connections with rout
8955
9529
  { key: "assignment", label: "Assignment", width: 30 },
8956
9530
  { key: "id", label: "ID", width: 16 }
8957
9531
  ], connections.map((c) => ({
8958
- label: c.label || chalk24.gray("-"),
8959
- phone: c.phoneNumber ? `+${c.phoneNumber}` : chalk24.gray("-"),
9532
+ label: c.label || chalk25.gray("-"),
9533
+ phone: c.phoneNumber ? `+${c.phoneNumber}` : chalk25.gray("-"),
8960
9534
  status: statusColor5(c.status),
8961
9535
  assignment: assignmentLabel(c),
8962
9536
  id: c._id.slice(-12)
@@ -8964,7 +9538,7 @@ whatsappCommand.command("list").description("List WhatsApp connections with rout
8964
9538
  console.log();
8965
9539
  });
8966
9540
  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) => {
8967
- await ensureAuth6();
9541
+ await ensureAuth7();
8968
9542
  const env = opts.env;
8969
9543
  const out = createOutput();
8970
9544
  const conn = await resolveConnection(env, connection, out);
@@ -9000,7 +9574,7 @@ whatsappCommand.command("set-router <connection> <router-slug>").description("As
9000
9574
  console.log();
9001
9575
  });
9002
9576
  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) => {
9003
- await ensureAuth6();
9577
+ await ensureAuth7();
9004
9578
  const env = opts.env;
9005
9579
  const out = createOutput();
9006
9580
  const conn = await resolveConnection(env, connection, out);
@@ -9026,10 +9600,927 @@ whatsappCommand.command("set-agent <connection> <agent-slug>").description("Assi
9026
9600
  out.succeed(`Connection ${connectionLabel(conn)} now assigned to agent ${agent.name} (${agentSlug})`);
9027
9601
  console.log();
9028
9602
  });
9603
+
9604
+ // src/cli/commands/diff.ts
9605
+ init_credentials();
9606
+ import { Command as Command24 } from "commander";
9607
+ import chalk27 from "chalk";
9608
+ import ora19 from "ora";
9609
+ init_convex();
9610
+
9611
+ // src/cli/utils/diff.ts
9612
+ import chalk26 from "chalk";
9613
+ function computeLCS(a, b) {
9614
+ const m = a.length;
9615
+ const n = b.length;
9616
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
9617
+ for (let i2 = 1;i2 <= m; i2++) {
9618
+ for (let j2 = 1;j2 <= n; j2++) {
9619
+ if (a[i2 - 1] === b[j2 - 1]) {
9620
+ dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
9621
+ } else {
9622
+ dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
9623
+ }
9624
+ }
9625
+ }
9626
+ const inLCS = [
9627
+ Array(m).fill(false),
9628
+ Array(n).fill(false)
9629
+ ];
9630
+ let i = m;
9631
+ let j = n;
9632
+ while (i > 0 && j > 0) {
9633
+ if (a[i - 1] === b[j - 1]) {
9634
+ inLCS[0][i - 1] = true;
9635
+ inLCS[1][j - 1] = true;
9636
+ i--;
9637
+ j--;
9638
+ } else if (dp[i - 1][j] >= dp[i][j - 1]) {
9639
+ i--;
9640
+ } else {
9641
+ j--;
9642
+ }
9643
+ }
9644
+ return inLCS;
9645
+ }
9646
+ function diffLines(oldText, newText, contextLines = 3) {
9647
+ const oldLines = oldText.split(`
9648
+ `);
9649
+ const newLines = newText.split(`
9650
+ `);
9651
+ const lcs = computeLCS(oldLines, newLines);
9652
+ const oldInLCS = lcs[0];
9653
+ const newInLCS = lcs[1];
9654
+ const rawDiff = [];
9655
+ let oi = 0;
9656
+ let ni = 0;
9657
+ while (oi < oldLines.length || ni < newLines.length) {
9658
+ if (oi < oldLines.length && ni < newLines.length && oldInLCS[oi] && newInLCS[ni]) {
9659
+ rawDiff.push({ type: "context", content: oldLines[oi] });
9660
+ oi++;
9661
+ ni++;
9662
+ } else if (oi < oldLines.length && !oldInLCS[oi]) {
9663
+ rawDiff.push({ type: "removed", content: oldLines[oi] });
9664
+ oi++;
9665
+ } else if (ni < newLines.length && !newInLCS[ni]) {
9666
+ rawDiff.push({ type: "added", content: newLines[ni] });
9667
+ ni++;
9668
+ } else {
9669
+ if (oi < oldLines.length) {
9670
+ rawDiff.push({ type: "context", content: oldLines[oi] });
9671
+ oi++;
9672
+ }
9673
+ if (ni < newLines.length) {
9674
+ rawDiff.push({ type: "context", content: newLines[ni] });
9675
+ ni++;
9676
+ }
9677
+ }
9678
+ }
9679
+ const changeIndices = new Set;
9680
+ for (let idx = 0;idx < rawDiff.length; idx++) {
9681
+ if (rawDiff[idx].type !== "context") {
9682
+ for (let c = Math.max(0, idx - contextLines);c <= Math.min(rawDiff.length - 1, idx + contextLines); c++) {
9683
+ changeIndices.add(c);
9684
+ }
9685
+ }
9686
+ }
9687
+ if (changeIndices.size === 0) {
9688
+ return [];
9689
+ }
9690
+ const result = [];
9691
+ let lastIncluded = -1;
9692
+ const sorted = Array.from(changeIndices).sort((a, b) => a - b);
9693
+ for (const idx of sorted) {
9694
+ if (lastIncluded >= 0 && idx > lastIncluded + 1) {
9695
+ result.push({ type: "context", content: "..." });
9696
+ }
9697
+ result.push(rawDiff[idx]);
9698
+ lastIncluded = idx;
9699
+ }
9700
+ return result;
9701
+ }
9702
+ function diffObjects(a, b) {
9703
+ const oldText = JSON.stringify(a, null, 2) ?? "";
9704
+ const newText = JSON.stringify(b, null, 2) ?? "";
9705
+ return diffLines(oldText, newText);
9706
+ }
9707
+ function renderDiff(lines) {
9708
+ for (const line of lines) {
9709
+ switch (line.type) {
9710
+ case "removed":
9711
+ console.log(chalk26.red("- " + line.content));
9712
+ break;
9713
+ case "added":
9714
+ console.log(chalk26.green("+ " + line.content));
9715
+ break;
9716
+ case "context":
9717
+ console.log(chalk26.gray(" " + line.content));
9718
+ break;
9719
+ }
9720
+ }
9721
+ }
9722
+ function hasDiff(a, b) {
9723
+ return JSON.stringify(a) !== JSON.stringify(b);
9724
+ }
9725
+
9726
+ // src/cli/commands/diff.ts
9727
+ function buildLocalAgentComparable(agent) {
9728
+ return {
9729
+ systemPrompt: typeof agent.systemPrompt === "function" ? "" : agent.systemPrompt,
9730
+ model: agent.model || { model: "openai/gpt-5-mini" },
9731
+ tools: agent.tools || []
9732
+ };
9733
+ }
9734
+ function buildRemoteAgentComparable(agent) {
9735
+ return {
9736
+ systemPrompt: agent.systemPrompt || "",
9737
+ model: agent.model,
9738
+ tools: agent.tools || []
9739
+ };
9740
+ }
9741
+ function diffAgent(local, remote) {
9742
+ const changes = [];
9743
+ const lc = buildLocalAgentComparable(local);
9744
+ const rc = buildRemoteAgentComparable(remote);
9745
+ if (lc.systemPrompt !== rc.systemPrompt) {
9746
+ changes.push({ field: "systemPrompt", old: rc.systemPrompt, new: lc.systemPrompt });
9747
+ }
9748
+ if (hasDiff(lc.model, rc.model)) {
9749
+ changes.push({ field: "model", old: rc.model, new: lc.model });
9750
+ }
9751
+ if (hasDiff(lc.tools, rc.tools)) {
9752
+ changes.push({ field: "tools", old: rc.tools, new: lc.tools });
9753
+ }
9754
+ return changes;
9755
+ }
9756
+ function diffEntityType(local, remote) {
9757
+ const changes = [];
9758
+ if (hasDiff(local.schema, remote.schema)) {
9759
+ changes.push({ field: "schema", old: remote.schema, new: local.schema });
9760
+ }
9761
+ if (hasDiff(local.searchFields || [], remote.searchFields || [])) {
9762
+ changes.push({ field: "searchFields", old: remote.searchFields || [], new: local.searchFields || [] });
9763
+ }
9764
+ if (hasDiff(local.displayConfig || {}, remote.displayConfig || {})) {
9765
+ changes.push({ field: "displayConfig", old: remote.displayConfig || {}, new: local.displayConfig || {} });
9766
+ }
9767
+ return changes;
9768
+ }
9769
+ function diffRole(local, remote) {
9770
+ const changes = [];
9771
+ if ((local.description || "") !== (remote.description || "")) {
9772
+ changes.push({ field: "description", old: remote.description || "", new: local.description || "" });
9773
+ }
9774
+ if (hasDiff(local.policies, remote.policies)) {
9775
+ changes.push({ field: "policies", old: remote.policies, new: local.policies });
9776
+ }
9777
+ if (hasDiff(local.scopeRules || [], remote.scopeRules || [])) {
9778
+ changes.push({ field: "scopeRules", old: remote.scopeRules || [], new: local.scopeRules || [] });
9779
+ }
9780
+ if (hasDiff(local.fieldMasks || [], remote.fieldMasks || [])) {
9781
+ changes.push({ field: "fieldMasks", old: remote.fieldMasks || [], new: local.fieldMasks || [] });
9782
+ }
9783
+ return changes;
9784
+ }
9785
+ function buildLocalTriggerOn(trigger) {
9786
+ return trigger.on;
9787
+ }
9788
+ function buildRemoteTriggerOn(trigger) {
9789
+ if (trigger.cronSchedule) {
9790
+ const on2 = { schedule: trigger.cronSchedule };
9791
+ if (trigger.cronTimezone)
9792
+ on2.timezone = trigger.cronTimezone;
9793
+ return on2;
9794
+ }
9795
+ const on = {};
9796
+ if (trigger.entityType)
9797
+ on.entityType = trigger.entityType;
9798
+ if (trigger.action)
9799
+ on.action = trigger.action;
9800
+ if (trigger.condition)
9801
+ on.condition = trigger.condition;
9802
+ return on;
9803
+ }
9804
+ function diffTrigger(local, remote) {
9805
+ const changes = [];
9806
+ const localOn = buildLocalTriggerOn(local);
9807
+ const remoteOn = buildRemoteTriggerOn(remote);
9808
+ if (hasDiff(localOn, remoteOn)) {
9809
+ changes.push({ field: "on", old: remoteOn, new: localOn });
9810
+ }
9811
+ if (hasDiff(local.actions, remote.actions)) {
9812
+ changes.push({ field: "actions", old: remote.actions, new: local.actions });
9813
+ }
9814
+ return changes;
9815
+ }
9816
+ function diffRouter(local, remote) {
9817
+ const changes = [];
9818
+ if (local.mode !== remote.mode) {
9819
+ changes.push({ field: "mode", old: remote.mode, new: local.mode });
9820
+ }
9821
+ if (hasDiff(local.agents, remote.agents)) {
9822
+ changes.push({ field: "agents", old: remote.agents, new: local.agents });
9823
+ }
9824
+ if (hasDiff(local.rules || [], remote.rules || [])) {
9825
+ changes.push({ field: "rules", old: remote.rules || [], new: local.rules || [] });
9826
+ }
9827
+ if (local.fallback !== remote.fallback) {
9828
+ changes.push({ field: "fallback", old: remote.fallback, new: local.fallback });
9829
+ }
9830
+ return changes;
9831
+ }
9832
+ function renderChanges(changes) {
9833
+ for (const change of changes) {
9834
+ console.log(chalk27.cyan(` ${change.field}:`));
9835
+ if (change.field === "systemPrompt") {
9836
+ const lines = diffLines(String(change.old), String(change.new));
9837
+ if (lines.length > 0) {
9838
+ renderDiff(lines);
9839
+ }
9840
+ } else if (change.field === "tools") {
9841
+ const oldTools = change.old || [];
9842
+ const newTools = change.new || [];
9843
+ const added = newTools.filter((t) => !oldTools.includes(t));
9844
+ const removed = oldTools.filter((t) => !newTools.includes(t));
9845
+ for (const t of removed) {
9846
+ console.log(chalk27.red(`- ${t}`));
9847
+ }
9848
+ for (const t of added) {
9849
+ console.log(chalk27.green(`+ ${t}`));
9850
+ }
9851
+ } else {
9852
+ const lines = diffObjects(change.old, change.new);
9853
+ if (lines.length > 0) {
9854
+ renderDiff(lines);
9855
+ }
9856
+ }
9857
+ }
9858
+ }
9859
+ var diffCommand = new Command24("diff").description("Show content differences between local and remote resources").option("--env <environment>", "Environment to diff against", "development").option("--resource <type>", "Filter to resource type (agents|triggers|entity-types|roles|routers)").option("--name <slug>", "Filter to specific resource by slug/name").option("--json", "Output raw JSON diff").option("--stat", "Show summary stats only (no content diff)").action(async (opts) => {
9860
+ const spinner = ora19();
9861
+ const cwd = process.cwd();
9862
+ const jsonMode = !!opts.json;
9863
+ const nonInteractive = !isInteractive();
9864
+ if (!jsonMode) {
9865
+ console.log();
9866
+ console.log(chalk27.bold("Struere Diff"));
9867
+ console.log();
9868
+ }
9869
+ if (!hasProject(cwd)) {
9870
+ if (nonInteractive) {
9871
+ if (jsonMode) {
9872
+ console.log(JSON.stringify({ error: "No struere.json found. Run struere init first." }));
9873
+ } else {
9874
+ console.log(chalk27.red("No struere.json found. Run struere init first."));
9875
+ }
9876
+ process.exit(1);
9877
+ }
9878
+ console.log(chalk27.yellow("No struere.json found - initializing project..."));
9879
+ console.log();
9880
+ const success = await runInit(cwd);
9881
+ if (!success) {
9882
+ process.exit(1);
9883
+ }
9884
+ console.log();
9885
+ }
9886
+ const project = loadProject(cwd);
9887
+ if (!project) {
9888
+ if (jsonMode) {
9889
+ console.log(JSON.stringify({ error: "Failed to load struere.json" }));
9890
+ } else {
9891
+ console.log(chalk27.red("Failed to load struere.json"));
9892
+ }
9893
+ process.exit(1);
9894
+ }
9895
+ let credentials = loadCredentials();
9896
+ const apiKey = getApiKey();
9897
+ if (!credentials && !apiKey) {
9898
+ if (nonInteractive) {
9899
+ if (jsonMode) {
9900
+ console.log(JSON.stringify({ error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
9901
+ } else {
9902
+ console.log(chalk27.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
9903
+ }
9904
+ process.exit(1);
9905
+ }
9906
+ console.log(chalk27.yellow("Not logged in - authenticating..."));
9907
+ console.log();
9908
+ credentials = await performLogin();
9909
+ if (!credentials) {
9910
+ console.log(chalk27.red("Authentication failed"));
9911
+ process.exit(1);
9912
+ }
9913
+ console.log();
9914
+ }
9915
+ if (!jsonMode)
9916
+ spinner.start("Loading local resources");
9917
+ let localResources;
9918
+ try {
9919
+ localResources = await loadAllResources(cwd);
9920
+ if (!jsonMode)
9921
+ spinner.succeed("Local resources loaded");
9922
+ } catch (error2) {
9923
+ if (jsonMode) {
9924
+ console.log(JSON.stringify({ error: error2 instanceof Error ? error2.message : String(error2) }));
9925
+ } else {
9926
+ spinner.fail("Failed to load local resources");
9927
+ console.log(chalk27.red("Error:"), error2 instanceof Error ? error2.message : String(error2));
9928
+ }
9929
+ process.exit(1);
9930
+ }
9931
+ if (!jsonMode)
9932
+ spinner.start("Fetching remote state");
9933
+ const environment = opts.env;
9934
+ const { state, error } = await getPullState(project.organization.id, environment);
9935
+ if (error || !state) {
9936
+ if (jsonMode) {
9937
+ console.log(JSON.stringify({ error: error || "Failed to fetch remote state" }));
9938
+ } else {
9939
+ spinner.fail("Failed to fetch remote state");
9940
+ console.log(chalk27.red("Error:"), error || "Unknown error");
9941
+ }
9942
+ process.exit(1);
9943
+ }
9944
+ if (!jsonMode)
9945
+ spinner.succeed("Remote state fetched");
9946
+ const entries = [];
9947
+ const shouldInclude = (type, key) => {
9948
+ if (opts.resource && opts.resource !== type)
9949
+ return false;
9950
+ if (opts.name && opts.name !== key)
9951
+ return false;
9952
+ return true;
9953
+ };
9954
+ const localAgentSlugs = new Set(localResources.agents.map((a) => a.slug));
9955
+ const remoteAgentSlugs = new Set(state.agents.map((a) => a.slug));
9956
+ for (const local of localResources.agents) {
9957
+ if (!shouldInclude("agents", local.slug))
9958
+ continue;
9959
+ const remote = state.agents.find((a) => a.slug === local.slug);
9960
+ if (!remote) {
9961
+ entries.push({ type: "agents", slug: local.slug, status: "new" });
9962
+ } else {
9963
+ const changes = diffAgent(local, remote);
9964
+ if (changes.length > 0) {
9965
+ entries.push({ type: "agents", slug: local.slug, status: "modified", changes });
9966
+ }
9967
+ }
9968
+ }
9969
+ for (const remote of state.agents) {
9970
+ if (!shouldInclude("agents", remote.slug))
9971
+ continue;
9972
+ if (!localAgentSlugs.has(remote.slug)) {
9973
+ entries.push({ type: "agents", slug: remote.slug, status: "deleted" });
9974
+ }
9975
+ }
9976
+ const localEntityTypeSlugs = new Set(localResources.entityTypes.map((e) => e.slug));
9977
+ const remoteEntityTypeSlugs = new Set(state.entityTypes.map((e) => e.slug));
9978
+ for (const local of localResources.entityTypes) {
9979
+ if (!shouldInclude("entity-types", local.slug))
9980
+ continue;
9981
+ const remote = state.entityTypes.find((e) => e.slug === local.slug);
9982
+ if (!remote) {
9983
+ entries.push({ type: "entity-types", slug: local.slug, status: "new" });
9984
+ } else {
9985
+ const changes = diffEntityType(local, remote);
9986
+ if (changes.length > 0) {
9987
+ entries.push({ type: "entity-types", slug: local.slug, status: "modified", changes });
9988
+ }
9989
+ }
9990
+ }
9991
+ for (const remote of state.entityTypes) {
9992
+ if (!shouldInclude("entity-types", remote.slug))
9993
+ continue;
9994
+ if (!localEntityTypeSlugs.has(remote.slug)) {
9995
+ entries.push({ type: "entity-types", slug: remote.slug, status: "deleted" });
9996
+ }
9997
+ }
9998
+ const localRoleNames = new Set(localResources.roles.map((r) => r.name));
9999
+ const remoteRoleNames = new Set(state.roles.map((r) => r.name));
10000
+ for (const local of localResources.roles) {
10001
+ if (!shouldInclude("roles", local.name))
10002
+ continue;
10003
+ const remote = state.roles.find((r) => r.name === local.name);
10004
+ if (!remote) {
10005
+ entries.push({ type: "roles", slug: local.name, status: "new" });
10006
+ } else {
10007
+ const changes = diffRole(local, remote);
10008
+ if (changes.length > 0) {
10009
+ entries.push({ type: "roles", slug: local.name, status: "modified", changes });
10010
+ }
10011
+ }
10012
+ }
10013
+ for (const remote of state.roles) {
10014
+ if (!shouldInclude("roles", remote.name))
10015
+ continue;
10016
+ if (!localRoleNames.has(remote.name)) {
10017
+ entries.push({ type: "roles", slug: remote.name, status: "deleted" });
10018
+ }
10019
+ }
10020
+ const localTriggerSlugs = new Set(localResources.triggers.map((t) => t.slug));
10021
+ const remoteTriggers = state.triggers || [];
10022
+ const remoteTriggerSlugs = new Set(remoteTriggers.map((t) => t.slug));
10023
+ for (const local of localResources.triggers) {
10024
+ if (!shouldInclude("triggers", local.slug))
10025
+ continue;
10026
+ const remote = remoteTriggers.find((t) => t.slug === local.slug);
10027
+ if (!remote) {
10028
+ entries.push({ type: "triggers", slug: local.slug, status: "new" });
10029
+ } else {
10030
+ const changes = diffTrigger(local, remote);
10031
+ if (changes.length > 0) {
10032
+ entries.push({ type: "triggers", slug: local.slug, status: "modified", changes });
10033
+ }
10034
+ }
10035
+ }
10036
+ for (const remote of remoteTriggers) {
10037
+ if (!shouldInclude("triggers", remote.slug))
10038
+ continue;
10039
+ if (!localTriggerSlugs.has(remote.slug)) {
10040
+ entries.push({ type: "triggers", slug: remote.slug, status: "deleted" });
10041
+ }
10042
+ }
10043
+ const localRouterSlugs = new Set(localResources.routers.map((r) => r.slug));
10044
+ const remoteRouters = state.routers || [];
10045
+ const remoteRouterSlugs = new Set(remoteRouters.map((r) => r.slug));
10046
+ for (const local of localResources.routers) {
10047
+ if (!shouldInclude("routers", local.slug))
10048
+ continue;
10049
+ const remote = remoteRouters.find((r) => r.slug === local.slug);
10050
+ if (!remote) {
10051
+ entries.push({ type: "routers", slug: local.slug, status: "new" });
10052
+ } else {
10053
+ const changes = diffRouter(local, remote);
10054
+ if (changes.length > 0) {
10055
+ entries.push({ type: "routers", slug: local.slug, status: "modified", changes });
10056
+ }
10057
+ }
10058
+ }
10059
+ for (const remote of remoteRouters) {
10060
+ if (!shouldInclude("routers", remote.slug))
10061
+ continue;
10062
+ if (!localRouterSlugs.has(remote.slug)) {
10063
+ entries.push({ type: "routers", slug: remote.slug, status: "deleted" });
10064
+ }
10065
+ }
10066
+ const modified = entries.filter((e) => e.status === "modified").length;
10067
+ const newCount = entries.filter((e) => e.status === "new").length;
10068
+ const deleted = entries.filter((e) => e.status === "deleted").length;
10069
+ if (jsonMode) {
10070
+ console.log(JSON.stringify({
10071
+ resources: entries.map((e) => ({
10072
+ type: e.type,
10073
+ slug: e.slug,
10074
+ status: e.status,
10075
+ changes: e.changes?.map((c) => ({ field: c.field, old: c.old, new: c.new }))
10076
+ })),
10077
+ summary: { modified, new: newCount, deleted }
10078
+ }));
10079
+ return;
10080
+ }
10081
+ console.log();
10082
+ console.log(chalk27.bold(`struere diff`) + chalk27.gray(` (vs ${environment})`));
10083
+ console.log();
10084
+ if (entries.length === 0) {
10085
+ console.log(chalk27.green(" No differences found."));
10086
+ console.log();
10087
+ return;
10088
+ }
10089
+ for (const entry of entries) {
10090
+ const filePath = `${entry.type}/${entry.slug}.ts`;
10091
+ if (entry.status === "new") {
10092
+ console.log(chalk27.green(` ${filePath} (new \u2014 not yet synced)`));
10093
+ } else if (entry.status === "deleted") {
10094
+ console.log(chalk27.red(` ${filePath} (deleted \u2014 only on remote)`));
10095
+ } else if (entry.status === "modified") {
10096
+ console.log(chalk27.yellow(` ${filePath} (modified)`));
10097
+ if (!opts.stat) {
10098
+ console.log(chalk27.gray(" " + "\u2500".repeat(37)));
10099
+ renderChanges(entry.changes);
10100
+ }
10101
+ }
10102
+ if (!opts.stat)
10103
+ console.log();
10104
+ }
10105
+ console.log(chalk27.gray(` Summary: ${modified} modified, ${newCount} new, ${deleted} deleted`));
10106
+ console.log();
10107
+ });
10108
+
10109
+ // src/cli/commands/doctor.ts
10110
+ init_credentials();
10111
+ import { Command as Command25 } from "commander";
10112
+ import chalk28 from "chalk";
10113
+ import ora20 from "ora";
10114
+ init_convex();
10115
+ init_convex();
10116
+ var BUILTIN_ENTITY_TYPES = new Set(["payment"]);
10117
+ async function ensureAuth8() {
10118
+ const cwd = process.cwd();
10119
+ const nonInteractive = !isInteractive();
10120
+ if (!hasProject(cwd)) {
10121
+ if (nonInteractive) {
10122
+ console.error(chalk28.red("No struere.json found. Run struere init first."));
10123
+ process.exit(1);
10124
+ }
10125
+ console.log(chalk28.yellow("No struere.json found - initializing project..."));
10126
+ console.log();
10127
+ const success = await runInit(cwd);
10128
+ if (!success) {
10129
+ process.exit(1);
10130
+ }
10131
+ console.log();
10132
+ }
10133
+ let credentials = loadCredentials();
10134
+ const apiKey = getApiKey();
10135
+ if (!credentials && !apiKey) {
10136
+ if (nonInteractive) {
10137
+ console.error(chalk28.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
10138
+ process.exit(1);
10139
+ }
10140
+ console.log(chalk28.yellow("Not logged in - authenticating..."));
10141
+ console.log();
10142
+ credentials = await performLogin();
10143
+ if (!credentials) {
10144
+ console.log(chalk28.red("Authentication failed"));
10145
+ process.exit(1);
10146
+ }
10147
+ console.log();
10148
+ }
10149
+ return true;
10150
+ }
10151
+ async function withAuthRetry2(fn) {
10152
+ try {
10153
+ return await fn();
10154
+ } catch (err) {
10155
+ const msg = err instanceof Error ? err.message : String(err);
10156
+ if (msg.includes("Unauthenticated") || msg.includes("OIDC") || msg.includes("token") || msg.includes("expired")) {
10157
+ const refreshed = await refreshToken();
10158
+ if (!refreshed)
10159
+ throw err;
10160
+ return fn();
10161
+ }
10162
+ throw err;
10163
+ }
10164
+ }
10165
+ function checkWhatsAppConnection(resources) {
10166
+ const whatsappTools = new Set([
10167
+ "whatsapp.send",
10168
+ "whatsapp.sendTemplate",
10169
+ "whatsapp.sendInteractive",
10170
+ "whatsapp.sendMedia",
10171
+ "whatsapp.listTemplates",
10172
+ "whatsapp.getConversation",
10173
+ "whatsapp.getStatus"
10174
+ ]);
10175
+ const agentsUsingWhatsapp = resources.agents.filter((a) => a.tools?.some((t) => whatsappTools.has(t)));
10176
+ if (agentsUsingWhatsapp.length === 0) {
10177
+ return {
10178
+ id: "whatsapp",
10179
+ label: "WhatsApp connection",
10180
+ status: "ok",
10181
+ message: "No WhatsApp tools used"
10182
+ };
10183
+ }
10184
+ const names = agentsUsingWhatsapp.map((a) => a.name).join(", ");
10185
+ return {
10186
+ id: "whatsapp",
10187
+ label: "WhatsApp connection",
10188
+ status: "warn",
10189
+ message: `${agentsUsingWhatsapp.length} agent(s) use WhatsApp tools (${names}) - verify connection`,
10190
+ hint: "Run `struere integration whatsapp` to configure"
10191
+ };
10192
+ }
10193
+ function checkTemplateApprovals(resources) {
10194
+ const triggersUsingTemplates = resources.triggers.filter((t) => t.actions.some((a) => a.tool === "whatsapp.sendTemplate"));
10195
+ const agentsUsingTemplates = resources.agents.filter((a) => a.tools?.includes("whatsapp.sendTemplate"));
10196
+ const total = triggersUsingTemplates.length + agentsUsingTemplates.length;
10197
+ if (total === 0) {
10198
+ return {
10199
+ id: "templates",
10200
+ label: "Template approvals",
10201
+ status: "ok",
10202
+ message: "No template tools used"
10203
+ };
10204
+ }
10205
+ return {
10206
+ id: "templates",
10207
+ label: "Template approvals",
10208
+ status: "warn",
10209
+ message: `${total} resource(s) use whatsapp.sendTemplate - verify templates are approved`,
10210
+ hint: "Run `struere templates list` to check approval status"
10211
+ };
10212
+ }
10213
+ function checkEntityTypeReferences(resources) {
10214
+ const entityTypeSlugs = new Set(resources.entityTypes.map((et) => et.slug));
10215
+ const dangling = [];
10216
+ for (const trigger of resources.triggers) {
10217
+ const on = trigger.on;
10218
+ if (on.entityType && !entityTypeSlugs.has(on.entityType) && !BUILTIN_ENTITY_TYPES.has(on.entityType)) {
10219
+ dangling.push(`Trigger "${trigger.name}" references "${on.entityType}"`);
10220
+ }
10221
+ }
10222
+ if (dangling.length === 0) {
10223
+ return {
10224
+ id: "entity-refs",
10225
+ label: "Entity type references",
10226
+ status: "ok",
10227
+ message: "All triggers reference valid types"
10228
+ };
10229
+ }
10230
+ return {
10231
+ id: "entity-refs",
10232
+ label: "Entity type references",
10233
+ status: "error",
10234
+ message: dangling.join("; "),
10235
+ hint: "Create the missing entity type with `struere add data-type <name>`"
10236
+ };
10237
+ }
10238
+ function checkModelConfig(resources) {
10239
+ const issues = [];
10240
+ for (const agent of resources.agents) {
10241
+ if (!agent.model)
10242
+ continue;
10243
+ if (agent.model.temperature !== undefined && (agent.model.temperature < 0 || agent.model.temperature > 2)) {
10244
+ issues.push(`Temperature ${agent.model.temperature} in agent "${agent.name}" (should be 0-2)`);
10245
+ }
10246
+ if (agent.model.maxTokens !== undefined && agent.model.maxTokens <= 0) {
10247
+ issues.push(`maxTokens ${agent.model.maxTokens} in agent "${agent.name}" (should be > 0)`);
10248
+ }
10249
+ }
10250
+ if (issues.length === 0) {
10251
+ return {
10252
+ id: "model-config",
10253
+ label: "Model config",
10254
+ status: "ok",
10255
+ message: "All agent model configs valid"
10256
+ };
10257
+ }
10258
+ return {
10259
+ id: "model-config",
10260
+ label: "Model config",
10261
+ status: "warn",
10262
+ message: issues.join("; "),
10263
+ hint: "Check agent model config - temperature should be 0-2"
10264
+ };
10265
+ }
10266
+ async function checkStuckThreads(environment) {
10267
+ const threads = await withAuthRetry2(() => listThreads({ environment, limit: 100 }));
10268
+ const issues = [];
10269
+ const now = Date.now();
10270
+ const fiveMinutes = 5 * 60 * 1000;
10271
+ for (const thread of threads) {
10272
+ if (thread.messageCount > 500) {
10273
+ issues.push(`Thread ${thread._id.slice(-12)} has ${thread.messageCount} messages`);
10274
+ }
10275
+ if (thread.lastMessage?.role === "assistant" && !thread.lastMessage.content) {
10276
+ issues.push(`Thread ${thread._id.slice(-12)} has empty assistant response`);
10277
+ }
10278
+ }
10279
+ if (issues.length === 0) {
10280
+ return {
10281
+ id: "stuck-threads",
10282
+ label: "Stuck threads",
10283
+ status: "ok",
10284
+ message: "No stuck threads detected"
10285
+ };
10286
+ }
10287
+ const hasEmpty = issues.some((i) => i.includes("empty assistant"));
10288
+ return {
10289
+ id: "stuck-threads",
10290
+ label: "Stuck threads",
10291
+ status: hasEmpty ? "error" : "warn",
10292
+ message: `${issues.length} issue(s): ${issues.join("; ")}`,
10293
+ hint: "Run `struere threads archive <id>` to archive stuck threads"
10294
+ };
10295
+ }
10296
+ async function checkTriggerHealth(environment) {
10297
+ const executions = await withAuthRetry2(() => listTriggerExecutions({ environment, limit: 100 }));
10298
+ if (executions.length === 0) {
10299
+ return {
10300
+ id: "trigger-health",
10301
+ label: "Trigger health",
10302
+ status: "ok",
10303
+ message: "No recent trigger executions"
10304
+ };
10305
+ }
10306
+ const bySlug = {};
10307
+ for (const exec of executions) {
10308
+ const slug = exec.payload?.triggerSlug ?? "unknown";
10309
+ if (!bySlug[slug])
10310
+ bySlug[slug] = { total: 0, failed: 0 };
10311
+ bySlug[slug].total++;
10312
+ if (exec.eventType === "trigger.failed")
10313
+ bySlug[slug].failed++;
10314
+ }
10315
+ const issues = [];
10316
+ let worstStatus = "ok";
10317
+ for (const [slug, stats] of Object.entries(bySlug)) {
10318
+ const rate = stats.failed / stats.total;
10319
+ if (rate > 0.5) {
10320
+ issues.push(`"${slug}" ${Math.round(rate * 100)}% failure rate (${stats.failed}/${stats.total})`);
10321
+ worstStatus = "error";
10322
+ } else if (rate > 0.2) {
10323
+ issues.push(`"${slug}" ${Math.round(rate * 100)}% failure rate (${stats.failed}/${stats.total})`);
10324
+ if (worstStatus !== "error")
10325
+ worstStatus = "warn";
10326
+ }
10327
+ }
10328
+ if (issues.length === 0) {
10329
+ return {
10330
+ id: "trigger-health",
10331
+ label: "Trigger health",
10332
+ status: "ok",
10333
+ message: "All triggers healthy"
10334
+ };
10335
+ }
10336
+ return {
10337
+ id: "trigger-health",
10338
+ label: "Trigger health",
10339
+ status: worstStatus,
10340
+ message: issues.join("; "),
10341
+ hint: "Run `struere triggers logs <slug>` to investigate failures"
10342
+ };
10343
+ }
10344
+ async function checkSyncDrift(resources, environment) {
10345
+ const { state, error } = await withAuthRetry2(() => getSyncState(undefined, environment));
10346
+ if (error || !state) {
10347
+ return {
10348
+ id: "sync-drift",
10349
+ label: "Sync drift",
10350
+ status: "warn",
10351
+ message: `Could not fetch remote state: ${error ?? "unknown"}`,
10352
+ hint: "Run `struere sync` to deploy local changes"
10353
+ };
10354
+ }
10355
+ const localAgentSlugs = new Set(resources.agents.map((a) => a.slug));
10356
+ const remoteAgentSlugs = new Set(state.agents.map((a) => a.slug));
10357
+ const localEntitySlugs = new Set(resources.entityTypes.map((e) => e.slug));
10358
+ const remoteEntitySlugs = new Set(state.entityTypes.map((e) => e.slug));
10359
+ const localRoleNames = new Set(resources.roles.map((r) => r.name));
10360
+ const remoteRoleNames = new Set(state.roles.map((r) => r.name));
10361
+ const localTriggerSlugs = new Set(resources.triggers.map((t) => t.slug));
10362
+ const remoteTriggerSlugs = new Set((state.triggers ?? []).map((t) => t.slug));
10363
+ const localRouterSlugs = new Set(resources.routers.map((r) => r.slug));
10364
+ const remoteRouterSlugs = new Set((state.routers ?? []).map((r) => r.slug));
10365
+ const localOnly = [];
10366
+ const remoteOnly = [];
10367
+ for (const slug of localAgentSlugs) {
10368
+ if (!remoteAgentSlugs.has(slug))
10369
+ localOnly.push(`agent:${slug}`);
10370
+ }
10371
+ for (const slug of remoteAgentSlugs) {
10372
+ if (!localAgentSlugs.has(slug))
10373
+ remoteOnly.push(`agent:${slug}`);
10374
+ }
10375
+ for (const slug of localEntitySlugs) {
10376
+ if (!remoteEntitySlugs.has(slug))
10377
+ localOnly.push(`data:${slug}`);
10378
+ }
10379
+ for (const slug of remoteEntitySlugs) {
10380
+ if (!localEntitySlugs.has(slug))
10381
+ remoteOnly.push(`data:${slug}`);
10382
+ }
10383
+ for (const name of localRoleNames) {
10384
+ if (!remoteRoleNames.has(name))
10385
+ localOnly.push(`role:${name}`);
10386
+ }
10387
+ for (const name of remoteRoleNames) {
10388
+ if (!localRoleNames.has(name))
10389
+ remoteOnly.push(`role:${name}`);
10390
+ }
10391
+ for (const slug of localTriggerSlugs) {
10392
+ if (!remoteTriggerSlugs.has(slug))
10393
+ localOnly.push(`trigger:${slug}`);
10394
+ }
10395
+ for (const slug of remoteTriggerSlugs) {
10396
+ if (!localTriggerSlugs.has(slug))
10397
+ remoteOnly.push(`trigger:${slug}`);
10398
+ }
10399
+ for (const slug of localRouterSlugs) {
10400
+ if (!remoteRouterSlugs.has(slug))
10401
+ localOnly.push(`router:${slug}`);
10402
+ }
10403
+ for (const slug of remoteRouterSlugs) {
10404
+ if (!localRouterSlugs.has(slug))
10405
+ remoteOnly.push(`router:${slug}`);
10406
+ }
10407
+ if (localOnly.length === 0 && remoteOnly.length === 0) {
10408
+ return {
10409
+ id: "sync-drift",
10410
+ label: "Sync drift",
10411
+ status: "ok",
10412
+ message: "Local and remote are in sync"
10413
+ };
10414
+ }
10415
+ if (remoteOnly.length > 0) {
10416
+ const parts = [];
10417
+ if (localOnly.length > 0)
10418
+ parts.push(`${localOnly.length} local-only (${localOnly.join(", ")})`);
10419
+ parts.push(`${remoteOnly.length} remote-only (${remoteOnly.join(", ")})`);
10420
+ return {
10421
+ id: "sync-drift",
10422
+ label: "Sync drift",
10423
+ status: "error",
10424
+ message: parts.join("; "),
10425
+ hint: "Run `struere pull` to fetch remote resources, or `struere sync --force` to delete them"
10426
+ };
10427
+ }
10428
+ return {
10429
+ id: "sync-drift",
10430
+ label: "Sync drift",
10431
+ status: "warn",
10432
+ message: `${localOnly.length} local resources not yet synced (${localOnly.join(", ")})`,
10433
+ hint: "Run `struere sync` to deploy local changes"
10434
+ };
10435
+ }
10436
+ async function runCheck(fn) {
10437
+ try {
10438
+ return await fn();
10439
+ } catch (err) {
10440
+ const message = err instanceof Error ? err.message : String(err);
10441
+ return {
10442
+ id: "unknown",
10443
+ label: "Unknown",
10444
+ status: "warn",
10445
+ message: `Check failed: ${message}`
10446
+ };
10447
+ }
10448
+ }
10449
+ var doctorCommand = new Command25("doctor").description("Run diagnostic checks on your Struere project").option("--env <environment>", "Environment to check", "development").option("--json", "Output raw JSON").action(async (opts) => {
10450
+ await ensureAuth8();
10451
+ const spinner = ora20();
10452
+ const cwd = process.cwd();
10453
+ const environment = opts.env;
10454
+ try {
10455
+ spinner.start("Loading resources...");
10456
+ const resources = await loadAllResources(cwd);
10457
+ spinner.succeed("Resources loaded");
10458
+ spinner.start("Running diagnostics...");
10459
+ const results = await Promise.allSettled([
10460
+ runCheck(() => checkWhatsAppConnection(resources)),
10461
+ runCheck(() => checkTemplateApprovals(resources)),
10462
+ runCheck(() => checkEntityTypeReferences(resources)),
10463
+ runCheck(() => checkModelConfig(resources)),
10464
+ runCheck(() => checkStuckThreads(environment)),
10465
+ runCheck(() => checkTriggerHealth(environment)),
10466
+ runCheck(() => checkSyncDrift(resources, environment))
10467
+ ]);
10468
+ spinner.stop();
10469
+ const checks = results.map((r) => r.status === "fulfilled" ? r.value : { id: "unknown", label: "Unknown", status: "warn", message: `Check failed: ${r.reason}` });
10470
+ const summary = {
10471
+ ok: checks.filter((c) => c.status === "ok").length,
10472
+ warn: checks.filter((c) => c.status === "warn").length,
10473
+ error: checks.filter((c) => c.status === "error").length
10474
+ };
10475
+ if (opts.json) {
10476
+ console.log(JSON.stringify({ checks, summary }, null, 2));
10477
+ return;
10478
+ }
10479
+ console.log();
10480
+ console.log(chalk28.bold(`Struere Doctor`) + chalk28.gray(` (${environment})`));
10481
+ console.log();
10482
+ const maxLabelLen = Math.max(...checks.map((c) => c.label.length));
10483
+ for (const check of checks) {
10484
+ const icon = check.status === "ok" ? chalk28.green("\u25CF") : check.status === "warn" ? chalk28.yellow("\u25CB") : chalk28.red("\u2716");
10485
+ const paddedLabel = check.label.padEnd(maxLabelLen);
10486
+ console.log(` ${icon} ${paddedLabel} ${check.message}`);
10487
+ }
10488
+ console.log();
10489
+ const parts = [];
10490
+ if (summary.ok > 0)
10491
+ parts.push(chalk28.green(`${summary.ok} passed`));
10492
+ if (summary.warn > 0)
10493
+ parts.push(chalk28.yellow(`${summary.warn} warning${summary.warn > 1 ? "s" : ""}`));
10494
+ if (summary.error > 0)
10495
+ parts.push(chalk28.red(`${summary.error} error${summary.error > 1 ? "s" : ""}`));
10496
+ console.log(` Summary: ${parts.join(", ")}`);
10497
+ const hints = checks.filter((c) => c.hint && c.status !== "ok");
10498
+ if (hints.length > 0) {
10499
+ console.log();
10500
+ for (const h of hints) {
10501
+ const color = h.status === "error" ? chalk28.red : chalk28.yellow;
10502
+ console.log(` ${color("\u2192")} ${chalk28.dim(h.hint)}`);
10503
+ }
10504
+ }
10505
+ console.log();
10506
+ if (summary.error > 0) {
10507
+ process.exit(1);
10508
+ }
10509
+ } catch (err) {
10510
+ const message = err instanceof Error ? err.message : String(err);
10511
+ spinner.fail("Diagnostics failed");
10512
+ if (opts.json) {
10513
+ console.log(JSON.stringify({ success: false, error: message }));
10514
+ } else {
10515
+ console.log(chalk28.red("Error:"), message);
10516
+ }
10517
+ process.exit(1);
10518
+ }
10519
+ });
9029
10520
  // package.json
9030
10521
  var package_default = {
9031
10522
  name: "struere",
9032
- version: "0.12.9",
10523
+ version: "0.13.1",
9033
10524
  description: "Build, test, and deploy AI agents",
9034
10525
  keywords: [
9035
10526
  "ai",
@@ -9147,8 +10638,11 @@ program.addCommand(evalCommand);
9147
10638
  program.addCommand(templatesCommand);
9148
10639
  program.addCommand(integrationCommand);
9149
10640
  program.addCommand(triggersCommand);
10641
+ program.addCommand(threadsCommand);
9150
10642
  program.addCommand(compilePromptCommand);
9151
10643
  program.addCommand(runToolCommand);
9152
10644
  program.addCommand(chatCommand);
9153
10645
  program.addCommand(whatsappCommand);
10646
+ program.addCommand(diffCommand);
10647
+ program.addCommand(doctorCommand);
9154
10648
  program.parse();