struere 0.12.8 → 0.13.0

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() };
@@ -3110,7 +3123,8 @@ function extractAgentPayload(agent, customToolsMap) {
3110
3123
  model: {
3111
3124
  model: agent.model?.model || "openai/gpt-5-mini",
3112
3125
  temperature: agent.model?.temperature,
3113
- maxTokens: agent.model?.maxTokens
3126
+ maxTokens: agent.model?.maxTokens,
3127
+ reasoning: agent.model?.reasoning
3114
3128
  },
3115
3129
  tools
3116
3130
  };
@@ -3163,10 +3177,11 @@ function validateResources(payload, resources) {
3163
3177
  }
3164
3178
  }
3165
3179
  }
3180
+ const BUILTIN_ENTITY_TYPES = new Set(["payment"]);
3166
3181
  const entityTypeSlugs = new Set(payload.entityTypes.map((et) => et.slug));
3167
3182
  if (payload.triggers) {
3168
3183
  for (const trigger of payload.triggers) {
3169
- if (trigger.entityType && !entityTypeSlugs.has(trigger.entityType)) {
3184
+ if (trigger.entityType && !entityTypeSlugs.has(trigger.entityType) && !BUILTIN_ENTITY_TYPES.has(trigger.entityType)) {
3170
3185
  errors.push(`Trigger "${trigger.name}" references entity type "${trigger.entityType}" but no entity type with that slug exists`);
3171
3186
  }
3172
3187
  }
@@ -3687,7 +3702,7 @@ import chalk8 from "chalk";
3687
3702
  import ora7 from "ora";
3688
3703
  import { confirm as confirm4 } from "@inquirer/prompts";
3689
3704
  init_convex();
3690
- 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) => {
3691
3706
  const spinner = ora7();
3692
3707
  const cwd = process.cwd();
3693
3708
  const nonInteractive = !isInteractive();
@@ -3776,6 +3791,24 @@ var deployCommand = new Command7("deploy").description("Deploy all resources to
3776
3791
  }
3777
3792
  process.exit(1);
3778
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
+ }
3779
3812
  if (resources.agents.length === 0) {
3780
3813
  if (jsonMode) {
3781
3814
  console.log(JSON.stringify({ success: false, error: "No agents found to deploy" }));
@@ -3819,8 +3852,10 @@ var deployCommand = new Command7("deploy").description("Deploy all resources to
3819
3852
  if (!jsonMode)
3820
3853
  spinner.stop();
3821
3854
  } catch {
3822
- if (!jsonMode)
3855
+ if (!jsonMode) {
3823
3856
  spinner.stop();
3857
+ console.log(chalk8.yellow("Could not check remote state for deletions \u2014 proceeding without deletion warnings"));
3858
+ }
3824
3859
  }
3825
3860
  if (options.dryRun) {
3826
3861
  if (jsonMode) {
@@ -3890,25 +3925,30 @@ var deployCommand = new Command7("deploy").description("Deploy all resources to
3890
3925
  }
3891
3926
  if (!jsonMode)
3892
3927
  spinner.start("Deploying to production");
3928
+ let syncResult;
3929
+ const startTime = Date.now();
3893
3930
  try {
3894
- const syncResult = await syncOrganization({
3931
+ syncResult = await syncOrganization({
3895
3932
  agents: payload.agents,
3896
3933
  tools: payload.tools,
3897
3934
  entityTypes: payload.entityTypes,
3898
3935
  roles: payload.roles,
3899
3936
  triggers: payload.triggers,
3937
+ routers: payload.routers,
3900
3938
  organizationId: project.organization.id,
3901
3939
  environment: "production"
3902
3940
  });
3903
3941
  if (!syncResult.success) {
3904
3942
  throw new Error(syncResult.error || "Deploy failed");
3905
3943
  }
3944
+ const elapsed = Date.now() - startTime;
3906
3945
  if (!jsonMode)
3907
- spinner.succeed("Deployed to production");
3946
+ spinner.succeed(`Deployed to production in ${(elapsed / 1000).toFixed(1)}s`);
3908
3947
  if (jsonMode) {
3909
3948
  console.log(JSON.stringify({
3910
3949
  success: true,
3911
3950
  environment: "production",
3951
+ durationMs: elapsed,
3912
3952
  agents: {
3913
3953
  created: syncResult.agents?.created || [],
3914
3954
  updated: syncResult.agents?.updated || [],
@@ -3923,26 +3963,71 @@ var deployCommand = new Command7("deploy").description("Deploy all resources to
3923
3963
  created: syncResult.roles?.created || [],
3924
3964
  updated: syncResult.roles?.updated || [],
3925
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 || []
3926
3982
  }
3927
3983
  }));
3928
3984
  } else {
3929
3985
  console.log();
3930
3986
  console.log(chalk8.green("Success!"), "All resources deployed to production");
3931
3987
  console.log();
3932
- if (syncResult.agents?.created && syncResult.agents.created.length > 0) {
3933
- console.log("New agents:");
3934
- for (const slug of syncResult.agents.created) {
3935
- const agent = resources.agents.find((a) => a.slug === slug);
3936
- 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
+ }
3937
4009
  }
3938
- }
3939
- if (syncResult.agents?.updated && syncResult.agents.updated.length > 0) {
3940
- console.log("Updated agents:");
3941
- for (const slug of syncResult.agents.updated) {
3942
- const agent = resources.agents.find((a) => a.slug === slug);
3943
- 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
+ }
3944
4025
  }
3945
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
+ }
3946
4031
  console.log();
3947
4032
  console.log(chalk8.gray("Test your agents:"));
3948
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"}'`));
@@ -3970,6 +4055,22 @@ var deployCommand = new Command7("deploy").description("Deploy all resources to
3970
4055
  spinner.fail("Deployment failed");
3971
4056
  console.log();
3972
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
+ }
3973
4074
  console.log();
3974
4075
  }
3975
4076
  process.exit(1);
@@ -7317,6 +7418,7 @@ init_credentials();
7317
7418
  import { Command as Command18 } from "commander";
7318
7419
  import chalk20 from "chalk";
7319
7420
  import ora14 from "ora";
7421
+ init_convex();
7320
7422
 
7321
7423
  // src/cli/utils/triggers.ts
7322
7424
  init_credentials();
@@ -7456,6 +7558,20 @@ async function retryImmediateExecution(eventId, environment) {
7456
7558
  }
7457
7559
 
7458
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
+ }
7459
7575
  async function ensureAuth5() {
7460
7576
  const cwd = process.cwd();
7461
7577
  const nonInteractive = !isInteractive();
@@ -7882,11 +7998,11 @@ triggersCommand.command("logs [slug]").description("View trigger execution histo
7882
7998
  const spinner = ora14();
7883
7999
  try {
7884
8000
  spinner.start("Fetching execution logs");
7885
- const executions = await listTriggerExecutions({
8001
+ const executions = await withTriggerAuthRetry(() => listTriggerExecutions({
7886
8002
  environment: opts.env,
7887
8003
  triggerSlug: slug,
7888
8004
  limit: parseInt(opts.limit, 10)
7889
- });
8005
+ }));
7890
8006
  spinner.succeed(`Found ${executions.length} executions`);
7891
8007
  if (opts.json) {
7892
8008
  console.log(JSON.stringify(executions, null, 2));
@@ -7945,15 +8061,20 @@ triggersCommand.command("log <identifier>").description("View detailed trigger e
7945
8061
  await ensureAuth5();
7946
8062
  const spinner = ora14();
7947
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
+ }
7948
8069
  let eventId = identifier;
7949
- const isConvexId = identifier.includes("_") || identifier.length > 30;
8070
+ const isConvexId = /^[0-9a-zA-Z]{20,}$/.test(identifier);
7950
8071
  if (!isConvexId) {
7951
8072
  spinner.start("Resolving trigger slug to latest execution");
7952
- const executions = await listTriggerExecutions({
8073
+ const executions = await withTriggerAuthRetry(() => listTriggerExecutions({
7953
8074
  environment: opts.env,
7954
8075
  triggerSlug: identifier,
7955
- limit: parseInt(opts.nth, 10)
7956
- });
8076
+ limit: nth
8077
+ }));
7957
8078
  if (!executions.length) {
7958
8079
  spinner.fail(`No executions found for trigger "${identifier}" in ${opts.env}`);
7959
8080
  if (opts.json) {
@@ -7961,16 +8082,16 @@ triggersCommand.command("log <identifier>").description("View detailed trigger e
7961
8082
  }
7962
8083
  process.exit(1);
7963
8084
  }
7964
- const idx = parseInt(opts.nth, 10) - 1;
8085
+ const idx = nth - 1;
7965
8086
  if (idx >= executions.length) {
7966
- spinner.fail(`Only ${executions.length} executions found, cannot get #${opts.nth}`);
8087
+ spinner.fail(`Only ${executions.length} executions found, cannot get #${nth}`);
7967
8088
  process.exit(1);
7968
8089
  }
7969
8090
  eventId = executions[idx]._id;
7970
8091
  spinner.succeed(`Found execution for "${identifier}"`);
7971
8092
  }
7972
8093
  spinner.start("Fetching execution detail");
7973
- const event = await getTriggerExecutionDetail(eventId, opts.env);
8094
+ const event = await withTriggerAuthRetry(() => getTriggerExecutionDetail(eventId, opts.env));
7974
8095
  if (!event) {
7975
8096
  spinner.fail("Execution not found");
7976
8097
  if (opts.json) {
@@ -8250,27 +8371,182 @@ triggersCommand.command("fire <slug>").description("Manually fire a trigger").op
8250
8371
  }
8251
8372
  });
8252
8373
 
8253
- // src/cli/commands/compile-prompt.ts
8374
+ // src/cli/commands/threads.ts
8254
8375
  init_credentials();
8255
8376
  import { Command as Command19 } from "commander";
8256
8377
  import chalk21 from "chalk";
8257
8378
  import ora15 from "ora";
8379
+
8380
+ // src/cli/utils/threads.ts
8381
+ init_credentials();
8382
+ init_config();
8258
8383
  init_convex();
8259
- 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) => {
8260
- acc.push(val);
8261
- return acc;
8262
- }, []).option("--json", "Output full JSON (raw + compiled + context)").option("--raw", "Show raw uncompiled template instead of compiled").action(async (agentSlug, options) => {
8263
- 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() {
8264
8545
  const cwd = process.cwd();
8265
8546
  const nonInteractive = !isInteractive();
8266
- const jsonMode = !!options.json;
8267
8547
  if (!hasProject(cwd)) {
8268
8548
  if (nonInteractive) {
8269
- if (jsonMode) {
8270
- console.log(JSON.stringify({ success: false, error: "No struere.json found" }));
8271
- } else {
8272
- console.log(chalk21.red("No struere.json found. Run struere init first."));
8273
- }
8549
+ console.error(chalk21.red("No struere.json found. Run struere init first."));
8274
8550
  process.exit(1);
8275
8551
  }
8276
8552
  console.log(chalk21.yellow("No struere.json found - initializing project..."));
@@ -8281,24 +8557,11 @@ var compilePromptCommand = new Command19("compile-prompt").description("Compile
8281
8557
  }
8282
8558
  console.log();
8283
8559
  }
8284
- const project = loadProject(cwd);
8285
- if (!project) {
8286
- if (jsonMode) {
8287
- console.log(JSON.stringify({ success: false, error: "Failed to load struere.json" }));
8288
- } else {
8289
- console.log(chalk21.red("Failed to load struere.json"));
8290
- }
8291
- process.exit(1);
8292
- }
8293
8560
  let credentials = loadCredentials();
8294
8561
  const apiKey = getApiKey();
8295
8562
  if (!credentials && !apiKey) {
8296
8563
  if (nonInteractive) {
8297
- if (jsonMode) {
8298
- console.log(JSON.stringify({ success: false, error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
8299
- } else {
8300
- console.log(chalk21.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
8301
- }
8564
+ console.error(chalk21.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
8302
8565
  process.exit(1);
8303
8566
  }
8304
8567
  console.log(chalk21.yellow("Not logged in - authenticating..."));
@@ -8310,55 +8573,351 @@ var compilePromptCommand = new Command19("compile-prompt").description("Compile
8310
8573
  }
8311
8574
  console.log();
8312
8575
  }
8313
- const threadMetadata = {};
8314
- for (const param of options.param) {
8315
- const eqIndex = param.indexOf("=");
8316
- if (eqIndex === -1) {
8317
- if (jsonMode) {
8318
- console.log(JSON.stringify({ success: false, error: `Invalid param format: ${param}. Use key=value.` }));
8319
- } else {
8320
- console.log(chalk21.red(`Invalid param format: ${param}. Use key=value.`));
8321
- }
8322
- process.exit(1);
8323
- }
8324
- const key = param.slice(0, eqIndex);
8325
- const value = param.slice(eqIndex + 1);
8326
- threadMetadata[key] = value;
8327
- }
8328
- const environment = options.env;
8329
- if (!jsonMode) {
8330
- 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);
8331
8618
  }
8332
- const doCompile = async () => {
8333
- return compilePrompt({
8334
- slug: agentSlug,
8335
- environment,
8336
- organizationId: project?.organization.id,
8337
- message: options.message,
8338
- channel: options.channel,
8339
- 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)
8340
8630
  });
8341
- };
8342
- const { result, error } = await doCompile();
8343
- if (error) {
8344
- if (jsonMode) {
8345
- console.log(JSON.stringify({ success: false, error }));
8346
- } else {
8347
- spinner.fail("Failed to compile prompt");
8348
- 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;
8349
8635
  }
8350
- process.exit(1);
8351
- }
8352
- if (!result) {
8353
- if (jsonMode) {
8354
- 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 }));
8355
8658
  } else {
8356
- spinner.fail("No result returned");
8659
+ console.log(chalk21.red("Error:"), message);
8357
8660
  }
8358
8661
  process.exit(1);
8359
8662
  }
8360
- if (!jsonMode)
8361
- spinner.succeed("Compiled prompt");
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)
8920
+ spinner.succeed("Compiled prompt");
8362
8921
  if (jsonMode) {
8363
8922
  console.log(JSON.stringify({
8364
8923
  success: true,
@@ -8368,27 +8927,33 @@ var compilePromptCommand = new Command19("compile-prompt").description("Compile
8368
8927
  }, null, 2));
8369
8928
  } else if (options.raw) {
8370
8929
  console.log();
8371
- console.log(chalk21.bold("Raw System Prompt"));
8372
- console.log(chalk21.gray("\u2500".repeat(60)));
8930
+ console.log(chalk22.bold("Raw System Prompt"));
8931
+ console.log(chalk22.gray("\u2500".repeat(60)));
8373
8932
  console.log(result.raw);
8374
- 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
+ }
8375
8937
  } else {
8376
8938
  console.log();
8377
- console.log(chalk21.bold("Compiled System Prompt"));
8378
- console.log(chalk21.gray("\u2500".repeat(60)));
8939
+ console.log(chalk22.bold("Compiled System Prompt"));
8940
+ console.log(chalk22.gray("\u2500".repeat(60)));
8379
8941
  console.log(result.compiled);
8380
- 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
+ }
8381
8946
  }
8382
8947
  });
8383
8948
 
8384
8949
  // src/cli/commands/run-tool.ts
8385
8950
  init_credentials();
8386
- import { Command as Command20 } from "commander";
8387
- import chalk22 from "chalk";
8388
- import ora16 from "ora";
8951
+ import { Command as Command21 } from "commander";
8952
+ import chalk23 from "chalk";
8953
+ import ora17 from "ora";
8389
8954
  init_convex();
8390
- 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) => {
8391
- 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();
8392
8957
  const cwd = process.cwd();
8393
8958
  const nonInteractive = !isInteractive();
8394
8959
  const jsonMode = !!options.json;
@@ -8397,11 +8962,11 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8397
8962
  if (jsonMode) {
8398
8963
  console.log(JSON.stringify({ success: false, error: "No struere.json found" }));
8399
8964
  } else {
8400
- 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."));
8401
8966
  }
8402
8967
  process.exit(1);
8403
8968
  }
8404
- console.log(chalk22.yellow("No struere.json found - initializing project..."));
8969
+ console.log(chalk23.yellow("No struere.json found - initializing project..."));
8405
8970
  console.log();
8406
8971
  const success = await runInit(cwd);
8407
8972
  if (!success) {
@@ -8414,7 +8979,7 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8414
8979
  if (jsonMode) {
8415
8980
  console.log(JSON.stringify({ success: false, error: "Failed to load struere.json" }));
8416
8981
  } else {
8417
- console.log(chalk22.red("Failed to load struere.json"));
8982
+ console.log(chalk23.red("Failed to load struere.json"));
8418
8983
  }
8419
8984
  process.exit(1);
8420
8985
  }
@@ -8425,15 +8990,15 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8425
8990
  if (jsonMode) {
8426
8991
  console.log(JSON.stringify({ success: false, error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
8427
8992
  } else {
8428
- 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."));
8429
8994
  }
8430
8995
  process.exit(1);
8431
8996
  }
8432
- console.log(chalk22.yellow("Not logged in - authenticating..."));
8997
+ console.log(chalk23.yellow("Not logged in - authenticating..."));
8433
8998
  console.log();
8434
8999
  credentials = await performLogin();
8435
9000
  if (!credentials) {
8436
- console.log(chalk22.red("Authentication failed"));
9001
+ console.log(chalk23.red("Authentication failed"));
8437
9002
  process.exit(1);
8438
9003
  }
8439
9004
  console.log();
@@ -8451,26 +9016,26 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8451
9016
  if (jsonMode) {
8452
9017
  console.log(JSON.stringify({ success: false, error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}` }));
8453
9018
  } else {
8454
- 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)}`));
8455
9020
  }
8456
9021
  process.exit(1);
8457
9022
  }
8458
9023
  const environment = options.env;
8459
9024
  if (!options.agent && !toolName.includes(".") && !jsonMode) {
8460
- 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.`));
8461
9026
  }
8462
9027
  if (environment === "production" && !options.confirm && !nonInteractive) {
8463
9028
  const readline = await import("readline");
8464
9029
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8465
9030
  await new Promise((resolve) => {
8466
- rl.question(chalk22.yellow(`WARNING: Running tool against PRODUCTION environment.
9031
+ rl.question(chalk23.yellow(`WARNING: Running tool against PRODUCTION environment.
8467
9032
  This will execute real operations with real data.
8468
9033
  Press Enter to continue or Ctrl+C to cancel: `), resolve);
8469
9034
  });
8470
9035
  rl.close();
8471
9036
  }
8472
9037
  if (!jsonMode) {
8473
- 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})`);
8474
9039
  }
8475
9040
  const doRunTool = async () => {
8476
9041
  return runTool({
@@ -8487,7 +9052,7 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8487
9052
  console.log(JSON.stringify({ success: false, error }));
8488
9053
  } else {
8489
9054
  spinner.fail("Failed to run tool");
8490
- console.log(chalk22.red("Error:"), error);
9055
+ console.log(chalk23.red("Error:"), error);
8491
9056
  }
8492
9057
  process.exit(1);
8493
9058
  }
@@ -8503,45 +9068,51 @@ var runToolCommand = new Command20("run-tool").description("Run a tool as it wou
8503
9068
  if (jsonMode) {
8504
9069
  console.log(JSON.stringify({ success: false, error: `${result.errorType}: ${result.message}`, result }));
8505
9070
  } else {
8506
- spinner.fail(chalk22.red(`${result.errorType}: ${result.message}`));
9071
+ spinner.fail(chalk23.red(`${result.errorType}: ${result.message}`));
8507
9072
  }
8508
9073
  process.exit(1);
8509
9074
  }
8510
9075
  if (!jsonMode) {
8511
- 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`);
8512
9077
  }
8513
9078
  if (jsonMode) {
8514
9079
  console.log(JSON.stringify(result, null, 2));
8515
9080
  } else {
8516
9081
  console.log();
8517
- console.log(chalk22.dim("\u2500".repeat(50)));
9082
+ console.log(chalk23.dim("\u2500".repeat(50)));
8518
9083
  console.log(JSON.stringify(result.result, null, 2));
8519
- console.log(chalk22.dim("\u2500".repeat(50)));
9084
+ console.log(chalk23.dim("\u2500".repeat(50)));
8520
9085
  console.log();
8521
- 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)`));
8522
9087
  }
8523
9088
  });
8524
9089
 
8525
9090
  // src/cli/commands/chat.ts
8526
9091
  init_credentials();
8527
- import { Command as Command21 } from "commander";
8528
- import chalk23 from "chalk";
8529
- import ora17 from "ora";
9092
+ import { Command as Command22 } from "commander";
9093
+ import chalk24 from "chalk";
9094
+ import ora18 from "ora";
8530
9095
  import readline from "readline";
8531
9096
  init_convex();
8532
9097
  function printExecutionMeta(meta) {
8533
- console.log(chalk23.dim(`Model: ${meta.model} | Duration: ${meta.durationMs}ms`));
9098
+ console.log(chalk24.dim(`Model: ${meta.model} | Duration: ${meta.durationMs}ms`));
8534
9099
  if (meta.toolCallSummary.length > 0) {
8535
- console.log(chalk23.dim("Tool calls:"));
9100
+ console.log(chalk24.dim("Tool calls:"));
8536
9101
  for (const tc of meta.toolCallSummary) {
8537
- const status = tc.status === "success" ? chalk23.green("ok") : chalk23.red(tc.status);
8538
- const err = tc.errorMessage ? chalk23.red(` \u2014 ${tc.errorMessage}`) : "";
8539
- 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}`));
8540
9105
  }
8541
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
+ }
8542
9113
  }
8543
- 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) => {
8544
- 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();
8545
9116
  const cwd = process.cwd();
8546
9117
  const nonInteractive = !isInteractive();
8547
9118
  const jsonMode = !!options.json;
@@ -8550,11 +9121,11 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8550
9121
  if (jsonMode) {
8551
9122
  console.log(JSON.stringify({ success: false, error: "No struere.json found" }));
8552
9123
  } else {
8553
- 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."));
8554
9125
  }
8555
9126
  process.exit(1);
8556
9127
  }
8557
- console.log(chalk23.yellow("No struere.json found - initializing project..."));
9128
+ console.log(chalk24.yellow("No struere.json found - initializing project..."));
8558
9129
  console.log();
8559
9130
  const success = await runInit(cwd);
8560
9131
  if (!success) {
@@ -8567,7 +9138,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8567
9138
  if (jsonMode) {
8568
9139
  console.log(JSON.stringify({ success: false, error: "Failed to load struere.json" }));
8569
9140
  } else {
8570
- console.log(chalk23.red("Failed to load struere.json"));
9141
+ console.log(chalk24.red("Failed to load struere.json"));
8571
9142
  }
8572
9143
  process.exit(1);
8573
9144
  }
@@ -8578,15 +9149,15 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8578
9149
  if (jsonMode) {
8579
9150
  console.log(JSON.stringify({ success: false, error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
8580
9151
  } else {
8581
- 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."));
8582
9153
  }
8583
9154
  process.exit(1);
8584
9155
  }
8585
- console.log(chalk23.yellow("Not logged in - authenticating..."));
9156
+ console.log(chalk24.yellow("Not logged in - authenticating..."));
8586
9157
  console.log();
8587
9158
  credentials = await performLogin();
8588
9159
  if (!credentials) {
8589
- console.log(chalk23.red("Authentication failed"));
9160
+ console.log(chalk24.red("Authentication failed"));
8590
9161
  process.exit(1);
8591
9162
  }
8592
9163
  console.log();
@@ -8597,14 +9168,14 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8597
9168
  if (jsonMode) {
8598
9169
  console.log(JSON.stringify({ success: false, error: "--phone is required when using --router" }));
8599
9170
  } else {
8600
- console.log(chalk23.red("--phone is required when using --router"));
9171
+ console.log(chalk24.red("--phone is required when using --router"));
8601
9172
  }
8602
9173
  process.exit(1);
8603
9174
  }
8604
9175
  if (environment === "production" && !nonInteractive && !options.confirm) {
8605
9176
  const confirmRl = readline.createInterface({ input: process.stdin, output: process.stdout });
8606
9177
  await new Promise((resolve) => {
8607
- confirmRl.question(chalk23.yellow(`WARNING: Chatting with agent in PRODUCTION environment.
9178
+ confirmRl.question(chalk24.yellow(`WARNING: Chatting with agent in PRODUCTION environment.
8608
9179
  Press Enter to continue or Ctrl+C to cancel: `), resolve);
8609
9180
  });
8610
9181
  confirmRl.close();
@@ -8632,6 +9203,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8632
9203
  signal
8633
9204
  });
8634
9205
  };
9206
+ let threadId = options.thread;
8635
9207
  if (options.message) {
8636
9208
  if (!jsonMode) {
8637
9209
  spinner.start("Sending message...");
@@ -8646,7 +9218,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8646
9218
  if (jsonMode) {
8647
9219
  console.log(JSON.stringify({ success: false, error: "Authentication failed" }));
8648
9220
  } else {
8649
- console.log(chalk23.red("Authentication failed"));
9221
+ console.log(chalk24.red("Authentication failed"));
8650
9222
  }
8651
9223
  process.exit(1);
8652
9224
  }
@@ -8661,7 +9233,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8661
9233
  console.log(JSON.stringify({ success: false, error }));
8662
9234
  } else {
8663
9235
  spinner.fail("Failed to send message");
8664
- console.log(chalk23.red("Error:"), error);
9236
+ console.log(chalk24.red("Error:"), error);
8665
9237
  }
8666
9238
  process.exit(1);
8667
9239
  }
@@ -8688,36 +9260,39 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8688
9260
  console.log("\u2500".repeat(60));
8689
9261
  console.log();
8690
9262
  if (routerResult.routedToAgent) {
8691
- 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(":"));
8692
9264
  } else {
8693
- console.log(chalk23.green("Agent:"));
9265
+ console.log(chalk24.green("Agent:"));
8694
9266
  }
8695
9267
  console.log(result.message);
8696
9268
  console.log();
8697
9269
  if (options.verbose) {
8698
- console.log(chalk23.dim(`Thread: ${result.threadId}`));
8699
- 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)`));
8700
9272
  if (result._executionMeta) {
8701
9273
  printExecutionMeta(result._executionMeta);
8702
9274
  }
8703
9275
  } else {
8704
- console.log(chalk23.dim(`Thread: ${result.threadId} | Tokens: ${result.usage.totalTokens}`));
9276
+ console.log(chalk24.dim(`Thread: ${result.threadId} | Tokens: ${result.usage.totalTokens}`));
8705
9277
  }
8706
9278
  console.log();
8707
9279
  console.log("\u2500".repeat(60));
8708
9280
  }
8709
- return;
9281
+ if (options.follow && isInteractive()) {
9282
+ threadId = result.threadId;
9283
+ } else {
9284
+ return;
9285
+ }
8710
9286
  }
8711
- const headerLabel = isRouterMode ? `router ${chalk23.cyan(slug)}` : chalk23.cyan(slug);
8712
- console.log(chalk23.bold(`Chat with ${headerLabel} (${environment})`));
8713
- 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"));
8714
9290
  console.log();
8715
- let threadId = options.thread;
8716
9291
  let processing = false;
8717
9292
  let generation = 0;
8718
9293
  let currentAbort = null;
8719
9294
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8720
- rl.setPrompt(chalk23.cyan("You: "));
9295
+ rl.setPrompt(chalk24.cyan("You: "));
8721
9296
  rl.prompt();
8722
9297
  rl.on("SIGINT", () => {
8723
9298
  if (processing) {
@@ -8729,7 +9304,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8729
9304
  spinner.stop();
8730
9305
  processing = false;
8731
9306
  console.log();
8732
- console.log(chalk23.yellow("Cancelled"));
9307
+ console.log(chalk24.yellow("Cancelled"));
8733
9308
  console.log();
8734
9309
  rl.resume();
8735
9310
  rl.prompt();
@@ -8767,7 +9342,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8767
9342
  if (thisGeneration !== generation)
8768
9343
  return;
8769
9344
  if (!credentials) {
8770
- console.log(chalk23.red("Authentication failed"));
9345
+ console.log(chalk24.red("Authentication failed"));
8771
9346
  rl.close();
8772
9347
  return;
8773
9348
  }
@@ -8781,7 +9356,7 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8781
9356
  }
8782
9357
  if (error) {
8783
9358
  spinner.fail("");
8784
- console.log(chalk23.red("Error:"), error);
9359
+ console.log(chalk24.red("Error:"), error);
8785
9360
  processing = false;
8786
9361
  currentAbort = null;
8787
9362
  rl.resume();
@@ -8801,20 +9376,20 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8801
9376
  const interactiveRouterResult = result;
8802
9377
  console.log();
8803
9378
  if (interactiveRouterResult.routedToAgent) {
8804
- 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(":"));
8805
9380
  } else {
8806
- console.log(chalk23.green("Agent:"));
9381
+ console.log(chalk24.green("Agent:"));
8807
9382
  }
8808
9383
  console.log(result.message);
8809
9384
  console.log();
8810
9385
  if (options.verbose) {
8811
- console.log(chalk23.dim(`Thread: ${result.threadId}`));
8812
- 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)`));
8813
9388
  if (result._executionMeta) {
8814
9389
  printExecutionMeta(result._executionMeta);
8815
9390
  }
8816
9391
  } else {
8817
- console.log(chalk23.dim(`Tokens: ${result.usage.totalTokens}`));
9392
+ console.log(chalk24.dim(`Thread: ${result.threadId} | Tokens: ${result.usage.totalTokens}`));
8818
9393
  }
8819
9394
  console.log();
8820
9395
  processing = false;
@@ -8824,24 +9399,24 @@ var chatCommand = new Command21("chat").description("Chat with an agent or via a
8824
9399
  });
8825
9400
  rl.on("close", () => {
8826
9401
  console.log();
8827
- console.log(chalk23.dim("Goodbye!"));
9402
+ console.log(chalk24.dim("Goodbye!"));
8828
9403
  process.exit(0);
8829
9404
  });
8830
9405
  });
8831
9406
 
8832
9407
  // src/cli/commands/whatsapp.ts
8833
9408
  init_credentials();
8834
- import { Command as Command22 } from "commander";
8835
- import chalk24 from "chalk";
8836
- async function ensureAuth6() {
9409
+ import { Command as Command23 } from "commander";
9410
+ import chalk25 from "chalk";
9411
+ async function ensureAuth7() {
8837
9412
  const cwd = process.cwd();
8838
9413
  const nonInteractive = !isInteractive();
8839
9414
  if (!hasProject(cwd)) {
8840
9415
  if (nonInteractive) {
8841
- 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."));
8842
9417
  process.exit(1);
8843
9418
  }
8844
- console.log(chalk24.yellow("No struere.json found - initializing project..."));
9419
+ console.log(chalk25.yellow("No struere.json found - initializing project..."));
8845
9420
  console.log();
8846
9421
  const success = await runInit(cwd);
8847
9422
  if (!success) {
@@ -8853,14 +9428,14 @@ async function ensureAuth6() {
8853
9428
  const apiKey = getApiKey();
8854
9429
  if (!credentials && !apiKey) {
8855
9430
  if (nonInteractive) {
8856
- 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."));
8857
9432
  process.exit(1);
8858
9433
  }
8859
- console.log(chalk24.yellow("Not logged in - authenticating..."));
9434
+ console.log(chalk25.yellow("Not logged in - authenticating..."));
8860
9435
  console.log();
8861
9436
  credentials = await performLogin();
8862
9437
  if (!credentials) {
8863
- console.log(chalk24.red("Authentication failed"));
9438
+ console.log(chalk25.red("Authentication failed"));
8864
9439
  process.exit(1);
8865
9440
  }
8866
9441
  console.log();
@@ -8903,13 +9478,13 @@ function connectionLabel(conn) {
8903
9478
  function statusColor5(status) {
8904
9479
  switch (status) {
8905
9480
  case "connected":
8906
- return chalk24.green(status);
9481
+ return chalk25.green(status);
8907
9482
  case "pending_setup":
8908
- return chalk24.yellow("pending");
9483
+ return chalk25.yellow("pending");
8909
9484
  case "disconnected":
8910
- return chalk24.red(status);
9485
+ return chalk25.red(status);
8911
9486
  default:
8912
- return chalk24.gray(status);
9487
+ return chalk25.gray(status);
8913
9488
  }
8914
9489
  }
8915
9490
  function assignmentLabel(conn) {
@@ -8921,11 +9496,11 @@ function assignmentLabel(conn) {
8921
9496
  return `Router: ${conn.routerId.slice(-8)}`;
8922
9497
  if (conn.agentId)
8923
9498
  return `Agent: ${conn.agentId.slice(-8)}`;
8924
- return chalk24.gray("none");
9499
+ return chalk25.gray("none");
8925
9500
  }
8926
- var whatsappCommand = new Command22("whatsapp").description("Manage WhatsApp connections and routing");
9501
+ var whatsappCommand = new Command23("whatsapp").description("Manage WhatsApp connections and routing");
8927
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) => {
8928
- await ensureAuth6();
9503
+ await ensureAuth7();
8929
9504
  const env = opts.env;
8930
9505
  const out = createOutput();
8931
9506
  out.start("Fetching WhatsApp connections");
@@ -8943,7 +9518,7 @@ whatsappCommand.command("list").description("List WhatsApp connections with rout
8943
9518
  }
8944
9519
  console.log();
8945
9520
  if (connections.length === 0) {
8946
- console.log(chalk24.gray(" No WhatsApp connections found"));
9521
+ console.log(chalk25.gray(" No WhatsApp connections found"));
8947
9522
  console.log();
8948
9523
  return;
8949
9524
  }
@@ -8954,8 +9529,8 @@ whatsappCommand.command("list").description("List WhatsApp connections with rout
8954
9529
  { key: "assignment", label: "Assignment", width: 30 },
8955
9530
  { key: "id", label: "ID", width: 16 }
8956
9531
  ], connections.map((c) => ({
8957
- label: c.label || chalk24.gray("-"),
8958
- phone: c.phoneNumber ? `+${c.phoneNumber}` : chalk24.gray("-"),
9532
+ label: c.label || chalk25.gray("-"),
9533
+ phone: c.phoneNumber ? `+${c.phoneNumber}` : chalk25.gray("-"),
8959
9534
  status: statusColor5(c.status),
8960
9535
  assignment: assignmentLabel(c),
8961
9536
  id: c._id.slice(-12)
@@ -8963,7 +9538,7 @@ whatsappCommand.command("list").description("List WhatsApp connections with rout
8963
9538
  console.log();
8964
9539
  });
8965
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) => {
8966
- await ensureAuth6();
9541
+ await ensureAuth7();
8967
9542
  const env = opts.env;
8968
9543
  const out = createOutput();
8969
9544
  const conn = await resolveConnection(env, connection, out);
@@ -8999,7 +9574,7 @@ whatsappCommand.command("set-router <connection> <router-slug>").description("As
8999
9574
  console.log();
9000
9575
  });
9001
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) => {
9002
- await ensureAuth6();
9577
+ await ensureAuth7();
9003
9578
  const env = opts.env;
9004
9579
  const out = createOutput();
9005
9580
  const conn = await resolveConnection(env, connection, out);
@@ -9025,10 +9600,927 @@ whatsappCommand.command("set-agent <connection> <agent-slug>").description("Assi
9025
9600
  out.succeed(`Connection ${connectionLabel(conn)} now assigned to agent ${agent.name} (${agentSlug})`);
9026
9601
  console.log();
9027
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
+ });
9028
10520
  // package.json
9029
10521
  var package_default = {
9030
10522
  name: "struere",
9031
- version: "0.12.8",
10523
+ version: "0.13.0",
9032
10524
  description: "Build, test, and deploy AI agents",
9033
10525
  keywords: [
9034
10526
  "ai",
@@ -9146,8 +10638,11 @@ program.addCommand(evalCommand);
9146
10638
  program.addCommand(templatesCommand);
9147
10639
  program.addCommand(integrationCommand);
9148
10640
  program.addCommand(triggersCommand);
10641
+ program.addCommand(threadsCommand);
9149
10642
  program.addCommand(compilePromptCommand);
9150
10643
  program.addCommand(runToolCommand);
9151
10644
  program.addCommand(chatCommand);
9152
10645
  program.addCommand(whatsappCommand);
10646
+ program.addCommand(diffCommand);
10647
+ program.addCommand(doctorCommand);
9153
10648
  program.parse();