@probelabs/visor 0.1.132 → 0.1.137

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/dist/config/config-reloader.d.ts +1 -0
  2. package/dist/config/config-reloader.d.ts.map +1 -1
  3. package/dist/config/config-watcher.d.ts +1 -0
  4. package/dist/config/config-watcher.d.ts.map +1 -1
  5. package/dist/config.d.ts +4 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/docs/ai-custom-tools-usage.md +37 -0
  8. package/dist/docs/ai-custom-tools.md +43 -1
  9. package/dist/docs/custom-tools.md +70 -1
  10. package/dist/docs/script.md +542 -27
  11. package/dist/docs/testing/cookbook.md +47 -0
  12. package/dist/examples/README.md +4 -0
  13. package/dist/examples/api-tools-ai-example.yaml +63 -0
  14. package/dist/examples/api-tools-inline-overlay-example.yaml +126 -0
  15. package/dist/examples/api-tools-library.yaml +18 -0
  16. package/dist/examples/api-tools-mcp-example.yaml +55 -0
  17. package/dist/examples/openapi/profiles-overlay-rename.yaml +3 -0
  18. package/dist/examples/openapi/users-api.json +91 -0
  19. package/dist/examples/openapi/users-overlay-rename.yaml +3 -0
  20. package/dist/generated/config-schema.d.ts +223 -74
  21. package/dist/generated/config-schema.d.ts.map +1 -1
  22. package/dist/generated/config-schema.json +251 -79
  23. package/dist/index.js +45128 -25001
  24. package/dist/output/traces/{run-2026-02-18T11-06-48-673Z.ndjson → run-2026-02-23T08-59-32-321Z.ndjson} +84 -84
  25. package/dist/{traces/run-2026-02-18T11-07-37-310Z.ndjson → output/traces/run-2026-02-23T09-00-20-148Z.ndjson} +1148 -1063
  26. package/dist/providers/ai-check-provider.d.ts.map +1 -1
  27. package/dist/providers/api-tool-executor.d.ts +43 -0
  28. package/dist/providers/api-tool-executor.d.ts.map +1 -0
  29. package/dist/providers/command-check-provider.d.ts.map +1 -1
  30. package/dist/providers/custom-tool-executor.d.ts +21 -0
  31. package/dist/providers/custom-tool-executor.d.ts.map +1 -1
  32. package/dist/providers/mcp-check-provider.d.ts.map +1 -1
  33. package/dist/providers/mcp-custom-sse-server.d.ts.map +1 -1
  34. package/dist/providers/script-check-provider.d.ts +18 -2
  35. package/dist/providers/script-check-provider.d.ts.map +1 -1
  36. package/dist/sdk/{check-provider-registry-7TCA3NSG.mjs → check-provider-registry-BCGP62RY.mjs} +9 -8
  37. package/dist/sdk/{check-provider-registry-RRUZHGJI.mjs → check-provider-registry-SA2WHPLO.mjs} +9 -8
  38. package/dist/sdk/{check-provider-registry-4WLTLPMU.mjs → check-provider-registry-SCL4KP55.mjs} +9 -8
  39. package/dist/sdk/{chunk-LMJNI6RM.mjs → chunk-ALB3N4ZQ.mjs} +12 -5
  40. package/dist/sdk/{chunk-LMJNI6RM.mjs.map → chunk-ALB3N4ZQ.mjs.map} +1 -1
  41. package/dist/sdk/{chunk-27RV5RR2.mjs → chunk-BRD36I43.mjs} +3 -3
  42. package/dist/sdk/{chunk-5VY5QJTY.mjs → chunk-DFKP7LY6.mjs} +1896 -1762
  43. package/dist/sdk/chunk-DFKP7LY6.mjs.map +1 -0
  44. package/dist/sdk/{chunk-UBDHAGYY.mjs → chunk-E2N3U5HU.mjs} +5 -5
  45. package/dist/sdk/{chunk-XGI47XIH.mjs → chunk-F4K5WFSM.mjs} +1896 -1762
  46. package/dist/sdk/chunk-F4K5WFSM.mjs.map +1 -0
  47. package/dist/sdk/{chunk-BOGVSF57.mjs → chunk-J6F5K5EG.mjs} +1896 -1762
  48. package/dist/sdk/chunk-J6F5K5EG.mjs.map +1 -0
  49. package/dist/sdk/{chunk-U3BLLEW3.mjs → chunk-KPRFDKQX.mjs} +329 -80
  50. package/dist/sdk/chunk-KPRFDKQX.mjs.map +1 -0
  51. package/dist/sdk/{chunk-VF6XIUE4.mjs → chunk-LW3INISN.mjs} +32 -1
  52. package/dist/sdk/{chunk-VF6XIUE4.mjs.map → chunk-LW3INISN.mjs.map} +1 -1
  53. package/dist/sdk/{chunk-VG7FWDC2.mjs → chunk-QUEWQWDX.mjs} +11 -4
  54. package/dist/sdk/{chunk-VG7FWDC2.mjs.map → chunk-QUEWQWDX.mjs.map} +1 -1
  55. package/dist/sdk/{chunk-BGBXLPLL.mjs → chunk-UMFEBYCN.mjs} +5 -5
  56. package/dist/sdk/chunk-XKCER23W.mjs +1490 -0
  57. package/dist/sdk/chunk-XKCER23W.mjs.map +1 -0
  58. package/dist/sdk/{chunk-FAKITJ3J.mjs → chunk-YTAGJZHN.mjs} +3 -3
  59. package/dist/sdk/{chunk-XJQKTK6V.mjs → chunk-ZUEQNCKB.mjs} +2 -2
  60. package/dist/sdk/{config-FMIIATKX.mjs → config-3UIU4TMP.mjs} +3 -3
  61. package/dist/sdk/{failure-condition-evaluator-PNONVBXD.mjs → failure-condition-evaluator-3B3G5NYW.mjs} +4 -4
  62. package/dist/sdk/{failure-condition-evaluator-MUUAK7MN.mjs → failure-condition-evaluator-B5JJFYKU.mjs} +4 -4
  63. package/dist/sdk/{github-frontend-DWF6BLZH.mjs → github-frontend-VAWVSCNX.mjs} +4 -4
  64. package/dist/sdk/{github-frontend-WR4S3NG5.mjs → github-frontend-ZOVXPPHQ.mjs} +4 -4
  65. package/dist/sdk/{host-S3LSWESP.mjs → host-LOQWBHWT.mjs} +2 -2
  66. package/dist/sdk/{host-U7V54J2H.mjs → host-TEQ7HKKH.mjs} +2 -2
  67. package/dist/sdk/{liquid-extensions-YDIIH33Q.mjs → liquid-extensions-PLBOMRLI.mjs} +3 -3
  68. package/dist/sdk/{routing-MVDVJDYJ.mjs → routing-HR6N43RQ.mjs} +6 -6
  69. package/dist/sdk/{routing-F4FOWVKF.mjs → routing-SEQYM4N6.mjs} +6 -6
  70. package/dist/sdk/schedule-tool-2COUUTF7.mjs +18 -0
  71. package/dist/sdk/{schedule-tool-handler-FRN3KKRM.mjs → schedule-tool-handler-5BDMLHS5.mjs} +9 -8
  72. package/dist/sdk/{schedule-tool-handler-VFES42DD.mjs → schedule-tool-handler-OXGTPLST.mjs} +9 -8
  73. package/dist/sdk/{schedule-tool-handler-7DNEGDZC.mjs → schedule-tool-handler-Y2UABBXN.mjs} +9 -8
  74. package/dist/sdk/sdk.d.mts +55 -2
  75. package/dist/sdk/sdk.d.ts +55 -2
  76. package/dist/sdk/sdk.js +2367 -482
  77. package/dist/sdk/sdk.js.map +1 -1
  78. package/dist/sdk/sdk.mjs +8 -7
  79. package/dist/sdk/sdk.mjs.map +1 -1
  80. package/dist/sdk/{trace-helpers-RDPXIN4S.mjs → trace-helpers-FAAGLXBI.mjs} +2 -2
  81. package/dist/sdk/{trace-helpers-KSPGA24B.mjs → trace-helpers-IGMH7ZPP.mjs} +2 -2
  82. package/dist/sdk/{workflow-check-provider-BMVJ6X7N.mjs → workflow-check-provider-7SR7ZWSV.mjs} +9 -8
  83. package/dist/sdk/{workflow-check-provider-CPGIRZMH.mjs → workflow-check-provider-L2ZUOMJR.mjs} +9 -8
  84. package/dist/sdk/{workflow-check-provider-4NFWH6YO.mjs → workflow-check-provider-WLA7LO56.mjs} +9 -8
  85. package/dist/sdk/workflow-check-provider-WLA7LO56.mjs.map +1 -0
  86. package/dist/state-machine/dispatch/execution-invoker.d.ts.map +1 -1
  87. package/dist/state-machine-execution-engine.d.ts.map +1 -1
  88. package/dist/test-runner/core/test-execution-wrapper.d.ts.map +1 -1
  89. package/dist/test-runner/index.d.ts.map +1 -1
  90. package/dist/test-runner/validator.d.ts.map +1 -1
  91. package/dist/traces/{run-2026-02-18T11-06-48-673Z.ndjson → run-2026-02-23T08-59-32-321Z.ndjson} +84 -84
  92. package/dist/{output/traces/run-2026-02-18T11-07-37-310Z.ndjson → traces/run-2026-02-23T09-00-20-148Z.ndjson} +1148 -1063
  93. package/dist/types/config.d.ts +55 -2
  94. package/dist/types/config.d.ts.map +1 -1
  95. package/dist/utils/config-loader.d.ts +5 -0
  96. package/dist/utils/config-loader.d.ts.map +1 -1
  97. package/dist/utils/sandbox.d.ts +8 -0
  98. package/dist/utils/sandbox.d.ts.map +1 -1
  99. package/dist/utils/script-tool-environment.d.ts +90 -0
  100. package/dist/utils/script-tool-environment.d.ts.map +1 -0
  101. package/dist/utils/tool-resolver.d.ts +18 -0
  102. package/dist/utils/tool-resolver.d.ts.map +1 -0
  103. package/package.json +11 -4
  104. package/dist/sdk/chunk-5VY5QJTY.mjs.map +0 -1
  105. package/dist/sdk/chunk-BOGVSF57.mjs.map +0 -1
  106. package/dist/sdk/chunk-U3BLLEW3.mjs.map +0 -1
  107. package/dist/sdk/chunk-XGI47XIH.mjs.map +0 -1
  108. /package/dist/sdk/{check-provider-registry-4WLTLPMU.mjs.map → check-provider-registry-BCGP62RY.mjs.map} +0 -0
  109. /package/dist/sdk/{check-provider-registry-7TCA3NSG.mjs.map → check-provider-registry-SA2WHPLO.mjs.map} +0 -0
  110. /package/dist/sdk/{check-provider-registry-RRUZHGJI.mjs.map → check-provider-registry-SCL4KP55.mjs.map} +0 -0
  111. /package/dist/sdk/{chunk-27RV5RR2.mjs.map → chunk-BRD36I43.mjs.map} +0 -0
  112. /package/dist/sdk/{chunk-BGBXLPLL.mjs.map → chunk-E2N3U5HU.mjs.map} +0 -0
  113. /package/dist/sdk/{chunk-UBDHAGYY.mjs.map → chunk-UMFEBYCN.mjs.map} +0 -0
  114. /package/dist/sdk/{chunk-FAKITJ3J.mjs.map → chunk-YTAGJZHN.mjs.map} +0 -0
  115. /package/dist/sdk/{chunk-XJQKTK6V.mjs.map → chunk-ZUEQNCKB.mjs.map} +0 -0
  116. /package/dist/sdk/{config-FMIIATKX.mjs.map → config-3UIU4TMP.mjs.map} +0 -0
  117. /package/dist/sdk/{failure-condition-evaluator-MUUAK7MN.mjs.map → failure-condition-evaluator-3B3G5NYW.mjs.map} +0 -0
  118. /package/dist/sdk/{failure-condition-evaluator-PNONVBXD.mjs.map → failure-condition-evaluator-B5JJFYKU.mjs.map} +0 -0
  119. /package/dist/sdk/{github-frontend-DWF6BLZH.mjs.map → github-frontend-VAWVSCNX.mjs.map} +0 -0
  120. /package/dist/sdk/{github-frontend-WR4S3NG5.mjs.map → github-frontend-ZOVXPPHQ.mjs.map} +0 -0
  121. /package/dist/sdk/{host-S3LSWESP.mjs.map → host-LOQWBHWT.mjs.map} +0 -0
  122. /package/dist/sdk/{host-U7V54J2H.mjs.map → host-TEQ7HKKH.mjs.map} +0 -0
  123. /package/dist/sdk/{liquid-extensions-YDIIH33Q.mjs.map → liquid-extensions-PLBOMRLI.mjs.map} +0 -0
  124. /package/dist/sdk/{routing-F4FOWVKF.mjs.map → routing-HR6N43RQ.mjs.map} +0 -0
  125. /package/dist/sdk/{routing-MVDVJDYJ.mjs.map → routing-SEQYM4N6.mjs.map} +0 -0
  126. /package/dist/sdk/{schedule-tool-handler-7DNEGDZC.mjs.map → schedule-tool-2COUUTF7.mjs.map} +0 -0
  127. /package/dist/sdk/{schedule-tool-handler-FRN3KKRM.mjs.map → schedule-tool-handler-5BDMLHS5.mjs.map} +0 -0
  128. /package/dist/sdk/{schedule-tool-handler-VFES42DD.mjs.map → schedule-tool-handler-OXGTPLST.mjs.map} +0 -0
  129. /package/dist/sdk/{trace-helpers-KSPGA24B.mjs.map → schedule-tool-handler-Y2UABBXN.mjs.map} +0 -0
  130. /package/dist/sdk/{trace-helpers-RDPXIN4S.mjs.map → trace-helpers-FAAGLXBI.mjs.map} +0 -0
  131. /package/dist/sdk/{workflow-check-provider-4NFWH6YO.mjs.map → trace-helpers-IGMH7ZPP.mjs.map} +0 -0
  132. /package/dist/sdk/{workflow-check-provider-BMVJ6X7N.mjs.map → workflow-check-provider-7SR7ZWSV.mjs.map} +0 -0
  133. /package/dist/sdk/{workflow-check-provider-CPGIRZMH.mjs.map → workflow-check-provider-L2ZUOMJR.mjs.map} +0 -0
@@ -28,7 +28,18 @@ import {
28
28
  import {
29
29
  config_exports,
30
30
  init_config
31
- } from "./chunk-U3BLLEW3.mjs";
31
+ } from "./chunk-KPRFDKQX.mjs";
32
+ import {
33
+ ScheduleStore,
34
+ buildScheduleToolContext,
35
+ getScheduleToolDefinition,
36
+ handleScheduleAction,
37
+ init_schedule_parser,
38
+ init_schedule_store,
39
+ init_schedule_tool,
40
+ init_store,
41
+ isScheduleTool
42
+ } from "./chunk-XKCER23W.mjs";
32
43
  import {
33
44
  ExecutionJournal,
34
45
  checkLoopBudget,
@@ -37,11 +48,11 @@ import {
37
48
  init_routing,
38
49
  init_snapshot_store,
39
50
  snapshot_store_exports
40
- } from "./chunk-UBDHAGYY.mjs";
51
+ } from "./chunk-UMFEBYCN.mjs";
41
52
  import {
42
53
  FailureConditionEvaluator,
43
54
  init_failure_condition_evaluator
44
- } from "./chunk-FAKITJ3J.mjs";
55
+ } from "./chunk-YTAGJZHN.mjs";
45
56
  import {
46
57
  addEvent,
47
58
  emitNdjsonFallback,
@@ -52,7 +63,7 @@ import {
52
63
  setSpanAttributes,
53
64
  trace_helpers_exports,
54
65
  withActiveSpan
55
- } from "./chunk-LMJNI6RM.mjs";
66
+ } from "./chunk-ALB3N4ZQ.mjs";
56
67
  import {
57
68
  addDiagramBlock,
58
69
  init_metrics
@@ -60,7 +71,7 @@ import {
60
71
  import {
61
72
  createExtendedLiquid,
62
73
  init_liquid_extensions
63
- } from "./chunk-XJQKTK6V.mjs";
74
+ } from "./chunk-ZUEQNCKB.mjs";
64
75
  import {
65
76
  createPermissionHelpers,
66
77
  detectLocalMode,
@@ -69,9 +80,10 @@ import {
69
80
  } from "./chunk-25IC7KXZ.mjs";
70
81
  import {
71
82
  compileAndRun,
83
+ compileAndRunAsync,
72
84
  createSecureSandbox,
73
85
  init_sandbox
74
- } from "./chunk-VF6XIUE4.mjs";
86
+ } from "./chunk-LW3INISN.mjs";
75
87
  import {
76
88
  MemoryStore,
77
89
  init_memory_store,
@@ -116,7 +128,7 @@ async function processDiffWithOutline(diffContent) {
116
128
  }
117
129
  try {
118
130
  const originalProbePath = process.env.PROBE_PATH;
119
- const fs11 = __require("fs");
131
+ const fs10 = __require("fs");
120
132
  const possiblePaths = [
121
133
  // Relative to current working directory (most common in production)
122
134
  path.join(process.cwd(), "node_modules/@probelabs/probe/bin/probe-binary"),
@@ -127,7 +139,7 @@ async function processDiffWithOutline(diffContent) {
127
139
  ];
128
140
  let probeBinaryPath;
129
141
  for (const candidatePath of possiblePaths) {
130
- if (fs11.existsSync(candidatePath)) {
142
+ if (fs10.existsSync(candidatePath)) {
131
143
  probeBinaryPath = candidatePath;
132
144
  break;
133
145
  }
@@ -1210,8 +1222,8 @@ ${schemaString}`);
1210
1222
  }
1211
1223
  if (process.env.VISOR_DEBUG_AI_SESSIONS === "true") {
1212
1224
  try {
1213
- const fs11 = __require("fs");
1214
- const path14 = __require("path");
1225
+ const fs10 = __require("fs");
1226
+ const path13 = __require("path");
1215
1227
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1216
1228
  const provider = this.config.provider || "auto";
1217
1229
  const model = this.config.model || "default";
@@ -1325,20 +1337,20 @@ ${"=".repeat(60)}
1325
1337
  `;
1326
1338
  readableVersion += `${"=".repeat(60)}
1327
1339
  `;
1328
- const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path14.join(process.cwd(), "debug-artifacts");
1329
- if (!fs11.existsSync(debugArtifactsDir)) {
1330
- fs11.mkdirSync(debugArtifactsDir, { recursive: true });
1340
+ const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path13.join(process.cwd(), "debug-artifacts");
1341
+ if (!fs10.existsSync(debugArtifactsDir)) {
1342
+ fs10.mkdirSync(debugArtifactsDir, { recursive: true });
1331
1343
  }
1332
- const debugFile = path14.join(
1344
+ const debugFile = path13.join(
1333
1345
  debugArtifactsDir,
1334
1346
  `prompt-${_checkName || "unknown"}-${timestamp}.json`
1335
1347
  );
1336
- fs11.writeFileSync(debugFile, debugJson, "utf-8");
1337
- const readableFile = path14.join(
1348
+ fs10.writeFileSync(debugFile, debugJson, "utf-8");
1349
+ const readableFile = path13.join(
1338
1350
  debugArtifactsDir,
1339
1351
  `prompt-${_checkName || "unknown"}-${timestamp}.txt`
1340
1352
  );
1341
- fs11.writeFileSync(readableFile, readableVersion, "utf-8");
1353
+ fs10.writeFileSync(readableFile, readableVersion, "utf-8");
1342
1354
  log(`
1343
1355
  \u{1F4BE} Full debug info saved to:`);
1344
1356
  log(` JSON: ${debugFile}`);
@@ -1371,8 +1383,8 @@ ${"=".repeat(60)}
1371
1383
  log(`\u{1F4E4} Response length: ${response.length} characters`);
1372
1384
  if (process.env.VISOR_DEBUG_AI_SESSIONS === "true") {
1373
1385
  try {
1374
- const fs11 = __require("fs");
1375
- const path14 = __require("path");
1386
+ const fs10 = __require("fs");
1387
+ const path13 = __require("path");
1376
1388
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1377
1389
  const agentAny2 = agent;
1378
1390
  let fullHistory = [];
@@ -1383,8 +1395,8 @@ ${"=".repeat(60)}
1383
1395
  } else if (agentAny2._messages) {
1384
1396
  fullHistory = agentAny2._messages;
1385
1397
  }
1386
- const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path14.join(process.cwd(), "debug-artifacts");
1387
- const sessionBase = path14.join(
1398
+ const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path13.join(process.cwd(), "debug-artifacts");
1399
+ const sessionBase = path13.join(
1388
1400
  debugArtifactsDir,
1389
1401
  `session-${_checkName || "unknown"}-${timestamp}`
1390
1402
  );
@@ -1396,7 +1408,7 @@ ${"=".repeat(60)}
1396
1408
  schema: effectiveSchema,
1397
1409
  totalMessages: fullHistory.length
1398
1410
  };
1399
- fs11.writeFileSync(sessionBase + ".json", JSON.stringify(sessionData, null, 2), "utf-8");
1411
+ fs10.writeFileSync(sessionBase + ".json", JSON.stringify(sessionData, null, 2), "utf-8");
1400
1412
  let readable = `=============================================================
1401
1413
  `;
1402
1414
  readable += `COMPLETE AI SESSION HISTORY (AFTER RESPONSE)
@@ -1423,7 +1435,7 @@ ${"=".repeat(60)}
1423
1435
  `;
1424
1436
  readable += content + "\n";
1425
1437
  });
1426
- fs11.writeFileSync(sessionBase + ".summary.txt", readable, "utf-8");
1438
+ fs10.writeFileSync(sessionBase + ".summary.txt", readable, "utf-8");
1427
1439
  log(`\u{1F4BE} Complete session history saved:`);
1428
1440
  log(` - Contains ALL ${fullHistory.length} messages (prompts + responses)`);
1429
1441
  } catch (error) {
@@ -1432,11 +1444,11 @@ ${"=".repeat(60)}
1432
1444
  }
1433
1445
  if (process.env.VISOR_DEBUG_AI_SESSIONS === "true") {
1434
1446
  try {
1435
- const fs11 = __require("fs");
1436
- const path14 = __require("path");
1447
+ const fs10 = __require("fs");
1448
+ const path13 = __require("path");
1437
1449
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1438
- const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path14.join(process.cwd(), "debug-artifacts");
1439
- const responseFile = path14.join(
1450
+ const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path13.join(process.cwd(), "debug-artifacts");
1451
+ const responseFile = path13.join(
1440
1452
  debugArtifactsDir,
1441
1453
  `response-${_checkName || "unknown"}-${timestamp}.txt`
1442
1454
  );
@@ -1469,7 +1481,7 @@ ${"=".repeat(60)}
1469
1481
  `;
1470
1482
  responseContent += `${"=".repeat(60)}
1471
1483
  `;
1472
- fs11.writeFileSync(responseFile, responseContent, "utf-8");
1484
+ fs10.writeFileSync(responseFile, responseContent, "utf-8");
1473
1485
  log(`\u{1F4BE} Response saved to: ${responseFile}`);
1474
1486
  } catch (error) {
1475
1487
  log(`\u26A0\uFE0F Could not save response file: ${error}`);
@@ -1485,9 +1497,9 @@ ${"=".repeat(60)}
1485
1497
  await agentAny._telemetryConfig.shutdown();
1486
1498
  log(`\u{1F4CA} OpenTelemetry trace saved to: ${agentAny._traceFilePath}`);
1487
1499
  if (process.env.GITHUB_ACTIONS) {
1488
- const fs11 = __require("fs");
1489
- if (fs11.existsSync(agentAny._traceFilePath)) {
1490
- const stats = fs11.statSync(agentAny._traceFilePath);
1500
+ const fs10 = __require("fs");
1501
+ if (fs10.existsSync(agentAny._traceFilePath)) {
1502
+ const stats = fs10.statSync(agentAny._traceFilePath);
1491
1503
  console.log(
1492
1504
  `::notice title=AI Trace Saved::${agentAny._traceFilePath} (${stats.size} bytes)`
1493
1505
  );
@@ -1688,8 +1700,8 @@ ${schemaString}`);
1688
1700
  const model = this.config.model || "default";
1689
1701
  if (process.env.VISOR_DEBUG_AI_SESSIONS === "true") {
1690
1702
  try {
1691
- const fs11 = __require("fs");
1692
- const path14 = __require("path");
1703
+ const fs10 = __require("fs");
1704
+ const path13 = __require("path");
1693
1705
  const os = __require("os");
1694
1706
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1695
1707
  const debugData = {
@@ -1763,18 +1775,18 @@ ${"=".repeat(60)}
1763
1775
  readableVersion += `${"=".repeat(60)}
1764
1776
  `;
1765
1777
  const tempDir = os.tmpdir();
1766
- const promptFile = path14.join(tempDir, `visor-prompt-${timestamp}.txt`);
1767
- fs11.writeFileSync(promptFile, prompt, "utf-8");
1778
+ const promptFile = path13.join(tempDir, `visor-prompt-${timestamp}.txt`);
1779
+ fs10.writeFileSync(promptFile, prompt, "utf-8");
1768
1780
  log(`
1769
1781
  \u{1F4BE} Prompt saved to: ${promptFile}`);
1770
- const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path14.join(process.cwd(), "debug-artifacts");
1782
+ const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path13.join(process.cwd(), "debug-artifacts");
1771
1783
  try {
1772
- const base = path14.join(
1784
+ const base = path13.join(
1773
1785
  debugArtifactsDir,
1774
1786
  `prompt-${_checkName || "unknown"}-${timestamp}`
1775
1787
  );
1776
- fs11.writeFileSync(base + ".json", debugJson, "utf-8");
1777
- fs11.writeFileSync(base + ".summary.txt", readableVersion, "utf-8");
1788
+ fs10.writeFileSync(base + ".json", debugJson, "utf-8");
1789
+ fs10.writeFileSync(base + ".summary.txt", readableVersion, "utf-8");
1778
1790
  log(`
1779
1791
  \u{1F4BE} Full debug info saved to directory: ${debugArtifactsDir}`);
1780
1792
  } catch {
@@ -1819,8 +1831,8 @@ $ ${cliCommand}
1819
1831
  log(`\u{1F4E4} Response length: ${response.length} characters`);
1820
1832
  if (process.env.VISOR_DEBUG_AI_SESSIONS === "true") {
1821
1833
  try {
1822
- const fs11 = __require("fs");
1823
- const path14 = __require("path");
1834
+ const fs10 = __require("fs");
1835
+ const path13 = __require("path");
1824
1836
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1825
1837
  const agentAny = agent;
1826
1838
  let fullHistory = [];
@@ -1831,8 +1843,8 @@ $ ${cliCommand}
1831
1843
  } else if (agentAny._messages) {
1832
1844
  fullHistory = agentAny._messages;
1833
1845
  }
1834
- const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path14.join(process.cwd(), "debug-artifacts");
1835
- const sessionBase = path14.join(
1846
+ const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path13.join(process.cwd(), "debug-artifacts");
1847
+ const sessionBase = path13.join(
1836
1848
  debugArtifactsDir,
1837
1849
  `session-${_checkName || "unknown"}-${timestamp}`
1838
1850
  );
@@ -1844,7 +1856,7 @@ $ ${cliCommand}
1844
1856
  schema: effectiveSchema,
1845
1857
  totalMessages: fullHistory.length
1846
1858
  };
1847
- fs11.writeFileSync(sessionBase + ".json", JSON.stringify(sessionData, null, 2), "utf-8");
1859
+ fs10.writeFileSync(sessionBase + ".json", JSON.stringify(sessionData, null, 2), "utf-8");
1848
1860
  let readable = `=============================================================
1849
1861
  `;
1850
1862
  readable += `COMPLETE AI SESSION HISTORY (AFTER RESPONSE)
@@ -1871,7 +1883,7 @@ ${"=".repeat(60)}
1871
1883
  `;
1872
1884
  readable += content + "\n";
1873
1885
  });
1874
- fs11.writeFileSync(sessionBase + ".summary.txt", readable, "utf-8");
1886
+ fs10.writeFileSync(sessionBase + ".summary.txt", readable, "utf-8");
1875
1887
  log(`\u{1F4BE} Complete session history saved:`);
1876
1888
  log(` - Contains ALL ${fullHistory.length} messages (prompts + responses)`);
1877
1889
  } catch (error) {
@@ -1880,11 +1892,11 @@ ${"=".repeat(60)}
1880
1892
  }
1881
1893
  if (process.env.VISOR_DEBUG_AI_SESSIONS === "true") {
1882
1894
  try {
1883
- const fs11 = __require("fs");
1884
- const path14 = __require("path");
1895
+ const fs10 = __require("fs");
1896
+ const path13 = __require("path");
1885
1897
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1886
- const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path14.join(process.cwd(), "debug-artifacts");
1887
- const responseFile = path14.join(
1898
+ const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path13.join(process.cwd(), "debug-artifacts");
1899
+ const responseFile = path13.join(
1888
1900
  debugArtifactsDir,
1889
1901
  `response-${_checkName || "unknown"}-${timestamp}.txt`
1890
1902
  );
@@ -1917,7 +1929,7 @@ ${"=".repeat(60)}
1917
1929
  `;
1918
1930
  responseContent += `${"=".repeat(60)}
1919
1931
  `;
1920
- fs11.writeFileSync(responseFile, responseContent, "utf-8");
1932
+ fs10.writeFileSync(responseFile, responseContent, "utf-8");
1921
1933
  log(`\u{1F4BE} Response saved to: ${responseFile}`);
1922
1934
  } catch (error) {
1923
1935
  log(`\u26A0\uFE0F Could not save response file: ${error}`);
@@ -1935,9 +1947,9 @@ ${"=".repeat(60)}
1935
1947
  await telemetry.shutdown();
1936
1948
  log(`\u{1F4CA} OpenTelemetry trace saved to: ${traceFilePath}`);
1937
1949
  if (process.env.GITHUB_ACTIONS) {
1938
- const fs11 = __require("fs");
1939
- if (fs11.existsSync(traceFilePath)) {
1940
- const stats = fs11.statSync(traceFilePath);
1950
+ const fs10 = __require("fs");
1951
+ if (fs10.existsSync(traceFilePath)) {
1952
+ const stats = fs10.statSync(traceFilePath);
1941
1953
  console.log(
1942
1954
  `::notice title=AI Trace Saved::OpenTelemetry trace file size: ${stats.size} bytes`
1943
1955
  );
@@ -1975,8 +1987,8 @@ ${"=".repeat(60)}
1975
1987
  * Load schema content from schema files or inline definitions
1976
1988
  */
1977
1989
  async loadSchemaContent(schema) {
1978
- const fs11 = __require("fs").promises;
1979
- const path14 = __require("path");
1990
+ const fs10 = __require("fs").promises;
1991
+ const path13 = __require("path");
1980
1992
  if (typeof schema === "object" && schema !== null) {
1981
1993
  log("\u{1F4CB} Using inline schema object from configuration");
1982
1994
  return JSON.stringify(schema);
@@ -1989,14 +2001,14 @@ ${"=".repeat(60)}
1989
2001
  }
1990
2002
  } catch {
1991
2003
  }
1992
- if ((schema.startsWith("./") || schema.includes(".json")) && !path14.isAbsolute(schema)) {
2004
+ if ((schema.startsWith("./") || schema.includes(".json")) && !path13.isAbsolute(schema)) {
1993
2005
  if (schema.includes("..") || schema.includes("\0")) {
1994
2006
  throw new Error("Invalid schema path: path traversal not allowed");
1995
2007
  }
1996
2008
  try {
1997
- const schemaPath = path14.resolve(process.cwd(), schema);
2009
+ const schemaPath = path13.resolve(process.cwd(), schema);
1998
2010
  log(`\u{1F4CB} Loading custom schema from file: ${schemaPath}`);
1999
- const schemaContent = await fs11.readFile(schemaPath, "utf-8");
2011
+ const schemaContent = await fs10.readFile(schemaPath, "utf-8");
2000
2012
  return schemaContent.trim();
2001
2013
  } catch (error) {
2002
2014
  throw new Error(
@@ -2010,22 +2022,22 @@ ${"=".repeat(60)}
2010
2022
  }
2011
2023
  const candidatePaths = [
2012
2024
  // GitHub Action bundle location
2013
- path14.join(__dirname, "output", sanitizedSchemaName, "schema.json"),
2025
+ path13.join(__dirname, "output", sanitizedSchemaName, "schema.json"),
2014
2026
  // Historical fallback when src/output was inadvertently bundled as output1/
2015
- path14.join(__dirname, "output1", sanitizedSchemaName, "schema.json"),
2027
+ path13.join(__dirname, "output1", sanitizedSchemaName, "schema.json"),
2016
2028
  // Local dev (repo root)
2017
- path14.join(process.cwd(), "output", sanitizedSchemaName, "schema.json")
2029
+ path13.join(process.cwd(), "output", sanitizedSchemaName, "schema.json")
2018
2030
  ];
2019
2031
  for (const schemaPath of candidatePaths) {
2020
2032
  try {
2021
- const schemaContent = await fs11.readFile(schemaPath, "utf-8");
2033
+ const schemaContent = await fs10.readFile(schemaPath, "utf-8");
2022
2034
  return schemaContent.trim();
2023
2035
  } catch {
2024
2036
  }
2025
2037
  }
2026
- const distPath = path14.join(__dirname, "output", sanitizedSchemaName, "schema.json");
2027
- const distAltPath = path14.join(__dirname, "output1", sanitizedSchemaName, "schema.json");
2028
- const cwdPath = path14.join(process.cwd(), "output", sanitizedSchemaName, "schema.json");
2038
+ const distPath = path13.join(__dirname, "output", sanitizedSchemaName, "schema.json");
2039
+ const distAltPath = path13.join(__dirname, "output1", sanitizedSchemaName, "schema.json");
2040
+ const cwdPath = path13.join(process.cwd(), "output", sanitizedSchemaName, "schema.json");
2029
2041
  throw new Error(
2030
2042
  `Failed to load schema '${sanitizedSchemaName}'. Tried: ${distPath}, ${distAltPath}, and ${cwdPath}. Ensure build copies 'output/' into dist (build:cli), or provide a custom schema file/path.`
2031
2043
  );
@@ -2787,6 +2799,758 @@ var init_state_capture = __esm({
2787
2799
  }
2788
2800
  });
2789
2801
 
2802
+ // src/providers/api-tool-executor.ts
2803
+ import fs2 from "fs/promises";
2804
+ import path3 from "path";
2805
+ import YAML from "js-yaml";
2806
+ import SwaggerParser from "@apidevtools/swagger-parser";
2807
+ import deepmerge from "deepmerge";
2808
+ import { JSONPath } from "jsonpath-plus";
2809
+ import { minimatch } from "minimatch";
2810
+ function isHttpUrl(value) {
2811
+ return value.startsWith("http://") || value.startsWith("https://");
2812
+ }
2813
+ function toStringArray(value) {
2814
+ if (!value) return [];
2815
+ if (Array.isArray(value)) {
2816
+ return value.map((item) => String(item).trim()).filter(Boolean);
2817
+ }
2818
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
2819
+ }
2820
+ function isPlainObject(value) {
2821
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2822
+ }
2823
+ function toOverlaySourceArray(value) {
2824
+ if (!value) return [];
2825
+ if (typeof value === "string" || isPlainObject(value)) {
2826
+ return [value];
2827
+ }
2828
+ if (Array.isArray(value)) {
2829
+ return value.filter((item) => typeof item === "string" || isPlainObject(item));
2830
+ }
2831
+ return [];
2832
+ }
2833
+ function resolvePathOrUrl(candidate, baseDir) {
2834
+ if (isHttpUrl(candidate)) return candidate;
2835
+ if (path3.isAbsolute(candidate)) return candidate;
2836
+ if (isHttpUrl(baseDir)) {
2837
+ return new URL(candidate, baseDir).toString();
2838
+ }
2839
+ return path3.resolve(baseDir, candidate);
2840
+ }
2841
+ async function readTextFromPathOrUrl(location) {
2842
+ if (isHttpUrl(location)) {
2843
+ const res = await fetch(location);
2844
+ if (!res.ok) {
2845
+ throw new Error(`Failed to fetch ${location}: ${res.status} ${res.statusText}`);
2846
+ }
2847
+ return await res.text();
2848
+ }
2849
+ return await fs2.readFile(location, "utf8");
2850
+ }
2851
+ function parseJsonOrYaml(raw, location) {
2852
+ const ext = path3.extname(location).toLowerCase();
2853
+ if (ext === ".yaml" || ext === ".yml") {
2854
+ return YAML.load(raw);
2855
+ }
2856
+ try {
2857
+ return JSON.parse(raw);
2858
+ } catch {
2859
+ return YAML.load(raw);
2860
+ }
2861
+ }
2862
+ function parseOverlayTargetPath(pathValue) {
2863
+ if (!Array.isArray(pathValue) || pathValue.length === 0) return "$";
2864
+ return pathValue.map((segment, index) => {
2865
+ if (index === 0) return "$";
2866
+ if (typeof segment === "number") return `[${segment}]`;
2867
+ if (/^[A-Za-z_][\w$]*$/.test(segment)) return `.${segment}`;
2868
+ return `['${segment.replace(/'/g, "\\'")}']`;
2869
+ }).join("");
2870
+ }
2871
+ function applyOverlayActions(target, overlay) {
2872
+ const cloned = JSON.parse(JSON.stringify(target));
2873
+ if (!overlay || typeof overlay !== "object") return cloned;
2874
+ const actions = Array.isArray(overlay.actions) ? overlay.actions : [];
2875
+ if (actions.length === 0) {
2876
+ return deepmerge(cloned, overlay, {
2877
+ arrayMerge: (dst, src) => dst.concat(src)
2878
+ });
2879
+ }
2880
+ for (const action of actions) {
2881
+ const targetExpr = action?.target;
2882
+ if (!targetExpr || typeof targetExpr !== "string") continue;
2883
+ let matches = [];
2884
+ try {
2885
+ matches = JSONPath({
2886
+ path: targetExpr,
2887
+ json: cloned,
2888
+ resultType: "all"
2889
+ });
2890
+ } catch (error) {
2891
+ logger.warn(`[ApiToolExecutor] Invalid overlay target "${targetExpr}": ${error}`);
2892
+ continue;
2893
+ }
2894
+ if (matches.length === 0) {
2895
+ continue;
2896
+ }
2897
+ for (const match of matches) {
2898
+ if (!match || typeof match !== "object") continue;
2899
+ const parent = match.parent;
2900
+ const key = match.parentProperty;
2901
+ if (parent === void 0 || key === void 0) {
2902
+ const jsonPath = parseOverlayTargetPath(
2903
+ match.path || []
2904
+ );
2905
+ logger.debug(`[ApiToolExecutor] Overlay target has no writable parent: ${jsonPath}`);
2906
+ continue;
2907
+ }
2908
+ if (action.remove === true) {
2909
+ if (Array.isArray(parent)) {
2910
+ parent.splice(Number(key), 1);
2911
+ } else if (parent && typeof parent === "object") {
2912
+ delete parent[key];
2913
+ }
2914
+ continue;
2915
+ }
2916
+ if (action.update === void 0) {
2917
+ continue;
2918
+ }
2919
+ const current = match.value;
2920
+ if (Array.isArray(current)) {
2921
+ current.push(action.update);
2922
+ } else if (current && typeof current === "object") {
2923
+ const merged = deepmerge(current, action.update, {
2924
+ arrayMerge: (dst, src) => dst.concat(src)
2925
+ });
2926
+ if (Array.isArray(parent)) {
2927
+ parent[Number(key)] = merged;
2928
+ } else {
2929
+ parent[key] = merged;
2930
+ }
2931
+ } else if (Array.isArray(parent)) {
2932
+ parent[Number(key)] = action.update;
2933
+ } else {
2934
+ parent[key] = action.update;
2935
+ }
2936
+ }
2937
+ }
2938
+ return cloned;
2939
+ }
2940
+ function isRefObject(value) {
2941
+ return Boolean(value && typeof value === "object" && "$ref" in value);
2942
+ }
2943
+ function isSchemaObject(value) {
2944
+ return Boolean(
2945
+ value && typeof value === "object" && !Array.isArray(value) && !isRefObject(value)
2946
+ );
2947
+ }
2948
+ function getSchemaFromContent(content) {
2949
+ if (!content || typeof content !== "object") return void 0;
2950
+ const entries = Object.values(content);
2951
+ const withSchema = entries.find((entry) => entry && typeof entry === "object" && entry.schema);
2952
+ return withSchema?.schema;
2953
+ }
2954
+ function mapOpenApiTypeToJsonType(schema) {
2955
+ if (!schema || !schema.type) return { type: "string" };
2956
+ const openApiType = String(schema.type);
2957
+ const nullable = schema.nullable === true;
2958
+ switch (openApiType) {
2959
+ case "integer":
2960
+ case "number":
2961
+ case "boolean":
2962
+ case "string":
2963
+ case "array":
2964
+ case "object":
2965
+ return { type: openApiType, format: schema.format, nullable };
2966
+ default:
2967
+ return { type: "string", format: schema.format, nullable };
2968
+ }
2969
+ }
2970
+ function openApiSchemaToJsonSchema(schema) {
2971
+ if (!schema) {
2972
+ return { type: "string" };
2973
+ }
2974
+ const mapped = mapOpenApiTypeToJsonType(schema);
2975
+ const result = {
2976
+ type: mapped.nullable ? [mapped.type, "null"] : mapped.type
2977
+ };
2978
+ if (mapped.format) result.format = mapped.format;
2979
+ if (schema.description !== void 0) result.description = schema.description;
2980
+ if (schema.default !== void 0) result.default = schema.default;
2981
+ if (schema.enum !== void 0) result.enum = schema.enum;
2982
+ if (schema.example !== void 0) result.example = schema.example;
2983
+ if (schema.minimum !== void 0) result.minimum = schema.minimum;
2984
+ if (schema.maximum !== void 0) result.maximum = schema.maximum;
2985
+ if (schema.minLength !== void 0) result.minLength = schema.minLength;
2986
+ if (schema.maxLength !== void 0) result.maxLength = schema.maxLength;
2987
+ if (schema.pattern !== void 0) result.pattern = schema.pattern;
2988
+ if (schema.multipleOf !== void 0) result.multipleOf = schema.multipleOf;
2989
+ if (schema.minItems !== void 0) result.minItems = schema.minItems;
2990
+ if (schema.maxItems !== void 0) result.maxItems = schema.maxItems;
2991
+ if (schema.uniqueItems !== void 0) result.uniqueItems = schema.uniqueItems;
2992
+ if (schema.type === "object" && schema.properties && typeof schema.properties === "object") {
2993
+ const props = {};
2994
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
2995
+ if (isSchemaObject(propSchema)) {
2996
+ props[propName] = openApiSchemaToJsonSchema(propSchema);
2997
+ }
2998
+ }
2999
+ result.properties = props;
3000
+ if (Array.isArray(schema.required) && schema.required.length > 0) {
3001
+ result.required = schema.required;
3002
+ }
3003
+ if (schema.additionalProperties === true || schema.additionalProperties === false) {
3004
+ result.additionalProperties = schema.additionalProperties;
3005
+ } else if (isSchemaObject(schema.additionalProperties)) {
3006
+ result.additionalProperties = openApiSchemaToJsonSchema(schema.additionalProperties);
3007
+ }
3008
+ }
3009
+ if (schema.type === "array" && isSchemaObject(schema.items)) {
3010
+ result.items = openApiSchemaToJsonSchema(schema.items);
3011
+ }
3012
+ for (const [key, value] of Object.entries(schema)) {
3013
+ if (key.startsWith("x-")) {
3014
+ result[key] = value;
3015
+ }
3016
+ }
3017
+ return result;
3018
+ }
3019
+ function shouldIncludeOperation(operationId, pathValue, method, whitelist, blacklist) {
3020
+ const methodPath = `${method.toUpperCase()}:${pathValue}`;
3021
+ const opKey = operationId || methodPath;
3022
+ if (whitelist.length > 0) {
3023
+ return whitelist.some((pattern) => minimatch(opKey, pattern) || minimatch(methodPath, pattern));
3024
+ }
3025
+ if (blacklist.length > 0) {
3026
+ return !blacklist.some((pattern) => minimatch(opKey, pattern) || minimatch(methodPath, pattern));
3027
+ }
3028
+ return true;
3029
+ }
3030
+ function getApiToolConfig(tool) {
3031
+ return {
3032
+ customHeaders: tool.headers || {},
3033
+ disableXMcp: Boolean(tool.disableXMcp ?? tool.disable_x_mcp ?? false),
3034
+ apiKey: tool.apiKey ?? tool.api_key,
3035
+ securitySchemeName: tool.securitySchemeName ?? tool.security_scheme_name,
3036
+ securityCredentials: tool.securityCredentials || tool.security_credentials || {},
3037
+ requestTimeoutMs: tool.requestTimeoutMs ?? tool.request_timeout_ms ?? tool.timeout ?? 3e4
3038
+ };
3039
+ }
3040
+ function buildOutputSchema(operation) {
3041
+ const responses = operation.responses;
3042
+ if (!responses || typeof responses !== "object") return void 0;
3043
+ const successCode = Object.keys(responses).find((code) => code.startsWith("2"));
3044
+ if (!successCode) return void 0;
3045
+ const response = responses[successCode];
3046
+ if (!response || typeof response !== "object" || isRefObject(response)) return void 0;
3047
+ const jsonSchema = response.content?.["application/json"]?.schema || getSchemaFromContent(response.content);
3048
+ if (!isSchemaObject(jsonSchema)) return void 0;
3049
+ const mapped = openApiSchemaToJsonSchema(jsonSchema);
3050
+ if (response.description && typeof response.description === "string") {
3051
+ mapped.description = response.description;
3052
+ }
3053
+ return mapped;
3054
+ }
3055
+ function getToolName(operationId, operation, pathItem, tool) {
3056
+ let toolName = operationId;
3057
+ const opExtension = operation["x-mcp"];
3058
+ const pathExtension = pathItem["x-mcp"];
3059
+ if (opExtension && typeof opExtension === "object" && typeof opExtension.name === "string") {
3060
+ toolName = opExtension.name;
3061
+ } else if (pathExtension && typeof pathExtension === "object" && typeof pathExtension.name === "string") {
3062
+ toolName = pathExtension.name;
3063
+ }
3064
+ const prefix = tool.namePrefix || tool.name_prefix;
3065
+ if (prefix) {
3066
+ return `${prefix}${toolName}`;
3067
+ }
3068
+ return toolName;
3069
+ }
3070
+ function getToolDescription(operation, pathItem) {
3071
+ const opExtension = operation["x-mcp"];
3072
+ const pathExtension = pathItem["x-mcp"];
3073
+ if (opExtension && typeof opExtension === "object" && typeof opExtension.description === "string") {
3074
+ return opExtension.description;
3075
+ }
3076
+ if (pathExtension && typeof pathExtension === "object" && typeof pathExtension.description === "string") {
3077
+ return pathExtension.description;
3078
+ }
3079
+ return typeof operation.description === "string" && operation.description || typeof operation.summary === "string" && operation.summary || typeof pathItem.summary === "string" && pathItem.summary || "No description available.";
3080
+ }
3081
+ function isApiToolDefinition(tool) {
3082
+ return Boolean(tool && tool.type === "api");
3083
+ }
3084
+ async function loadOpenApiDocument(tool) {
3085
+ if (!tool.spec) {
3086
+ throw new Error(`API tool '${tool.name}' is missing required field: spec`);
3087
+ }
3088
+ const configuredBaseDir = tool.__baseDir;
3089
+ const baseDir = (() => {
3090
+ if (tool.cwd) {
3091
+ if (path3.isAbsolute(tool.cwd) || isHttpUrl(tool.cwd)) {
3092
+ return tool.cwd;
3093
+ }
3094
+ if (configuredBaseDir) {
3095
+ return resolvePathOrUrl(tool.cwd, configuredBaseDir);
3096
+ }
3097
+ return path3.resolve(tool.cwd);
3098
+ }
3099
+ return configuredBaseDir || process.cwd();
3100
+ })();
3101
+ const dereferenceWithContext = async (source, spec) => {
3102
+ try {
3103
+ return await SwaggerParser.dereference(spec);
3104
+ } catch (error) {
3105
+ const errorMessage = error instanceof Error ? error.message : String(error);
3106
+ throw new Error(
3107
+ `Failed to dereference OpenAPI spec for API tool '${tool.name}' from ${source}: ${errorMessage}`
3108
+ );
3109
+ }
3110
+ };
3111
+ let openapi;
3112
+ if (typeof tool.spec === "string") {
3113
+ const specLocation = resolvePathOrUrl(tool.spec, baseDir);
3114
+ if (isHttpUrl(specLocation)) {
3115
+ const raw = await readTextFromPathOrUrl(specLocation);
3116
+ const parsed = parseJsonOrYaml(raw, specLocation);
3117
+ openapi = await dereferenceWithContext(specLocation, parsed);
3118
+ } else {
3119
+ openapi = await dereferenceWithContext(specLocation, specLocation);
3120
+ }
3121
+ } else if (isPlainObject(tool.spec)) {
3122
+ openapi = await dereferenceWithContext("inline spec", JSON.parse(JSON.stringify(tool.spec)));
3123
+ } else {
3124
+ throw new Error(
3125
+ `API tool '${tool.name}' has invalid spec field (expected string path/URL or object)`
3126
+ );
3127
+ }
3128
+ const overlays = toOverlaySourceArray(tool.overlays);
3129
+ let working = openapi;
3130
+ for (const overlaySource of overlays) {
3131
+ let overlay = overlaySource;
3132
+ if (typeof overlaySource === "string") {
3133
+ const resolved = resolvePathOrUrl(overlaySource, baseDir);
3134
+ const raw = await readTextFromPathOrUrl(resolved);
3135
+ overlay = parseJsonOrYaml(raw, resolved);
3136
+ }
3137
+ working = applyOverlayActions(working, overlay);
3138
+ }
3139
+ return working;
3140
+ }
3141
+ function mapOpenApiToTools(openapi, tool) {
3142
+ const paths = openapi?.paths;
3143
+ if (!paths || typeof paths !== "object") {
3144
+ return [];
3145
+ }
3146
+ const targetUrl = tool.targetUrl || tool.target_url;
3147
+ const baseServerUrl = String(targetUrl || openapi?.servers?.[0]?.url || "").replace(/\/$/, "");
3148
+ if (!baseServerUrl) {
3149
+ throw new Error(
3150
+ `API tool '${tool.name}' cannot determine target API URL. Set targetUrl/target_url or provide OpenAPI servers[].`
3151
+ );
3152
+ }
3153
+ const whitelist = toStringArray(tool.whitelist);
3154
+ const blacklist = toStringArray(tool.blacklist);
3155
+ const globalSecurity = Array.isArray(openapi?.security) ? openapi.security : null;
3156
+ const securitySchemes = openapi?.components?.securitySchemes;
3157
+ const mapped = [];
3158
+ const apiToolConfig = getApiToolConfig(tool);
3159
+ for (const [pathValue, pathItemRaw] of Object.entries(paths)) {
3160
+ const pathItem = pathItemRaw;
3161
+ if (!pathItem || typeof pathItem !== "object") continue;
3162
+ for (const [method, operationRaw] of Object.entries(pathItem)) {
3163
+ if (!HTTP_METHODS.has(method.toLowerCase())) continue;
3164
+ const operation = operationRaw;
3165
+ if (!operation || typeof operation !== "object") continue;
3166
+ const operationId = typeof operation.operationId === "string" ? String(operation.operationId) : void 0;
3167
+ if (!operationId) {
3168
+ logger.debug(
3169
+ `[ApiToolExecutor] Skipping ${method.toUpperCase()} ${pathValue} (missing operationId)`
3170
+ );
3171
+ continue;
3172
+ }
3173
+ if (!shouldIncludeOperation(operationId, pathValue, method, whitelist, blacklist)) {
3174
+ continue;
3175
+ }
3176
+ const toolName = getToolName(operationId, operation, pathItem, tool);
3177
+ const toolDescription = getToolDescription(operation, pathItem);
3178
+ const inputSchema = {
3179
+ type: "object",
3180
+ properties: {}
3181
+ };
3182
+ const requiredNames = /* @__PURE__ */ new Set();
3183
+ const allParameters = [
3184
+ ...Array.isArray(pathItem.parameters) ? pathItem.parameters : [],
3185
+ ...Array.isArray(operation.parameters) ? operation.parameters : []
3186
+ ].filter((param) => param && typeof param === "object" && !isRefObject(param));
3187
+ for (const param of allParameters) {
3188
+ const paramObj = param;
3189
+ const paramName = typeof paramObj.name === "string" ? paramObj.name : "";
3190
+ if (!paramName) continue;
3191
+ if (!isSchemaObject(paramObj.schema)) continue;
3192
+ const schema = openApiSchemaToJsonSchema(paramObj.schema);
3193
+ if (typeof paramObj.description === "string") {
3194
+ schema.description = paramObj.description;
3195
+ }
3196
+ schema["x-parameter-location"] = paramObj.in || "query";
3197
+ if (paramObj.example !== void 0) schema.example = paramObj.example;
3198
+ if (paramObj.deprecated === true) schema.deprecated = true;
3199
+ inputSchema.properties[paramName] = schema;
3200
+ if (paramObj.required === true) requiredNames.add(paramName);
3201
+ }
3202
+ const requestBody = !isRefObject(operation.requestBody) ? operation.requestBody : void 0;
3203
+ if (requestBody && typeof requestBody === "object") {
3204
+ const reqSchema = requestBody.content?.["application/json"]?.schema || getSchemaFromContent(requestBody.content);
3205
+ if (isSchemaObject(reqSchema)) {
3206
+ const bodySchema = openApiSchemaToJsonSchema(reqSchema);
3207
+ if (typeof requestBody.description === "string") {
3208
+ bodySchema.description = requestBody.description;
3209
+ }
3210
+ const contentTypes = Object.keys(requestBody.content || {});
3211
+ if (contentTypes.length > 0) {
3212
+ bodySchema["x-content-types"] = contentTypes;
3213
+ }
3214
+ inputSchema.properties.requestBody = bodySchema;
3215
+ if (requestBody.required === true) {
3216
+ requiredNames.add("requestBody");
3217
+ }
3218
+ }
3219
+ }
3220
+ if (requiredNames.size > 0) {
3221
+ inputSchema.required = Array.from(requiredNames);
3222
+ }
3223
+ const securityRequirements = Array.isArray(operation.security) ? operation.security : globalSecurity;
3224
+ mapped.push({
3225
+ sourceToolName: tool.name,
3226
+ mcpToolDefinition: {
3227
+ name: toolName,
3228
+ description: toolDescription,
3229
+ inputSchema,
3230
+ outputSchema: buildOutputSchema(operation)
3231
+ },
3232
+ apiCallDetails: {
3233
+ method: method.toUpperCase(),
3234
+ pathTemplate: pathValue,
3235
+ serverUrl: baseServerUrl,
3236
+ parameters: allParameters,
3237
+ requestBody,
3238
+ securityRequirements,
3239
+ securitySchemes,
3240
+ apiToolConfig
3241
+ }
3242
+ });
3243
+ }
3244
+ }
3245
+ return mapped;
3246
+ }
3247
+ function validateParameterValue(value, paramDef) {
3248
+ const schema = paramDef.schema;
3249
+ if (!schema || typeof schema !== "object") return null;
3250
+ const schemaType = schema.type;
3251
+ if (schemaType === "integer") {
3252
+ if (typeof value !== "number" || !Number.isInteger(value)) {
3253
+ return "must be an integer";
3254
+ }
3255
+ } else if (schemaType === "number") {
3256
+ if (typeof value !== "number") {
3257
+ return `expected number, got ${typeof value}`;
3258
+ }
3259
+ } else if (schemaType === "boolean") {
3260
+ if (typeof value !== "boolean") {
3261
+ return `expected boolean, got ${typeof value}`;
3262
+ }
3263
+ } else if (schemaType === "string") {
3264
+ if (typeof value !== "string") {
3265
+ return `expected string, got ${typeof value}`;
3266
+ }
3267
+ } else if (schemaType === "array") {
3268
+ if (!Array.isArray(value)) {
3269
+ return `expected array, got ${typeof value}`;
3270
+ }
3271
+ } else if (schemaType === "object") {
3272
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3273
+ return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
3274
+ }
3275
+ }
3276
+ if (schema.minimum !== void 0 && typeof value === "number" && value < schema.minimum) {
3277
+ return `must be >= ${schema.minimum}`;
3278
+ }
3279
+ if (schema.maximum !== void 0 && typeof value === "number" && value > schema.maximum) {
3280
+ return `must be <= ${schema.maximum}`;
3281
+ }
3282
+ if (schema.minLength !== void 0 && typeof value === "string" && value.length < schema.minLength) {
3283
+ return `length must be >= ${schema.minLength}`;
3284
+ }
3285
+ if (schema.maxLength !== void 0 && typeof value === "string" && value.length > schema.maxLength) {
3286
+ return `length must be <= ${schema.maxLength}`;
3287
+ }
3288
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
3289
+ return `must be one of: ${schema.enum.join(", ")}`;
3290
+ }
3291
+ return null;
3292
+ }
3293
+ function applySecurityToRequest(details, headers, queryParams) {
3294
+ const requirements = details.securityRequirements;
3295
+ if (!requirements || requirements.length === 0) {
3296
+ return;
3297
+ }
3298
+ const schemes = details.securitySchemes || {};
3299
+ const cfg = details.apiToolConfig;
3300
+ const tryResolveCredential = (schemeName) => {
3301
+ if (cfg.securityCredentials[schemeName]) {
3302
+ return cfg.securityCredentials[schemeName];
3303
+ }
3304
+ if (cfg.securitySchemeName && cfg.securitySchemeName !== schemeName) {
3305
+ return void 0;
3306
+ }
3307
+ return cfg.apiKey;
3308
+ };
3309
+ for (const requirement of requirements) {
3310
+ const schemeNames = Object.keys(requirement);
3311
+ if (schemeNames.length === 0) return;
3312
+ const tempHeaders = { ...headers };
3313
+ const tempQuery = new URLSearchParams(queryParams);
3314
+ let satisfied = true;
3315
+ for (const schemeName of schemeNames) {
3316
+ const scheme = schemes[schemeName];
3317
+ if (!scheme || typeof scheme !== "object") {
3318
+ satisfied = false;
3319
+ break;
3320
+ }
3321
+ const credential = tryResolveCredential(schemeName);
3322
+ if (!credential) {
3323
+ satisfied = false;
3324
+ break;
3325
+ }
3326
+ switch (scheme.type) {
3327
+ case "apiKey":
3328
+ if (scheme.in === "header") {
3329
+ tempHeaders[String(scheme.name)] = credential;
3330
+ } else if (scheme.in === "query") {
3331
+ tempQuery.set(String(scheme.name), credential);
3332
+ } else if (scheme.in === "cookie") {
3333
+ const cookieName = String(scheme.name);
3334
+ const existing = tempHeaders["Cookie"];
3335
+ tempHeaders["Cookie"] = existing ? `${existing}; ${cookieName}=${credential}` : `${cookieName}=${credential}`;
3336
+ } else {
3337
+ satisfied = false;
3338
+ }
3339
+ break;
3340
+ case "http":
3341
+ if (typeof scheme.scheme !== "string") {
3342
+ satisfied = false;
3343
+ break;
3344
+ }
3345
+ if (scheme.scheme.toLowerCase() === "bearer") {
3346
+ tempHeaders["Authorization"] = `Bearer ${credential}`;
3347
+ } else if (scheme.scheme.toLowerCase() === "basic") {
3348
+ tempHeaders["Authorization"] = `Basic ${Buffer.from(credential).toString("base64")}`;
3349
+ } else {
3350
+ satisfied = false;
3351
+ }
3352
+ break;
3353
+ case "oauth2":
3354
+ case "openIdConnect":
3355
+ tempHeaders["Authorization"] = `Bearer ${credential}`;
3356
+ break;
3357
+ default:
3358
+ satisfied = false;
3359
+ }
3360
+ if (!satisfied) {
3361
+ break;
3362
+ }
3363
+ }
3364
+ if (satisfied) {
3365
+ Object.assign(headers, tempHeaders);
3366
+ queryParams.forEach((_value, key) => queryParams.delete(key));
3367
+ tempQuery.forEach((value, key) => queryParams.append(key, value));
3368
+ return;
3369
+ }
3370
+ }
3371
+ }
3372
+ function responseBodyToString(body) {
3373
+ if (typeof body === "string") return body;
3374
+ try {
3375
+ return JSON.stringify(body);
3376
+ } catch {
3377
+ return String(body);
3378
+ }
3379
+ }
3380
+ async function executeMappedApiTool(mappedTool, args) {
3381
+ const { apiCallDetails } = mappedTool;
3382
+ const { method, pathTemplate, serverUrl, parameters, requestBody, apiToolConfig } = apiCallDetails;
3383
+ const urlPath = pathTemplate.replace(/{([^}]+)}/g, (_token, rawName) => {
3384
+ const value = args[rawName];
3385
+ if (value === void 0 || value === null) {
3386
+ return `{${rawName}}`;
3387
+ }
3388
+ return encodeURIComponent(String(value));
3389
+ });
3390
+ if (urlPath.includes("{") || urlPath.includes("}")) {
3391
+ throw new Error(`Missing required path parameters for ${method} ${pathTemplate}`);
3392
+ }
3393
+ let endpoint;
3394
+ try {
3395
+ endpoint = new URL(`${serverUrl}${urlPath}`);
3396
+ } catch (error) {
3397
+ const errorMessage = error instanceof Error ? error.message : String(error);
3398
+ throw new Error(
3399
+ `Failed to construct endpoint URL for API tool '${mappedTool.sourceToolName}' operation '${mappedTool.mcpToolDefinition.name}' (${method} ${pathTemplate}) with serverUrl '${serverUrl}': ${errorMessage}`
3400
+ );
3401
+ }
3402
+ const queryParams = new URLSearchParams(endpoint.search);
3403
+ const headers = { ...apiToolConfig.customHeaders };
3404
+ let requestBodyValue;
3405
+ for (const param of parameters) {
3406
+ const name = String(param.name || "");
3407
+ if (!name) continue;
3408
+ const value = args[name];
3409
+ if (value === void 0 || value === null) {
3410
+ if (param.required) {
3411
+ throw new Error(`Missing required parameter: ${name}`);
3412
+ }
3413
+ continue;
3414
+ }
3415
+ const validationError = validateParameterValue(value, param);
3416
+ if (validationError) {
3417
+ throw new Error(`Parameter '${name}' ${validationError}`);
3418
+ }
3419
+ switch (param.in) {
3420
+ case "query":
3421
+ if (Array.isArray(value)) {
3422
+ for (const item of value) {
3423
+ queryParams.append(name, String(item));
3424
+ }
3425
+ } else {
3426
+ queryParams.set(name, String(value));
3427
+ }
3428
+ break;
3429
+ case "header":
3430
+ headers[name] = String(value);
3431
+ break;
3432
+ case "path":
3433
+ break;
3434
+ case "cookie": {
3435
+ const existing = headers["Cookie"];
3436
+ headers["Cookie"] = existing ? `${existing}; ${name}=${String(value)}` : `${name}=${String(value)}`;
3437
+ break;
3438
+ }
3439
+ default:
3440
+ break;
3441
+ }
3442
+ }
3443
+ if (requestBody && requestBody.required && args.requestBody === void 0) {
3444
+ throw new Error("Missing required requestBody parameter");
3445
+ }
3446
+ if (args.requestBody !== void 0) {
3447
+ requestBodyValue = args.requestBody;
3448
+ if (!headers["Content-Type"]) {
3449
+ headers["Content-Type"] = "application/json";
3450
+ }
3451
+ }
3452
+ if (!apiToolConfig.disableXMcp) {
3453
+ headers["X-MCP"] = "1";
3454
+ }
3455
+ applySecurityToRequest(apiCallDetails, headers, queryParams);
3456
+ endpoint.search = queryParams.toString();
3457
+ const controller = new AbortController();
3458
+ const timeout = setTimeout(() => controller.abort(), apiToolConfig.requestTimeoutMs);
3459
+ try {
3460
+ const response = await fetch(endpoint.toString(), {
3461
+ method,
3462
+ headers,
3463
+ body: requestBodyValue === void 0 ? void 0 : headers["Content-Type"]?.includes("application/json") ? JSON.stringify(requestBodyValue) : String(requestBodyValue),
3464
+ signal: controller.signal
3465
+ });
3466
+ const raw = await response.text();
3467
+ let body = raw;
3468
+ const contentType = response.headers.get("content-type") || "";
3469
+ if (contentType.includes("json") && raw.trim().length > 0) {
3470
+ try {
3471
+ body = JSON.parse(raw);
3472
+ } catch {
3473
+ body = raw;
3474
+ }
3475
+ } else if (raw.trim().length > 0) {
3476
+ try {
3477
+ body = JSON.parse(raw);
3478
+ } catch {
3479
+ body = raw;
3480
+ }
3481
+ } else {
3482
+ body = null;
3483
+ }
3484
+ if (response.ok) {
3485
+ return body;
3486
+ }
3487
+ throw new Error(`API Error ${response.status}: ${responseBodyToString(body)}`);
3488
+ } catch (error) {
3489
+ if (error instanceof Error && error.name === "AbortError") {
3490
+ throw new Error(`API request timed out after ${apiToolConfig.requestTimeoutMs}ms`);
3491
+ }
3492
+ throw error;
3493
+ } finally {
3494
+ clearTimeout(timeout);
3495
+ }
3496
+ }
3497
+ var HTTP_METHODS, ApiToolRegistry;
3498
+ var init_api_tool_executor = __esm({
3499
+ "src/providers/api-tool-executor.ts"() {
3500
+ "use strict";
3501
+ init_logger();
3502
+ HTTP_METHODS = /* @__PURE__ */ new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
3503
+ ApiToolRegistry = class {
3504
+ bundleCache = /* @__PURE__ */ new Map();
3505
+ operationCache = /* @__PURE__ */ new Map();
3506
+ registerMappedTools(sourceToolName, mappedTools) {
3507
+ this.bundleCache.set(sourceToolName, mappedTools);
3508
+ for (const mapped of mappedTools) {
3509
+ const existing = this.operationCache.get(mapped.mcpToolDefinition.name);
3510
+ if (existing && existing.sourceToolName !== sourceToolName) {
3511
+ logger.warn(
3512
+ `[ApiToolExecutor] Tool name collision: '${mapped.mcpToolDefinition.name}' from '${sourceToolName}' overrides '${existing.sourceToolName}'. Use namePrefix to avoid collisions.`
3513
+ );
3514
+ }
3515
+ this.operationCache.set(mapped.mcpToolDefinition.name, mapped);
3516
+ }
3517
+ }
3518
+ async ensureBundle(sourceToolName, tool) {
3519
+ const cached = this.bundleCache.get(sourceToolName);
3520
+ if (cached) return cached;
3521
+ if (!isApiToolDefinition(tool)) {
3522
+ return [];
3523
+ }
3524
+ const openapi = await loadOpenApiDocument(tool);
3525
+ const mapped = mapOpenApiToTools(openapi, tool);
3526
+ this.registerMappedTools(sourceToolName, mapped);
3527
+ return mapped;
3528
+ }
3529
+ async ensureAll(toolMap) {
3530
+ for (const [sourceToolName, tool] of toolMap.entries()) {
3531
+ if (!isApiToolDefinition(tool)) continue;
3532
+ await this.ensureBundle(sourceToolName, tool);
3533
+ }
3534
+ }
3535
+ async listMappedTools(toolMap) {
3536
+ await this.ensureAll(toolMap);
3537
+ return Array.from(this.operationCache.values());
3538
+ }
3539
+ async getMappedTool(toolName, toolMap) {
3540
+ const cached = this.operationCache.get(toolName);
3541
+ if (cached) return cached;
3542
+ for (const [sourceToolName, tool] of toolMap.entries()) {
3543
+ if (!isApiToolDefinition(tool)) continue;
3544
+ await this.ensureBundle(sourceToolName, tool);
3545
+ const resolved = this.operationCache.get(toolName);
3546
+ if (resolved) return resolved;
3547
+ }
3548
+ return void 0;
3549
+ }
3550
+ };
3551
+ }
3552
+ });
3553
+
2790
3554
  // src/providers/custom-tool-executor.ts
2791
3555
  import Ajv from "ajv";
2792
3556
  var CustomToolExecutor;
@@ -2797,11 +3561,13 @@ var init_custom_tool_executor = __esm({
2797
3561
  init_sandbox();
2798
3562
  init_logger();
2799
3563
  init_command_executor();
3564
+ init_api_tool_executor();
2800
3565
  CustomToolExecutor = class {
2801
3566
  liquid;
2802
3567
  sandbox;
2803
3568
  tools;
2804
3569
  ajv;
3570
+ apiToolRegistry;
2805
3571
  constructor(tools) {
2806
3572
  this.liquid = createExtendedLiquid({
2807
3573
  cache: false,
@@ -2809,7 +3575,8 @@ var init_custom_tool_executor = __esm({
2809
3575
  strictVariables: false
2810
3576
  });
2811
3577
  this.tools = new Map(Object.entries(tools || {}));
2812
- this.ajv = new Ajv({ allErrors: true, verbose: true });
3578
+ this.ajv = new Ajv({ allErrors: true, verbose: true, strict: false });
3579
+ this.apiToolRegistry = new ApiToolRegistry();
2813
3580
  }
2814
3581
  /**
2815
3582
  * Register a custom tool
@@ -2818,6 +3585,13 @@ var init_custom_tool_executor = __esm({
2818
3585
  if (!tool.name) {
2819
3586
  throw new Error("Tool must have a name");
2820
3587
  }
3588
+ if (isApiToolDefinition(tool)) {
3589
+ if (!tool.spec) {
3590
+ throw new Error(`API tool '${tool.name}' must define 'spec'`);
3591
+ }
3592
+ } else if (!tool.exec) {
3593
+ throw new Error(`Tool '${tool.name}' must define 'exec' (or set type: 'api')`);
3594
+ }
2821
3595
  this.tools.set(tool.name, tool);
2822
3596
  }
2823
3597
  /**
@@ -2860,12 +3634,45 @@ var init_custom_tool_executor = __esm({
2860
3634
  throw new Error(`Input validation failed for tool '${tool.name}': ${errors}`);
2861
3635
  }
2862
3636
  }
3637
+ /**
3638
+ * Validate input against a JSON schema object
3639
+ */
3640
+ validateInputSchema(toolName, schema, input) {
3641
+ if (!schema) {
3642
+ return;
3643
+ }
3644
+ const validate = this.ajv.compile(schema);
3645
+ const valid = validate(input);
3646
+ if (!valid) {
3647
+ const errors = validate.errors?.map((err) => {
3648
+ if (err.instancePath) {
3649
+ return `${err.instancePath}: ${err.message}`;
3650
+ }
3651
+ return err.message;
3652
+ }).join(", ");
3653
+ throw new Error(`Input validation failed for tool '${toolName}': ${errors}`);
3654
+ }
3655
+ }
2863
3656
  /**
2864
3657
  * Execute a custom tool
2865
3658
  */
2866
3659
  async execute(toolName, args, context2) {
2867
3660
  const tool = this.tools.get(toolName);
3661
+ if (tool && isApiToolDefinition(tool)) {
3662
+ throw new Error(
3663
+ `Tool '${toolName}' is an API bundle. Call one of its generated operations instead.`
3664
+ );
3665
+ }
2868
3666
  if (!tool) {
3667
+ const apiMappedTool = await this.apiToolRegistry.getMappedTool(toolName, this.tools);
3668
+ if (apiMappedTool) {
3669
+ this.validateInputSchema(
3670
+ toolName,
3671
+ apiMappedTool.mcpToolDefinition.inputSchema,
3672
+ args
3673
+ );
3674
+ return await executeMappedApiTool(apiMappedTool, args);
3675
+ }
2869
3676
  throw new Error(`Tool not found: ${toolName}`);
2870
3677
  }
2871
3678
  this.validateInput(tool, args);
@@ -2874,6 +3681,9 @@ var init_custom_tool_executor = __esm({
2874
3681
  args,
2875
3682
  input: args
2876
3683
  };
3684
+ if (!tool.exec) {
3685
+ throw new Error(`Tool '${toolName}' is missing exec command`);
3686
+ }
2877
3687
  const command = await this.liquid.parseAndRender(tool.exec, templateContext);
2878
3688
  let stdin;
2879
3689
  if (tool.stdin) {
@@ -2933,6 +3743,50 @@ var init_custom_tool_executor = __esm({
2933
3743
  }
2934
3744
  return output;
2935
3745
  }
3746
+ /**
3747
+ * Check if a tool exists (direct or API-generated)
3748
+ */
3749
+ async hasTool(toolName) {
3750
+ if (this.tools.has(toolName)) {
3751
+ const tool = this.tools.get(toolName);
3752
+ return !isApiToolDefinition(tool);
3753
+ }
3754
+ const apiMappedTool = await this.apiToolRegistry.getMappedTool(toolName, this.tools);
3755
+ return Boolean(apiMappedTool);
3756
+ }
3757
+ /**
3758
+ * Get all available tool names, including API-generated operations
3759
+ */
3760
+ async getToolNames() {
3761
+ const names = [];
3762
+ for (const tool of this.tools.values()) {
3763
+ if (!isApiToolDefinition(tool)) {
3764
+ names.push(tool.name);
3765
+ }
3766
+ }
3767
+ const apiTools = await this.apiToolRegistry.listMappedTools(this.tools);
3768
+ for (const mapped of apiTools) {
3769
+ names.push(mapped.mcpToolDefinition.name);
3770
+ }
3771
+ return names;
3772
+ }
3773
+ /**
3774
+ * List MCP-compatible tool definitions including API-generated operations
3775
+ */
3776
+ async listMcpTools() {
3777
+ const directTools = this.getTools().filter((tool) => !isApiToolDefinition(tool)).map((tool) => ({
3778
+ name: tool.name,
3779
+ description: tool.description,
3780
+ inputSchema: tool.inputSchema
3781
+ }));
3782
+ const apiTools = await this.apiToolRegistry.listMappedTools(this.tools);
3783
+ const mappedApiTools = apiTools.map((tool) => ({
3784
+ name: tool.mcpToolDefinition.name,
3785
+ description: tool.mcpToolDefinition.description,
3786
+ inputSchema: tool.mcpToolDefinition.inputSchema
3787
+ }));
3788
+ return [...directTools, ...mappedApiTools];
3789
+ }
2936
3790
  /**
2937
3791
  * Apply JavaScript transform to output
2938
3792
  */
@@ -2960,7 +3814,7 @@ var init_custom_tool_executor = __esm({
2960
3814
  * Convert custom tools to MCP tool format
2961
3815
  */
2962
3816
  toMcpTools() {
2963
- return Array.from(this.tools.values()).map((tool) => ({
3817
+ return Array.from(this.tools.values()).filter((tool) => !isApiToolDefinition(tool)).map((tool) => ({
2964
3818
  name: tool.name,
2965
3819
  description: tool.description,
2966
3820
  inputSchema: tool.inputSchema,
@@ -3068,7 +3922,7 @@ async function executeWorkflowAsTool(workflowId, args, context2, argsOverrides)
3068
3922
  ...args,
3069
3923
  ...argsOverrides
3070
3924
  };
3071
- const { WorkflowCheckProvider: WorkflowCheckProvider2 } = await import("./workflow-check-provider-BMVJ6X7N.mjs");
3925
+ const { WorkflowCheckProvider: WorkflowCheckProvider2 } = await import("./workflow-check-provider-7SR7ZWSV.mjs");
3072
3926
  const provider = new WorkflowCheckProvider2();
3073
3927
  const checkConfig = {
3074
3928
  type: "workflow",
@@ -3136,1476 +3990,6 @@ var init_workflow_tool_executor = __esm({
3136
3990
  }
3137
3991
  });
3138
3992
 
3139
- // src/scheduler/store/sqlite-store.ts
3140
- import path3 from "path";
3141
- import fs2 from "fs";
3142
- import { v4 as uuidv4 } from "uuid";
3143
- function toDbRow(schedule) {
3144
- return {
3145
- id: schedule.id,
3146
- creator_id: schedule.creatorId,
3147
- creator_context: schedule.creatorContext ?? null,
3148
- creator_name: schedule.creatorName ?? null,
3149
- timezone: schedule.timezone,
3150
- schedule_expr: schedule.schedule,
3151
- run_at: schedule.runAt ?? null,
3152
- is_recurring: schedule.isRecurring ? 1 : 0,
3153
- original_expression: schedule.originalExpression,
3154
- workflow: schedule.workflow ?? null,
3155
- workflow_inputs: schedule.workflowInputs ? JSON.stringify(schedule.workflowInputs) : null,
3156
- output_context: schedule.outputContext ? JSON.stringify(schedule.outputContext) : null,
3157
- status: schedule.status,
3158
- created_at: schedule.createdAt,
3159
- last_run_at: schedule.lastRunAt ?? null,
3160
- next_run_at: schedule.nextRunAt ?? null,
3161
- run_count: schedule.runCount,
3162
- failure_count: schedule.failureCount,
3163
- last_error: schedule.lastError ?? null,
3164
- previous_response: schedule.previousResponse ?? null
3165
- };
3166
- }
3167
- function safeJsonParse(value) {
3168
- if (!value) return void 0;
3169
- try {
3170
- return JSON.parse(value);
3171
- } catch {
3172
- return void 0;
3173
- }
3174
- }
3175
- function fromDbRow(row) {
3176
- return {
3177
- id: row.id,
3178
- creatorId: row.creator_id,
3179
- creatorContext: row.creator_context ?? void 0,
3180
- creatorName: row.creator_name ?? void 0,
3181
- timezone: row.timezone,
3182
- schedule: row.schedule_expr,
3183
- runAt: row.run_at ?? void 0,
3184
- isRecurring: row.is_recurring === 1,
3185
- originalExpression: row.original_expression,
3186
- workflow: row.workflow ?? void 0,
3187
- workflowInputs: safeJsonParse(row.workflow_inputs),
3188
- outputContext: safeJsonParse(row.output_context),
3189
- status: row.status,
3190
- createdAt: row.created_at,
3191
- lastRunAt: row.last_run_at ?? void 0,
3192
- nextRunAt: row.next_run_at ?? void 0,
3193
- runCount: row.run_count,
3194
- failureCount: row.failure_count,
3195
- lastError: row.last_error ?? void 0,
3196
- previousResponse: row.previous_response ?? void 0
3197
- };
3198
- }
3199
- var SqliteStoreBackend;
3200
- var init_sqlite_store = __esm({
3201
- "src/scheduler/store/sqlite-store.ts"() {
3202
- "use strict";
3203
- init_logger();
3204
- SqliteStoreBackend = class {
3205
- db = null;
3206
- dbPath;
3207
- // In-memory locks (single-node only; SQLite doesn't support distributed locking)
3208
- locks = /* @__PURE__ */ new Map();
3209
- constructor(filename) {
3210
- this.dbPath = filename || ".visor/schedules.db";
3211
- }
3212
- async initialize() {
3213
- const resolvedPath = path3.resolve(process.cwd(), this.dbPath);
3214
- const dir = path3.dirname(resolvedPath);
3215
- fs2.mkdirSync(dir, { recursive: true });
3216
- const { createRequire } = __require("module");
3217
- const runtimeRequire = createRequire(__filename);
3218
- let Database;
3219
- try {
3220
- Database = runtimeRequire("better-sqlite3");
3221
- } catch (err) {
3222
- const code = err?.code;
3223
- if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
3224
- throw new Error(
3225
- "better-sqlite3 is required for SQLite schedule storage. Install it with: npm install better-sqlite3"
3226
- );
3227
- }
3228
- throw err;
3229
- }
3230
- this.db = new Database(resolvedPath);
3231
- this.db.pragma("journal_mode = WAL");
3232
- this.migrateSchema();
3233
- logger.info(`[SqliteStore] Initialized at ${this.dbPath}`);
3234
- }
3235
- async shutdown() {
3236
- if (this.db) {
3237
- this.db.close();
3238
- this.db = null;
3239
- }
3240
- this.locks.clear();
3241
- }
3242
- // --- Schema Migration ---
3243
- migrateSchema() {
3244
- const db = this.getDb();
3245
- db.exec(`
3246
- CREATE TABLE IF NOT EXISTS schedules (
3247
- id VARCHAR(36) PRIMARY KEY,
3248
- creator_id VARCHAR(255) NOT NULL,
3249
- creator_context VARCHAR(255),
3250
- creator_name VARCHAR(255),
3251
- timezone VARCHAR(64) NOT NULL DEFAULT 'UTC',
3252
- schedule_expr VARCHAR(255),
3253
- run_at BIGINT,
3254
- is_recurring BOOLEAN NOT NULL,
3255
- original_expression TEXT,
3256
- workflow VARCHAR(255),
3257
- workflow_inputs TEXT,
3258
- output_context TEXT,
3259
- status VARCHAR(20) NOT NULL,
3260
- created_at BIGINT NOT NULL,
3261
- last_run_at BIGINT,
3262
- next_run_at BIGINT,
3263
- run_count INTEGER NOT NULL DEFAULT 0,
3264
- failure_count INTEGER NOT NULL DEFAULT 0,
3265
- last_error TEXT,
3266
- previous_response TEXT,
3267
- claimed_by VARCHAR(255),
3268
- claimed_at BIGINT,
3269
- lock_token VARCHAR(36)
3270
- );
3271
-
3272
- CREATE INDEX IF NOT EXISTS idx_schedules_creator_id
3273
- ON schedules(creator_id);
3274
-
3275
- CREATE INDEX IF NOT EXISTS idx_schedules_status
3276
- ON schedules(status);
3277
-
3278
- CREATE INDEX IF NOT EXISTS idx_schedules_status_next_run
3279
- ON schedules(status, next_run_at);
3280
-
3281
- CREATE TABLE IF NOT EXISTS scheduler_locks (
3282
- lock_id VARCHAR(255) PRIMARY KEY,
3283
- node_id VARCHAR(255) NOT NULL,
3284
- lock_token VARCHAR(36) NOT NULL,
3285
- acquired_at BIGINT NOT NULL,
3286
- expires_at BIGINT NOT NULL
3287
- );
3288
- `);
3289
- }
3290
- // --- Helpers ---
3291
- getDb() {
3292
- if (!this.db) {
3293
- throw new Error("[SqliteStore] Database not initialized. Call initialize() first.");
3294
- }
3295
- return this.db;
3296
- }
3297
- // --- CRUD ---
3298
- async create(schedule) {
3299
- const db = this.getDb();
3300
- const newSchedule = {
3301
- ...schedule,
3302
- id: uuidv4(),
3303
- createdAt: Date.now(),
3304
- runCount: 0,
3305
- failureCount: 0,
3306
- status: "active"
3307
- };
3308
- const row = toDbRow(newSchedule);
3309
- db.prepare(
3310
- `
3311
- INSERT INTO schedules (
3312
- id, creator_id, creator_context, creator_name, timezone,
3313
- schedule_expr, run_at, is_recurring, original_expression,
3314
- workflow, workflow_inputs, output_context,
3315
- status, created_at, last_run_at, next_run_at,
3316
- run_count, failure_count, last_error, previous_response
3317
- ) VALUES (
3318
- ?, ?, ?, ?, ?,
3319
- ?, ?, ?, ?,
3320
- ?, ?, ?,
3321
- ?, ?, ?, ?,
3322
- ?, ?, ?, ?
3323
- )
3324
- `
3325
- ).run(
3326
- row.id,
3327
- row.creator_id,
3328
- row.creator_context,
3329
- row.creator_name,
3330
- row.timezone,
3331
- row.schedule_expr,
3332
- row.run_at,
3333
- row.is_recurring,
3334
- row.original_expression,
3335
- row.workflow,
3336
- row.workflow_inputs,
3337
- row.output_context,
3338
- row.status,
3339
- row.created_at,
3340
- row.last_run_at,
3341
- row.next_run_at,
3342
- row.run_count,
3343
- row.failure_count,
3344
- row.last_error,
3345
- row.previous_response
3346
- );
3347
- logger.info(
3348
- `[SqliteStore] Created schedule ${newSchedule.id} for user ${newSchedule.creatorId}`
3349
- );
3350
- return newSchedule;
3351
- }
3352
- async importSchedule(schedule) {
3353
- const db = this.getDb();
3354
- const row = toDbRow(schedule);
3355
- db.prepare(
3356
- `
3357
- INSERT OR IGNORE INTO schedules (
3358
- id, creator_id, creator_context, creator_name, timezone,
3359
- schedule_expr, run_at, is_recurring, original_expression,
3360
- workflow, workflow_inputs, output_context,
3361
- status, created_at, last_run_at, next_run_at,
3362
- run_count, failure_count, last_error, previous_response
3363
- ) VALUES (
3364
- ?, ?, ?, ?, ?,
3365
- ?, ?, ?, ?,
3366
- ?, ?, ?,
3367
- ?, ?, ?, ?,
3368
- ?, ?, ?, ?
3369
- )
3370
- `
3371
- ).run(
3372
- row.id,
3373
- row.creator_id,
3374
- row.creator_context,
3375
- row.creator_name,
3376
- row.timezone,
3377
- row.schedule_expr,
3378
- row.run_at,
3379
- row.is_recurring,
3380
- row.original_expression,
3381
- row.workflow,
3382
- row.workflow_inputs,
3383
- row.output_context,
3384
- row.status,
3385
- row.created_at,
3386
- row.last_run_at,
3387
- row.next_run_at,
3388
- row.run_count,
3389
- row.failure_count,
3390
- row.last_error,
3391
- row.previous_response
3392
- );
3393
- }
3394
- async get(id) {
3395
- const db = this.getDb();
3396
- const row = db.prepare("SELECT * FROM schedules WHERE id = ?").get(id);
3397
- return row ? fromDbRow(row) : void 0;
3398
- }
3399
- async update(id, patch) {
3400
- const db = this.getDb();
3401
- const existing = db.prepare("SELECT * FROM schedules WHERE id = ?").get(id);
3402
- if (!existing) return void 0;
3403
- const current = fromDbRow(existing);
3404
- const updated = { ...current, ...patch, id: current.id };
3405
- const row = toDbRow(updated);
3406
- db.prepare(
3407
- `
3408
- UPDATE schedules SET
3409
- creator_id = ?, creator_context = ?, creator_name = ?, timezone = ?,
3410
- schedule_expr = ?, run_at = ?, is_recurring = ?, original_expression = ?,
3411
- workflow = ?, workflow_inputs = ?, output_context = ?,
3412
- status = ?, last_run_at = ?, next_run_at = ?,
3413
- run_count = ?, failure_count = ?, last_error = ?, previous_response = ?
3414
- WHERE id = ?
3415
- `
3416
- ).run(
3417
- row.creator_id,
3418
- row.creator_context,
3419
- row.creator_name,
3420
- row.timezone,
3421
- row.schedule_expr,
3422
- row.run_at,
3423
- row.is_recurring,
3424
- row.original_expression,
3425
- row.workflow,
3426
- row.workflow_inputs,
3427
- row.output_context,
3428
- row.status,
3429
- row.last_run_at,
3430
- row.next_run_at,
3431
- row.run_count,
3432
- row.failure_count,
3433
- row.last_error,
3434
- row.previous_response,
3435
- row.id
3436
- );
3437
- return updated;
3438
- }
3439
- async delete(id) {
3440
- const db = this.getDb();
3441
- const result = db.prepare("DELETE FROM schedules WHERE id = ?").run(id);
3442
- if (result.changes > 0) {
3443
- logger.info(`[SqliteStore] Deleted schedule ${id}`);
3444
- return true;
3445
- }
3446
- return false;
3447
- }
3448
- // --- Queries ---
3449
- async getByCreator(creatorId) {
3450
- const db = this.getDb();
3451
- const rows = db.prepare("SELECT * FROM schedules WHERE creator_id = ?").all(creatorId);
3452
- return rows.map(fromDbRow);
3453
- }
3454
- async getActiveSchedules() {
3455
- const db = this.getDb();
3456
- const rows = db.prepare("SELECT * FROM schedules WHERE status = 'active'").all();
3457
- return rows.map(fromDbRow);
3458
- }
3459
- async getDueSchedules(now) {
3460
- const ts = now ?? Date.now();
3461
- const db = this.getDb();
3462
- const rows = db.prepare(
3463
- `SELECT * FROM schedules
3464
- WHERE status = 'active'
3465
- AND (
3466
- (is_recurring = 0 AND run_at IS NOT NULL AND run_at <= ?)
3467
- OR
3468
- (is_recurring = 1 AND next_run_at IS NOT NULL AND next_run_at <= ?)
3469
- )`
3470
- ).all(ts, ts);
3471
- return rows.map(fromDbRow);
3472
- }
3473
- async findByWorkflow(creatorId, workflowName) {
3474
- const db = this.getDb();
3475
- const escaped = workflowName.toLowerCase().replace(/[%_\\]/g, "\\$&");
3476
- const pattern = `%${escaped}%`;
3477
- const rows = db.prepare(
3478
- `SELECT * FROM schedules
3479
- WHERE creator_id = ? AND status = 'active'
3480
- AND LOWER(workflow) LIKE ? ESCAPE '\\'`
3481
- ).all(creatorId, pattern);
3482
- return rows.map(fromDbRow);
3483
- }
3484
- async getAll() {
3485
- const db = this.getDb();
3486
- const rows = db.prepare("SELECT * FROM schedules").all();
3487
- return rows.map(fromDbRow);
3488
- }
3489
- async getStats() {
3490
- const db = this.getDb();
3491
- const row = db.prepare(
3492
- `SELECT
3493
- COUNT(*) as total,
3494
- SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
3495
- SUM(CASE WHEN status = 'paused' THEN 1 ELSE 0 END) as paused,
3496
- SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
3497
- SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
3498
- SUM(CASE WHEN is_recurring = 1 THEN 1 ELSE 0 END) as recurring,
3499
- SUM(CASE WHEN is_recurring = 0 THEN 1 ELSE 0 END) as one_time
3500
- FROM schedules`
3501
- ).get();
3502
- return {
3503
- total: row.total,
3504
- active: row.active,
3505
- paused: row.paused,
3506
- completed: row.completed,
3507
- failed: row.failed,
3508
- recurring: row.recurring,
3509
- oneTime: row.one_time
3510
- };
3511
- }
3512
- async validateLimits(creatorId, isRecurring, limits) {
3513
- const db = this.getDb();
3514
- if (limits.maxGlobal) {
3515
- const row = db.prepare("SELECT COUNT(*) as cnt FROM schedules").get();
3516
- if (row.cnt >= limits.maxGlobal) {
3517
- throw new Error(`Global schedule limit reached (${limits.maxGlobal})`);
3518
- }
3519
- }
3520
- if (limits.maxPerUser) {
3521
- const row = db.prepare("SELECT COUNT(*) as cnt FROM schedules WHERE creator_id = ?").get(creatorId);
3522
- if (row.cnt >= limits.maxPerUser) {
3523
- throw new Error(`You have reached the maximum number of schedules (${limits.maxPerUser})`);
3524
- }
3525
- }
3526
- if (isRecurring && limits.maxRecurringPerUser) {
3527
- const row = db.prepare("SELECT COUNT(*) as cnt FROM schedules WHERE creator_id = ? AND is_recurring = 1").get(creatorId);
3528
- if (row.cnt >= limits.maxRecurringPerUser) {
3529
- throw new Error(
3530
- `You have reached the maximum number of recurring schedules (${limits.maxRecurringPerUser})`
3531
- );
3532
- }
3533
- }
3534
- }
3535
- // --- HA Locking (in-memory for SQLite — single-node only) ---
3536
- async tryAcquireLock(scheduleId, nodeId, ttlSeconds) {
3537
- const now = Date.now();
3538
- const existing = this.locks.get(scheduleId);
3539
- if (existing && existing.expiresAt > now) {
3540
- if (existing.nodeId === nodeId) {
3541
- return existing.token;
3542
- }
3543
- return null;
3544
- }
3545
- const token = uuidv4();
3546
- this.locks.set(scheduleId, {
3547
- nodeId,
3548
- token,
3549
- expiresAt: now + ttlSeconds * 1e3
3550
- });
3551
- return token;
3552
- }
3553
- async releaseLock(scheduleId, lockToken) {
3554
- const existing = this.locks.get(scheduleId);
3555
- if (existing && existing.token === lockToken) {
3556
- this.locks.delete(scheduleId);
3557
- }
3558
- }
3559
- async renewLock(scheduleId, lockToken, ttlSeconds) {
3560
- const existing = this.locks.get(scheduleId);
3561
- if (!existing || existing.token !== lockToken) {
3562
- return false;
3563
- }
3564
- existing.expiresAt = Date.now() + ttlSeconds * 1e3;
3565
- return true;
3566
- }
3567
- async flush() {
3568
- }
3569
- };
3570
- }
3571
- });
3572
-
3573
- // src/scheduler/store/index.ts
3574
- async function createStoreBackend(storageConfig, haConfig) {
3575
- const driver = storageConfig?.driver || "sqlite";
3576
- switch (driver) {
3577
- case "sqlite": {
3578
- const conn = storageConfig?.connection;
3579
- return new SqliteStoreBackend(conn?.filename);
3580
- }
3581
- case "postgresql":
3582
- case "mysql":
3583
- case "mssql": {
3584
- try {
3585
- const loaderPath = "../../enterprise/loader";
3586
- const { loadEnterpriseStoreBackend } = await import(loaderPath);
3587
- return await loadEnterpriseStoreBackend(driver, storageConfig, haConfig);
3588
- } catch (err) {
3589
- const msg = err instanceof Error ? err.message : String(err);
3590
- logger.error(`[StoreFactory] Failed to load enterprise ${driver} backend: ${msg}`);
3591
- throw new Error(
3592
- `The ${driver} schedule storage driver requires a Visor Enterprise license. Install the enterprise package or use driver: 'sqlite' (default). Original error: ${msg}`
3593
- );
3594
- }
3595
- }
3596
- default:
3597
- throw new Error(`Unknown schedule storage driver: ${driver}`);
3598
- }
3599
- }
3600
- var init_store = __esm({
3601
- "src/scheduler/store/index.ts"() {
3602
- "use strict";
3603
- init_logger();
3604
- init_sqlite_store();
3605
- }
3606
- });
3607
-
3608
- // src/scheduler/store/json-migrator.ts
3609
- import fs3 from "fs/promises";
3610
- import path4 from "path";
3611
- async function migrateJsonToBackend(jsonPath, backend) {
3612
- const resolvedPath = path4.resolve(process.cwd(), jsonPath);
3613
- let content;
3614
- try {
3615
- content = await fs3.readFile(resolvedPath, "utf-8");
3616
- } catch (err) {
3617
- if (err.code === "ENOENT") {
3618
- return 0;
3619
- }
3620
- throw err;
3621
- }
3622
- let data;
3623
- try {
3624
- data = JSON.parse(content);
3625
- } catch {
3626
- logger.warn(`[JsonMigrator] Failed to parse ${jsonPath}, skipping migration`);
3627
- return 0;
3628
- }
3629
- const schedules = data.schedules;
3630
- if (!Array.isArray(schedules) || schedules.length === 0) {
3631
- logger.debug("[JsonMigrator] No schedules to migrate");
3632
- await renameToMigrated(resolvedPath);
3633
- return 0;
3634
- }
3635
- let migrated = 0;
3636
- for (const schedule of schedules) {
3637
- if (!schedule.id) {
3638
- logger.warn("[JsonMigrator] Skipping schedule without ID");
3639
- continue;
3640
- }
3641
- const existing = await backend.get(schedule.id);
3642
- if (existing) {
3643
- logger.debug(`[JsonMigrator] Schedule ${schedule.id} already exists, skipping`);
3644
- continue;
3645
- }
3646
- try {
3647
- await backend.importSchedule(schedule);
3648
- migrated++;
3649
- } catch (err) {
3650
- logger.warn(
3651
- `[JsonMigrator] Failed to migrate schedule ${schedule.id}: ${err instanceof Error ? err.message : err}`
3652
- );
3653
- }
3654
- }
3655
- await renameToMigrated(resolvedPath);
3656
- logger.info(`[JsonMigrator] Migrated ${migrated}/${schedules.length} schedules from ${jsonPath}`);
3657
- return migrated;
3658
- }
3659
- async function renameToMigrated(resolvedPath) {
3660
- const migratedPath = `${resolvedPath}.migrated`;
3661
- try {
3662
- await fs3.rename(resolvedPath, migratedPath);
3663
- logger.info(`[JsonMigrator] Backed up ${resolvedPath} \u2192 ${migratedPath}`);
3664
- } catch (err) {
3665
- logger.warn(
3666
- `[JsonMigrator] Failed to rename ${resolvedPath}: ${err instanceof Error ? err.message : err}`
3667
- );
3668
- }
3669
- }
3670
- var init_json_migrator = __esm({
3671
- "src/scheduler/store/json-migrator.ts"() {
3672
- "use strict";
3673
- init_logger();
3674
- }
3675
- });
3676
-
3677
- // src/scheduler/schedule-store.ts
3678
- var ScheduleStore;
3679
- var init_schedule_store = __esm({
3680
- "src/scheduler/schedule-store.ts"() {
3681
- "use strict";
3682
- init_logger();
3683
- init_store();
3684
- init_json_migrator();
3685
- ScheduleStore = class _ScheduleStore {
3686
- static instance;
3687
- backend = null;
3688
- initialized = false;
3689
- limits;
3690
- config;
3691
- externalBackend = null;
3692
- constructor(config, limits, backend) {
3693
- this.config = config || {};
3694
- this.limits = {
3695
- maxPerUser: limits?.maxPerUser ?? 25,
3696
- maxRecurringPerUser: limits?.maxRecurringPerUser ?? 10,
3697
- maxGlobal: limits?.maxGlobal ?? 1e3
3698
- };
3699
- if (backend) {
3700
- this.externalBackend = backend;
3701
- }
3702
- }
3703
- /**
3704
- * Get singleton instance
3705
- *
3706
- * Note: Config and limits are only applied on first call. Subsequent calls
3707
- * with different parameters will log a warning and return the existing instance.
3708
- * Use createIsolated() for testing with different configurations.
3709
- */
3710
- static getInstance(config, limits) {
3711
- if (!_ScheduleStore.instance) {
3712
- _ScheduleStore.instance = new _ScheduleStore(config, limits);
3713
- } else if (config || limits) {
3714
- logger.warn(
3715
- "[ScheduleStore] getInstance() called with config/limits but instance already exists. Parameters ignored. Use createIsolated() for testing or resetInstance() first."
3716
- );
3717
- }
3718
- return _ScheduleStore.instance;
3719
- }
3720
- /**
3721
- * Create a new isolated instance (for testing)
3722
- */
3723
- static createIsolated(config, limits, backend) {
3724
- return new _ScheduleStore(config, limits, backend);
3725
- }
3726
- /**
3727
- * Reset singleton instance (for testing)
3728
- */
3729
- static resetInstance() {
3730
- if (_ScheduleStore.instance) {
3731
- if (_ScheduleStore.instance.backend) {
3732
- _ScheduleStore.instance.backend.shutdown().catch(() => {
3733
- });
3734
- }
3735
- }
3736
- _ScheduleStore.instance = void 0;
3737
- }
3738
- /**
3739
- * Initialize the store - creates backend and runs migrations
3740
- */
3741
- async initialize() {
3742
- if (this.initialized) {
3743
- return;
3744
- }
3745
- if (this.externalBackend) {
3746
- this.backend = this.externalBackend;
3747
- } else {
3748
- this.backend = await createStoreBackend(this.config.storage, this.config.ha);
3749
- }
3750
- await this.backend.initialize();
3751
- const jsonPath = this.config.path || ".visor/schedules.json";
3752
- try {
3753
- await migrateJsonToBackend(jsonPath, this.backend);
3754
- } catch (err) {
3755
- logger.warn(
3756
- `[ScheduleStore] JSON migration failed (non-fatal): ${err instanceof Error ? err.message : err}`
3757
- );
3758
- }
3759
- this.initialized = true;
3760
- }
3761
- /**
3762
- * Create a new schedule (async, persists immediately)
3763
- */
3764
- async createAsync(schedule) {
3765
- const backend = this.getBackend();
3766
- await backend.validateLimits(schedule.creatorId, schedule.isRecurring, this.limits);
3767
- return backend.create(schedule);
3768
- }
3769
- /**
3770
- * Get a schedule by ID
3771
- */
3772
- async getAsync(id) {
3773
- return this.getBackend().get(id);
3774
- }
3775
- /**
3776
- * Update a schedule
3777
- */
3778
- async updateAsync(id, patch) {
3779
- return this.getBackend().update(id, patch);
3780
- }
3781
- /**
3782
- * Delete a schedule
3783
- */
3784
- async deleteAsync(id) {
3785
- return this.getBackend().delete(id);
3786
- }
3787
- /**
3788
- * Get all schedules for a specific creator
3789
- */
3790
- async getByCreatorAsync(creatorId) {
3791
- return this.getBackend().getByCreator(creatorId);
3792
- }
3793
- /**
3794
- * Get all active schedules
3795
- */
3796
- async getActiveSchedulesAsync() {
3797
- return this.getBackend().getActiveSchedules();
3798
- }
3799
- /**
3800
- * Get all schedules due for execution
3801
- * @param now Current timestamp in milliseconds
3802
- */
3803
- async getDueSchedulesAsync(now = Date.now()) {
3804
- return this.getBackend().getDueSchedules(now);
3805
- }
3806
- /**
3807
- * Find schedules by workflow name
3808
- */
3809
- async findByWorkflowAsync(creatorId, workflowName) {
3810
- return this.getBackend().findByWorkflow(creatorId, workflowName);
3811
- }
3812
- /**
3813
- * Get schedule count statistics
3814
- */
3815
- async getStatsAsync() {
3816
- return this.getBackend().getStats();
3817
- }
3818
- /**
3819
- * Force immediate save (useful for shutdown)
3820
- */
3821
- async flush() {
3822
- if (this.backend) {
3823
- await this.backend.flush();
3824
- }
3825
- }
3826
- /**
3827
- * Check if initialized
3828
- */
3829
- isInitialized() {
3830
- return this.initialized;
3831
- }
3832
- /**
3833
- * Check if there are unsaved changes
3834
- */
3835
- hasPendingChanges() {
3836
- return false;
3837
- }
3838
- /**
3839
- * Get all schedules
3840
- */
3841
- async getAllAsync() {
3842
- return this.getBackend().getAll();
3843
- }
3844
- /**
3845
- * Get the underlying backend (for HA lock operations)
3846
- */
3847
- getBackend() {
3848
- if (!this.backend) {
3849
- throw new Error("[ScheduleStore] Not initialized. Call initialize() first.");
3850
- }
3851
- return this.backend;
3852
- }
3853
- /**
3854
- * Shut down the backend cleanly
3855
- */
3856
- async shutdown() {
3857
- if (this.backend) {
3858
- await this.backend.shutdown();
3859
- this.backend = null;
3860
- }
3861
- this.initialized = false;
3862
- }
3863
- };
3864
- }
3865
- });
3866
-
3867
- // src/scheduler/schedule-parser.ts
3868
- function getNextRunTime(cronExpression, _timezone = "UTC") {
3869
- const parts = cronExpression.split(" ");
3870
- if (parts.length !== 5) {
3871
- throw new Error(`Invalid cron expression: ${cronExpression}`);
3872
- }
3873
- const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
3874
- const now = /* @__PURE__ */ new Date();
3875
- const next = new Date(now);
3876
- next.setSeconds(0, 0);
3877
- next.setMinutes(next.getMinutes() + 1);
3878
- const maxAttempts = 365 * 24 * 60;
3879
- for (let i = 0; i < maxAttempts; i++) {
3880
- if (matchesCronPart(next.getMinutes(), minute) && matchesCronPart(next.getHours(), hour) && matchesCronPart(next.getDate(), dayOfMonth) && matchesCronPart(next.getMonth() + 1, month) && matchesCronPart(next.getDay(), dayOfWeek)) {
3881
- return next;
3882
- }
3883
- next.setMinutes(next.getMinutes() + 1);
3884
- }
3885
- const fallback = new Date(now);
3886
- fallback.setDate(fallback.getDate() + 1);
3887
- fallback.setHours(parseInt(hour, 10) || 9);
3888
- fallback.setMinutes(parseInt(minute, 10) || 0);
3889
- fallback.setSeconds(0, 0);
3890
- return fallback;
3891
- }
3892
- function matchesCronPart(value, cronPart) {
3893
- if (cronPart === "*") return true;
3894
- if (cronPart.startsWith("*/")) {
3895
- const step = parseInt(cronPart.slice(2), 10);
3896
- return value % step === 0;
3897
- }
3898
- if (cronPart.includes("-")) {
3899
- const [start, end] = cronPart.split("-").map((n) => parseInt(n, 10));
3900
- return value >= start && value <= end;
3901
- }
3902
- if (cronPart.includes(",")) {
3903
- return cronPart.split(",").map((n) => parseInt(n, 10)).includes(value);
3904
- }
3905
- return parseInt(cronPart, 10) === value;
3906
- }
3907
- function isValidCronExpression(expr) {
3908
- if (!expr || typeof expr !== "string") return false;
3909
- const parts = expr.trim().split(/\s+/);
3910
- if (parts.length !== 5) return false;
3911
- const ranges = [
3912
- [0, 59],
3913
- // minute
3914
- [0, 23],
3915
- // hour
3916
- [1, 31],
3917
- // day of month
3918
- [1, 12],
3919
- // month
3920
- [0, 7]
3921
- // day of week (0 and 7 are Sunday)
3922
- ];
3923
- return parts.every((part, i) => {
3924
- if (part === "*") return true;
3925
- if (part.startsWith("*/")) {
3926
- const step = parseInt(part.slice(2), 10);
3927
- return !isNaN(step) && step > 0;
3928
- }
3929
- if (part.includes("-")) {
3930
- const [start, end] = part.split("-").map((n) => parseInt(n, 10));
3931
- return !isNaN(start) && !isNaN(end) && start >= ranges[i][0] && end <= ranges[i][1];
3932
- }
3933
- if (part.includes(",")) {
3934
- return part.split(",").every((n) => {
3935
- const val2 = parseInt(n, 10);
3936
- return !isNaN(val2) && val2 >= ranges[i][0] && val2 <= ranges[i][1];
3937
- });
3938
- }
3939
- const val = parseInt(part, 10);
3940
- return !isNaN(val) && val >= ranges[i][0] && val <= ranges[i][1];
3941
- });
3942
- }
3943
- var init_schedule_parser = __esm({
3944
- "src/scheduler/schedule-parser.ts"() {
3945
- "use strict";
3946
- }
3947
- });
3948
-
3949
- // src/scheduler/schedule-tool.ts
3950
- function matchGlobPattern(pattern, value) {
3951
- const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
3952
- return new RegExp(`^${regexPattern}$`).test(value);
3953
- }
3954
- function isWorkflowAllowedByPatterns(workflow, allowedPatterns, deniedPatterns) {
3955
- if (deniedPatterns && deniedPatterns.length > 0) {
3956
- for (const pattern of deniedPatterns) {
3957
- if (matchGlobPattern(pattern, workflow)) {
3958
- return {
3959
- allowed: false,
3960
- reason: `Workflow "${workflow}" matches denied pattern "${pattern}"`
3961
- };
3962
- }
3963
- }
3964
- }
3965
- if (allowedPatterns && allowedPatterns.length > 0) {
3966
- for (const pattern of allowedPatterns) {
3967
- if (matchGlobPattern(pattern, workflow)) {
3968
- return { allowed: true };
3969
- }
3970
- }
3971
- return {
3972
- allowed: false,
3973
- reason: `Workflow "${workflow}" does not match any allowed patterns: ${allowedPatterns.join(", ")}`
3974
- };
3975
- }
3976
- return { allowed: true };
3977
- }
3978
- function checkSchedulePermissions(context2, workflow, requestedScheduleType) {
3979
- const permissions = context2.permissions;
3980
- const scheduleType = requestedScheduleType || context2.scheduleType || "personal";
3981
- if (context2.allowedScheduleType && scheduleType !== context2.allowedScheduleType) {
3982
- const contextNames = {
3983
- personal: "a direct message (DM)",
3984
- channel: "a channel",
3985
- dm: "a group DM"
3986
- };
3987
- const targetNames = {
3988
- personal: "personal",
3989
- channel: "channel",
3990
- dm: "group"
3991
- };
3992
- return {
3993
- allowed: false,
3994
- reason: `From ${contextNames[context2.allowedScheduleType]}, you can only create ${targetNames[context2.allowedScheduleType]} schedules. To create a ${targetNames[scheduleType]} schedule, please use the appropriate context.`
3995
- };
3996
- }
3997
- if (!permissions) {
3998
- return { allowed: true };
3999
- }
4000
- switch (scheduleType) {
4001
- case "personal":
4002
- if (permissions.allowPersonal === false) {
4003
- return {
4004
- allowed: false,
4005
- reason: "Personal schedules are not allowed in this configuration"
4006
- };
4007
- }
4008
- break;
4009
- case "channel":
4010
- if (permissions.allowChannel === false) {
4011
- return {
4012
- allowed: false,
4013
- reason: "Channel schedules are not allowed in this configuration"
4014
- };
4015
- }
4016
- break;
4017
- case "dm":
4018
- if (permissions.allowDm === false) {
4019
- return {
4020
- allowed: false,
4021
- reason: "DM schedules are not allowed in this configuration"
4022
- };
4023
- }
4024
- break;
4025
- }
4026
- return isWorkflowAllowedByPatterns(
4027
- workflow,
4028
- permissions.allowedWorkflows,
4029
- permissions.deniedWorkflows
4030
- );
4031
- }
4032
- function formatSchedule(schedule) {
4033
- const time = schedule.isRecurring ? schedule.originalExpression : new Date(schedule.runAt).toLocaleString();
4034
- const status = schedule.status !== "active" ? ` (${schedule.status})` : "";
4035
- const displayName = schedule.workflow || schedule.workflowInputs?.text || "scheduled message";
4036
- const truncatedName = displayName.length > 30 ? displayName.substring(0, 27) + "..." : displayName;
4037
- const output = schedule.outputContext?.type || "none";
4038
- return `\`${schedule.id.substring(0, 8)}\` - "${truncatedName}" - ${time} (\u2192 ${output})${status}`;
4039
- }
4040
- function formatCreateConfirmation(schedule) {
4041
- const outputDesc = schedule.outputContext?.type ? `${schedule.outputContext.type}${schedule.outputContext.target ? `:${schedule.outputContext.target}` : ""}` : "none";
4042
- const displayName = schedule.workflow || schedule.workflowInputs?.text || "scheduled message";
4043
- if (schedule.isRecurring) {
4044
- const nextRun = schedule.nextRunAt ? new Date(schedule.nextRunAt).toLocaleString("en-US", {
4045
- weekday: "long",
4046
- month: "short",
4047
- day: "numeric",
4048
- hour: "numeric",
4049
- minute: "2-digit"
4050
- }) : "calculating...";
4051
- return `**Schedule created!**
4052
-
4053
- **${schedule.workflow ? "Workflow" : "Reminder"}**: ${displayName}
4054
- **When**: ${schedule.originalExpression}
4055
- **Output**: ${outputDesc}
4056
- **Next run**: ${nextRun}
4057
-
4058
- ID: \`${schedule.id.substring(0, 8)}\``;
4059
- } else {
4060
- const when = new Date(schedule.runAt).toLocaleString("en-US", {
4061
- weekday: "long",
4062
- month: "short",
4063
- day: "numeric",
4064
- hour: "numeric",
4065
- minute: "2-digit"
4066
- });
4067
- return `**Schedule created!**
4068
-
4069
- **${schedule.workflow ? "Workflow" : "Reminder"}**: ${displayName}
4070
- **When**: ${when}
4071
- **Output**: ${outputDesc}
4072
-
4073
- ID: \`${schedule.id.substring(0, 8)}\``;
4074
- }
4075
- }
4076
- function formatScheduleList(schedules) {
4077
- if (schedules.length === 0) {
4078
- return `You don't have any active schedules.
4079
-
4080
- To create one: "remind me every Monday at 9am to check PRs" or "schedule %daily-report every Monday at 9am"`;
4081
- }
4082
- const lines = schedules.map((s, i) => `${i + 1}. ${formatSchedule(s)}`);
4083
- return `**Your active schedules:**
4084
-
4085
- ${lines.join("\n")}
4086
-
4087
- To cancel: "cancel schedule <id>"
4088
- To pause: "pause schedule <id>"`;
4089
- }
4090
- async function handleScheduleAction(args, context2) {
4091
- const store = ScheduleStore.getInstance();
4092
- if (!store.isInitialized()) {
4093
- await store.initialize();
4094
- }
4095
- switch (args.action) {
4096
- case "create":
4097
- return handleCreate(args, context2, store);
4098
- case "list":
4099
- return handleList(context2, store);
4100
- case "cancel":
4101
- return handleCancel(args, context2, store);
4102
- case "pause":
4103
- return handlePauseResume(args, context2, store, "paused");
4104
- case "resume":
4105
- return handlePauseResume(args, context2, store, "active");
4106
- default:
4107
- return {
4108
- success: false,
4109
- message: `Unknown action: ${args.action}`,
4110
- error: `Supported actions: create, list, cancel, pause, resume`
4111
- };
4112
- }
4113
- }
4114
- async function handleCreate(args, context2, store) {
4115
- if (!args.reminder_text && !args.workflow) {
4116
- return {
4117
- success: false,
4118
- message: "Missing reminder content",
4119
- error: "Please specify either reminder_text (what to say) or workflow (what to run)"
4120
- };
4121
- }
4122
- if (!args.cron && !args.run_at) {
4123
- return {
4124
- success: false,
4125
- message: "Missing schedule timing",
4126
- error: 'Please specify either cron (for recurring, e.g., "* * * * *") or run_at (ISO timestamp for one-time)'
4127
- };
4128
- }
4129
- if (args.cron && !isValidCronExpression(args.cron)) {
4130
- return {
4131
- success: false,
4132
- message: "Invalid cron expression",
4133
- error: `"${args.cron}" is not a valid cron expression. Format: "minute hour day-of-month month day-of-week"`
4134
- };
4135
- }
4136
- let runAtTimestamp;
4137
- if (args.run_at) {
4138
- const parsed = new Date(args.run_at);
4139
- if (isNaN(parsed.getTime())) {
4140
- return {
4141
- success: false,
4142
- message: "Invalid run_at timestamp",
4143
- error: `"${args.run_at}" is not a valid ISO 8601 timestamp`
4144
- };
4145
- }
4146
- if (parsed.getTime() <= Date.now()) {
4147
- return {
4148
- success: false,
4149
- message: "run_at must be in the future",
4150
- error: "Cannot schedule a reminder in the past"
4151
- };
4152
- }
4153
- runAtTimestamp = parsed.getTime();
4154
- }
4155
- if (args.target_type && !args.target_id) {
4156
- return {
4157
- success: false,
4158
- message: "Missing target_id",
4159
- error: `target_type "${args.target_type}" requires a target_id (channel ID, user ID, or thread_ts)`
4160
- };
4161
- }
4162
- let scheduleType = "personal";
4163
- if (args.target_type === "channel") {
4164
- scheduleType = "channel";
4165
- } else if (args.target_type === "user") {
4166
- scheduleType = "dm";
4167
- }
4168
- const workflowName = args.workflow || "reminder";
4169
- const permissionCheck = checkSchedulePermissions(context2, workflowName, scheduleType);
4170
- if (!permissionCheck.allowed) {
4171
- logger.warn(
4172
- `[ScheduleTool] Permission denied for user ${context2.userId}: ${permissionCheck.reason}`
4173
- );
4174
- return {
4175
- success: false,
4176
- message: "Permission denied",
4177
- error: permissionCheck.reason || "You do not have permission to create this schedule"
4178
- };
4179
- }
4180
- if (args.workflow && context2.availableWorkflows && !context2.availableWorkflows.includes(args.workflow)) {
4181
- return {
4182
- success: false,
4183
- message: `Workflow "${args.workflow}" not found`,
4184
- error: `Available workflows: ${context2.availableWorkflows.slice(0, 5).join(", ")}${context2.availableWorkflows.length > 5 ? "..." : ""}`
4185
- };
4186
- }
4187
- try {
4188
- const timezone = context2.timezone || "UTC";
4189
- const isRecurring = args.is_recurring === true || !!args.cron;
4190
- let outputContext;
4191
- if (args.target_type && args.target_id) {
4192
- outputContext = {
4193
- type: "slack",
4194
- // Currently only Slack supported
4195
- target: args.target_id,
4196
- // Channel ID (C... or D...)
4197
- threadId: args.thread_ts,
4198
- // Thread timestamp for replies
4199
- metadata: {
4200
- targetType: args.target_type,
4201
- reminderText: args.reminder_text
4202
- }
4203
- };
4204
- }
4205
- let nextRunAt;
4206
- if (isRecurring && args.cron) {
4207
- nextRunAt = getNextRunTime(args.cron, timezone).getTime();
4208
- } else if (runAtTimestamp) {
4209
- nextRunAt = runAtTimestamp;
4210
- }
4211
- const schedule = await store.createAsync({
4212
- creatorId: context2.userId,
4213
- creatorContext: context2.contextType,
4214
- creatorName: context2.userName,
4215
- timezone,
4216
- schedule: args.cron || "",
4217
- runAt: runAtTimestamp,
4218
- isRecurring,
4219
- originalExpression: args.original_expression || args.cron || args.run_at || "",
4220
- workflow: args.workflow,
4221
- // Only set if explicitly provided
4222
- workflowInputs: args.workflow_inputs || (args.reminder_text ? { text: args.reminder_text } : void 0),
4223
- outputContext,
4224
- nextRunAt
4225
- });
4226
- const displayText = args.reminder_text || args.workflow || "scheduled task";
4227
- logger.info(
4228
- `[ScheduleTool] Created schedule ${schedule.id} for user ${context2.userId}: "${displayText}"`
4229
- );
4230
- return {
4231
- success: true,
4232
- message: formatCreateConfirmation(schedule),
4233
- schedule
4234
- };
4235
- } catch (error) {
4236
- const errorMsg = error instanceof Error ? error.message : "Unknown error";
4237
- logger.warn(`[ScheduleTool] Failed to create schedule: ${errorMsg}`);
4238
- return {
4239
- success: false,
4240
- message: `Failed to create schedule: ${errorMsg}`,
4241
- error: errorMsg
4242
- };
4243
- }
4244
- }
4245
- async function handleList(context2, store) {
4246
- const allUserSchedules = await store.getByCreatorAsync(context2.userId);
4247
- const schedules = allUserSchedules.filter((s) => s.status !== "completed");
4248
- let filteredSchedules = schedules;
4249
- if (context2.allowedScheduleType) {
4250
- filteredSchedules = schedules.filter((s) => {
4251
- const scheduleOutputType = s.outputContext?.type;
4252
- if (!scheduleOutputType || scheduleOutputType === "none") {
4253
- return context2.allowedScheduleType === "personal";
4254
- }
4255
- if (scheduleOutputType === "slack") {
4256
- const target = s.outputContext?.target || "";
4257
- if (target.startsWith("#") || target.match(/^C[A-Z0-9]+$/)) {
4258
- return context2.allowedScheduleType === "channel";
4259
- }
4260
- if (target.startsWith("@") || target.match(/^U[A-Z0-9]+$/)) {
4261
- return context2.allowedScheduleType === "dm";
4262
- }
4263
- }
4264
- return context2.allowedScheduleType === "personal";
4265
- });
4266
- }
4267
- return {
4268
- success: true,
4269
- message: formatScheduleList(filteredSchedules),
4270
- schedules: filteredSchedules
4271
- };
4272
- }
4273
- async function handleCancel(args, context2, store) {
4274
- let schedule;
4275
- if (args.schedule_id) {
4276
- const userSchedules = await store.getByCreatorAsync(context2.userId);
4277
- schedule = userSchedules.find((s) => s.id === args.schedule_id);
4278
- if (!schedule) {
4279
- schedule = userSchedules.find((s) => s.id.startsWith(args.schedule_id));
4280
- }
4281
- }
4282
- if (!schedule) {
4283
- return {
4284
- success: false,
4285
- message: "Schedule not found",
4286
- error: `Could not find schedule with ID "${args.schedule_id}" in your schedules. Use "list my schedules" to see your schedules.`
4287
- };
4288
- }
4289
- if (schedule.creatorId !== context2.userId) {
4290
- logger.warn(
4291
- `[ScheduleTool] Attempted cross-user schedule cancellation: ${context2.userId} tried to cancel ${schedule.id} owned by ${schedule.creatorId}`
4292
- );
4293
- return {
4294
- success: false,
4295
- message: "Not your schedule",
4296
- error: "You can only cancel your own schedules."
4297
- };
4298
- }
4299
- await store.deleteAsync(schedule.id);
4300
- logger.info(`[ScheduleTool] Cancelled schedule ${schedule.id} for user ${context2.userId}`);
4301
- return {
4302
- success: true,
4303
- message: `**Schedule cancelled!**
4304
-
4305
- Was: "${schedule.workflow}" scheduled for ${schedule.originalExpression}`
4306
- };
4307
- }
4308
- async function handlePauseResume(args, context2, store, newStatus) {
4309
- if (!args.schedule_id) {
4310
- return {
4311
- success: false,
4312
- message: "Missing schedule ID",
4313
- error: "Please specify which schedule to pause/resume."
4314
- };
4315
- }
4316
- const userSchedules = await store.getByCreatorAsync(context2.userId);
4317
- let schedule = userSchedules.find((s) => s.id === args.schedule_id);
4318
- if (!schedule) {
4319
- schedule = userSchedules.find((s) => s.id.startsWith(args.schedule_id));
4320
- }
4321
- if (!schedule) {
4322
- return {
4323
- success: false,
4324
- message: "Schedule not found",
4325
- error: `Could not find schedule with ID "${args.schedule_id}" in your schedules.`
4326
- };
4327
- }
4328
- if (schedule.creatorId !== context2.userId) {
4329
- logger.warn(
4330
- `[ScheduleTool] Attempted cross-user schedule modification: ${context2.userId} tried to modify ${schedule.id} owned by ${schedule.creatorId}`
4331
- );
4332
- return {
4333
- success: false,
4334
- message: "Not your schedule",
4335
- error: "You can only modify your own schedules."
4336
- };
4337
- }
4338
- const updated = await store.updateAsync(schedule.id, { status: newStatus });
4339
- const action = newStatus === "paused" ? "paused" : "resumed";
4340
- logger.info(`[ScheduleTool] ${action} schedule ${schedule.id} for user ${context2.userId}`);
4341
- return {
4342
- success: true,
4343
- message: `**Schedule ${action}!**
4344
-
4345
- "${schedule.workflow}" - ${schedule.originalExpression}`,
4346
- schedule: updated
4347
- };
4348
- }
4349
- function getScheduleToolDefinition() {
4350
- return {
4351
- name: "schedule",
4352
- description: `Schedule, list, and manage reminders or workflow executions.
4353
-
4354
- YOU (the AI) must extract and structure all scheduling parameters. Do NOT pass natural language time expressions - convert them to cron or ISO timestamps.
4355
-
4356
- CRITICAL WORKFLOW RULE:
4357
- - To schedule a WORKFLOW, the user MUST use a '%' prefix (e.g., "schedule %my-workflow daily").
4358
- - If the '%' prefix is present, extract the word following it as the 'workflow' parameter (without the '%').
4359
- - If the '%' prefix is NOT present, the request is a simple text reminder. The ENTIRE user request (excluding the schedule expression) MUST be placed in the 'reminder_text' parameter.
4360
- - DO NOT guess or infer a workflow name from a user's request without the '%' prefix.
4361
-
4362
- ACTIONS:
4363
- - create: Schedule a new reminder or workflow
4364
- - list: Show user's active schedules
4365
- - cancel: Remove a schedule by ID
4366
- - pause/resume: Temporarily disable/enable a schedule
4367
-
4368
- FOR CREATE ACTION - Extract these from user's request:
4369
- 1. WHAT:
4370
- - If user says "schedule %some-workflow ...", populate 'workflow' with "some-workflow".
4371
- - Otherwise, populate 'reminder_text' with the user's full request text.
4372
- 2. WHERE: Use the CURRENT channel from context
4373
- - target_id: The channel ID from context (C... for channels, D... for DMs)
4374
- - target_type: "channel" for public/private channels, "dm" for direct messages
4375
- - ONLY use target_type="thread" with thread_ts if user is INSIDE a thread
4376
- - When NOT in a thread, reminders post as NEW messages (not thread replies)
4377
- 3. WHEN: Either cron (for recurring) OR run_at (ISO 8601 for one-time)
4378
- - Recurring: Generate cron expression (minute hour day-of-month month day-of-week)
4379
- - One-time: Generate ISO 8601 timestamp
4380
-
4381
- CRON EXAMPLES:
4382
- - "every minute" \u2192 cron: "* * * * *"
4383
- - "every hour" \u2192 cron: "0 * * * *"
4384
- - "every day at 9am" \u2192 cron: "0 9 * * *"
4385
- - "every Monday at 9am" \u2192 cron: "0 9 * * 1"
4386
- - "weekdays at 8:30am" \u2192 cron: "30 8 * * 1-5"
4387
- - "every 5 minutes" \u2192 cron: "*/5 * * * *"
4388
-
4389
- ONE-TIME EXAMPLES:
4390
- - "in 2 hours" \u2192 run_at: "<ISO timestamp 2 hours from now>"
4391
- - "tomorrow at 3pm" \u2192 run_at: "2026-02-08T15:00:00Z"
4392
-
4393
- USAGE EXAMPLES:
4394
-
4395
- User in DM: "remind me to check builds every day at 9am"
4396
- \u2192 {
4397
- "action": "create",
4398
- "reminder_text": "check builds",
4399
- "is_recurring": true,
4400
- "cron": "0 9 * * *",
4401
- "target_type": "dm",
4402
- "target_id": "<DM channel ID from context, e.g., D09SZABNLG3>",
4403
- "original_expression": "every day at 9am"
4404
- }
4405
-
4406
- User in #security channel: "schedule %security-scan every Monday at 10am"
4407
- \u2192 {
4408
- "action": "create",
4409
- "workflow": "security-scan",
4410
- "is_recurring": true,
4411
- "cron": "0 10 * * 1",
4412
- "target_type": "channel",
4413
- "target_id": "<channel ID from context, e.g., C05ABC123>",
4414
- "original_expression": "every Monday at 10am"
4415
- }
4416
-
4417
- User in #security channel: "run security-scan every Monday at 10am" (NO % prefix!)
4418
- \u2192 {
4419
- "action": "create",
4420
- "reminder_text": "run security-scan every Monday at 10am",
4421
- "is_recurring": true,
4422
- "cron": "0 10 * * 1",
4423
- "target_type": "channel",
4424
- "target_id": "<channel ID from context, e.g., C05ABC123>",
4425
- "original_expression": "every Monday at 10am"
4426
- }
4427
-
4428
- User in DM: "remind me in 2 hours to review the PR"
4429
- \u2192 {
4430
- "action": "create",
4431
- "reminder_text": "review the PR",
4432
- "is_recurring": false,
4433
- "run_at": "2026-02-07T18:00:00Z",
4434
- "target_type": "dm",
4435
- "target_id": "<DM channel ID from context>",
4436
- "original_expression": "in 2 hours"
4437
- }
4438
-
4439
- User inside a thread: "remind me about this tomorrow"
4440
- \u2192 {
4441
- "action": "create",
4442
- "reminder_text": "Check this thread",
4443
- "is_recurring": false,
4444
- "run_at": "2026-02-08T09:00:00Z",
4445
- "target_type": "thread",
4446
- "target_id": "<channel ID>",
4447
- "thread_ts": "<thread_ts from context>",
4448
- "original_expression": "tomorrow"
4449
- }
4450
-
4451
- User: "list my schedules"
4452
- \u2192 { "action": "list" }
4453
-
4454
- User: "cancel schedule abc123"
4455
- \u2192 { "action": "cancel", "schedule_id": "abc123" }`,
4456
- inputSchema: {
4457
- type: "object",
4458
- properties: {
4459
- action: {
4460
- type: "string",
4461
- enum: ["create", "list", "cancel", "pause", "resume"],
4462
- description: "What to do: create new, list existing, cancel/pause/resume by ID"
4463
- },
4464
- // WHAT to do
4465
- reminder_text: {
4466
- type: "string",
4467
- description: "For create: the message/reminder text to send when triggered"
4468
- },
4469
- workflow: {
4470
- type: "string",
4471
- description: 'For create: workflow ID to run. ONLY populate this if the user used the % prefix (e.g., "%my-workflow"). Extract the name without the % symbol. If no % prefix, use reminder_text instead.'
4472
- },
4473
- workflow_inputs: {
4474
- type: "object",
4475
- description: "For create: optional inputs to pass to the workflow"
4476
- },
4477
- // WHERE to send
4478
- target_type: {
4479
- type: "string",
4480
- enum: ["channel", "dm", "thread", "user"],
4481
- description: "For create: where to send output. channel=public/private channel, dm=DM to self (current DM channel), user=DM to specific user, thread=reply in current thread"
4482
- },
4483
- target_id: {
4484
- type: "string",
4485
- description: "For create: Slack channel ID. Channels start with C, DMs start with D. Always use the channel ID from the current context."
4486
- },
4487
- thread_ts: {
4488
- type: "string",
4489
- description: "For create with target_type=thread: the thread timestamp to reply to. Get this from the current thread context."
4490
- },
4491
- // WHEN to run
4492
- is_recurring: {
4493
- type: "boolean",
4494
- description: "For create: true for recurring schedules (cron), false for one-time (run_at)"
4495
- },
4496
- cron: {
4497
- type: "string",
4498
- description: 'For create recurring: cron expression (minute hour day-of-month month day-of-week). Examples: "0 9 * * *" (daily 9am), "* * * * *" (every minute), "0 9 * * 1" (Mondays 9am)'
4499
- },
4500
- run_at: {
4501
- type: "string",
4502
- description: 'For create one-time: ISO 8601 timestamp when to run (e.g., "2026-02-07T15:00:00Z")'
4503
- },
4504
- original_expression: {
4505
- type: "string",
4506
- description: "For create: the original natural language expression from user (for display only)"
4507
- },
4508
- // For cancel/pause/resume
4509
- schedule_id: {
4510
- type: "string",
4511
- description: "For cancel/pause/resume: the schedule ID to act on (first 8 chars is enough)"
4512
- }
4513
- },
4514
- required: ["action"]
4515
- },
4516
- exec: ""
4517
- // Not used - this tool has a custom handler
4518
- };
4519
- }
4520
- function isScheduleTool(toolName) {
4521
- return toolName === "schedule";
4522
- }
4523
- function determineScheduleType(contextType, outputType, outputTarget) {
4524
- if (outputType === "slack" && outputTarget) {
4525
- if (outputTarget.startsWith("#") || outputTarget.match(/^C[A-Z0-9]+$/)) {
4526
- return "channel";
4527
- }
4528
- if (outputTarget.startsWith("@") || outputTarget.match(/^U[A-Z0-9]+$/)) {
4529
- return "dm";
4530
- }
4531
- }
4532
- if (contextType === "cli" || contextType.startsWith("github:")) {
4533
- return "personal";
4534
- }
4535
- return "personal";
4536
- }
4537
- function slackChannelTypeToScheduleType(channelType) {
4538
- switch (channelType) {
4539
- case "channel":
4540
- return "channel";
4541
- case "group":
4542
- return "dm";
4543
- // Group DMs map to 'dm' schedule type
4544
- case "dm":
4545
- default:
4546
- return "personal";
4547
- }
4548
- }
4549
- function buildScheduleToolContext(sources, availableWorkflows, permissions, outputInfo) {
4550
- if (sources.slackContext) {
4551
- const contextType = `slack:${sources.slackContext.userId}`;
4552
- const scheduleType = determineScheduleType(
4553
- contextType,
4554
- outputInfo?.outputType,
4555
- outputInfo?.outputTarget
4556
- );
4557
- let allowedScheduleType;
4558
- if (sources.slackContext.channelType) {
4559
- allowedScheduleType = slackChannelTypeToScheduleType(sources.slackContext.channelType);
4560
- }
4561
- let finalScheduleType = scheduleType;
4562
- if (!outputInfo?.outputType && sources.slackContext.channelType) {
4563
- finalScheduleType = slackChannelTypeToScheduleType(sources.slackContext.channelType);
4564
- }
4565
- return {
4566
- userId: sources.slackContext.userId,
4567
- userName: sources.slackContext.userName,
4568
- contextType,
4569
- timezone: sources.slackContext.timezone,
4570
- availableWorkflows,
4571
- scheduleType: finalScheduleType,
4572
- permissions,
4573
- allowedScheduleType
4574
- };
4575
- }
4576
- if (sources.githubContext) {
4577
- return {
4578
- userId: sources.githubContext.login,
4579
- contextType: `github:${sources.githubContext.login}`,
4580
- timezone: "UTC",
4581
- // GitHub doesn't provide timezone
4582
- availableWorkflows,
4583
- scheduleType: "personal",
4584
- permissions,
4585
- allowedScheduleType: "personal"
4586
- // GitHub context only allows personal schedules
4587
- };
4588
- }
4589
- return {
4590
- userId: sources.cliContext?.userId || process.env.USER || "cli-user",
4591
- contextType: "cli",
4592
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
4593
- availableWorkflows,
4594
- scheduleType: "personal",
4595
- permissions,
4596
- allowedScheduleType: "personal"
4597
- // CLI context only allows personal schedules
4598
- };
4599
- }
4600
- var init_schedule_tool = __esm({
4601
- "src/scheduler/schedule-tool.ts"() {
4602
- "use strict";
4603
- init_schedule_store();
4604
- init_schedule_parser();
4605
- init_logger();
4606
- }
4607
- });
4608
-
4609
3993
  // src/state-machine/states/init.ts
4610
3994
  async function handleInit(context2, state, transition) {
4611
3995
  if (context2.debug) {
@@ -5389,8 +4773,8 @@ var init_wave_planning = __esm({
5389
4773
  });
5390
4774
 
5391
4775
  // src/utils/mermaid-telemetry.ts
5392
- import * as fs4 from "fs";
5393
- import * as path5 from "path";
4776
+ import * as fs3 from "fs";
4777
+ import * as path4 from "path";
5394
4778
  function emitMermaidFromMarkdown(checkName, markdown, origin) {
5395
4779
  if (!markdown || typeof markdown !== "string") return 0;
5396
4780
  let m;
@@ -5403,16 +4787,16 @@ function emitMermaidFromMarkdown(checkName, markdown, origin) {
5403
4787
  addEvent("diagram.block", { check: checkName, origin, code });
5404
4788
  addDiagramBlock(origin);
5405
4789
  if (process.env.VISOR_TRACE_REPORT === "true") {
5406
- const outDir = process.env.VISOR_TRACE_DIR || path5.join(process.cwd(), "output", "traces");
4790
+ const outDir = process.env.VISOR_TRACE_DIR || path4.join(process.cwd(), "output", "traces");
5407
4791
  try {
5408
- if (!fs4.existsSync(outDir)) fs4.mkdirSync(outDir, { recursive: true });
4792
+ if (!fs3.existsSync(outDir)) fs3.mkdirSync(outDir, { recursive: true });
5409
4793
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5410
- const jsonPath = path5.join(outDir, `${ts}.trace.json`);
5411
- const htmlPath = path5.join(outDir, `${ts}.report.html`);
4794
+ const jsonPath = path4.join(outDir, `${ts}.trace.json`);
4795
+ const htmlPath = path4.join(outDir, `${ts}.report.html`);
5412
4796
  let data = { spans: [] };
5413
- if (fs4.existsSync(jsonPath)) {
4797
+ if (fs3.existsSync(jsonPath)) {
5414
4798
  try {
5415
- data = JSON.parse(fs4.readFileSync(jsonPath, "utf8"));
4799
+ data = JSON.parse(fs3.readFileSync(jsonPath, "utf8"));
5416
4800
  } catch {
5417
4801
  data = { spans: [] };
5418
4802
  }
@@ -5420,9 +4804,9 @@ function emitMermaidFromMarkdown(checkName, markdown, origin) {
5420
4804
  data.spans.push({
5421
4805
  events: [{ name: "diagram.block", attrs: { check: checkName, origin, code } }]
5422
4806
  });
5423
- fs4.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf8");
5424
- if (!fs4.existsSync(htmlPath)) {
5425
- fs4.writeFileSync(
4807
+ fs3.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf8");
4808
+ if (!fs3.existsSync(htmlPath)) {
4809
+ fs3.writeFileSync(
5426
4810
  htmlPath,
5427
4811
  '<!doctype html><html><head><meta charset="utf-8"/><title>Visor Trace Report</title></head><body><h2>Visor Trace Report</h2></body></html>',
5428
4812
  "utf8"
@@ -6078,9 +5462,9 @@ var init_dependency_gating = __esm({
6078
5462
  // src/state-machine/dispatch/template-renderer.ts
6079
5463
  async function renderTemplateContent(checkId, checkConfig, reviewSummary) {
6080
5464
  try {
6081
- const { createExtendedLiquid: createExtendedLiquid2 } = await import("./liquid-extensions-YDIIH33Q.mjs");
6082
- const fs11 = await import("fs/promises");
6083
- const path14 = await import("path");
5465
+ const { createExtendedLiquid: createExtendedLiquid2 } = await import("./liquid-extensions-PLBOMRLI.mjs");
5466
+ const fs10 = await import("fs/promises");
5467
+ const path13 = await import("path");
6084
5468
  const schemaRaw = checkConfig.schema || "plain";
6085
5469
  const schema = typeof schemaRaw === "string" ? schemaRaw : "code-review";
6086
5470
  let templateContent;
@@ -6088,24 +5472,24 @@ async function renderTemplateContent(checkId, checkConfig, reviewSummary) {
6088
5472
  templateContent = String(checkConfig.template.content);
6089
5473
  } else if (checkConfig.template && checkConfig.template.file) {
6090
5474
  const file = String(checkConfig.template.file);
6091
- const resolved = path14.resolve(process.cwd(), file);
6092
- templateContent = await fs11.readFile(resolved, "utf-8");
5475
+ const resolved = path13.resolve(process.cwd(), file);
5476
+ templateContent = await fs10.readFile(resolved, "utf-8");
6093
5477
  } else if (schema && schema !== "plain") {
6094
5478
  const sanitized = String(schema).replace(/[^a-zA-Z0-9-]/g, "");
6095
5479
  if (sanitized) {
6096
5480
  const candidatePaths = [
6097
- path14.join(__dirname, "output", sanitized, "template.liquid"),
5481
+ path13.join(__dirname, "output", sanitized, "template.liquid"),
6098
5482
  // bundled: dist/output/
6099
- path14.join(__dirname, "..", "..", "output", sanitized, "template.liquid"),
5483
+ path13.join(__dirname, "..", "..", "output", sanitized, "template.liquid"),
6100
5484
  // source: output/
6101
- path14.join(process.cwd(), "output", sanitized, "template.liquid"),
5485
+ path13.join(process.cwd(), "output", sanitized, "template.liquid"),
6102
5486
  // fallback: cwd/output/
6103
- path14.join(process.cwd(), "dist", "output", sanitized, "template.liquid")
5487
+ path13.join(process.cwd(), "dist", "output", sanitized, "template.liquid")
6104
5488
  // fallback: cwd/dist/output/
6105
5489
  ];
6106
5490
  for (const p of candidatePaths) {
6107
5491
  try {
6108
- templateContent = await fs11.readFile(p, "utf-8");
5492
+ templateContent = await fs10.readFile(p, "utf-8");
6109
5493
  if (templateContent) break;
6110
5494
  } catch {
6111
5495
  }
@@ -7394,6 +6778,7 @@ async function executeSingleCheck(checkId, context2, state, emitEvent, transitio
7394
6778
  ...checkConfig,
7395
6779
  eventContext: context2.prInfo?.eventContext || {},
7396
6780
  __outputHistory: outputHistory,
6781
+ __globalTools: context2.config.tools || {},
7397
6782
  // Propagate workflow inputs for template access via {{ inputs.* }}
7398
6783
  workflowInputs,
7399
6784
  ai: {
@@ -8790,7 +8175,7 @@ async function executeCheckWithForEachItems2(checkId, forEachParent, forEachItem
8790
8175
  }
8791
8176
  }
8792
8177
  try {
8793
- const { evaluateTransitions } = await import("./routing-MVDVJDYJ.mjs");
8178
+ const { evaluateTransitions } = await import("./routing-HR6N43RQ.mjs");
8794
8179
  const transTarget = await evaluateTransitions(
8795
8180
  onFinish.transitions,
8796
8181
  forEachParent,
@@ -8850,7 +8235,7 @@ async function executeCheckWithForEachItems2(checkId, forEachParent, forEachItem
8850
8235
  `[LevelDispatch] Error evaluating on_finish transitions for ${forEachParent}: ${e instanceof Error ? e.message : String(e)}`
8851
8236
  );
8852
8237
  }
8853
- const { evaluateGoto: evaluateGoto2 } = await import("./routing-MVDVJDYJ.mjs");
8238
+ const { evaluateGoto: evaluateGoto2 } = await import("./routing-HR6N43RQ.mjs");
8854
8239
  if (context2.debug) {
8855
8240
  logger.info(
8856
8241
  `[LevelDispatch] Evaluating on_finish.goto_js for forEach parent: ${forEachParent}`
@@ -10102,9 +9487,9 @@ function updateStats2(results, state, isForEachIteration = false) {
10102
9487
  }
10103
9488
  async function renderTemplateContent2(checkId, checkConfig, reviewSummary) {
10104
9489
  try {
10105
- const { createExtendedLiquid: createExtendedLiquid2 } = await import("./liquid-extensions-YDIIH33Q.mjs");
10106
- const fs11 = await import("fs/promises");
10107
- const path14 = await import("path");
9490
+ const { createExtendedLiquid: createExtendedLiquid2 } = await import("./liquid-extensions-PLBOMRLI.mjs");
9491
+ const fs10 = await import("fs/promises");
9492
+ const path13 = await import("path");
10108
9493
  const schemaRaw = checkConfig.schema || "plain";
10109
9494
  const schema = typeof schemaRaw === "string" && !schemaRaw.includes("{{") && !schemaRaw.includes("{%") ? schemaRaw : typeof schemaRaw === "object" ? "code-review" : "plain";
10110
9495
  let templateContent;
@@ -10113,27 +9498,27 @@ async function renderTemplateContent2(checkId, checkConfig, reviewSummary) {
10113
9498
  logger.debug(`[LevelDispatch] Using inline template for ${checkId}`);
10114
9499
  } else if (checkConfig.template && checkConfig.template.file) {
10115
9500
  const file = String(checkConfig.template.file);
10116
- const resolved = path14.resolve(process.cwd(), file);
10117
- templateContent = await fs11.readFile(resolved, "utf-8");
9501
+ const resolved = path13.resolve(process.cwd(), file);
9502
+ templateContent = await fs10.readFile(resolved, "utf-8");
10118
9503
  logger.debug(`[LevelDispatch] Using template file for ${checkId}: ${resolved}`);
10119
9504
  } else if (schema && schema !== "plain") {
10120
9505
  const sanitized = String(schema).replace(/[^a-zA-Z0-9-]/g, "");
10121
9506
  if (sanitized) {
10122
9507
  const candidatePaths = [
10123
- path14.join(__dirname, "output", sanitized, "template.liquid"),
9508
+ path13.join(__dirname, "output", sanitized, "template.liquid"),
10124
9509
  // bundled: dist/output/
10125
- path14.join(__dirname, "..", "..", "output", sanitized, "template.liquid"),
9510
+ path13.join(__dirname, "..", "..", "output", sanitized, "template.liquid"),
10126
9511
  // source (from state-machine/states)
10127
- path14.join(__dirname, "..", "..", "..", "output", sanitized, "template.liquid"),
9512
+ path13.join(__dirname, "..", "..", "..", "output", sanitized, "template.liquid"),
10128
9513
  // source (alternate)
10129
- path14.join(process.cwd(), "output", sanitized, "template.liquid"),
9514
+ path13.join(process.cwd(), "output", sanitized, "template.liquid"),
10130
9515
  // fallback: cwd/output/
10131
- path14.join(process.cwd(), "dist", "output", sanitized, "template.liquid")
9516
+ path13.join(process.cwd(), "dist", "output", sanitized, "template.liquid")
10132
9517
  // fallback: cwd/dist/output/
10133
9518
  ];
10134
9519
  for (const p of candidatePaths) {
10135
9520
  try {
10136
- templateContent = await fs11.readFile(p, "utf-8");
9521
+ templateContent = await fs10.readFile(p, "utf-8");
10137
9522
  if (templateContent) {
10138
9523
  logger.debug(`[LevelDispatch] Using schema template for ${checkId}: ${p}`);
10139
9524
  break;
@@ -11375,7 +10760,7 @@ var init_sandbox_manager = __esm({
11375
10760
 
11376
10761
  // src/utils/workspace-manager.ts
11377
10762
  import * as fsp from "fs/promises";
11378
- import * as path6 from "path";
10763
+ import * as path5 from "path";
11379
10764
  function shellEscape(str) {
11380
10765
  return "'" + str.replace(/'/g, "'\\''") + "'";
11381
10766
  }
@@ -11419,7 +10804,7 @@ var init_workspace_manager = __esm({
11419
10804
  };
11420
10805
  this.basePath = this.config.basePath;
11421
10806
  const workspaceDirName = sanitizePathComponent(this.config.name || this.sessionId);
11422
- this.workspacePath = path6.join(this.basePath, workspaceDirName);
10807
+ this.workspacePath = path5.join(this.basePath, workspaceDirName);
11423
10808
  }
11424
10809
  /**
11425
10810
  * Get or create a WorkspaceManager instance for a session
@@ -11514,7 +10899,7 @@ var init_workspace_manager = __esm({
11514
10899
  configuredMainProjectName || this.extractProjectName(this.originalPath)
11515
10900
  );
11516
10901
  this.usedNames.add(mainProjectName);
11517
- const mainProjectPath = path6.join(this.workspacePath, mainProjectName);
10902
+ const mainProjectPath = path5.join(this.workspacePath, mainProjectName);
11518
10903
  const isGitRepo = await this.isGitRepository(this.originalPath);
11519
10904
  if (isGitRepo) {
11520
10905
  await this.createMainProjectWorktree(mainProjectPath);
@@ -11555,7 +10940,7 @@ var init_workspace_manager = __esm({
11555
10940
  let projectName = sanitizePathComponent(description || this.extractRepoName(repository));
11556
10941
  projectName = this.getUniqueName(projectName);
11557
10942
  this.usedNames.add(projectName);
11558
- const workspacePath = path6.join(this.workspacePath, projectName);
10943
+ const workspacePath = path5.join(this.workspacePath, projectName);
11559
10944
  await fsp.rm(workspacePath, { recursive: true, force: true });
11560
10945
  try {
11561
10946
  await fsp.symlink(worktreePath, workspacePath);
@@ -11694,7 +11079,7 @@ var init_workspace_manager = __esm({
11694
11079
  * Extract project name from path
11695
11080
  */
11696
11081
  extractProjectName(dirPath) {
11697
- return path6.basename(dirPath);
11082
+ return path5.basename(dirPath);
11698
11083
  }
11699
11084
  /**
11700
11085
  * Extract repository name from owner/repo format
@@ -11925,8 +11310,8 @@ var init_summary = __esm({
11925
11310
  });
11926
11311
 
11927
11312
  // src/state-machine-execution-engine.ts
11928
- import * as path7 from "path";
11929
- import * as fs5 from "fs";
11313
+ import * as path6 from "path";
11314
+ import * as fs4 from "fs";
11930
11315
  function serializeRunState(state) {
11931
11316
  return {
11932
11317
  ...state,
@@ -12023,7 +11408,7 @@ var init_state_machine_execution_engine = __esm({
12023
11408
  try {
12024
11409
  const map = options?.webhookContext?.webhookData;
12025
11410
  if (map) {
12026
- const { CheckProviderRegistry: CheckProviderRegistry2 } = await import("./check-provider-registry-RRUZHGJI.mjs");
11411
+ const { CheckProviderRegistry: CheckProviderRegistry2 } = await import("./check-provider-registry-SA2WHPLO.mjs");
12027
11412
  const reg = CheckProviderRegistry2.getInstance();
12028
11413
  const p = reg.getProvider("http_input");
12029
11414
  if (p && typeof p.setWebhookContext === "function") p.setWebhookContext(map);
@@ -12136,7 +11521,7 @@ var init_state_machine_execution_engine = __esm({
12136
11521
  logger.info("[StateMachine] Using state machine engine");
12137
11522
  }
12138
11523
  if (!config) {
12139
- const { ConfigManager } = await import("./config-FMIIATKX.mjs");
11524
+ const { ConfigManager } = await import("./config-3UIU4TMP.mjs");
12140
11525
  const configManager = new ConfigManager();
12141
11526
  config = await configManager.getDefaultConfig();
12142
11527
  logger.debug("[StateMachine] Using default configuration (no config provided)");
@@ -12145,6 +11530,15 @@ var init_state_machine_execution_engine = __esm({
12145
11530
  ...config,
12146
11531
  tag_filter: tagFilter
12147
11532
  } : config;
11533
+ try {
11534
+ const { CheckProviderRegistry: CheckProviderRegistry2 } = await import("./check-provider-registry-SA2WHPLO.mjs");
11535
+ const registry = CheckProviderRegistry2.getInstance();
11536
+ registry.setCustomTools(configWithTagFilter.tools || {});
11537
+ } catch (error) {
11538
+ logger.warn(
11539
+ `[StateMachine] Failed to register custom tools: ${error instanceof Error ? error.message : String(error)}`
11540
+ );
11541
+ }
12148
11542
  const context2 = this.buildEngineContext(
12149
11543
  configWithTagFilter,
12150
11544
  prInfo,
@@ -12201,7 +11595,7 @@ var init_state_machine_execution_engine = __esm({
12201
11595
  try {
12202
11596
  const webhookData = this.executionContext?.webhookContext?.webhookData;
12203
11597
  if (webhookData instanceof Map) {
12204
- const { extractSlackContext: extractSlackContext2 } = await import("./schedule-tool-handler-7DNEGDZC.mjs");
11598
+ const { extractSlackContext: extractSlackContext2 } = await import("./schedule-tool-handler-Y2UABBXN.mjs");
12205
11599
  const slackCtx = extractSlackContext2(webhookData);
12206
11600
  if (slackCtx) {
12207
11601
  const payload = Array.from(webhookData.values())[0];
@@ -12230,7 +11624,7 @@ var init_state_machine_execution_engine = __esm({
12230
11624
  if (Array.isArray(configWithTagFilter.frontends) && configWithTagFilter.frontends.length > 0) {
12231
11625
  try {
12232
11626
  const { EventBus } = await import("./event-bus-5K3Y2FCS.mjs");
12233
- const { FrontendsHost } = await import("./host-S3LSWESP.mjs");
11627
+ const { FrontendsHost } = await import("./host-LOQWBHWT.mjs");
12234
11628
  const bus = new EventBus();
12235
11629
  context2.eventBus = bus;
12236
11630
  frontendsHost = new FrontendsHost(bus, logger);
@@ -12331,9 +11725,9 @@ var init_state_machine_execution_engine = __esm({
12331
11725
  }
12332
11726
  const checkId = String(ev?.checkId || "unknown");
12333
11727
  const threadKey = ev?.threadKey || (channel && threadTs ? `${channel}:${threadTs}` : "session");
12334
- const baseDir = process.env.VISOR_SNAPSHOT_DIR || path7.resolve(process.cwd(), ".visor", "snapshots");
12335
- fs5.mkdirSync(baseDir, { recursive: true });
12336
- const filePath = path7.join(baseDir, `${threadKey}-${checkId}.json`);
11728
+ const baseDir = process.env.VISOR_SNAPSHOT_DIR || path6.resolve(process.cwd(), ".visor", "snapshots");
11729
+ fs4.mkdirSync(baseDir, { recursive: true });
11730
+ const filePath = path6.join(baseDir, `${threadKey}-${checkId}.json`);
12337
11731
  await this.saveSnapshotToFile(filePath);
12338
11732
  logger.info(`[Snapshot] Saved run snapshot: ${filePath}`);
12339
11733
  try {
@@ -12474,7 +11868,7 @@ var init_state_machine_execution_engine = __esm({
12474
11868
  * Does not include secrets. Intended for debugging and future resume support.
12475
11869
  */
12476
11870
  async saveSnapshotToFile(filePath) {
12477
- const fs11 = await import("fs/promises");
11871
+ const fs10 = await import("fs/promises");
12478
11872
  const ctx = this._lastContext;
12479
11873
  const runner = this._lastRunner;
12480
11874
  if (!ctx || !runner) {
@@ -12494,14 +11888,14 @@ var init_state_machine_execution_engine = __esm({
12494
11888
  journal: entries,
12495
11889
  requestedChecks: ctx.requestedChecks || []
12496
11890
  };
12497
- await fs11.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
11891
+ await fs10.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
12498
11892
  }
12499
11893
  /**
12500
11894
  * Load a snapshot JSON from file and return it. Resume support can build on this.
12501
11895
  */
12502
11896
  async loadSnapshotFromFile(filePath) {
12503
- const fs11 = await import("fs/promises");
12504
- const raw = await fs11.readFile(filePath, "utf8");
11897
+ const fs10 = await import("fs/promises");
11898
+ const raw = await fs10.readFile(filePath, "utf8");
12505
11899
  return JSON.parse(raw);
12506
11900
  }
12507
11901
  /**
@@ -12580,9 +11974,9 @@ var init_state_machine_execution_engine = __esm({
12580
11974
  * @returns Array of failure condition evaluation results
12581
11975
  */
12582
11976
  async evaluateFailureConditions(checkName, reviewSummary, config, previousOutputs, authorAssociation) {
12583
- const { FailureConditionEvaluator: FailureConditionEvaluator2 } = await import("./failure-condition-evaluator-MUUAK7MN.mjs");
11977
+ const { FailureConditionEvaluator: FailureConditionEvaluator2 } = await import("./failure-condition-evaluator-3B3G5NYW.mjs");
12584
11978
  const evaluator = new FailureConditionEvaluator2();
12585
- const { addEvent: addEvent3 } = await import("./trace-helpers-KSPGA24B.mjs");
11979
+ const { addEvent: addEvent3 } = await import("./trace-helpers-IGMH7ZPP.mjs");
12586
11980
  const { addFailIfTriggered } = await import("./metrics-I6A7IHG4.mjs");
12587
11981
  const checkConfig = config.checks?.[checkName];
12588
11982
  if (!checkConfig) {
@@ -13466,7 +12860,23 @@ var init_mcp_custom_sse_server = __esm({
13466
12860
  * Handle tools/list MCP request
13467
12861
  */
13468
12862
  async handleToolsList(id) {
13469
- const allTools = Array.from(this.tools.values());
12863
+ const normalizeInputSchema = (schema) => {
12864
+ if (schema && schema.type === "object") {
12865
+ return schema;
12866
+ }
12867
+ return {
12868
+ type: "object",
12869
+ properties: {},
12870
+ required: []
12871
+ };
12872
+ };
12873
+ const regularTools = await this.toolExecutor.listMcpTools();
12874
+ const workflowTools = Array.from(this.tools.values()).filter(isWorkflowTool).map((tool) => ({
12875
+ name: tool.name,
12876
+ description: tool.description || `Execute ${tool.name}`,
12877
+ inputSchema: normalizeInputSchema(tool.inputSchema)
12878
+ }));
12879
+ const allTools = [...regularTools, ...workflowTools];
13470
12880
  if (this.debug) {
13471
12881
  logger.debug(
13472
12882
  `[CustomToolsSSEServer:${this.sessionId}] Listing ${allTools.length} tools: ${allTools.map((t) => t.name).join(", ")}`
@@ -13479,11 +12889,7 @@ var init_mcp_custom_sse_server = __esm({
13479
12889
  tools: allTools.map((tool) => ({
13480
12890
  name: tool.name,
13481
12891
  description: tool.description || `Execute ${tool.name}`,
13482
- inputSchema: tool.inputSchema || {
13483
- type: "object",
13484
- properties: {},
13485
- required: []
13486
- }
12892
+ inputSchema: normalizeInputSchema(tool.inputSchema)
13487
12893
  }))
13488
12894
  }
13489
12895
  };
@@ -13673,9 +13079,46 @@ var init_mcp_custom_sse_server = __esm({
13673
13079
  }
13674
13080
  });
13675
13081
 
13082
+ // src/utils/tool-resolver.ts
13083
+ function resolveTools(toolItems, globalTools, logPrefix = "[ToolResolver]") {
13084
+ const tools = /* @__PURE__ */ new Map();
13085
+ for (const item of toolItems) {
13086
+ const workflowTool = resolveWorkflowToolFromItem(item);
13087
+ if (workflowTool) {
13088
+ logger.debug(`${logPrefix} Loaded workflow '${workflowTool.name}' as tool`);
13089
+ tools.set(workflowTool.name, workflowTool);
13090
+ continue;
13091
+ }
13092
+ if (typeof item === "string") {
13093
+ if (globalTools && globalTools[item]) {
13094
+ const tool = globalTools[item];
13095
+ tool.name = tool.name || item;
13096
+ tools.set(item, tool);
13097
+ continue;
13098
+ }
13099
+ logger.warn(`${logPrefix} Tool '${item}' not found in global tools or workflow registry`);
13100
+ } else if (isWorkflowToolReference(item)) {
13101
+ logger.warn(`${logPrefix} Workflow '${item.workflow}' referenced but not found in registry`);
13102
+ }
13103
+ }
13104
+ if (tools.size === 0 && toolItems.length > 0 && !globalTools) {
13105
+ logger.warn(
13106
+ `${logPrefix} Tools specified but no global tools found in configuration and no workflows matched`
13107
+ );
13108
+ }
13109
+ return tools;
13110
+ }
13111
+ var init_tool_resolver = __esm({
13112
+ "src/utils/tool-resolver.ts"() {
13113
+ "use strict";
13114
+ init_workflow_tool_executor();
13115
+ init_logger();
13116
+ }
13117
+ });
13118
+
13676
13119
  // src/providers/ai-check-provider.ts
13677
- import fs6 from "fs/promises";
13678
- import path8 from "path";
13120
+ import fs5 from "fs/promises";
13121
+ import path7 from "path";
13679
13122
  var AICheckProvider;
13680
13123
  var init_ai_check_provider = __esm({
13681
13124
  "src/providers/ai-check-provider.ts"() {
@@ -13690,6 +13133,7 @@ var init_ai_check_provider = __esm({
13690
13133
  init_mcp_custom_sse_server();
13691
13134
  init_logger();
13692
13135
  init_workflow_tool_executor();
13136
+ init_tool_resolver();
13693
13137
  init_sandbox();
13694
13138
  init_schedule_tool();
13695
13139
  init_schedule_tool_handler();
@@ -13848,7 +13292,7 @@ var init_ai_check_provider = __esm({
13848
13292
  const hasFileExtension = /\.[a-zA-Z0-9]{1,10}$/i.test(str);
13849
13293
  const hasPathSeparators = /[\/\\]/.test(str);
13850
13294
  const isRelativePath = /^\.{1,2}\//.test(str);
13851
- const isAbsolutePath = path8.isAbsolute(str);
13295
+ const isAbsolutePath = path7.isAbsolute(str);
13852
13296
  const hasTypicalFileChars = /^[a-zA-Z0-9._\-\/\\:~]+$/.test(str);
13853
13297
  if (!(hasFileExtension || isRelativePath || isAbsolutePath || hasPathSeparators)) {
13854
13298
  return false;
@@ -13858,14 +13302,14 @@ var init_ai_check_provider = __esm({
13858
13302
  }
13859
13303
  try {
13860
13304
  let resolvedPath;
13861
- if (path8.isAbsolute(str)) {
13862
- resolvedPath = path8.normalize(str);
13305
+ if (path7.isAbsolute(str)) {
13306
+ resolvedPath = path7.normalize(str);
13863
13307
  } else {
13864
- resolvedPath = path8.resolve(process.cwd(), str);
13308
+ resolvedPath = path7.resolve(process.cwd(), str);
13865
13309
  }
13866
- const fs11 = __require("fs").promises;
13310
+ const fs10 = __require("fs").promises;
13867
13311
  try {
13868
- const stat = await fs11.stat(resolvedPath);
13312
+ const stat = await fs10.stat(resolvedPath);
13869
13313
  return stat.isFile();
13870
13314
  } catch {
13871
13315
  return hasFileExtension && (isRelativePath || isAbsolutePath || hasPathSeparators);
@@ -13882,14 +13326,14 @@ var init_ai_check_provider = __esm({
13882
13326
  throw new Error("Prompt file must have .liquid extension");
13883
13327
  }
13884
13328
  let resolvedPath;
13885
- if (path8.isAbsolute(promptPath)) {
13329
+ if (path7.isAbsolute(promptPath)) {
13886
13330
  resolvedPath = promptPath;
13887
13331
  } else {
13888
- resolvedPath = path8.resolve(process.cwd(), promptPath);
13332
+ resolvedPath = path7.resolve(process.cwd(), promptPath);
13889
13333
  }
13890
- if (!path8.isAbsolute(promptPath)) {
13891
- const normalizedPath = path8.normalize(resolvedPath);
13892
- const currentDir = path8.resolve(process.cwd());
13334
+ if (!path7.isAbsolute(promptPath)) {
13335
+ const normalizedPath = path7.normalize(resolvedPath);
13336
+ const currentDir = path7.resolve(process.cwd());
13893
13337
  if (!normalizedPath.startsWith(currentDir)) {
13894
13338
  throw new Error("Invalid prompt file path: path traversal detected");
13895
13339
  }
@@ -13898,7 +13342,7 @@ var init_ai_check_provider = __esm({
13898
13342
  throw new Error("Invalid prompt file path: path traversal detected");
13899
13343
  }
13900
13344
  try {
13901
- const promptContent = await fs6.readFile(resolvedPath, "utf-8");
13345
+ const promptContent = await fs5.readFile(resolvedPath, "utf-8");
13902
13346
  return promptContent;
13903
13347
  } catch (error) {
13904
13348
  throw new Error(
@@ -15301,37 +14745,8 @@ ${processedPrompt}` : processedPrompt;
15301
14745
  * Supports both traditional custom tools and workflow-as-tool references
15302
14746
  */
15303
14747
  loadCustomTools(toolItems, config) {
15304
- const tools = /* @__PURE__ */ new Map();
15305
14748
  const globalTools = config.__globalTools;
15306
- for (const item of toolItems) {
15307
- const workflowTool = resolveWorkflowToolFromItem(item);
15308
- if (workflowTool) {
15309
- logger.debug(`[AICheckProvider] Loaded workflow '${workflowTool.name}' as custom tool`);
15310
- tools.set(workflowTool.name, workflowTool);
15311
- continue;
15312
- }
15313
- if (typeof item === "string") {
15314
- if (globalTools && globalTools[item]) {
15315
- const tool = globalTools[item];
15316
- tool.name = tool.name || item;
15317
- tools.set(item, tool);
15318
- continue;
15319
- }
15320
- logger.warn(
15321
- `[AICheckProvider] Custom tool '${item}' not found in global tools or workflow registry`
15322
- );
15323
- } else if (isWorkflowToolReference(item)) {
15324
- logger.warn(
15325
- `[AICheckProvider] Workflow '${item.workflow}' referenced but not found in registry`
15326
- );
15327
- }
15328
- }
15329
- if (tools.size === 0 && toolItems.length > 0 && !globalTools) {
15330
- logger.warn(
15331
- `[AICheckProvider] ai_custom_tools specified but no global tools found in configuration and no workflows matched`
15332
- );
15333
- }
15334
- return tools;
14749
+ return resolveTools(toolItems, globalTools, "[AICheckProvider]");
15335
14750
  }
15336
14751
  /**
15337
14752
  * Intersect config-level allowedTools with policy-level allowedTools.
@@ -15881,8 +15296,8 @@ var init_template_context = __esm({
15881
15296
  });
15882
15297
 
15883
15298
  // src/providers/http-client-provider.ts
15884
- import * as fs7 from "fs";
15885
- import * as path9 from "path";
15299
+ import * as fs6 from "fs";
15300
+ import * as path8 from "path";
15886
15301
  var HttpClientProvider;
15887
15302
  var init_http_client_provider = __esm({
15888
15303
  "src/providers/http-client-provider.ts"() {
@@ -15987,14 +15402,14 @@ var init_http_client_provider = __esm({
15987
15402
  const parentContext = context2?._parentContext;
15988
15403
  const workingDirectory = parentContext?.workingDirectory;
15989
15404
  const workspaceEnabled = parentContext?.workspace?.isEnabled?.();
15990
- if (workspaceEnabled && workingDirectory && !path9.isAbsolute(resolvedOutputFile)) {
15991
- resolvedOutputFile = path9.join(workingDirectory, resolvedOutputFile);
15405
+ if (workspaceEnabled && workingDirectory && !path8.isAbsolute(resolvedOutputFile)) {
15406
+ resolvedOutputFile = path8.join(workingDirectory, resolvedOutputFile);
15992
15407
  logger.debug(
15993
15408
  `[http_client] Resolved relative output_file to workspace: ${resolvedOutputFile}`
15994
15409
  );
15995
15410
  }
15996
- if (skipIfExists && fs7.existsSync(resolvedOutputFile)) {
15997
- const stats = fs7.statSync(resolvedOutputFile);
15411
+ if (skipIfExists && fs6.existsSync(resolvedOutputFile)) {
15412
+ const stats = fs6.statSync(resolvedOutputFile);
15998
15413
  logger.verbose(`[http_client] File cached: ${resolvedOutputFile} (${stats.size} bytes)`);
15999
15414
  return {
16000
15415
  issues: [],
@@ -16205,13 +15620,13 @@ var init_http_client_provider = __esm({
16205
15620
  ]
16206
15621
  };
16207
15622
  }
16208
- const parentDir = path9.dirname(outputFile);
16209
- if (parentDir && !fs7.existsSync(parentDir)) {
16210
- fs7.mkdirSync(parentDir, { recursive: true });
15623
+ const parentDir = path8.dirname(outputFile);
15624
+ if (parentDir && !fs6.existsSync(parentDir)) {
15625
+ fs6.mkdirSync(parentDir, { recursive: true });
16211
15626
  }
16212
15627
  const arrayBuffer = await response.arrayBuffer();
16213
15628
  const buffer = Buffer.from(arrayBuffer);
16214
- fs7.writeFileSync(outputFile, buffer);
15629
+ fs6.writeFileSync(outputFile, buffer);
16215
15630
  const contentType = response.headers.get("content-type") || "application/octet-stream";
16216
15631
  logger.verbose(`[http_client] Downloaded: ${outputFile} (${buffer.length} bytes)`);
16217
15632
  return {
@@ -16967,8 +16382,8 @@ var init_claude_code_types = __esm({
16967
16382
  });
16968
16383
 
16969
16384
  // src/providers/claude-code-check-provider.ts
16970
- import fs8 from "fs/promises";
16971
- import path10 from "path";
16385
+ import fs7 from "fs/promises";
16386
+ import path9 from "path";
16972
16387
  function isClaudeCodeConstructor(value) {
16973
16388
  return typeof value === "function";
16974
16389
  }
@@ -17127,7 +16542,7 @@ var init_claude_code_check_provider = __esm({
17127
16542
  const hasFileExtension = /\.[a-zA-Z0-9]{1,10}$/i.test(str);
17128
16543
  const hasPathSeparators = /[\/\\]/.test(str);
17129
16544
  const isRelativePath = /^\.{1,2}\//.test(str);
17130
- const isAbsolutePath = path10.isAbsolute(str);
16545
+ const isAbsolutePath = path9.isAbsolute(str);
17131
16546
  const hasTypicalFileChars = /^[a-zA-Z0-9._\-\/\\:~]+$/.test(str);
17132
16547
  if (!(hasFileExtension || isRelativePath || isAbsolutePath || hasPathSeparators)) {
17133
16548
  return false;
@@ -17137,13 +16552,13 @@ var init_claude_code_check_provider = __esm({
17137
16552
  }
17138
16553
  try {
17139
16554
  let resolvedPath;
17140
- if (path10.isAbsolute(str)) {
17141
- resolvedPath = path10.normalize(str);
16555
+ if (path9.isAbsolute(str)) {
16556
+ resolvedPath = path9.normalize(str);
17142
16557
  } else {
17143
- resolvedPath = path10.resolve(process.cwd(), str);
16558
+ resolvedPath = path9.resolve(process.cwd(), str);
17144
16559
  }
17145
16560
  try {
17146
- const stat = await fs8.stat(resolvedPath);
16561
+ const stat = await fs7.stat(resolvedPath);
17147
16562
  return stat.isFile();
17148
16563
  } catch {
17149
16564
  return hasFileExtension && (isRelativePath || isAbsolutePath || hasPathSeparators);
@@ -17160,14 +16575,14 @@ var init_claude_code_check_provider = __esm({
17160
16575
  throw new Error("Prompt file must have .liquid extension");
17161
16576
  }
17162
16577
  let resolvedPath;
17163
- if (path10.isAbsolute(promptPath)) {
16578
+ if (path9.isAbsolute(promptPath)) {
17164
16579
  resolvedPath = promptPath;
17165
16580
  } else {
17166
- resolvedPath = path10.resolve(process.cwd(), promptPath);
16581
+ resolvedPath = path9.resolve(process.cwd(), promptPath);
17167
16582
  }
17168
- if (!path10.isAbsolute(promptPath)) {
17169
- const normalizedPath = path10.normalize(resolvedPath);
17170
- const currentDir = path10.resolve(process.cwd());
16583
+ if (!path9.isAbsolute(promptPath)) {
16584
+ const normalizedPath = path9.normalize(resolvedPath);
16585
+ const currentDir = path9.resolve(process.cwd());
17171
16586
  if (!normalizedPath.startsWith(currentDir)) {
17172
16587
  throw new Error("Invalid prompt file path: path traversal detected");
17173
16588
  }
@@ -17176,7 +16591,7 @@ var init_claude_code_check_provider = __esm({
17176
16591
  throw new Error("Invalid prompt file path: path traversal detected");
17177
16592
  }
17178
16593
  try {
17179
- const promptContent = await fs8.readFile(resolvedPath, "utf-8");
16594
+ const promptContent = await fs7.readFile(resolvedPath, "utf-8");
17180
16595
  return promptContent;
17181
16596
  } catch (error) {
17182
16597
  throw new Error(
@@ -17528,6 +16943,7 @@ var init_command_check_provider = __esm({
17528
16943
  init_sandbox();
17529
16944
  init_liquid_extensions();
17530
16945
  init_logger();
16946
+ init_env_resolver();
17531
16947
  init_command_executor();
17532
16948
  init_author_permissions();
17533
16949
  init_lazy_otel();
@@ -17729,7 +17145,7 @@ var init_command_check_provider = __esm({
17729
17145
  if (config.env) {
17730
17146
  for (const [key, value] of Object.entries(config.env)) {
17731
17147
  if (value !== void 0 && value !== null) {
17732
- scriptEnv[key] = String(value);
17148
+ scriptEnv[key] = String(EnvironmentResolver.resolveValue(value));
17733
17149
  }
17734
17150
  }
17735
17151
  }
@@ -19745,14 +19161,14 @@ var require_util = __commonJS({
19745
19161
  }
19746
19162
  const port = url.port != null ? url.port : url.protocol === "https:" ? 443 : 80;
19747
19163
  let origin = url.origin != null ? url.origin : `${url.protocol}//${url.hostname}:${port}`;
19748
- let path14 = url.path != null ? url.path : `${url.pathname || ""}${url.search || ""}`;
19164
+ let path13 = url.path != null ? url.path : `${url.pathname || ""}${url.search || ""}`;
19749
19165
  if (origin.endsWith("/")) {
19750
19166
  origin = origin.substring(0, origin.length - 1);
19751
19167
  }
19752
- if (path14 && !path14.startsWith("/")) {
19753
- path14 = `/${path14}`;
19168
+ if (path13 && !path13.startsWith("/")) {
19169
+ path13 = `/${path13}`;
19754
19170
  }
19755
- url = new URL(origin + path14);
19171
+ url = new URL(origin + path13);
19756
19172
  }
19757
19173
  return url;
19758
19174
  }
@@ -21366,20 +20782,20 @@ var require_parseParams = __commonJS({
21366
20782
  var require_basename = __commonJS({
21367
20783
  "node_modules/@fastify/busboy/lib/utils/basename.js"(exports, module) {
21368
20784
  "use strict";
21369
- module.exports = function basename2(path14) {
21370
- if (typeof path14 !== "string") {
20785
+ module.exports = function basename2(path13) {
20786
+ if (typeof path13 !== "string") {
21371
20787
  return "";
21372
20788
  }
21373
- for (var i = path14.length - 1; i >= 0; --i) {
21374
- switch (path14.charCodeAt(i)) {
20789
+ for (var i = path13.length - 1; i >= 0; --i) {
20790
+ switch (path13.charCodeAt(i)) {
21375
20791
  case 47:
21376
20792
  // '/'
21377
20793
  case 92:
21378
- path14 = path14.slice(i + 1);
21379
- return path14 === ".." || path14 === "." ? "" : path14;
20794
+ path13 = path13.slice(i + 1);
20795
+ return path13 === ".." || path13 === "." ? "" : path13;
21380
20796
  }
21381
20797
  }
21382
- return path14 === ".." || path14 === "." ? "" : path14;
20798
+ return path13 === ".." || path13 === "." ? "" : path13;
21383
20799
  };
21384
20800
  }
21385
20801
  });
@@ -24410,7 +23826,7 @@ var require_request = __commonJS({
24410
23826
  }
24411
23827
  var Request = class _Request {
24412
23828
  constructor(origin, {
24413
- path: path14,
23829
+ path: path13,
24414
23830
  method,
24415
23831
  body,
24416
23832
  headers,
@@ -24424,11 +23840,11 @@ var require_request = __commonJS({
24424
23840
  throwOnError,
24425
23841
  expectContinue
24426
23842
  }, handler) {
24427
- if (typeof path14 !== "string") {
23843
+ if (typeof path13 !== "string") {
24428
23844
  throw new InvalidArgumentError("path must be a string");
24429
- } else if (path14[0] !== "/" && !(path14.startsWith("http://") || path14.startsWith("https://")) && method !== "CONNECT") {
23845
+ } else if (path13[0] !== "/" && !(path13.startsWith("http://") || path13.startsWith("https://")) && method !== "CONNECT") {
24430
23846
  throw new InvalidArgumentError("path must be an absolute URL or start with a slash");
24431
- } else if (invalidPathRegex.exec(path14) !== null) {
23847
+ } else if (invalidPathRegex.exec(path13) !== null) {
24432
23848
  throw new InvalidArgumentError("invalid request path");
24433
23849
  }
24434
23850
  if (typeof method !== "string") {
@@ -24491,7 +23907,7 @@ var require_request = __commonJS({
24491
23907
  this.completed = false;
24492
23908
  this.aborted = false;
24493
23909
  this.upgrade = upgrade || null;
24494
- this.path = query ? util.buildURL(path14, query) : path14;
23910
+ this.path = query ? util.buildURL(path13, query) : path13;
24495
23911
  this.origin = origin;
24496
23912
  this.idempotent = idempotent == null ? method === "HEAD" || method === "GET" : idempotent;
24497
23913
  this.blocking = blocking == null ? false : blocking;
@@ -25499,9 +24915,9 @@ var require_RedirectHandler = __commonJS({
25499
24915
  return this.handler.onHeaders(statusCode, headers, resume, statusText);
25500
24916
  }
25501
24917
  const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)));
25502
- const path14 = search ? `${pathname}${search}` : pathname;
24918
+ const path13 = search ? `${pathname}${search}` : pathname;
25503
24919
  this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin);
25504
- this.opts.path = path14;
24920
+ this.opts.path = path13;
25505
24921
  this.opts.origin = origin;
25506
24922
  this.opts.maxRedirections = 0;
25507
24923
  this.opts.query = null;
@@ -26743,7 +26159,7 @@ var require_client = __commonJS({
26743
26159
  writeH2(client, client[kHTTP2Session], request);
26744
26160
  return;
26745
26161
  }
26746
- const { body, method, path: path14, host, upgrade, headers, blocking, reset } = request;
26162
+ const { body, method, path: path13, host, upgrade, headers, blocking, reset } = request;
26747
26163
  const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH";
26748
26164
  if (body && typeof body.read === "function") {
26749
26165
  body.read(0);
@@ -26793,7 +26209,7 @@ var require_client = __commonJS({
26793
26209
  if (blocking) {
26794
26210
  socket[kBlocking] = true;
26795
26211
  }
26796
- let header = `${method} ${path14} HTTP/1.1\r
26212
+ let header = `${method} ${path13} HTTP/1.1\r
26797
26213
  `;
26798
26214
  if (typeof host === "string") {
26799
26215
  header += `host: ${host}\r
@@ -26856,7 +26272,7 @@ upgrade: ${upgrade}\r
26856
26272
  return true;
26857
26273
  }
26858
26274
  function writeH2(client, session, request) {
26859
- const { body, method, path: path14, host, upgrade, expectContinue, signal, headers: reqHeaders } = request;
26275
+ const { body, method, path: path13, host, upgrade, expectContinue, signal, headers: reqHeaders } = request;
26860
26276
  let headers;
26861
26277
  if (typeof reqHeaders === "string") headers = Request[kHTTP2CopyHeaders](reqHeaders.trim());
26862
26278
  else headers = reqHeaders;
@@ -26899,7 +26315,7 @@ upgrade: ${upgrade}\r
26899
26315
  });
26900
26316
  return true;
26901
26317
  }
26902
- headers[HTTP2_HEADER_PATH] = path14;
26318
+ headers[HTTP2_HEADER_PATH] = path13;
26903
26319
  headers[HTTP2_HEADER_SCHEME] = "https";
26904
26320
  const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH";
26905
26321
  if (body && typeof body.read === "function") {
@@ -29142,20 +28558,20 @@ var require_mock_utils = __commonJS({
29142
28558
  }
29143
28559
  return true;
29144
28560
  }
29145
- function safeUrl(path14) {
29146
- if (typeof path14 !== "string") {
29147
- return path14;
28561
+ function safeUrl(path13) {
28562
+ if (typeof path13 !== "string") {
28563
+ return path13;
29148
28564
  }
29149
- const pathSegments = path14.split("?");
28565
+ const pathSegments = path13.split("?");
29150
28566
  if (pathSegments.length !== 2) {
29151
- return path14;
28567
+ return path13;
29152
28568
  }
29153
28569
  const qp = new URLSearchParams(pathSegments.pop());
29154
28570
  qp.sort();
29155
28571
  return [...pathSegments, qp.toString()].join("?");
29156
28572
  }
29157
- function matchKey(mockDispatch2, { path: path14, method, body, headers }) {
29158
- const pathMatch = matchValue(mockDispatch2.path, path14);
28573
+ function matchKey(mockDispatch2, { path: path13, method, body, headers }) {
28574
+ const pathMatch = matchValue(mockDispatch2.path, path13);
29159
28575
  const methodMatch = matchValue(mockDispatch2.method, method);
29160
28576
  const bodyMatch = typeof mockDispatch2.body !== "undefined" ? matchValue(mockDispatch2.body, body) : true;
29161
28577
  const headersMatch = matchHeaders(mockDispatch2, headers);
@@ -29173,7 +28589,7 @@ var require_mock_utils = __commonJS({
29173
28589
  function getMockDispatch(mockDispatches, key) {
29174
28590
  const basePath = key.query ? buildURL(key.path, key.query) : key.path;
29175
28591
  const resolvedPath = typeof basePath === "string" ? safeUrl(basePath) : basePath;
29176
- let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path: path14 }) => matchValue(safeUrl(path14), resolvedPath));
28592
+ let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path: path13 }) => matchValue(safeUrl(path13), resolvedPath));
29177
28593
  if (matchedMockDispatches.length === 0) {
29178
28594
  throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`);
29179
28595
  }
@@ -29210,9 +28626,9 @@ var require_mock_utils = __commonJS({
29210
28626
  }
29211
28627
  }
29212
28628
  function buildKey(opts) {
29213
- const { path: path14, method, body, headers, query } = opts;
28629
+ const { path: path13, method, body, headers, query } = opts;
29214
28630
  return {
29215
- path: path14,
28631
+ path: path13,
29216
28632
  method,
29217
28633
  body,
29218
28634
  headers,
@@ -29661,10 +29077,10 @@ var require_pending_interceptors_formatter = __commonJS({
29661
29077
  }
29662
29078
  format(pendingInterceptors) {
29663
29079
  const withPrettyHeaders = pendingInterceptors.map(
29664
- ({ method, path: path14, data: { statusCode }, persist, times, timesInvoked, origin }) => ({
29080
+ ({ method, path: path13, data: { statusCode }, persist, times, timesInvoked, origin }) => ({
29665
29081
  Method: method,
29666
29082
  Origin: origin,
29667
- Path: path14,
29083
+ Path: path13,
29668
29084
  "Status code": statusCode,
29669
29085
  Persistent: persist ? "\u2705" : "\u274C",
29670
29086
  Invocations: timesInvoked,
@@ -34285,8 +33701,8 @@ var require_util6 = __commonJS({
34285
33701
  }
34286
33702
  }
34287
33703
  }
34288
- function validateCookiePath(path14) {
34289
- for (const char of path14) {
33704
+ function validateCookiePath(path13) {
33705
+ for (const char of path13) {
34290
33706
  const code = char.charCodeAt(0);
34291
33707
  if (code < 33 || char === ";") {
34292
33708
  throw new Error("Invalid cookie path");
@@ -35966,11 +35382,11 @@ var require_undici = __commonJS({
35966
35382
  if (typeof opts.path !== "string") {
35967
35383
  throw new InvalidArgumentError("invalid opts.path");
35968
35384
  }
35969
- let path14 = opts.path;
35385
+ let path13 = opts.path;
35970
35386
  if (!opts.path.startsWith("/")) {
35971
- path14 = `/${path14}`;
35387
+ path13 = `/${path13}`;
35972
35388
  }
35973
- url = new URL(util.parseOrigin(url).origin + path14);
35389
+ url = new URL(util.parseOrigin(url).origin + path13);
35974
35390
  } else {
35975
35391
  if (!opts) {
35976
35392
  opts = typeof url === "object" ? url : {};
@@ -36398,10 +35814,11 @@ var init_mcp_check_provider = __esm({
36398
35814
  'No custom tools available. Define tools in the "tools" section of your configuration.'
36399
35815
  );
36400
35816
  }
36401
- const tool = this.customToolExecutor.getTool(config.method);
36402
- if (!tool) {
35817
+ const hasTool = await this.customToolExecutor.hasTool(config.method);
35818
+ if (!hasTool) {
35819
+ const availableToolNames = await this.customToolExecutor.getToolNames();
36403
35820
  throw new Error(
36404
- `Custom tool not found: ${config.method}. Available tools: ${this.customToolExecutor.getTools().map((t) => t.name).join(", ")}`
35821
+ `Custom tool not found: ${config.method}. Available tools: ${availableToolNames.join(", ")}`
36405
35822
  );
36406
35823
  }
36407
35824
  const context2 = {
@@ -36512,11 +35929,24 @@ var init_mcp_check_provider = __esm({
36512
35929
  * Execute MCP method using stdio transport
36513
35930
  */
36514
35931
  async executeStdioMethod(config, methodArgs, timeout) {
35932
+ const env = {};
35933
+ for (const [key, value] of Object.entries(process.env)) {
35934
+ if (value !== void 0) {
35935
+ env[key] = value;
35936
+ }
35937
+ }
35938
+ if (config.env) {
35939
+ for (const [key, value] of Object.entries(config.env)) {
35940
+ if (value !== void 0 && value !== null) {
35941
+ env[key] = String(EnvironmentResolver.resolveValue(value));
35942
+ }
35943
+ }
35944
+ }
36515
35945
  return this.executeWithTransport(
36516
35946
  () => new StdioClientTransport({
36517
35947
  command: config.command,
36518
35948
  args: config.command_args,
36519
- env: config.env,
35949
+ env,
36520
35950
  cwd: config.workingDirectory,
36521
35951
  stderr: "pipe"
36522
35952
  // Prevent child stderr from corrupting TUI
@@ -37092,8 +36522,8 @@ var init_stdin_reader = __esm({
37092
36522
  });
37093
36523
 
37094
36524
  // src/providers/human-input-check-provider.ts
37095
- import * as fs9 from "fs";
37096
- import * as path11 from "path";
36525
+ import * as fs8 from "fs";
36526
+ import * as path10 from "path";
37097
36527
  var HumanInputCheckProvider;
37098
36528
  var init_human_input_check_provider = __esm({
37099
36529
  "src/providers/human-input-check-provider.ts"() {
@@ -37276,19 +36706,19 @@ var init_human_input_check_provider = __esm({
37276
36706
  */
37277
36707
  async tryReadFile(filePath) {
37278
36708
  try {
37279
- const absolutePath = path11.isAbsolute(filePath) ? filePath : path11.resolve(process.cwd(), filePath);
37280
- const normalizedPath = path11.normalize(absolutePath);
36709
+ const absolutePath = path10.isAbsolute(filePath) ? filePath : path10.resolve(process.cwd(), filePath);
36710
+ const normalizedPath = path10.normalize(absolutePath);
37281
36711
  const cwd = process.cwd();
37282
- if (!normalizedPath.startsWith(cwd + path11.sep) && normalizedPath !== cwd) {
36712
+ if (!normalizedPath.startsWith(cwd + path10.sep) && normalizedPath !== cwd) {
37283
36713
  return null;
37284
36714
  }
37285
36715
  try {
37286
- await fs9.promises.access(normalizedPath, fs9.constants.R_OK);
37287
- const stats = await fs9.promises.stat(normalizedPath);
36716
+ await fs8.promises.access(normalizedPath, fs8.constants.R_OK);
36717
+ const stats = await fs8.promises.stat(normalizedPath);
37288
36718
  if (!stats.isFile()) {
37289
36719
  return null;
37290
36720
  }
37291
- const content = await fs9.promises.readFile(normalizedPath, "utf-8");
36721
+ const content = await fs8.promises.readFile(normalizedPath, "utf-8");
37292
36722
  return content.trim();
37293
36723
  } catch {
37294
36724
  return null;
@@ -37573,6 +37003,494 @@ ${snippet}`
37573
37003
  }
37574
37004
  });
37575
37005
 
37006
+ // src/utils/script-tool-environment.ts
37007
+ import * as acorn from "acorn";
37008
+ import * as walk from "acorn-walk";
37009
+ function formatSyntaxError(code, err) {
37010
+ const line = err.loc?.line ?? 0;
37011
+ const col = err.loc?.column ?? 0;
37012
+ const baseMsg = err.message?.replace(/\s*\(\d+:\d+\)$/, "") || "Syntax error";
37013
+ if (!line) return `Syntax error: ${baseMsg}`;
37014
+ const lines = code.split("\n");
37015
+ const snippetLines = [];
37016
+ const start = Math.max(0, line - 2);
37017
+ const end = Math.min(lines.length, line + 1);
37018
+ for (let i = start; i < end; i++) {
37019
+ const lineNum = String(i + 1).padStart(3, " ");
37020
+ if (i === line - 1) {
37021
+ snippetLines.push(` > ${lineNum} | ${lines[i]}`);
37022
+ snippetLines.push(` ${" ".repeat(lineNum.length)} | ${" ".repeat(col)}^`);
37023
+ } else {
37024
+ snippetLines.push(` ${lineNum} | ${lines[i]}`);
37025
+ }
37026
+ }
37027
+ return `Syntax error at line ${line}, column ${col}: ${baseMsg}
37028
+
37029
+ ${snippetLines.join("\n")}`;
37030
+ }
37031
+ function levenshtein(a, b) {
37032
+ const m = a.length;
37033
+ const n = b.length;
37034
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
37035
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
37036
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
37037
+ for (let i = 1; i <= m; i++) {
37038
+ for (let j = 1; j <= n; j++) {
37039
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
37040
+ }
37041
+ }
37042
+ return dp[m][n];
37043
+ }
37044
+ function transformScriptForAsync(code, asyncFunctionNames, opts) {
37045
+ if (asyncFunctionNames.size === 0) {
37046
+ return `return (() => {
37047
+ ${code}
37048
+ })()`;
37049
+ }
37050
+ let ast;
37051
+ try {
37052
+ ast = acorn.parse(code, {
37053
+ ecmaVersion: 2022,
37054
+ sourceType: "script",
37055
+ allowReturnOutsideFunction: true,
37056
+ locations: true
37057
+ });
37058
+ } catch (e) {
37059
+ throw new Error(formatSyntaxError(code, e));
37060
+ }
37061
+ if (opts?.knownGlobals) {
37062
+ lintUnknownCalls(code, ast, opts.knownGlobals, opts.disabledBuiltins);
37063
+ }
37064
+ const insertions = [];
37065
+ const functionsNeedingAsync = /* @__PURE__ */ new Set();
37066
+ const functionScopes = [];
37067
+ walk.full(ast, (node) => {
37068
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
37069
+ functionScopes.push(node);
37070
+ }
37071
+ });
37072
+ walk.full(ast, (node) => {
37073
+ if (node.type !== "CallExpression") return;
37074
+ const calleeName = getCalleeName(node);
37075
+ if (!calleeName || !asyncFunctionNames.has(calleeName)) return;
37076
+ insertions.push({ offset: node.start, text: "await " });
37077
+ for (const fn of functionScopes) {
37078
+ const body2 = fn.body;
37079
+ if (body2 && body2.start <= node.start && body2.end >= node.end) {
37080
+ functionsNeedingAsync.add(fn);
37081
+ }
37082
+ }
37083
+ });
37084
+ walk.full(ast, (node) => {
37085
+ if (node.type !== "CallExpression") return;
37086
+ const callNode = node;
37087
+ const calleeName = getCalleeName(callNode);
37088
+ if (calleeName !== "map" || !callNode.arguments || callNode.arguments.length < 2) return;
37089
+ const callback = callNode.arguments[1];
37090
+ if (callback.type === "ArrowFunctionExpression" || callback.type === "FunctionExpression") {
37091
+ let hasAsyncCall = false;
37092
+ walk.full(callback, (inner) => {
37093
+ if (inner.type === "CallExpression") {
37094
+ const innerName = getCalleeName(inner);
37095
+ if (innerName && asyncFunctionNames.has(innerName)) {
37096
+ hasAsyncCall = true;
37097
+ }
37098
+ }
37099
+ });
37100
+ if (hasAsyncCall) {
37101
+ functionsNeedingAsync.add(callback);
37102
+ }
37103
+ }
37104
+ });
37105
+ walk.full(ast, (node) => {
37106
+ if (node.type === "WhileStatement" || node.type === "ForStatement" || node.type === "ForOfStatement" || node.type === "ForInStatement") {
37107
+ const body2 = node.body;
37108
+ if (body2 && body2.type === "BlockStatement" && body2.body && body2.body.length > 0) {
37109
+ insertions.push({ offset: body2.start + 1, text: " __checkLoop();" });
37110
+ }
37111
+ }
37112
+ });
37113
+ for (const fn of functionsNeedingAsync) {
37114
+ insertions.push({ offset: fn.start, text: "async " });
37115
+ }
37116
+ const body = ast.body;
37117
+ if (body && body.length > 0) {
37118
+ const lastStmt = body[body.length - 1];
37119
+ if (lastStmt.type === "ExpressionStatement") {
37120
+ insertions.push({ offset: lastStmt.start, text: "return " });
37121
+ }
37122
+ }
37123
+ insertions.sort((a, b) => b.offset - a.offset);
37124
+ let transformed = code;
37125
+ for (const ins of insertions) {
37126
+ transformed = transformed.slice(0, ins.offset) + ins.text + transformed.slice(ins.offset);
37127
+ }
37128
+ return `return (async () => {
37129
+ ${transformed}
37130
+ })()`;
37131
+ }
37132
+ function getCalleeName(callExpr) {
37133
+ const callee = callExpr.callee;
37134
+ if (callee.type === "Identifier" && callee.name) {
37135
+ return callee.name;
37136
+ }
37137
+ return null;
37138
+ }
37139
+ function lintUnknownCalls(_code, ast, knownGlobals, disabledBuiltins) {
37140
+ const declaredFunctions = /* @__PURE__ */ new Set();
37141
+ walk.full(ast, (node) => {
37142
+ if (node.type === "FunctionDeclaration" && node.id?.name) {
37143
+ declaredFunctions.add(node.id.name);
37144
+ }
37145
+ if (node.type === "VariableDeclarator" && node.id?.name) {
37146
+ const init = node.init;
37147
+ if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
37148
+ declaredFunctions.add(node.id.name);
37149
+ }
37150
+ }
37151
+ });
37152
+ const warnings = [];
37153
+ walk.full(ast, (node) => {
37154
+ if (node.type !== "CallExpression") return;
37155
+ const name = getCalleeName(node);
37156
+ if (!name) return;
37157
+ if (knownGlobals.has(name) || JS_BUILTINS.has(name) || declaredFunctions.has(name)) return;
37158
+ const loc = node.loc?.start;
37159
+ const lineInfo = loc ? ` (line ${loc.line}, column ${loc.column})` : "";
37160
+ if (disabledBuiltins?.has(name)) {
37161
+ const hint = disabledBuiltins.get(name);
37162
+ warnings.push(`'${name}()' is not enabled${lineInfo}. ${hint}`);
37163
+ return;
37164
+ }
37165
+ const allNames = [...knownGlobals];
37166
+ let bestMatch = "";
37167
+ let bestDist = Infinity;
37168
+ for (const candidate of allNames) {
37169
+ const dist = levenshtein(name.toLowerCase(), candidate.toLowerCase());
37170
+ if (dist < bestDist) {
37171
+ bestDist = dist;
37172
+ bestMatch = candidate;
37173
+ }
37174
+ }
37175
+ const maxLen = Math.max(name.length, bestMatch.length);
37176
+ const suggestion = bestDist <= Math.ceil(maxLen * 0.4) ? ` Did you mean '${bestMatch}'?` : "";
37177
+ warnings.push(`Unknown function '${name}()'${lineInfo}.${suggestion}`);
37178
+ });
37179
+ if (warnings.length > 0) {
37180
+ throw new Error(`Script lint errors:
37181
+ ${warnings.map((w) => ` - ${w}`).join("\n")}`);
37182
+ }
37183
+ }
37184
+ function tryParseJSON(text) {
37185
+ if (typeof text !== "string") return text;
37186
+ const firstChar = text.trimStart()[0];
37187
+ if (firstChar === "{" || firstChar === "[") {
37188
+ try {
37189
+ return JSON.parse(text);
37190
+ } catch {
37191
+ }
37192
+ }
37193
+ return text;
37194
+ }
37195
+ function buildToolGlobals(opts) {
37196
+ const { resolvedTools, mcpClients, toolContext, workflowContext } = opts;
37197
+ const globals = {};
37198
+ const asyncFunctionNames = /* @__PURE__ */ new Set();
37199
+ const commandTools = {};
37200
+ for (const [name, tool] of resolvedTools) {
37201
+ if (!isWorkflowTool(tool)) {
37202
+ commandTools[name] = tool;
37203
+ }
37204
+ }
37205
+ const toolExecutor = new CustomToolExecutor(commandTools);
37206
+ const allToolInfo = [];
37207
+ for (const [name, tool] of resolvedTools) {
37208
+ const toolFn = async (args = {}) => {
37209
+ try {
37210
+ if (isWorkflowTool(tool)) {
37211
+ if (!workflowContext) {
37212
+ return `ERROR: Workflow context not available for tool '${name}'`;
37213
+ }
37214
+ return await executeWorkflowAsTool(
37215
+ tool.__workflowId,
37216
+ args,
37217
+ workflowContext,
37218
+ tool.__argsOverrides
37219
+ );
37220
+ }
37221
+ return await toolExecutor.execute(name, args, toolContext);
37222
+ } catch (e) {
37223
+ const msg = e instanceof Error ? e.message : String(e);
37224
+ logger.warn(`[script:${name}] Tool error: ${msg}`);
37225
+ return `ERROR: ${msg}`;
37226
+ }
37227
+ };
37228
+ globals[name] = toolFn;
37229
+ asyncFunctionNames.add(name);
37230
+ allToolInfo.push({ name, description: tool.description });
37231
+ }
37232
+ if (mcpClients) {
37233
+ for (const entry of mcpClients) {
37234
+ for (const mcpTool of entry.tools) {
37235
+ const globalName = `${entry.serverName}_${mcpTool.name}`;
37236
+ const mcpToolFn = async (args = {}) => {
37237
+ try {
37238
+ const result = await entry.client.callTool({
37239
+ name: mcpTool.name,
37240
+ arguments: args
37241
+ });
37242
+ const content = result?.content;
37243
+ if (Array.isArray(content) && content.length > 0) {
37244
+ const text = content[0]?.text;
37245
+ if (text !== void 0) {
37246
+ return tryParseJSON(text);
37247
+ }
37248
+ }
37249
+ return result;
37250
+ } catch (e) {
37251
+ const msg = e instanceof Error ? e.message : String(e);
37252
+ logger.warn(`[script:${globalName}] MCP tool error: ${msg}`);
37253
+ return `ERROR: ${msg}`;
37254
+ }
37255
+ };
37256
+ globals[globalName] = mcpToolFn;
37257
+ asyncFunctionNames.add(globalName);
37258
+ allToolInfo.push({ name: globalName, description: mcpTool.description });
37259
+ }
37260
+ }
37261
+ }
37262
+ const callToolFn = async (name, args = {}) => {
37263
+ const fn = globals[name];
37264
+ if (!fn || typeof fn !== "function") {
37265
+ const available = Array.from(asyncFunctionNames).join(", ");
37266
+ return `ERROR: Tool '${name}' not found. Available: ${available}`;
37267
+ }
37268
+ return fn(args);
37269
+ };
37270
+ globals.callTool = callToolFn;
37271
+ asyncFunctionNames.add("callTool");
37272
+ globals.listTools = () => {
37273
+ return [...allToolInfo];
37274
+ };
37275
+ return { globals, asyncFunctionNames };
37276
+ }
37277
+ function buildBuiltinGlobals(opts) {
37278
+ const globals = {};
37279
+ const asyncFunctionNames = /* @__PURE__ */ new Set();
37280
+ const scheduleFn = async (args = {}) => {
37281
+ try {
37282
+ const { handleScheduleAction: handleScheduleAction2, buildScheduleToolContext: buildScheduleToolContext2 } = await import("./schedule-tool-2COUUTF7.mjs");
37283
+ const { extractSlackContext: extractSlackContext2 } = await import("./schedule-tool-handler-Y2UABBXN.mjs");
37284
+ const parentCtx = opts.sessionInfo?._parentContext;
37285
+ const webhookData = parentCtx?.prInfo?.eventContext?.webhookData;
37286
+ const visorCfg = parentCtx?.config;
37287
+ const slackContext = webhookData ? extractSlackContext2(webhookData) : null;
37288
+ const availableWorkflows = visorCfg?.checks ? Object.keys(visorCfg.checks) : void 0;
37289
+ const permissions = visorCfg?.scheduler?.permissions;
37290
+ const context2 = buildScheduleToolContext2(
37291
+ {
37292
+ slackContext: slackContext || void 0,
37293
+ cliContext: slackContext ? void 0 : { userId: "script" }
37294
+ },
37295
+ availableWorkflows,
37296
+ permissions
37297
+ );
37298
+ return await handleScheduleAction2(args, context2);
37299
+ } catch (e) {
37300
+ const msg = e instanceof Error ? e.message : String(e);
37301
+ logger.warn(`[script:schedule] Error: ${msg}`);
37302
+ return `ERROR: ${msg}`;
37303
+ }
37304
+ };
37305
+ globals.schedule = scheduleFn;
37306
+ asyncFunctionNames.add("schedule");
37307
+ if (opts.config.enable_fetch === true) {
37308
+ const fetchFn = async (args = {}) => {
37309
+ try {
37310
+ const url = String(args.url || "");
37311
+ if (!url) return "ERROR: url is required";
37312
+ const method = String(args.method || "GET");
37313
+ const headers = args.headers || {};
37314
+ const body = args.body != null ? String(args.body) : void 0;
37315
+ const timeout = Number(args.timeout) || 3e4;
37316
+ const controller = new AbortController();
37317
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
37318
+ try {
37319
+ const resp = await globalThis.fetch(url, {
37320
+ method,
37321
+ headers,
37322
+ body: method !== "GET" ? body : void 0,
37323
+ signal: controller.signal
37324
+ });
37325
+ clearTimeout(timeoutId);
37326
+ const contentType = resp.headers.get("content-type") || "";
37327
+ if (contentType.includes("json")) return await resp.json();
37328
+ const text = await resp.text();
37329
+ try {
37330
+ return JSON.parse(text);
37331
+ } catch {
37332
+ return text;
37333
+ }
37334
+ } finally {
37335
+ clearTimeout(timeoutId);
37336
+ }
37337
+ } catch (e) {
37338
+ const msg = e instanceof Error ? e.message : String(e);
37339
+ logger.warn(`[script:fetch] Error: ${msg}`);
37340
+ return `ERROR: ${msg}`;
37341
+ }
37342
+ };
37343
+ globals.fetch = fetchFn;
37344
+ asyncFunctionNames.add("fetch");
37345
+ }
37346
+ const octokit = opts.config.eventContext?.octokit;
37347
+ if (octokit) {
37348
+ const githubFn = async (args = {}) => {
37349
+ try {
37350
+ const op = String(args.op || "");
37351
+ const repoEnv = process.env.GITHUB_REPOSITORY || "";
37352
+ let owner = "";
37353
+ let repo = "";
37354
+ if (repoEnv.includes("/")) {
37355
+ [owner, repo] = repoEnv.split("/");
37356
+ }
37357
+ if (!owner || !repo) {
37358
+ const ec = opts.config.eventContext || {};
37359
+ owner = owner || ec.repository?.owner?.login || "";
37360
+ repo = repo || ec.repository?.name || "";
37361
+ }
37362
+ const prNumber = opts.prInfo?.number;
37363
+ if (!owner || !repo || !prNumber) {
37364
+ return "ERROR: Missing GitHub repo/PR context";
37365
+ }
37366
+ const values = Array.isArray(args.values) ? args.values.map(String) : typeof args.value === "string" ? [args.value] : typeof args.values === "string" ? [args.values] : [];
37367
+ switch (op) {
37368
+ case "labels.add":
37369
+ await octokit.rest.issues.addLabels({
37370
+ owner,
37371
+ repo,
37372
+ issue_number: prNumber,
37373
+ labels: values
37374
+ });
37375
+ return { success: true, op };
37376
+ case "labels.remove":
37377
+ for (const name of values) {
37378
+ try {
37379
+ await octokit.rest.issues.removeLabel({
37380
+ owner,
37381
+ repo,
37382
+ issue_number: prNumber,
37383
+ name
37384
+ });
37385
+ } catch {
37386
+ }
37387
+ }
37388
+ return { success: true, op };
37389
+ case "comment.create":
37390
+ await octokit.rest.issues.createComment({
37391
+ owner,
37392
+ repo,
37393
+ issue_number: prNumber,
37394
+ body: values[0] || ""
37395
+ });
37396
+ return { success: true, op };
37397
+ default:
37398
+ return `ERROR: Unknown github op '${op}'. Supported: labels.add, labels.remove, comment.create`;
37399
+ }
37400
+ } catch (e) {
37401
+ const msg = e instanceof Error ? e.message : String(e);
37402
+ logger.warn(`[script:github] Error: ${msg}`);
37403
+ return `ERROR: ${msg}`;
37404
+ }
37405
+ };
37406
+ globals.github = githubFn;
37407
+ asyncFunctionNames.add("github");
37408
+ }
37409
+ if (opts.config.enable_bash === true) {
37410
+ const bashFn = async (args = {}) => {
37411
+ try {
37412
+ const { CommandExecutor } = await import("./command-executor-TOYBBE7S.mjs");
37413
+ const executor = CommandExecutor.getInstance();
37414
+ const command = String(args.command || "");
37415
+ if (!command) return "ERROR: command is required";
37416
+ return await executor.execute(command, {
37417
+ cwd: args.cwd ? String(args.cwd) : void 0,
37418
+ env: args.env,
37419
+ timeout: Number(args.timeout) || 3e4
37420
+ });
37421
+ } catch (e) {
37422
+ const msg = e instanceof Error ? e.message : String(e);
37423
+ logger.warn(`[script:bash] Error: ${msg}`);
37424
+ return `ERROR: ${msg}`;
37425
+ }
37426
+ };
37427
+ globals.bash = bashFn;
37428
+ asyncFunctionNames.add("bash");
37429
+ }
37430
+ return { globals, asyncFunctionNames };
37431
+ }
37432
+ var JS_BUILTINS;
37433
+ var init_script_tool_environment = __esm({
37434
+ "src/utils/script-tool-environment.ts"() {
37435
+ "use strict";
37436
+ init_custom_tool_executor();
37437
+ init_workflow_tool_executor();
37438
+ init_logger();
37439
+ JS_BUILTINS = /* @__PURE__ */ new Set([
37440
+ // Constructors & types
37441
+ "Array",
37442
+ "Object",
37443
+ "String",
37444
+ "Number",
37445
+ "Boolean",
37446
+ "Date",
37447
+ "RegExp",
37448
+ "Error",
37449
+ "TypeError",
37450
+ "RangeError",
37451
+ "Map",
37452
+ "Set",
37453
+ "WeakMap",
37454
+ "WeakSet",
37455
+ "Promise",
37456
+ "Symbol",
37457
+ "BigInt",
37458
+ "Proxy",
37459
+ "Reflect",
37460
+ // Static methods commonly called as functions
37461
+ "parseInt",
37462
+ "parseFloat",
37463
+ "isNaN",
37464
+ "isFinite",
37465
+ "encodeURIComponent",
37466
+ "decodeURIComponent",
37467
+ "encodeURI",
37468
+ "decodeURI",
37469
+ // Timers
37470
+ "setTimeout",
37471
+ "clearTimeout",
37472
+ "setInterval",
37473
+ "clearInterval",
37474
+ // JSON/Math accessed via method calls are on objects, but just in case
37475
+ "JSON",
37476
+ "Math",
37477
+ "console",
37478
+ // Node.js globals — sandbox blocks these at runtime with a better error
37479
+ "require",
37480
+ "process",
37481
+ "Buffer",
37482
+ "global",
37483
+ "globalThis",
37484
+ // Common patterns
37485
+ "eval",
37486
+ "Function",
37487
+ "alert",
37488
+ "confirm",
37489
+ "prompt"
37490
+ ]);
37491
+ }
37492
+ });
37493
+
37576
37494
  // src/providers/script-check-provider.ts
37577
37495
  var ScriptCheckProvider;
37578
37496
  var init_script_check_provider = __esm({
@@ -37585,6 +37503,10 @@ var init_script_check_provider = __esm({
37585
37503
  init_sandbox();
37586
37504
  init_template_context();
37587
37505
  init_script_memory_ops();
37506
+ init_tool_resolver();
37507
+ init_script_tool_environment();
37508
+ init_workflow_tool_executor();
37509
+ init_env_resolver();
37588
37510
  ScriptCheckProvider = class extends CheckProvider {
37589
37511
  liquid;
37590
37512
  constructor() {
@@ -37601,7 +37523,7 @@ var init_script_check_provider = __esm({
37601
37523
  return "script";
37602
37524
  }
37603
37525
  getDescription() {
37604
- return "Execute JavaScript with access to PR context, dependency outputs, and memory.";
37526
+ return "Execute JavaScript with access to PR context, dependency outputs, memory, and tools.";
37605
37527
  }
37606
37528
  async validateConfig(config) {
37607
37529
  if (!config || typeof config !== "object") return false;
@@ -37651,16 +37573,77 @@ var init_script_check_provider = __esm({
37651
37573
  ctx.atob = (str) => {
37652
37574
  return Buffer.from(String(str), "base64").toString("binary");
37653
37575
  };
37576
+ const { globals: builtinGlobals, asyncFunctionNames: builtinAsyncNames } = buildBuiltinGlobals({
37577
+ config,
37578
+ prInfo,
37579
+ sessionInfo: _sessionInfo
37580
+ });
37581
+ Object.assign(ctx, builtinGlobals);
37582
+ const hasTools = Array.isArray(config.tools) || config.tools_js || config.mcp_servers;
37654
37583
  const sandbox = this.createSecureSandbox();
37655
37584
  let result;
37585
+ let mcpClients = [];
37656
37586
  try {
37657
- result = compileAndRun(
37587
+ const asyncFunctionNames = new Set(builtinAsyncNames);
37588
+ if (hasTools) {
37589
+ const toolItems = this.resolveToolItems(config, prInfo, dependencyResults, ctx);
37590
+ const globalTools = config.__globalTools;
37591
+ const resolvedTools = resolveTools(toolItems, globalTools, "[script]");
37592
+ mcpClients = await this.connectMcpServers(config.mcp_servers);
37593
+ const toolContext = {
37594
+ pr: ctx.pr,
37595
+ files: ctx.files || prInfo.files,
37596
+ outputs: ctx.outputs,
37597
+ env: process.env
37598
+ };
37599
+ const parentCtx = _sessionInfo?._parentContext;
37600
+ const workflowContext = {
37601
+ prInfo,
37602
+ outputs: dependencyResults,
37603
+ executionContext: _sessionInfo,
37604
+ workspace: parentCtx?.workspace
37605
+ };
37606
+ const { globals: toolGlobals, asyncFunctionNames: toolAsyncNames } = buildToolGlobals({
37607
+ resolvedTools,
37608
+ mcpClients,
37609
+ toolContext,
37610
+ workflowContext
37611
+ });
37612
+ Object.assign(ctx, toolGlobals);
37613
+ for (const name of toolAsyncNames) asyncFunctionNames.add(name);
37614
+ }
37615
+ let loopIterations = 0;
37616
+ const maxLoopIterations = 1e4;
37617
+ ctx.__checkLoop = () => {
37618
+ loopIterations++;
37619
+ if (loopIterations > maxLoopIterations) {
37620
+ throw new Error(`Loop exceeded maximum of ${maxLoopIterations} iterations`);
37621
+ }
37622
+ };
37623
+ const knownGlobals = new Set(Object.keys(ctx));
37624
+ for (const name of asyncFunctionNames) knownGlobals.add(name);
37625
+ knownGlobals.add("__checkLoop");
37626
+ knownGlobals.add("log");
37627
+ const disabledBuiltins = /* @__PURE__ */ new Map();
37628
+ if (!config.enable_bash) {
37629
+ disabledBuiltins.set("bash", "Add 'enable_bash: true' to your check config to enable it.");
37630
+ }
37631
+ if (!config.enable_fetch) {
37632
+ disabledBuiltins.set(
37633
+ "fetch",
37634
+ "Add 'enable_fetch: true' to your check config to enable it."
37635
+ );
37636
+ }
37637
+ const transformed = transformScriptForAsync(script, asyncFunctionNames, {
37638
+ knownGlobals,
37639
+ disabledBuiltins
37640
+ });
37641
+ result = await compileAndRunAsync(
37658
37642
  sandbox,
37659
- script,
37643
+ transformed,
37660
37644
  { ...ctx },
37661
37645
  {
37662
37646
  injectLog: true,
37663
- wrapFunction: true,
37664
37647
  logPrefix: "[script]"
37665
37648
  }
37666
37649
  );
@@ -37680,6 +37663,8 @@ var init_script_check_provider = __esm({
37680
37663
  ],
37681
37664
  output: null
37682
37665
  };
37666
+ } finally {
37667
+ await this.disconnectMcpClients(mcpClients);
37683
37668
  }
37684
37669
  try {
37685
37670
  if (needsSave() && memoryStore.getConfig().storage === "file" && memoryStore.getConfig().auto_save) {
@@ -37705,17 +37690,167 @@ var init_script_check_provider = __esm({
37705
37690
  }
37706
37691
  return out;
37707
37692
  }
37693
+ /**
37694
+ * Resolve tool items from static config and optional JS expression.
37695
+ */
37696
+ resolveToolItems(config, prInfo, dependencyResults, ctx) {
37697
+ let items = [];
37698
+ const staticTools = config.tools;
37699
+ if (Array.isArray(staticTools)) {
37700
+ items = staticTools.filter(
37701
+ (item) => typeof item === "string" || isWorkflowToolReference(item)
37702
+ );
37703
+ }
37704
+ const toolsJsExpr = config.tools_js;
37705
+ if (toolsJsExpr && dependencyResults) {
37706
+ try {
37707
+ const jsSandbox = this.createSecureSandbox();
37708
+ const jsCtx = ctx || buildProviderTemplateContext(prInfo, dependencyResults);
37709
+ jsCtx.env = process.env;
37710
+ jsCtx.inputs = config.workflowInputs || {};
37711
+ const evalResult = compileAndRun(jsSandbox, toolsJsExpr, jsCtx, {
37712
+ injectLog: true,
37713
+ wrapFunction: true,
37714
+ logPrefix: "[tools_js]"
37715
+ });
37716
+ if (Array.isArray(evalResult)) {
37717
+ const dynamic = evalResult.filter(
37718
+ (item) => typeof item === "string" || isWorkflowToolReference(item)
37719
+ );
37720
+ const existingNames = new Set(items.map((i) => typeof i === "string" ? i : i.workflow));
37721
+ for (const tool of dynamic) {
37722
+ const name = typeof tool === "string" ? tool : tool.workflow;
37723
+ if (!existingNames.has(name)) {
37724
+ items.push(tool);
37725
+ }
37726
+ }
37727
+ }
37728
+ } catch (error) {
37729
+ logger.error(
37730
+ `[script] Failed to evaluate tools_js: ${error instanceof Error ? error.message : "Unknown error"}`
37731
+ );
37732
+ }
37733
+ }
37734
+ return items;
37735
+ }
37736
+ /**
37737
+ * Connect to MCP servers and discover their tools.
37738
+ */
37739
+ async connectMcpServers(mcpServersConfig) {
37740
+ if (!mcpServersConfig || Object.keys(mcpServersConfig).length === 0) {
37741
+ return [];
37742
+ }
37743
+ const entries = [];
37744
+ for (const [serverName, serverConfig] of Object.entries(mcpServersConfig)) {
37745
+ try {
37746
+ const { Client: Client2 } = await import("@modelcontextprotocol/sdk/client/index.js");
37747
+ const client = new Client2(
37748
+ { name: "visor-script-client", version: "1.0.0" },
37749
+ { capabilities: {} }
37750
+ );
37751
+ const env = {};
37752
+ for (const [key, value] of Object.entries(process.env)) {
37753
+ if (value !== void 0) env[key] = value;
37754
+ }
37755
+ if (serverConfig.env) {
37756
+ for (const [key, value] of Object.entries(serverConfig.env)) {
37757
+ env[key] = String(EnvironmentResolver.resolveValue(String(value)));
37758
+ }
37759
+ }
37760
+ const timeout = (serverConfig.timeout || 60) * 1e3;
37761
+ if (serverConfig.command) {
37762
+ const { StdioClientTransport: StdioClientTransport2 } = await import("@modelcontextprotocol/sdk/client/stdio.js");
37763
+ const transport = new StdioClientTransport2({
37764
+ command: serverConfig.command,
37765
+ args: serverConfig.args,
37766
+ env,
37767
+ stderr: "pipe"
37768
+ });
37769
+ await Promise.race([
37770
+ client.connect(transport),
37771
+ new Promise(
37772
+ (_, reject) => setTimeout(() => reject(new Error("MCP connection timeout")), timeout)
37773
+ )
37774
+ ]);
37775
+ } else if (serverConfig.url) {
37776
+ const transportType = serverConfig.transport || "sse";
37777
+ if (transportType === "sse") {
37778
+ const { SSEClientTransport: SSEClientTransport2 } = await import("@modelcontextprotocol/sdk/client/sse.js");
37779
+ await Promise.race([
37780
+ client.connect(new SSEClientTransport2(new URL(serverConfig.url))),
37781
+ new Promise(
37782
+ (_, reject) => setTimeout(() => reject(new Error("MCP connection timeout")), timeout)
37783
+ )
37784
+ ]);
37785
+ } else {
37786
+ const { StreamableHTTPClientTransport: StreamableHTTPClientTransport2 } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
37787
+ await Promise.race([
37788
+ client.connect(new StreamableHTTPClientTransport2(new URL(serverConfig.url))),
37789
+ new Promise(
37790
+ (_, reject) => setTimeout(() => reject(new Error("MCP connection timeout")), timeout)
37791
+ )
37792
+ ]);
37793
+ }
37794
+ } else {
37795
+ logger.warn(`[script] MCP server '${serverName}' has no command or url, skipping`);
37796
+ continue;
37797
+ }
37798
+ let tools = [];
37799
+ try {
37800
+ const listResult = await client.listTools();
37801
+ tools = (listResult?.tools || []).map((t) => ({
37802
+ name: t.name,
37803
+ description: t.description,
37804
+ inputSchema: t.inputSchema
37805
+ }));
37806
+ logger.debug(
37807
+ `[script] MCP '${serverName}': ${tools.length} tools [${tools.map((t) => t.name).join(", ")}]`
37808
+ );
37809
+ } catch (err) {
37810
+ logger.warn(
37811
+ `[script] Could not list tools from MCP '${serverName}': ${err instanceof Error ? err.message : String(err)}`
37812
+ );
37813
+ }
37814
+ entries.push({ client, serverName, tools });
37815
+ } catch (err) {
37816
+ logger.error(
37817
+ `[script] Failed to connect MCP '${serverName}': ${err instanceof Error ? err.message : String(err)}`
37818
+ );
37819
+ }
37820
+ }
37821
+ return entries;
37822
+ }
37823
+ /**
37824
+ * Disconnect all MCP clients.
37825
+ */
37826
+ async disconnectMcpClients(clients) {
37827
+ for (const entry of clients) {
37828
+ try {
37829
+ await entry.client.close();
37830
+ } catch (err) {
37831
+ logger.debug(
37832
+ `[script] Error closing MCP '${entry.serverName}': ${err instanceof Error ? err.message : String(err)}`
37833
+ );
37834
+ }
37835
+ }
37836
+ }
37708
37837
  getSupportedConfigKeys() {
37709
37838
  return [
37710
37839
  "type",
37711
37840
  "content",
37841
+ "tools",
37842
+ "tools_js",
37843
+ "mcp_servers",
37844
+ "enable_fetch",
37845
+ "enable_bash",
37712
37846
  "depends_on",
37713
37847
  "group",
37714
37848
  "on",
37715
37849
  "if",
37716
37850
  "fail_if",
37717
37851
  "on_fail",
37718
- "on_success"
37852
+ "on_success",
37853
+ "timeout"
37719
37854
  ];
37720
37855
  }
37721
37856
  async isAvailable() {
@@ -37724,15 +37859,14 @@ var init_script_check_provider = __esm({
37724
37859
  getRequirements() {
37725
37860
  return ["No external dependencies required"];
37726
37861
  }
37727
- // No local buildTemplateContext; uses shared builder above
37728
37862
  };
37729
37863
  }
37730
37864
  });
37731
37865
 
37732
37866
  // src/utils/worktree-manager.ts
37733
- import * as fs10 from "fs";
37867
+ import * as fs9 from "fs";
37734
37868
  import * as fsp2 from "fs/promises";
37735
- import * as path12 from "path";
37869
+ import * as path11 from "path";
37736
37870
  import * as crypto from "crypto";
37737
37871
  var WorktreeManager, worktreeManager;
37738
37872
  var init_worktree_manager = __esm({
@@ -37752,7 +37886,7 @@ var init_worktree_manager = __esm({
37752
37886
  } catch {
37753
37887
  cwd = "/tmp";
37754
37888
  }
37755
- const defaultBasePath = process.env.VISOR_WORKTREE_PATH || path12.join(cwd, ".visor", "worktrees");
37889
+ const defaultBasePath = process.env.VISOR_WORKTREE_PATH || path11.join(cwd, ".visor", "worktrees");
37756
37890
  this.config = {
37757
37891
  enabled: true,
37758
37892
  base_path: defaultBasePath,
@@ -37789,20 +37923,20 @@ var init_worktree_manager = __esm({
37789
37923
  }
37790
37924
  const reposDir = this.getReposDir();
37791
37925
  const worktreesDir = this.getWorktreesDir();
37792
- if (!fs10.existsSync(reposDir)) {
37793
- fs10.mkdirSync(reposDir, { recursive: true });
37926
+ if (!fs9.existsSync(reposDir)) {
37927
+ fs9.mkdirSync(reposDir, { recursive: true });
37794
37928
  logger.debug(`Created repos directory: ${reposDir}`);
37795
37929
  }
37796
- if (!fs10.existsSync(worktreesDir)) {
37797
- fs10.mkdirSync(worktreesDir, { recursive: true });
37930
+ if (!fs9.existsSync(worktreesDir)) {
37931
+ fs9.mkdirSync(worktreesDir, { recursive: true });
37798
37932
  logger.debug(`Created worktrees directory: ${worktreesDir}`);
37799
37933
  }
37800
37934
  }
37801
37935
  getReposDir() {
37802
- return path12.join(this.config.base_path, "repos");
37936
+ return path11.join(this.config.base_path, "repos");
37803
37937
  }
37804
37938
  getWorktreesDir() {
37805
- return path12.join(this.config.base_path, "worktrees");
37939
+ return path11.join(this.config.base_path, "worktrees");
37806
37940
  }
37807
37941
  /**
37808
37942
  * Generate a deterministic worktree ID based on repository and ref.
@@ -37820,8 +37954,8 @@ var init_worktree_manager = __esm({
37820
37954
  async getOrCreateBareRepo(repository, repoUrl, token, fetchDepth, cloneTimeoutMs) {
37821
37955
  const reposDir = this.getReposDir();
37822
37956
  const repoName = repository.replace(/\//g, "-");
37823
- const bareRepoPath = path12.join(reposDir, `${repoName}.git`);
37824
- if (fs10.existsSync(bareRepoPath)) {
37957
+ const bareRepoPath = path11.join(reposDir, `${repoName}.git`);
37958
+ if (fs9.existsSync(bareRepoPath)) {
37825
37959
  logger.debug(`Bare repository already exists: ${bareRepoPath}`);
37826
37960
  const verifyResult = await this.verifyBareRepoRemote(bareRepoPath, repoUrl);
37827
37961
  if (verifyResult === "timeout") {
@@ -37940,11 +38074,11 @@ var init_worktree_manager = __esm({
37940
38074
  options.cloneTimeoutMs
37941
38075
  );
37942
38076
  const worktreeId = this.generateWorktreeId(repository, ref);
37943
- let worktreePath = options.workingDirectory || path12.join(this.getWorktreesDir(), worktreeId);
38077
+ let worktreePath = options.workingDirectory || path11.join(this.getWorktreesDir(), worktreeId);
37944
38078
  if (options.workingDirectory) {
37945
38079
  worktreePath = this.validatePath(options.workingDirectory);
37946
38080
  }
37947
- if (fs10.existsSync(worktreePath)) {
38081
+ if (fs9.existsSync(worktreePath)) {
37948
38082
  logger.debug(`Worktree already exists: ${worktreePath}`);
37949
38083
  const metadata2 = await this.loadMetadata(worktreePath);
37950
38084
  if (metadata2) {
@@ -38185,9 +38319,9 @@ var init_worktree_manager = __esm({
38185
38319
  const result = await this.executeGitCommand(removeCmd, { timeout: 3e4 });
38186
38320
  if (result.exitCode !== 0) {
38187
38321
  logger.warn(`Failed to remove worktree via git: ${result.stderr}`);
38188
- if (fs10.existsSync(worktree_path)) {
38322
+ if (fs9.existsSync(worktree_path)) {
38189
38323
  logger.debug(`Manually removing worktree directory`);
38190
- fs10.rmSync(worktree_path, { recursive: true, force: true });
38324
+ fs9.rmSync(worktree_path, { recursive: true, force: true });
38191
38325
  }
38192
38326
  }
38193
38327
  this.activeWorktrees.delete(worktreeId);
@@ -38197,19 +38331,19 @@ var init_worktree_manager = __esm({
38197
38331
  * Save worktree metadata
38198
38332
  */
38199
38333
  async saveMetadata(worktreePath, metadata) {
38200
- const metadataPath = path12.join(worktreePath, ".visor-metadata.json");
38201
- fs10.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf8");
38334
+ const metadataPath = path11.join(worktreePath, ".visor-metadata.json");
38335
+ fs9.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf8");
38202
38336
  }
38203
38337
  /**
38204
38338
  * Load worktree metadata
38205
38339
  */
38206
38340
  async loadMetadata(worktreePath) {
38207
- const metadataPath = path12.join(worktreePath, ".visor-metadata.json");
38208
- if (!fs10.existsSync(metadataPath)) {
38341
+ const metadataPath = path11.join(worktreePath, ".visor-metadata.json");
38342
+ if (!fs9.existsSync(metadataPath)) {
38209
38343
  return null;
38210
38344
  }
38211
38345
  try {
38212
- const content = fs10.readFileSync(metadataPath, "utf8");
38346
+ const content = fs9.readFileSync(metadataPath, "utf8");
38213
38347
  return JSON.parse(content);
38214
38348
  } catch (error) {
38215
38349
  logger.warn(`Failed to load metadata: ${error}`);
@@ -38221,14 +38355,14 @@ var init_worktree_manager = __esm({
38221
38355
  */
38222
38356
  async listWorktrees() {
38223
38357
  const worktreesDir = this.getWorktreesDir();
38224
- if (!fs10.existsSync(worktreesDir)) {
38358
+ if (!fs9.existsSync(worktreesDir)) {
38225
38359
  return [];
38226
38360
  }
38227
- const entries = fs10.readdirSync(worktreesDir, { withFileTypes: true });
38361
+ const entries = fs9.readdirSync(worktreesDir, { withFileTypes: true });
38228
38362
  const worktrees = [];
38229
38363
  for (const entry of entries) {
38230
38364
  if (!entry.isDirectory()) continue;
38231
- const worktreePath = path12.join(worktreesDir, entry.name);
38365
+ const worktreePath = path11.join(worktreesDir, entry.name);
38232
38366
  const metadata = await this.loadMetadata(worktreePath);
38233
38367
  if (metadata) {
38234
38368
  worktrees.push({
@@ -38360,8 +38494,8 @@ var init_worktree_manager = __esm({
38360
38494
  * Validate path to prevent directory traversal
38361
38495
  */
38362
38496
  validatePath(userPath) {
38363
- const resolvedPath = path12.resolve(userPath);
38364
- if (!path12.isAbsolute(resolvedPath)) {
38497
+ const resolvedPath = path11.resolve(userPath);
38498
+ if (!path11.isAbsolute(resolvedPath)) {
38365
38499
  throw new Error("Path must be absolute");
38366
38500
  }
38367
38501
  const sensitivePatterns = [
@@ -39339,7 +39473,7 @@ var init_workflow_projection = __esm({
39339
39473
  });
39340
39474
 
39341
39475
  // src/providers/workflow-check-provider.ts
39342
- import * as path13 from "path";
39476
+ import * as path12 from "path";
39343
39477
  import * as yaml from "js-yaml";
39344
39478
  var WorkflowCheckProvider;
39345
39479
  var init_workflow_check_provider = __esm({
@@ -39558,13 +39692,13 @@ var init_workflow_check_provider = __esm({
39558
39692
  const loadConfigLiquid = createExtendedLiquid();
39559
39693
  const loadConfig = (filePath) => {
39560
39694
  try {
39561
- const normalizedBasePath = path13.normalize(basePath);
39562
- const resolvedPath = path13.isAbsolute(filePath) ? path13.normalize(filePath) : path13.normalize(path13.resolve(basePath, filePath));
39563
- const basePathWithSep = normalizedBasePath.endsWith(path13.sep) ? normalizedBasePath : normalizedBasePath + path13.sep;
39695
+ const normalizedBasePath = path12.normalize(basePath);
39696
+ const resolvedPath = path12.isAbsolute(filePath) ? path12.normalize(filePath) : path12.normalize(path12.resolve(basePath, filePath));
39697
+ const basePathWithSep = normalizedBasePath.endsWith(path12.sep) ? normalizedBasePath : normalizedBasePath + path12.sep;
39564
39698
  if (!resolvedPath.startsWith(basePathWithSep) && resolvedPath !== normalizedBasePath) {
39565
39699
  throw new Error(`Path '${filePath}' escapes base directory`);
39566
39700
  }
39567
- const configDir = path13.dirname(resolvedPath);
39701
+ const configDir = path12.dirname(resolvedPath);
39568
39702
  const rawContent = __require("fs").readFileSync(resolvedPath, "utf-8");
39569
39703
  const renderedContent = loadConfigLiquid.parseAndRenderSync(rawContent, {
39570
39704
  basePath: configDir
@@ -40015,17 +40149,17 @@ var init_workflow_check_provider = __esm({
40015
40149
  * so it can be executed by the state machine as a nested workflow.
40016
40150
  */
40017
40151
  async loadWorkflowFromConfigPath(sourcePath, baseDir) {
40018
- const path14 = __require("path");
40019
- const fs11 = __require("fs");
40152
+ const path13 = __require("path");
40153
+ const fs10 = __require("fs");
40020
40154
  const yaml2 = __require("js-yaml");
40021
- const resolved = path14.isAbsolute(sourcePath) ? sourcePath : path14.resolve(baseDir, sourcePath);
40022
- if (!fs11.existsSync(resolved)) {
40155
+ const resolved = path13.isAbsolute(sourcePath) ? sourcePath : path13.resolve(baseDir, sourcePath);
40156
+ if (!fs10.existsSync(resolved)) {
40023
40157
  throw new Error(`Workflow config not found at: ${resolved}`);
40024
40158
  }
40025
- const rawContent = fs11.readFileSync(resolved, "utf8");
40159
+ const rawContent = fs10.readFileSync(resolved, "utf8");
40026
40160
  const rawData = yaml2.load(rawContent);
40027
40161
  if (rawData.imports && Array.isArray(rawData.imports)) {
40028
- const configDir = path14.dirname(resolved);
40162
+ const configDir = path13.dirname(resolved);
40029
40163
  for (const source of rawData.imports) {
40030
40164
  const results = await this.registry.import(source, {
40031
40165
  basePath: configDir,
@@ -40055,8 +40189,8 @@ ${errors}`);
40055
40189
  if (!steps || Object.keys(steps).length === 0) {
40056
40190
  throw new Error(`Config '${resolved}' does not contain any steps to execute as a workflow`);
40057
40191
  }
40058
- const id = path14.basename(resolved).replace(/\.(ya?ml)$/i, "");
40059
- const name = loaded.name || `Workflow from ${path14.basename(resolved)}`;
40192
+ const id = path13.basename(resolved).replace(/\.(ya?ml)$/i, "");
40193
+ const name = loaded.name || `Workflow from ${path13.basename(resolved)}`;
40060
40194
  const workflowDef = {
40061
40195
  id,
40062
40196
  name,
@@ -40098,4 +40232,4 @@ undici/lib/fetch/body.js:
40098
40232
  undici/lib/websocket/frame.js:
40099
40233
  (*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> *)
40100
40234
  */
40101
- //# sourceMappingURL=chunk-BOGVSF57.mjs.map
40235
+ //# sourceMappingURL=chunk-J6F5K5EG.mjs.map