@kortyx/cli 0.11.4 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,15 +23,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
 
25
25
  // src/index.ts
26
- var import_promises4 = require("fs/promises");
26
+ var import_promises5 = require("fs/promises");
27
27
  var import_node_module = __toESM(require("module"));
28
28
  var import_node_os3 = require("os");
29
- var import_node_path4 = require("path");
30
- var import_agent = require("@kortyx/agent");
29
+ var import_node_path6 = require("path");
30
+ var import_agent2 = require("@kortyx/agent");
31
31
  var import_errors = require("@kortyx/core/errors");
32
32
  var import_telemetry_contracts6 = require("@kortyx/telemetry-contracts");
33
- var import_commander4 = require("commander");
33
+ var import_commander6 = require("commander");
34
34
  var import_api = require("tsx/cjs/api");
35
+ var import_api2 = require("tsx/esm/api");
36
+ var import_typescript2 = __toESM(require("typescript"));
35
37
 
36
38
  // src/connections-command.ts
37
39
  var import_telemetry_contracts2 = require("@kortyx/telemetry-contracts");
@@ -87,7 +89,7 @@ var normalizeConnectionUrl = (input) => {
87
89
  }
88
90
  return url.toString().replace(/\/+$/, "");
89
91
  };
90
- var StudioReadClient = class {
92
+ var StudioApiTransport = class {
91
93
  constructor(apiUrl, apiKey, request = fetch) {
92
94
  this.apiKey = apiKey;
93
95
  this.request = request;
@@ -106,27 +108,21 @@ var StudioReadClient = class {
106
108
  protocolVersion: null,
107
109
  release: null
108
110
  };
109
- async get(path, schema, query = {}) {
110
- if (!/^\/v1\/studio\/(?:context|catalogs|workflows|(?:runs|sessions|interrupts)(?:\/[^/?#]+)?)$/.test(
111
- path
112
- ) || /(?:^|\/)(?:\.|\.\.|%2e(?:%2e)?)(?:\/|$)/i.test(path)) {
113
- throw new StudioReadError(
114
- "invalid_endpoint",
115
- "Only supported Studio read endpoints are allowed."
116
- );
117
- }
111
+ async requestJson(method, path, schema, query = {}, body) {
118
112
  const url = new URL(`${this.apiUrl}${path}`);
119
113
  for (const [key, value2] of Object.entries(query))
120
114
  url.searchParams.set(key, value2);
121
115
  let response;
122
116
  try {
123
117
  response = await this.request(url, {
124
- method: "GET",
118
+ method,
125
119
  headers: {
126
120
  authorization: `Bearer ${this.apiKey}`,
127
121
  accept: "application/json",
128
- "x-kortyx-studio-api-version": import_telemetry_contracts.STUDIO_API_PROTOCOL_VERSION
122
+ "x-kortyx-studio-api-version": import_telemetry_contracts.STUDIO_API_PROTOCOL_VERSION,
123
+ ...body === void 0 ? {} : { "content-type": "application/json" }
129
124
  },
125
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
130
126
  redirect: "error",
131
127
  signal: AbortSignal.timeout(15e3)
132
128
  });
@@ -152,7 +148,7 @@ var StudioReadClient = class {
152
148
  }
153
149
  if (!response.ok) {
154
150
  await response.body?.cancel();
155
- const message = response.status === 401 ? "Invalid, expired, or revoked Studio read key." : response.status === 403 ? "The API key lacks studio:read permission." : response.status === 404 ? "Entity not found in this connection's project. Check the URL and selected connection." : `Studio API returned HTTP ${response.status}.`;
151
+ const message = response.status === 401 ? "Invalid, expired, or revoked Studio read key." : response.status === 403 ? "The API key lacks studio:read or the operation's required permission (eval:run for execution)." : response.status === 404 ? "Entity not found in this connection's project. Check the URL and selected connection." : `Studio API returned HTTP ${response.status}.`;
156
152
  throw new StudioReadError(
157
153
  "api_error",
158
154
  message,
@@ -169,7 +165,7 @@ var StudioReadClient = class {
169
165
  );
170
166
  try {
171
167
  const decoder = new TextDecoder();
172
- let body = "";
168
+ let body2 = "";
173
169
  let bytes = 0;
174
170
  for (; ; ) {
175
171
  const chunk = await reader.read();
@@ -182,10 +178,10 @@ var StudioReadClient = class {
182
178
  "Studio response exceeded the 20 MiB safety limit. Use a smaller query or inspect an individual run."
183
179
  );
184
180
  }
185
- body += decoder.decode(chunk.value, { stream: true });
181
+ body2 += decoder.decode(chunk.value, { stream: true });
186
182
  }
187
- body += decoder.decode();
188
- value = JSON.parse(body);
183
+ body2 += decoder.decode();
184
+ value = JSON.parse(body2);
189
185
  } catch (error) {
190
186
  if (error instanceof StudioReadError) throw error;
191
187
  throw new StudioReadError(
@@ -210,6 +206,19 @@ var StudioReadClient = class {
210
206
  return result.data;
211
207
  }
212
208
  };
209
+ var StudioReadClient = class extends StudioApiTransport {
210
+ async get(path, schema, query = {}) {
211
+ if (!/^\/v1\/studio\/(?:context|catalogs|workflows|(?:runs|sessions|interrupts)(?:\/[^/?#]+)?)$/.test(
212
+ path
213
+ ) || /(?:^|\/)(?:\.|\.\.|%2e(?:%2e)?)(?:\/|$)/i.test(path)) {
214
+ throw new StudioReadError(
215
+ "invalid_endpoint",
216
+ "Only supported Studio read endpoints are allowed."
217
+ );
218
+ }
219
+ return this.requestJson("GET", path, schema, query);
220
+ }
221
+ };
213
222
 
214
223
  // src/studio/state.ts
215
224
  var import_promises = require("fs/promises");
@@ -227,6 +236,7 @@ x-api-env: &api-env
227
236
  x-bootstrap-keys: &bootstrap-keys
228
237
  KORTYX_TELEMETRY_API_KEY: \${KORTYX_TELEMETRY_API_KEY}
229
238
  KORTYX_STUDIO_API_KEY: \${KORTYX_STUDIO_API_KEY}
239
+ KORTYX_STUDIO_ENABLE_EVALS: \${KORTYX_STUDIO_ENABLE_EVALS:-0}
230
240
  KORTYX_STUDIO_ENABLE_REVIEWS: \${KORTYX_STUDIO_ENABLE_REVIEWS:-0}
231
241
 
232
242
  services:
@@ -247,6 +257,7 @@ services:
247
257
  db-init:
248
258
  image: \${KORTYX_API_IMAGE_REF:-\${KORTYX_API_IMAGE:-ghcr.io/kortyx-io/kortyx-api}:\${KORTYX_STUDIO_IMAGE_TAG:-latest}}
249
259
  pull_policy: \${KORTYX_STUDIO_PULL_POLICY:-always}
260
+ user: "1000:1000"
250
261
  environment:
251
262
  <<: [*api-env, *bootstrap-keys]
252
263
  command: >
@@ -260,11 +271,18 @@ services:
260
271
  api:
261
272
  image: \${KORTYX_API_IMAGE_REF:-\${KORTYX_API_IMAGE:-ghcr.io/kortyx-io/kortyx-api}:\${KORTYX_STUDIO_IMAGE_TAG:-latest}}
262
273
  pull_policy: \${KORTYX_STUDIO_PULL_POLICY:-always}
274
+ user: "1000:1000"
263
275
  environment:
264
276
  <<: *api-env
265
277
  NODE_ENV: production
266
278
  API_HOST: 0.0.0.0
267
279
  API_PORT: 6400
280
+ KORTYX_EVAL_JUDGE_MODEL: \${KORTYX_EVAL_JUDGE_MODEL:-}
281
+ KORTYX_EVAL_JUDGE_API_KEY: \${KORTYX_EVAL_JUDGE_API_KEY:-}
282
+ KORTYX_EVAL_JUDGE_API: \${KORTYX_EVAL_JUDGE_API:-responses}
283
+ KORTYX_EVAL_JUDGE_BASE_URL: \${KORTYX_EVAL_JUDGE_BASE_URL:-}
284
+ KORTYX_EVAL_JUDGE_ID: \${KORTYX_EVAL_JUDGE_ID:-}
285
+ KORTYX_EVAL_JUDGE_VERSION: \${KORTYX_EVAL_JUDGE_VERSION:-}
268
286
  KORTYX_STUDIO_RELEASE: \${KORTYX_STUDIO_IMAGE_TAG:-latest}
269
287
  ports:
270
288
  - "127.0.0.1:\${API_PORT:-6400}:6400"
@@ -313,6 +331,9 @@ services:
313
331
  image: \${KORTYX_API_IMAGE_REF:-\${KORTYX_API_IMAGE:-ghcr.io/kortyx-io/kortyx-api}:\${KORTYX_STUDIO_IMAGE_TAG:-latest}}
314
332
  pull_policy: \${KORTYX_STUDIO_PULL_POLICY:-always}
315
333
  restart: unless-stopped
334
+ # The updater alone needs root for the host Docker socket and ownership-safe
335
+ # writes to the bind-mounted Studio state directory.
336
+ user: "0:0"
316
337
  command: ["node", "apps/api/dist/updater.js", "serve", "\${KORTYX_STUDIO_STATE_DIR}"]
317
338
  volumes:
318
339
  - type: bind
@@ -816,7 +837,7 @@ var createConnectionsCommand = (log = console.log, request = fetch) => {
816
837
  "/v1/studio/context",
817
838
  import_telemetry_contracts2.StudioContextResponseSchema
818
839
  );
819
- if (!context.apiKey.scopes.includes("studio:read"))
840
+ if (!context.apiKey?.scopes.includes("studio:read"))
820
841
  throw new StudioReadError(
821
842
  "missing_scope",
822
843
  "The API key lacks studio:read permission."
@@ -837,49 +858,1215 @@ var createConnectionsCommand = (log = console.log, request = fetch) => {
837
858
  await saveConnections(config, options.configHome);
838
859
  print({ connection: profile, verified: true }, options);
839
860
  }
840
- );
841
- common(
842
- command.command("use <name>").description(
843
- "Select the default connection; agents should prefer per-command --connection."
844
- )
845
- ).action(async (name, options) => {
846
- const config = await readConnections(options.configHome);
847
- if (name === "local" ? !await readStudioConfig(options.home) : !config.profiles.some((profile) => profile.name === name)) {
861
+ );
862
+ common(
863
+ command.command("use <name>").description(
864
+ "Select the default connection; agents should prefer per-command --connection."
865
+ )
866
+ ).action(async (name, options) => {
867
+ const config = await readConnections(options.configHome);
868
+ if (name === "local" ? !await readStudioConfig(options.home) : !config.profiles.some((profile) => profile.name === name)) {
869
+ throw new StudioReadError(
870
+ "unknown_connection",
871
+ "Connection not found. Run kortyx connections list."
872
+ );
873
+ }
874
+ config.current = name;
875
+ await saveConnections(config, options.configHome);
876
+ print({ current: name }, options);
877
+ });
878
+ common(
879
+ command.command("remove <name>").description(
880
+ "Remove a saved locator, not its credential or remote project."
881
+ )
882
+ ).action(async (name, options) => {
883
+ const config = await readConnections(options.configHome);
884
+ if (!config.profiles.some((profile) => profile.name === name))
885
+ throw new StudioReadError(
886
+ "unknown_connection",
887
+ "Saved connection not found; managed local cannot be removed."
888
+ );
889
+ config.profiles = config.profiles.filter(
890
+ (profile) => profile.name !== name
891
+ );
892
+ if (config.current === name) delete config.current;
893
+ await saveConnections(config, options.configHome);
894
+ print({ removed: name }, options);
895
+ });
896
+ return command;
897
+ };
898
+
899
+ // src/evals/command.ts
900
+ var import_node_path3 = require("path");
901
+ var import_agent = require("@kortyx/agent");
902
+ var import_commander2 = require("commander");
903
+
904
+ // src/evals/reporter.ts
905
+ var text = (value) => value.replace(/[\p{Cc}\p{Cf}]/gu, " ").trim();
906
+ var duration = (ms) => ms < 1e3 ? `${Math.round(ms)} ms` : `${(ms / 1e3).toFixed(2)} s`;
907
+ var wide = /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe6f\uff01-\uff60\uffe0-\uffe6]|\p{Extended_Pictographic}/u;
908
+ var fit = (value, columns) => {
909
+ const clean = value.replace(/[\p{Cc}\p{Cf}]/gu, " ").trimEnd();
910
+ let used = 0;
911
+ let result = "";
912
+ for (const char of clean) {
913
+ used += /\p{Mark}/u.test(char) ? 0 : wide.test(char) ? 2 : 1;
914
+ if (used > columns - 2) return `${result}\u2026`;
915
+ result += char;
916
+ }
917
+ return result;
918
+ };
919
+ var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
920
+ var phaseNames = {
921
+ params: "Preparing inputs",
922
+ setup: "Setting up test",
923
+ execute: "Running workflow",
924
+ responder: "Resolving human input",
925
+ reference: "Loading reference",
926
+ grading: "Checking expectations",
927
+ cleanup: "Cleaning up",
928
+ reporting: "Reporting results"
929
+ };
930
+ function createEvalTerminalReporter(write, color, options = {}) {
931
+ const now = options.now ?? Date.now;
932
+ const columns = () => Math.max(12, options.columns?.() ?? process.stdout.columns ?? 100);
933
+ const paint = (code, value) => color ? `\x1B[${code}m${value}\x1B[0m` : value;
934
+ const active = /* @__PURE__ */ new Map();
935
+ let names = /* @__PURE__ */ new Map();
936
+ let steps = /* @__PURE__ */ new Map();
937
+ let completed = 0;
938
+ let total = 0;
939
+ let started = 0;
940
+ let frame = 0;
941
+ let liveLines = 0;
942
+ let paused = false;
943
+ let running = false;
944
+ let timer;
945
+ const key = (id, repetition) => JSON.stringify([id, repetition]);
946
+ const clear = () => {
947
+ if (liveLines) write(`\x1B[${liveLines}A\r\x1B[0J`);
948
+ liveLines = 0;
949
+ };
950
+ const line = (value = "") => {
951
+ clear();
952
+ write(`${value}
953
+ `);
954
+ };
955
+ const progressBar = (done, count, width, pulse = false) => {
956
+ const filled = count ? Math.min(width, Math.floor(done / count * width)) : 0;
957
+ const remaining = width - filled;
958
+ const pending = pulse && remaining > 0 ? Array.from(
959
+ { length: remaining },
960
+ (_, index) => index === frame % remaining ? "\u2578" : "\u2591"
961
+ ).join("") : "\u2591".repeat(remaining);
962
+ return `${"\u2501".repeat(filled)}${pending}`;
963
+ };
964
+ const render = () => {
965
+ if (!color || !running || paused) return;
966
+ clear();
967
+ const width = columns();
968
+ const barWidth = Math.max(6, Math.min(22, width - 60));
969
+ const percent = total ? Math.floor(completed / total * 100) : 0;
970
+ const lines = [
971
+ paint(
972
+ 36,
973
+ fit(
974
+ ` RUN ${progressBar(completed, total, barWidth)} ${completed}/${total || "?"} \xB7 ${percent}%`,
975
+ width
976
+ )
977
+ ),
978
+ paint(
979
+ 90,
980
+ fit(
981
+ ` ${active.size} running \xB7 ${Math.max(0, total - completed - active.size)} queued \xB7 ${((now() - started) / 1e3).toFixed(1)}s elapsed`,
982
+ width
983
+ )
984
+ )
985
+ ];
986
+ for (const item of [...active.values()].slice(0, 6)) {
987
+ const name = names.get(item.caseId) ?? item.caseId;
988
+ lines.push(
989
+ paint(
990
+ 33,
991
+ fit(
992
+ ` ${frames[frame % frames.length]} ${name} #${item.repetition}`,
993
+ width
994
+ )
995
+ )
996
+ );
997
+ const stepCount = steps.get(item.caseId) ?? 1;
998
+ const step = item.stepIndex === void 0 ? "" : `Step ${item.stepIndex + 1}/${stepCount} \xB7 `;
999
+ lines.push(
1000
+ paint(
1001
+ item.phase === "grading" ? 35 : 90,
1002
+ fit(
1003
+ ` ${progressBar(item.completedSteps, stepCount, 6, true)} ${step}${phaseNames[item.phase]} \xB7 ${((now() - item.started) / 1e3).toFixed(1)}s`,
1004
+ width
1005
+ )
1006
+ )
1007
+ );
1008
+ }
1009
+ if (active.size > 6)
1010
+ lines.push(paint(90, ` +${active.size - 6} more running`));
1011
+ write(`${lines.join("\n")}
1012
+ `);
1013
+ liveLines = lines.length;
1014
+ frame++;
1015
+ };
1016
+ const stop = () => {
1017
+ if (timer) clearInterval(timer);
1018
+ timer = void 0;
1019
+ running = false;
1020
+ clear();
1021
+ };
1022
+ return {
1023
+ start(suite) {
1024
+ stop();
1025
+ names = new Map(
1026
+ suite.cases.map((item) => [item.id, item.name ?? item.id])
1027
+ );
1028
+ steps = new Map(suite.cases.map((item) => [item.id, item.steps.length]));
1029
+ active.clear();
1030
+ completed = total = frame = 0;
1031
+ paused = false;
1032
+ started = now();
1033
+ line();
1034
+ if (color)
1035
+ line(paint(35, " \u2501".padEnd(Math.min(columns() - 1, 64), "\u2501")));
1036
+ line(paint("1;35", `\u25C6 Kortyx Evals \xB7 ${text(suite.name ?? suite.id)}`));
1037
+ line(paint(90, ` ${text(suite.id)} \xB7 local execution`));
1038
+ line();
1039
+ running = true;
1040
+ if (color) {
1041
+ timer = setInterval(render, 100);
1042
+ timer.unref();
1043
+ }
1044
+ },
1045
+ progress(event) {
1046
+ if (event.type === "run-started") {
1047
+ total = event.caseIds.length * event.repetitions;
1048
+ line(
1049
+ paint(
1050
+ 90,
1051
+ ` ${event.caseIds.length} ${event.caseIds.length === 1 ? "test" : "tests"} \xB7 ${total} ${total === 1 ? "attempt" : "attempts"} \xB7 concurrency ${event.concurrency}`
1052
+ )
1053
+ );
1054
+ line();
1055
+ } else if (event.type === "case-started") {
1056
+ active.set(key(event.caseId, event.repetition), {
1057
+ caseId: event.caseId,
1058
+ repetition: event.repetition,
1059
+ started: now(),
1060
+ phase: "params",
1061
+ completedSteps: 0
1062
+ });
1063
+ if (!color)
1064
+ line(
1065
+ ` \u2192 ${text(names.get(event.caseId) ?? event.caseId)} [${text(event.caseId)}] #${event.repetition} RUNNING`
1066
+ );
1067
+ } else if (event.type === "case-progress") {
1068
+ const item = active.get(key(event.caseId, event.repetition));
1069
+ if (item) {
1070
+ item.phase = event.phase;
1071
+ item.stepIndex = event.stepIndex;
1072
+ }
1073
+ } else if (event.type === "step-completed") {
1074
+ const item = active.get(key(event.caseId, event.repetition));
1075
+ if (item) item.completedSteps = event.step.index + 1;
1076
+ const passed = event.step.status === "passed";
1077
+ const criterionCount = event.step.criteria.length;
1078
+ const detail = criterionCount ? ` \xB7 ${event.step.criteria.filter((criterion) => criterion.passed).length}/${criterionCount} criteria passed` : "";
1079
+ line(
1080
+ ` ${paint(passed ? 32 : 33, passed ? "\u2713" : "\u25C7")} ${text(event.caseId)} #${event.repetition} \xB7 Step ${event.step.index + 1}/${steps.get(event.caseId) ?? "?"} \xB7 ${event.step.observation.type === "interrupt" ? "Human input requested" : text(event.step.observation.type)}${detail}`
1081
+ );
1082
+ } else {
1083
+ const item = event.result;
1084
+ active.delete(key(item.caseId, item.repetition));
1085
+ completed++;
1086
+ const passed = item.status === "passed";
1087
+ const cancelled = item.status === "cancelled";
1088
+ const code = passed ? 32 : cancelled || item.status === "ungraded" ? 33 : 31;
1089
+ const symbol = passed ? "\u2713" : cancelled ? "\u2212" : item.status === "ungraded" ? "\u25C7" : "\u2717";
1090
+ line(
1091
+ ` ${paint(code, symbol)} ${text(names.get(item.caseId) ?? item.caseId)} ${paint(90, `[${text(item.caseId)}] #${item.repetition} \xB7 ${duration(item.durationMs)}`)} ${paint(`${code};1`, item.status.toUpperCase())}`
1092
+ );
1093
+ line();
1094
+ }
1095
+ render();
1096
+ },
1097
+ finish(result) {
1098
+ stop();
1099
+ for (const item of result.cases) {
1100
+ for (const step of item.steps) {
1101
+ if (step.status === "passed") continue;
1102
+ line();
1103
+ line(
1104
+ paint(
1105
+ 31,
1106
+ ` ${text(names.get(item.caseId) ?? item.caseId)} \u203A attempt ${item.repetition} \u203A step ${step.index + 1}`
1107
+ )
1108
+ );
1109
+ line(
1110
+ ` Expected ${step.expectation.type} \xB7 observed ${step.observation.type}`
1111
+ );
1112
+ if (step.reason) line(` ${text(step.reason)}`);
1113
+ for (const criterion of step.criteria.filter(
1114
+ (value) => !value.passed
1115
+ )) {
1116
+ line(` \u2717 ${text(criterion.text)}`);
1117
+ line(` ${text(criterion.reason)}`);
1118
+ for (const evidence of criterion.evidence)
1119
+ line(paint(90, ` Evidence: ${text(evidence)}`));
1120
+ }
1121
+ }
1122
+ for (const issue of item.errors)
1123
+ line(
1124
+ ` ${text(item.caseId)} \xB7 ${issue.phase} [${issue.code}] ${text(issue.message)}`
1125
+ );
1126
+ }
1127
+ for (const issue of result.errors)
1128
+ line(` ${issue.phase} [${issue.code}] ${text(issue.message)}`);
1129
+ const successful = result.status === "passed";
1130
+ const code = successful ? 32 : result.status === "cancelled" || result.status === "ungraded" ? 33 : 31;
1131
+ line();
1132
+ if (color) {
1133
+ line(
1134
+ paint(
1135
+ code,
1136
+ ` ${progressBar(result.cases.length, result.cases.length, Math.min(22, columns() - 3))} ${result.cases.length}/${result.cases.length}`
1137
+ )
1138
+ );
1139
+ line(
1140
+ paint(
1141
+ `${code};1`,
1142
+ ` ${successful ? "\u2713 SUITE PASSED" : `\u25C7 SUITE ${result.status.toUpperCase()}`}`
1143
+ )
1144
+ );
1145
+ }
1146
+ line(
1147
+ ` ${paint(result.counts.passed ? 32 : 90, `${result.counts.passed} passed`)} \xB7 ${paint(result.counts.failed ? 31 : 90, `${result.counts.failed} failed`)} \xB7 ${paint(result.counts.error ? 31 : 90, `${result.counts.error} errors`)} \xB7 ${paint(result.counts.cancelled ? 33 : 90, `${result.counts.cancelled} cancelled`)}${result.counts.ungraded ? ` \xB7 ${paint(33, `${result.counts.ungraded} ungraded`)}` : ""}`
1148
+ );
1149
+ line(
1150
+ paint(
1151
+ 90,
1152
+ ` Duration ${duration(result.durationMs)} \xB7 ${result.cases.length} ${result.cases.length === 1 ? "attempt" : "attempts"}`
1153
+ )
1154
+ );
1155
+ line();
1156
+ },
1157
+ pause() {
1158
+ paused = true;
1159
+ clear();
1160
+ },
1161
+ resume() {
1162
+ paused = false;
1163
+ render();
1164
+ },
1165
+ close: stop
1166
+ };
1167
+ }
1168
+
1169
+ // src/evals/command.ts
1170
+ var integer = (value) => {
1171
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1)
1172
+ throw new import_commander2.InvalidArgumentError("Use a positive integer.");
1173
+ return Number(value);
1174
+ };
1175
+ var isRunner = (value) => value !== null && typeof value === "object" && "run" in value && typeof value.run === "function" && "describe" in value && typeof value.describe === "function";
1176
+ function createLocalEvalsCommand(runtime) {
1177
+ const write = runtime.write ?? process.stdout.write.bind(process.stdout);
1178
+ const exitCode = runtime.setExitCode ?? ((value) => {
1179
+ process.exitCode = value;
1180
+ });
1181
+ const color = runtime.color ?? Boolean(
1182
+ process.stdout.isTTY && process.env.NO_COLOR === void 0 && process.env.FORCE_COLOR !== "0"
1183
+ );
1184
+ const load = async (options) => {
1185
+ const module2 = await runtime.load(
1186
+ (0, import_node_path3.resolve)(runtime.cwd ?? process.cwd(), options.entry),
1187
+ options.env
1188
+ );
1189
+ const selected = options.export ? module2[options.export] : module2.evals ?? module2.default;
1190
+ if (!isRunner(selected))
1191
+ throw new Error(
1192
+ `Export ${options.export ?? '"evals" or default'} must be a createEvals instance.`
1193
+ );
1194
+ const suites = selected.describe().suites.map((suite) => (0, import_agent.parseEvalSuite)(suite));
1195
+ if (!suites.length) throw new Error("No eval suites are configured.");
1196
+ return { runner: selected, suites };
1197
+ };
1198
+ const withRunner = async (options, action) => {
1199
+ try {
1200
+ await action(await load(options));
1201
+ } finally {
1202
+ await runtime.release?.();
1203
+ }
1204
+ };
1205
+ const entryOptions = (command) => command.requiredOption(
1206
+ "--entry <path>",
1207
+ "Module exporting your createEvals instance (TypeScript or JavaScript)."
1208
+ ).option("--export <name>", "Named export; defaults to evals or default.").option(
1209
+ "--env <path>",
1210
+ "Load an env file before the entry; repeat for overlays. Replaces automatic env loading.",
1211
+ (value, previous) => [...previous, value],
1212
+ []
1213
+ ).option(
1214
+ "--json",
1215
+ "Print machine-readable JSON instead of the terminal report."
1216
+ );
1217
+ const root = new import_commander2.Command("evals").description(
1218
+ "Run application evals locally, without Studio."
1219
+ );
1220
+ entryOptions(
1221
+ root.command("list").description("List suites exported by an application module.")
1222
+ ).action(
1223
+ (options) => withRunner(options, async ({ suites }) => {
1224
+ write(
1225
+ options.json ? `${JSON.stringify(suites)}
1226
+ ` : suites.map(
1227
+ (suite) => `${suite.id} ${suite.name ?? suite.id} ${suite.cases.length} cases
1228
+ `
1229
+ ).join("")
1230
+ );
1231
+ })
1232
+ );
1233
+ entryOptions(
1234
+ root.command("run").description(
1235
+ "Execute suites and report progress, failures and results locally."
1236
+ )
1237
+ ).option(
1238
+ "--suite <id>",
1239
+ "Run one suite; omitted runs all suites in definition order."
1240
+ ).option(
1241
+ "--case <id...>",
1242
+ "Select cases within --suite (space-separated IDs)."
1243
+ ).option("--repetitions <count>", "Override attempts per case.", integer).option(
1244
+ "--concurrency <count>",
1245
+ "Override concurrent cases within each suite.",
1246
+ integer
1247
+ ).option("--no-color", "Disable terminal colors and live progress.").option("--color", "Force terminal colors and live progress.").action(
1248
+ (options, command) => withRunner(options, async ({ runner, suites }) => {
1249
+ const selected = options.suite ? suites.filter((suite) => suite.id === options.suite) : suites;
1250
+ if (!selected.length)
1251
+ throw new Error(`Unknown eval suite: ${options.suite}`);
1252
+ if (options.case && (!options.suite || new Set(options.case).size !== options.case.length || options.case.some(
1253
+ (id) => !selected[0]?.cases.some((item) => item.id === id)
1254
+ )))
1255
+ throw new Error(
1256
+ "--case requires --suite and unique existing case IDs."
1257
+ );
1258
+ const controller = new AbortController();
1259
+ const removeInterrupt = (runtime.onInterrupt ?? ((abort) => {
1260
+ process.once("SIGINT", abort);
1261
+ return () => {
1262
+ process.removeListener("SIGINT", abort);
1263
+ };
1264
+ }))(() => controller.abort());
1265
+ const interactive = options.color && !options.json && (color || command.getOptionValueSource("color") === "cli");
1266
+ const reporter = createEvalTerminalReporter(write, interactive);
1267
+ const restoreOutput = [];
1268
+ if (!runtime.write && interactive) {
1269
+ for (const stream of [process.stdout, process.stderr]) {
1270
+ const original = stream.write;
1271
+ const wrapped = new Proxy(original, {
1272
+ apply(target, receiver, args) {
1273
+ reporter.pause();
1274
+ const written = Reflect.apply(target, receiver, args);
1275
+ if (String(args[0]).endsWith("\n")) reporter.resume();
1276
+ return written;
1277
+ }
1278
+ });
1279
+ stream.write = wrapped;
1280
+ restoreOutput.push(() => {
1281
+ if (stream.write === wrapped) stream.write = original;
1282
+ });
1283
+ }
1284
+ }
1285
+ const runs = [];
1286
+ try {
1287
+ for (const suite of selected) {
1288
+ if (controller.signal.aborted) break;
1289
+ if (!options.json) reporter.start(suite);
1290
+ const result = await runner.run({
1291
+ suiteId: suite.id,
1292
+ signal: controller.signal,
1293
+ includeActivity: !options.json,
1294
+ ...options.case ? { caseIds: options.case } : {},
1295
+ ...options.repetitions !== void 0 ? { repetitions: options.repetitions } : {},
1296
+ ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {},
1297
+ ...!options.json ? { onProgress: reporter.progress } : {}
1298
+ });
1299
+ runs.push(result);
1300
+ if (!options.json) reporter.finish(result);
1301
+ }
1302
+ const counts = { passed: 0, failed: 0, error: 0, cancelled: 0 };
1303
+ for (const run2 of runs)
1304
+ for (const key of Object.keys(counts))
1305
+ counts[key] += run2.counts[key];
1306
+ const cancelled = controller.signal.aborted || counts.cancelled > 0;
1307
+ const passed = runs.length === selected.length && runs.every((run2) => run2.status === "passed");
1308
+ const status = cancelled ? "cancelled" : counts.error || runs.some((run2) => run2.status === "error") ? "error" : passed ? "passed" : "failed";
1309
+ if (options.json)
1310
+ write(
1311
+ `${JSON.stringify({ schemaVersion: 1, status, counts, runs })}
1312
+ `
1313
+ );
1314
+ else if (selected.length > 1)
1315
+ write(
1316
+ `Overall ${status.toUpperCase()} \xB7 ${runs.length}/${selected.length} suites \xB7 ${counts.passed} passed \xB7 ${counts.failed} failed \xB7 ${counts.error} errors
1317
+ `
1318
+ );
1319
+ exitCode(cancelled ? 130 : passed ? 0 : 1);
1320
+ } finally {
1321
+ reporter.close();
1322
+ for (const restore of restoreOutput) restore();
1323
+ removeInterrupt();
1324
+ }
1325
+ })
1326
+ );
1327
+ return root;
1328
+ }
1329
+
1330
+ // src/evals/environment.ts
1331
+ var import_promises3 = require("fs/promises");
1332
+ var import_node_path4 = require("path");
1333
+ var import_node_util = require("util");
1334
+ var exists = async (file) => {
1335
+ try {
1336
+ await (0, import_promises3.lstat)(file);
1337
+ return true;
1338
+ } catch (error) {
1339
+ if (error.code === "ENOENT") return false;
1340
+ throw new Error(`Cannot read eval configuration: ${file}`);
1341
+ }
1342
+ };
1343
+ var findConfiguration = async (cwd) => {
1344
+ let directory = (0, import_node_path4.resolve)(cwd);
1345
+ const directories = [];
1346
+ for (; ; ) {
1347
+ directories.unshift(directory);
1348
+ const profile = (0, import_node_path4.join)(directory, ".env.evals.json");
1349
+ if (await exists(profile)) return { profile, directories };
1350
+ if (await exists((0, import_node_path4.join)(directory, ".git")) || await exists((0, import_node_path4.join)(directory, "pnpm-workspace.yaml")))
1351
+ return { directories };
1352
+ if ((0, import_node_path4.dirname)(directory) === directory)
1353
+ return { directories: [(0, import_node_path4.resolve)(cwd)] };
1354
+ directory = (0, import_node_path4.dirname)(directory);
1355
+ }
1356
+ };
1357
+ var read = async (file, privateFile) => {
1358
+ let stat;
1359
+ try {
1360
+ stat = await (0, import_promises3.lstat)(file);
1361
+ } catch {
1362
+ throw new Error(`Configured eval environment file is unavailable: ${file}`);
1363
+ }
1364
+ if (!stat.isFile() || privateFile && process.platform !== "win32" && ((stat.mode & 63) !== 0 || typeof process.getuid === "function" && stat.uid !== process.getuid()))
1365
+ throw new Error(
1366
+ `Eval configuration must be ${privateFile ? "an owner-only" : "a"} regular file: ${file}`
1367
+ );
1368
+ try {
1369
+ return await (0, import_promises3.readFile)(file, "utf8");
1370
+ } catch {
1371
+ throw new Error(`Cannot read eval configuration: ${file}`);
1372
+ }
1373
+ };
1374
+ async function loadEvalEnvironment(cwd, envFiles, shell = process.env) {
1375
+ let files;
1376
+ let privateFiles = false;
1377
+ if (envFiles?.length) {
1378
+ files = envFiles.map((file) => (0, import_node_path4.resolve)(cwd, file));
1379
+ } else {
1380
+ const { profile, directories } = await findConfiguration(cwd);
1381
+ if (profile) {
1382
+ let value;
1383
+ try {
1384
+ value = JSON.parse(await read(profile, true));
1385
+ } catch (error) {
1386
+ if (error instanceof SyntaxError)
1387
+ throw new Error(`Invalid eval environment profile: ${profile}`);
1388
+ throw error;
1389
+ }
1390
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => key !== "envFiles") || !("envFiles" in value) || !Array.isArray(value.envFiles) || !value.envFiles.length || value.envFiles.some(
1391
+ (file) => typeof file !== "string" || !file.trim()
1392
+ ))
1393
+ throw new Error(`Invalid eval environment profile: ${profile}`);
1394
+ files = value.envFiles.map(
1395
+ (file) => (0, import_node_path4.resolve)((0, import_node_path4.dirname)(profile), file)
1396
+ );
1397
+ privateFiles = true;
1398
+ } else {
1399
+ files = [];
1400
+ for (const directory of directories)
1401
+ for (const name of [".env", ".env.local"]) {
1402
+ const file = (0, import_node_path4.resolve)(directory, name);
1403
+ if (await exists(file)) files.push(file);
1404
+ }
1405
+ }
1406
+ }
1407
+ const environment = {};
1408
+ for (const file of files)
1409
+ Object.assign(environment, (0, import_node_util.parseEnv)(await read(file, privateFiles)));
1410
+ return { ...environment, ...shell };
1411
+ }
1412
+
1413
+ // src/studio/command.ts
1414
+ var import_commander5 = require("commander");
1415
+
1416
+ // src/studio/eval-command.ts
1417
+ var import_commander3 = require("commander");
1418
+
1419
+ // src/studio/eval-client.ts
1420
+ var import_evals = require("@kortyx/agent/evals");
1421
+ var import_zod3 = require("zod");
1422
+ var StartRequestSchema = import_evals.StudioEvalStartRequestSchema;
1423
+ var parseEvalRunTarget = (input) => {
1424
+ let id = input;
1425
+ let url;
1426
+ if (/^[a-z][a-z0-9+.-]*:/i.test(input)) {
1427
+ let locator;
1428
+ try {
1429
+ locator = new URL(input);
1430
+ } catch {
1431
+ throw new StudioReadError("invalid_target", "Invalid eval run URL.");
1432
+ }
1433
+ const match = locator.pathname.match(/\/evals\/runs\/([^/]+)\/?$/);
1434
+ if (!match || !["http:", "https:"].includes(locator.protocol) || locator.username || locator.password)
1435
+ throw new StudioReadError(
1436
+ "invalid_target",
1437
+ "Expected an HTTP(S) Studio /evals/runs/<uuid> URL without credentials."
1438
+ );
1439
+ id = match[1] ?? "";
1440
+ locator.search = "";
1441
+ locator.hash = "";
1442
+ url = locator.toString();
1443
+ }
1444
+ if (!import_zod3.z.uuid().safeParse(id).success)
1445
+ throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
1446
+ return { id, url };
1447
+ };
1448
+ var StudioEvalClient = class extends StudioApiTransport {
1449
+ targets() {
1450
+ return this.requestJson(
1451
+ "GET",
1452
+ "/v1/studio/evals/targets",
1453
+ import_evals.StudioEvalTargetsResponseSchema
1454
+ );
1455
+ }
1456
+ runs() {
1457
+ return this.requestJson(
1458
+ "GET",
1459
+ "/v1/studio/evals/runs",
1460
+ import_evals.StudioEvalHistorySchema
1461
+ );
1462
+ }
1463
+ run(id) {
1464
+ if (!import_zod3.z.uuid().safeParse(id).success)
1465
+ throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
1466
+ return this.requestJson(
1467
+ "GET",
1468
+ `/v1/studio/evals/runs/${id}`,
1469
+ import_evals.StudioEvalDetailSchema
1470
+ );
1471
+ }
1472
+ start(input) {
1473
+ const parsed = StartRequestSchema.safeParse(input);
1474
+ if (!parsed.success)
1475
+ throw new StudioReadError(
1476
+ "invalid_eval_request",
1477
+ "Invalid suite revision, case selection, or execution limits."
1478
+ );
1479
+ return this.requestJson(
1480
+ "POST",
1481
+ "/v1/studio/evals/runs",
1482
+ import_zod3.z.object({ id: import_zod3.z.uuid() }),
1483
+ {},
1484
+ parsed.data
1485
+ );
1486
+ }
1487
+ cancel(id) {
1488
+ if (!import_zod3.z.uuid().safeParse(id).success)
1489
+ throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
1490
+ return this.requestJson(
1491
+ "POST",
1492
+ `/v1/studio/evals/runs/${id}/cancel`,
1493
+ import_zod3.z.object({ ok: import_zod3.z.literal(true) })
1494
+ );
1495
+ }
1496
+ };
1497
+
1498
+ // src/studio/eval-doctor.ts
1499
+ var advice = {
1500
+ environment_forbidden: {
1501
+ message: "Target environment is not allowed in this Studio project.",
1502
+ remedy: "Allow the target environment in the project, or correct the target's environment label."
1503
+ },
1504
+ environment_unavailable: {
1505
+ message: "Studio could not check the target environment.",
1506
+ remedy: "Inspect Studio API database connectivity and private server logs."
1507
+ },
1508
+ endpoint_not_found: {
1509
+ message: "Application endpoint returned HTTP 404.",
1510
+ remedy: "Ensure the eval route is mounted and enabled in the deployed application; check the target URL and reverse proxy route."
1511
+ },
1512
+ endpoint_unauthorized: {
1513
+ message: "Application endpoint rejected discovery credentials.",
1514
+ remedy: "Match the consumer handler service key to the Studio target. If the app authenticates a test actor during discovery, also check that actor's credentials and permissions."
1515
+ },
1516
+ endpoint_http_error: {
1517
+ message: "Application endpoint returned an unsuccessful HTTP status.",
1518
+ remedy: "Inspect the consumer and proxy logs. Check deployment configuration and any app-owned test-identity initialization."
1519
+ },
1520
+ endpoint_unreachable: {
1521
+ message: "Studio API could not fetch the application manifest.",
1522
+ remedy: "Check API-to-consumer networking, DNS, TLS and timeouts. From Docker Desktop use host.docker.internal for a host app; redirects are not followed."
1523
+ },
1524
+ manifest_invalid: {
1525
+ message: "Application returned an empty, oversized or incompatible manifest.",
1526
+ remedy: "Mount createEvalRouteHandler on the exact target URL and use compatible SDK/Studio releases. Check whether a proxy returned HTML instead of JSON."
1527
+ }
1528
+ };
1529
+ function buildEvalDoctorReport(data, options) {
1530
+ const checks = [
1531
+ {
1532
+ id: "studio_access",
1533
+ status: "passed",
1534
+ message: "Authenticated Studio discovery (studio:read)."
1535
+ },
1536
+ {
1537
+ id: "execution_permission",
1538
+ status: data.canRun ? "passed" : "failed",
1539
+ message: data.canRun ? "Studio key has eval:run." : "Studio key lacks eval:run.",
1540
+ ...!data.canRun ? {
1541
+ remedy: "Grant eval:run to this project key. For local bootstrap rerun with KORTYX_STUDIO_ENABLE_EVALS=1 and the existing stored key."
1542
+ } : {}
1543
+ }
1544
+ ];
1545
+ const targets = data.targets.filter(
1546
+ (target) => (!options.target || target.id === options.target) && (!options.environment || target.environment === options.environment)
1547
+ );
1548
+ checks.push({
1549
+ id: "target_selection",
1550
+ status: targets.length ? "passed" : "failed",
1551
+ message: targets.length ? `${targets.length} matching application target(s).` : "No application target matches this connection and selection.",
1552
+ ...!targets.length ? {
1553
+ remedy: "Register the target with this key's organization/project and environment. Mount KORTYX_EVAL_TARGETS_FILE on the Studio API, restart it, and check --target/--environment and the connection's default environment."
1554
+ } : {}
1555
+ });
1556
+ for (const target of targets) {
1557
+ const prefix = `${target.id} (${target.environment})`;
1558
+ const diagnostic = target.diagnostic;
1559
+ const environmentFailed = diagnostic?.code === "environment_forbidden" || diagnostic?.code === "environment_unavailable";
1560
+ checks.push({
1561
+ id: `${target.id}:environment`,
1562
+ status: environmentFailed ? "failed" : target.manifest || diagnostic ? "passed" : "skipped",
1563
+ message: `${prefix}: ${environmentFailed ? advice[diagnostic.code].message : target.manifest || diagnostic ? "target environment allowed." : "environment check unavailable on this API."}`,
1564
+ ...environmentFailed ? { remedy: advice[diagnostic.code].remedy } : {}
1565
+ });
1566
+ if (!target.manifest) {
1567
+ const guidance = diagnostic && !environmentFailed ? advice[diagnostic.code] : void 0;
1568
+ checks.push({
1569
+ id: `${target.id}:manifest`,
1570
+ status: environmentFailed ? "skipped" : "failed",
1571
+ ...diagnostic ? { code: diagnostic.code } : {},
1572
+ message: `${prefix}: ${environmentFailed ? "discovery skipped because the environment check failed." : guidance?.message ?? "consumer unavailable; this API did not supply a failure category."}${diagnostic?.httpStatus ? ` (HTTP ${diagnostic.httpStatus})` : ""}`,
1573
+ ...!environmentFailed ? {
1574
+ remedy: guidance?.remedy ?? "Check the deployed consumer route, matching service key and API-to-consumer reachability. Upgrade the Studio API for detailed discovery diagnostics."
1575
+ } : {}
1576
+ });
1577
+ checks.push({
1578
+ id: `${target.id}:judge`,
1579
+ status: "skipped",
1580
+ message: `${prefix}: judge compatibility cannot be checked without a manifest.`
1581
+ });
1582
+ continue;
1583
+ }
1584
+ const suites = target.manifest.suites;
1585
+ const selectedSuite = options.suite ? suites.find((suite) => suite.id === options.suite) : void 0;
1586
+ const suitesReady = options.suite ? Boolean(selectedSuite) : suites.length > 0;
1587
+ checks.push({
1588
+ id: `${target.id}:manifest`,
1589
+ status: suitesReady ? "passed" : "failed",
1590
+ message: `${prefix}: authenticated manifest valid; ${suites.length} registered suite(s).${options.suite ? ` Requested suite ${options.suite} ${selectedSuite ? "found" : "missing"}.` : ""}`,
1591
+ ...!suitesReady ? {
1592
+ remedy: "Pass the intended suite to createEvals({ suites: [...] }), deploy the application and refresh discovery. Suites are fetched from the consumer; topology publication does not register them."
1593
+ } : {}
1594
+ });
1595
+ const judgeReady = options.judge === "studio" ? Boolean(data.studioJudge && target.manifest.studioJudging) : Boolean(target.manifest.judge);
1596
+ checks.push({
1597
+ id: `${target.id}:judge`,
1598
+ status: judgeReady ? "passed" : "failed",
1599
+ message: `${prefix}: ${options.judge} judge ${judgeReady ? "configured and advertised" : "unavailable"}.`,
1600
+ ...!judgeReady ? {
1601
+ remedy: options.judge === "app" ? "Provide a code judge to createEvals in the consumer, or explicitly select --judge studio." : !data.studioJudge ? "Configure KORTYX_EVAL_JUDGE_MODEL and KORTYX_EVAL_JUDGE_API_KEY on the Studio API and restart it, or explicitly select an available --judge app." : "Update the consumer SDK to advertise Studio judging support, or select an available --judge app."
1602
+ } : {}
1603
+ });
1604
+ }
1605
+ return {
1606
+ schemaVersion: 1,
1607
+ status: checks.some((check) => check.status === "failed") ? "failed" : "passed",
1608
+ checks
1609
+ };
1610
+ }
1611
+ function evalDoctorFailure(error) {
1612
+ const safe = error instanceof StudioReadError ? error : null;
1613
+ const connectionError = safe && [
1614
+ "invalid_config",
1615
+ "invalid_connection",
1616
+ "not_configured",
1617
+ "unknown_connection",
1618
+ "invalid_key_env",
1619
+ "missing_key",
1620
+ "invalid_key",
1621
+ "invalid_url",
1622
+ "incompatible_studio_api",
1623
+ "schema_mismatch",
1624
+ "connection_failed"
1625
+ ].includes(safe.code);
1626
+ return {
1627
+ schemaVersion: 1,
1628
+ status: "failed",
1629
+ checks: [
1630
+ {
1631
+ id: "studio_access",
1632
+ status: "failed",
1633
+ ...safe ? { code: safe.code } : {},
1634
+ message: connectionError ? safe.message : safe?.status === 401 ? "Studio rejected the project key (HTTP 401)." : safe?.status === 403 ? "Studio discovery requires studio:read (HTTP 403)." : safe?.status === 404 ? "Studio eval discovery endpoint was not found (HTTP 404)." : "Could not resolve the Studio connection or read a compatible discovery response.",
1635
+ remedy: "Check the selected connection, its API URL and project key, network access, and compatible CLI/Studio releases. No consumer or provider credentials belong in this CLI connection."
1636
+ }
1637
+ ]
1638
+ };
1639
+ }
1640
+ function formatEvalDoctorReport(report) {
1641
+ const symbols = { passed: "\u2713", failed: "\u2717", skipped: "\u2013" };
1642
+ return [
1643
+ "Kortyx Evals \xB7 Setup check",
1644
+ ...report.checks.flatMap((check) => [
1645
+ ` ${symbols[check.status]} ${check.message}`,
1646
+ ...check.remedy ? [` \u2192 ${check.remedy}`] : []
1647
+ ]),
1648
+ "",
1649
+ report.status === "passed" ? "Configuration checks passed. Run one representative suite to verify the test identity, tools, model access and saved results." : "Setup checks failed. Resolve the failures and run doctor again.",
1650
+ "No workflow or judge calls were started. Consumer GET discovery may run app-owned authentication logic."
1651
+ ].join("\n").replace(/\p{Cc}/gu, (character) => character === "\n" ? character : "");
1652
+ }
1653
+
1654
+ // src/studio/read-output.ts
1655
+ var parseStudioTarget = (input, entity) => {
1656
+ if (/^[a-z][a-z0-9+.-]*:/i.test(input)) {
1657
+ let url;
1658
+ try {
1659
+ url = new URL(input);
1660
+ } catch {
1661
+ throw new StudioReadError("invalid_target", "Invalid Studio URL.");
1662
+ }
1663
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
1664
+ throw new StudioReadError(
1665
+ "invalid_target",
1666
+ "Use an HTTP(S) Studio entity URL without embedded credentials."
1667
+ );
1668
+ }
1669
+ const match = url.pathname.match(
1670
+ /\/(runs|sessions|interrupts)\/([^/]+)\/?$/
1671
+ );
1672
+ if (!match)
1673
+ throw new StudioReadError(
1674
+ "invalid_target",
1675
+ "Expected a Studio /runs/<id>, /sessions/<id>, or /interrupts/<id> URL."
1676
+ );
1677
+ if (entity && match[1] !== entity)
1678
+ throw new StudioReadError(
1679
+ "invalid_target",
1680
+ "URL entity type does not match the command."
1681
+ );
1682
+ let id;
1683
+ try {
1684
+ id = decodeURIComponent(match[2] ?? "");
1685
+ } catch {
1686
+ throw new StudioReadError(
1687
+ "invalid_target",
1688
+ "Invalid entity ID encoding."
1689
+ );
1690
+ }
1691
+ validateId(id);
1692
+ const selection = {};
1693
+ for (const key of [
1694
+ "tab",
1695
+ "sessionTab",
1696
+ "call",
1697
+ "branch",
1698
+ "node",
1699
+ "event",
1700
+ "trace",
1701
+ "detailView"
1702
+ ]) {
1703
+ const value = url.searchParams.get(key);
1704
+ if (value) selection[key] = value;
1705
+ }
1706
+ url.search = "";
1707
+ url.hash = "";
1708
+ return {
1709
+ entity: match[1],
1710
+ id,
1711
+ url: url.toString(),
1712
+ selection
1713
+ };
1714
+ }
1715
+ if (!entity)
1716
+ throw new StudioReadError(
1717
+ "invalid_target",
1718
+ "inspect requires a Studio entity URL. Use runs/sessions/interrupts get for a bare ID."
1719
+ );
1720
+ validateId(input);
1721
+ return { entity, id: input };
1722
+ };
1723
+ var validateId = (id) => {
1724
+ if (!id || id.length > 512 || id === "." || id === ".." || /[\s/?#\\]/.test(id) || [...id].some(
1725
+ (char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127
1726
+ )) {
1727
+ throw new StudioReadError("invalid_target", "Invalid Studio entity ID.");
1728
+ }
1729
+ };
1730
+ var secretKey = /(?:password|secret|authorization|cookie|apikey|credential|resumetoken|accesstoken|refreshtoken|bearertoken|sessiontoken|authtoken)/i;
1731
+ var contentKey = /^(?:input|output|inputs|outputs|inputmessages|outputmessages|messages|prompt|systemprompt|question|response|result|latestresult|arguments|args|content|text|options)$/i;
1732
+ var sanitizeStudioData = (value, includeContent, key = "") => {
1733
+ const normalized = key.replace(/[^a-z0-9]/gi, "");
1734
+ const keyPolicy = normalized.toLowerCase() === "apikey" && value && typeof value === "object" && "scopes" in value && "mode" in value;
1735
+ if (secretKey.test(normalized) && !keyPolicy) return "[REDACTED]";
1736
+ if (!includeContent && contentKey.test(normalized))
1737
+ return "[CONTENT OMITTED: use --include-content]";
1738
+ if (typeof value === "string")
1739
+ return value.replace(/ktyx_(?:test|live)_[^\s"'<>]+/g, "[REDACTED]").replace(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]");
1740
+ if (Array.isArray(value))
1741
+ return value.map((item) => sanitizeStudioData(item, includeContent));
1742
+ if (value && typeof value === "object")
1743
+ return Object.fromEntries(
1744
+ Object.entries(value).map(([name, item]) => [
1745
+ name,
1746
+ sanitizeStudioData(item, includeContent, name)
1747
+ ])
1748
+ );
1749
+ return value;
1750
+ };
1751
+ var summarizeEvidence = (events) => {
1752
+ const findings = events.flatMap((event) => {
1753
+ const payload = event.payload;
1754
+ const error = payload.error && typeof payload.error === "object" && !Array.isArray(payload.error) ? payload.error : {};
1755
+ const suspension = event.type === "span.failed" && (error.controlFlow === true || ["GraphInterrupt", "NodeInterrupt", "ParallelChildWaiting"].includes(
1756
+ String(error.name)
1757
+ ));
1758
+ const failed = !suspension && (/(?:failed|error|fault)/i.test(event.type) || payload.outcome === "fault" || payload.status === "failed");
1759
+ const warning = suspension || /(?:retry|cancel|interrupt|limit_reached|denied|suspended|waiting)/i.test(
1760
+ event.type
1761
+ ) || ["denied", "cancelled"].includes(String(payload.outcome));
1762
+ if (!failed && !warning) return [];
1763
+ return [
1764
+ {
1765
+ eventId: event.id,
1766
+ runId: event.runId,
1767
+ nodeId: event.nodeId,
1768
+ type: event.type,
1769
+ occurredAt: event.occurredAt,
1770
+ severity: failed ? "error" : "context",
1771
+ evidence: payload
1772
+ }
1773
+ ];
1774
+ });
1775
+ return {
1776
+ findings,
1777
+ note: "Evidence, not an automated root-cause verdict. Interrupts, cancellations, and retries can be expected; correlate with the final run outcome. Missing telemetry does not prove an action did not occur."
1778
+ };
1779
+ };
1780
+ var formatReadOutput = (value, json) => {
1781
+ if (json) return JSON.stringify(value);
1782
+ return JSON.stringify(value, null, 2);
1783
+ };
1784
+
1785
+ // src/studio/eval-command.ts
1786
+ var integer2 = (max) => (value) => {
1787
+ if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > max)
1788
+ throw new import_commander3.InvalidArgumentError(`Expected an integer between 1 and ${max}.`);
1789
+ return Number(value);
1790
+ };
1791
+ var summary = ({ error: _error, ...run2 }) => run2;
1792
+ var compactDetail = (run2) => ({
1793
+ ...summary(run2),
1794
+ suite: void 0,
1795
+ events: void 0,
1796
+ result: void 0,
1797
+ caseResults: run2.result?.cases.map(
1798
+ ({ caseId, repetition, status, durationMs, steps }) => ({
1799
+ caseId,
1800
+ repetition,
1801
+ status,
1802
+ durationMs,
1803
+ steps: steps.map(({ index, status: status2, criteria }) => ({
1804
+ index,
1805
+ status: status2,
1806
+ criteria: criteria.map(({ id, passed }) => ({ id, passed }))
1807
+ }))
1808
+ })
1809
+ )
1810
+ });
1811
+ function registerStudioEvalCommands(studio, log, request = fetch) {
1812
+ const common = (command) => command.option(
1813
+ "--connection <name>",
1814
+ "Named project/API connection (or KORTYX_CONNECTION)."
1815
+ ).option(
1816
+ "--config-home <path>",
1817
+ "Connection configuration directory.",
1818
+ defaultConnectionsHome()
1819
+ ).option(
1820
+ "--home <path>",
1821
+ "Managed local Studio directory.",
1822
+ defaultStudioHome()
1823
+ ).option("--api-url <url>", "Direct API URL; requires --api-key-env.").option(
1824
+ "--api-key-env <name>",
1825
+ "Environment variable holding a Studio key."
1826
+ ).option("--json", "Print stable machine-readable JSON.").option(
1827
+ "--include-content",
1828
+ "Include suite definitions and captured execution content."
1829
+ );
1830
+ const print = (value, options) => log(
1831
+ formatReadOutput(
1832
+ sanitizeStudioData(
1833
+ { schemaVersion: 1, ...value },
1834
+ options.includeContent ?? false
1835
+ ),
1836
+ options.json ?? false
1837
+ )
1838
+ );
1839
+ const clientFor = async (options, url) => {
1840
+ const connection = await resolveConnection(options, url);
1841
+ return {
1842
+ connection,
1843
+ client: new StudioEvalClient(
1844
+ connection.apiUrl,
1845
+ connection.apiKey,
1846
+ request
1847
+ )
1848
+ };
1849
+ };
1850
+ const evals = studio.command("evals").description(
1851
+ "Discover suites, enqueue evals, inspect saved results and cancel runs."
1852
+ );
1853
+ const suites = evals.command("suites").description("Discover application-owned eval suites.");
1854
+ const discover = async (options) => {
1855
+ const { connection, client } = await clientFor(options);
1856
+ const data = await client.targets();
1857
+ const environment = options.environment ?? connection.environment;
1858
+ const targets = data.targets.filter(
1859
+ (target) => (!options.target || target.id === options.target) && (!environment || target.environment === environment)
1860
+ );
1861
+ return {
1862
+ connection,
1863
+ client,
1864
+ targets,
1865
+ canRun: data.canRun,
1866
+ studioJudge: data.studioJudge ?? null
1867
+ };
1868
+ };
1869
+ const selectionOptions = (command) => common(command).option("--target <id>", "Application eval target ID.").option(
1870
+ "--environment <name>",
1871
+ "Filter targets; defaults to the connection environment."
1872
+ );
1873
+ selectionOptions(
1874
+ evals.command("doctor").description(
1875
+ "Check deployment wiring without starting workflows or model calls."
1876
+ )
1877
+ ).option("--suite <id>", "Check that a specific suite is registered.").option(
1878
+ "--judge <location>",
1879
+ "Judge to check: studio (default) or app.",
1880
+ (value) => {
1881
+ if (value !== "studio" && value !== "app")
1882
+ throw new import_commander3.InvalidArgumentError("Expected studio or app.");
1883
+ return value;
1884
+ },
1885
+ "studio"
1886
+ ).action(async (options) => {
1887
+ let report;
1888
+ try {
1889
+ const { connection, client } = await clientFor(options);
1890
+ report = buildEvalDoctorReport(await client.targets(), {
1891
+ ...options,
1892
+ environment: options.environment ?? connection.environment,
1893
+ judge: options.judge ?? "studio"
1894
+ });
1895
+ } catch (error) {
1896
+ report = evalDoctorFailure(error);
1897
+ }
1898
+ log(
1899
+ options.json ? JSON.stringify(report) : formatEvalDoctorReport(report)
1900
+ );
1901
+ if (report.status === "failed") process.exitCode = 1;
1902
+ });
1903
+ selectionOptions(
1904
+ suites.command("list").description("List targets, suites, revisions and case IDs.")
1905
+ ).action(async (options) => {
1906
+ const { connection, targets, canRun, studioJudge } = await discover(options);
1907
+ print(
1908
+ {
1909
+ connection: connection.name,
1910
+ canRun,
1911
+ studioJudge,
1912
+ targets: targets.map((target) => ({
1913
+ id: target.id,
1914
+ name: target.name,
1915
+ environment: target.environment,
1916
+ available: target.manifest !== null,
1917
+ appJudge: target.manifest?.judge ?? null,
1918
+ studioJudging: Boolean(target.manifest?.studioJudging),
1919
+ suites: target.manifest?.suites.map((suite) => ({
1920
+ id: suite.id,
1921
+ name: suite.name,
1922
+ revision: target.revisions[suite.id],
1923
+ caseIds: suite.cases.map((item) => item.id)
1924
+ })) ?? []
1925
+ }))
1926
+ },
1927
+ options
1928
+ );
1929
+ });
1930
+ const select = async (suiteId, options) => {
1931
+ const data = await discover(options);
1932
+ const matches = data.targets.flatMap((target) => {
1933
+ const suite = target.manifest?.suites.find((item) => item.id === suiteId);
1934
+ return suite ? [{ target, suite }] : [];
1935
+ });
1936
+ const match = matches[0];
1937
+ if (matches.length !== 1 || !match)
848
1938
  throw new StudioReadError(
849
- "unknown_connection",
850
- "Connection not found. Run kortyx connections list."
1939
+ "invalid_suite_selection",
1940
+ matches.length ? "Suite exists on multiple targets. Select --target and/or --environment." : "Suite unavailable. List suites and check the target connection."
851
1941
  );
852
- }
853
- config.current = name;
854
- await saveConnections(config, options.configHome);
855
- print({ current: name }, options);
1942
+ return { ...data, ...match };
1943
+ };
1944
+ selectionOptions(
1945
+ suites.command("get <suite-id>").description(
1946
+ "Read a suite; --include-content returns its full definition."
1947
+ )
1948
+ ).action(async (suiteId, options) => {
1949
+ const { connection, target, suite } = await select(suiteId, options);
1950
+ print(
1951
+ {
1952
+ connection: connection.name,
1953
+ targetId: target.id,
1954
+ revision: target.revisions[suite.id],
1955
+ suite: options.includeContent ? suite : {
1956
+ id: suite.id,
1957
+ name: suite.name,
1958
+ caseIds: suite.cases.map((item) => item.id)
1959
+ }
1960
+ },
1961
+ options
1962
+ );
856
1963
  });
857
- common(
858
- command.command("remove <name>").description(
859
- "Remove a saved locator, not its credential or remote project."
1964
+ const runs = evals.command("runs").description("Start and inspect saved eval runs.");
1965
+ selectionOptions(
1966
+ runs.command("start <suite-id>").description(
1967
+ "Enqueue once and return immediately; does not wait for grades."
860
1968
  )
861
- ).action(async (name, options) => {
862
- const config = await readConnections(options.configHome);
863
- if (!config.profiles.some((profile) => profile.name === name))
1969
+ ).option(
1970
+ "--case <id>",
1971
+ "Select a case; repeat for multiple cases.",
1972
+ (value, previous) => [...previous, value],
1973
+ []
1974
+ ).option(
1975
+ "--repetitions <count>",
1976
+ "Attempts per selected case (1\u201320).",
1977
+ integer2(20),
1978
+ 1
1979
+ ).option("--concurrency <count>", "Parallel attempts (1\u20134).", integer2(4), 1).option(
1980
+ "--judge <location>",
1981
+ "Judge selection: studio (default) or app.",
1982
+ (value) => {
1983
+ if (value !== "studio" && value !== "app")
1984
+ throw new import_commander3.InvalidArgumentError("Expected studio or app.");
1985
+ return value;
1986
+ },
1987
+ "studio"
1988
+ ).action(async (suiteId, options) => {
1989
+ const { connection, client, target, suite, canRun } = await select(
1990
+ suiteId,
1991
+ options
1992
+ );
1993
+ if (!canRun)
864
1994
  throw new StudioReadError(
865
- "unknown_connection",
866
- "Saved connection not found; managed local cannot be removed."
1995
+ "eval_forbidden",
1996
+ "This Studio key requires eval:run to execute suites."
867
1997
  );
868
- config.profiles = config.profiles.filter(
869
- (profile) => profile.name !== name
1998
+ const caseIds = options.case ?? [];
1999
+ if (new Set(caseIds).size !== caseIds.length || caseIds.some((id) => !suite.cases.some((item) => item.id === id)) || (caseIds.length || suite.cases.length) * options.repetitions > 100)
2000
+ throw new StudioReadError(
2001
+ "invalid_case_selection",
2002
+ "Select unique existing case IDs and at most 100 total attempts."
2003
+ );
2004
+ const run2 = await client.start({
2005
+ targetId: target.id,
2006
+ judge: options.judge ?? "studio",
2007
+ suiteId,
2008
+ suiteRevision: target.revisions[suiteId] ?? "",
2009
+ repetitions: options.repetitions,
2010
+ concurrency: options.concurrency,
2011
+ ...caseIds.length ? { caseIds } : {}
2012
+ });
2013
+ print(
2014
+ {
2015
+ connection: connection.name,
2016
+ id: run2.id,
2017
+ status: "queued",
2018
+ studioUrl: connection.studioUrl ? `${connection.studioUrl}/evals/runs/${run2.id}` : null
2019
+ },
2020
+ options
870
2021
  );
871
- if (config.current === name) delete config.current;
872
- await saveConnections(config, options.configHome);
873
- print({ removed: name }, options);
874
2022
  });
875
- return command;
876
- };
877
-
878
- // src/studio/command.ts
879
- var import_commander3 = require("commander");
2023
+ common(
2024
+ runs.command("list").description("List the latest 100 saved runs in this project.")
2025
+ ).option(
2026
+ "--environment <name>",
2027
+ "Filter results; defaults to the connection environment."
2028
+ ).action(async (options) => {
2029
+ const { connection, client } = await clientFor(options);
2030
+ const data = await client.runs();
2031
+ const environment = options.environment ?? connection.environment;
2032
+ print(
2033
+ {
2034
+ connection: connection.name,
2035
+ runs: data.runs.filter((run2) => !environment || run2.environment === environment).map(summary)
2036
+ },
2037
+ options
2038
+ );
2039
+ });
2040
+ common(
2041
+ runs.command("get <id-or-url>").description(
2042
+ "Read status, counts and criterion verdicts; content is opt-in."
2043
+ )
2044
+ ).action(async (input, options) => {
2045
+ const target = parseEvalRunTarget(input);
2046
+ const { connection, client } = await clientFor(options, target.url);
2047
+ const { run: run2 } = await client.run(target.id);
2048
+ print(
2049
+ {
2050
+ connection: connection.name,
2051
+ run: options.includeContent ? run2 : compactDetail(run2)
2052
+ },
2053
+ options
2054
+ );
2055
+ });
2056
+ common(
2057
+ runs.command("cancel <id-or-url>").description(
2058
+ "Request cooperative cancellation of a queued or running eval."
2059
+ )
2060
+ ).action(async (input, options) => {
2061
+ const target = parseEvalRunTarget(input);
2062
+ const { connection, client } = await clientFor(options, target.url);
2063
+ const data = await client.cancel(target.id);
2064
+ print({ connection: connection.name, id: target.id, ...data }, options);
2065
+ });
2066
+ }
880
2067
 
881
2068
  // src/studio/local-stack.ts
882
- var import_promises3 = require("fs/promises");
2069
+ var import_promises4 = require("fs/promises");
883
2070
  var import_telemetry_contracts3 = require("@kortyx/telemetry-contracts");
884
2071
  var composeArgs = (home, args) => [
885
2072
  "compose",
@@ -1033,9 +2220,20 @@ var startStudio = async (options, runtime) => {
1033
2220
  const studioImage = String(
1034
2221
  environment.KORTYX_STUDIO_IMAGE_REF ?? `${environment.KORTYX_STUDIO_IMAGE ?? "ghcr.io/kortyx-io/kortyx-studio"}:${config.imageTag}`
1035
2222
  );
1036
- await runtime.run("docker", ["pull", apiImage], { inherit: true });
1037
- if (studioImage !== apiImage)
1038
- await runtime.run("docker", ["pull", studioImage], { inherit: true });
2223
+ const pullPolicy = process.env.KORTYX_STUDIO_PULL_POLICY ?? "always";
2224
+ for (const image of /* @__PURE__ */ new Set([apiImage, studioImage])) {
2225
+ if (pullPolicy === "never") continue;
2226
+ if (pullPolicy === "missing") {
2227
+ try {
2228
+ await runtime.run("docker", ["image", "inspect", image], {
2229
+ inherit: false
2230
+ });
2231
+ continue;
2232
+ } catch {
2233
+ }
2234
+ }
2235
+ await runtime.run("docker", ["pull", image], { inherit: true });
2236
+ }
1039
2237
  const capability = await runtime.run(
1040
2238
  "docker",
1041
2239
  [
@@ -1049,7 +2247,7 @@ var startStudio = async (options, runtime) => {
1049
2247
  ],
1050
2248
  { inherit: false }
1051
2249
  );
1052
- await (0, import_promises3.writeFile)(
2250
+ await (0, import_promises4.writeFile)(
1053
2251
  studioComposePath(options.home),
1054
2252
  studioComposeFile(
1055
2253
  process.platform !== "win32" && capability.stdout.trim() === "supported"
@@ -1152,7 +2350,7 @@ var resetStudio = async (home, confirm, runtime) => {
1152
2350
 
1153
2351
  // src/studio/read-command.ts
1154
2352
  var import_telemetry_contracts5 = require("@kortyx/telemetry-contracts");
1155
- var import_commander2 = require("commander");
2353
+ var import_commander4 = require("commander");
1156
2354
 
1157
2355
  // src/studio/read-analysis.ts
1158
2356
  var import_telemetry_contracts4 = require("@kortyx/telemetry-contracts");
@@ -1459,141 +2657,10 @@ var compareRunAnalysis = (left, right) => {
1459
2657
  };
1460
2658
  };
1461
2659
 
1462
- // src/studio/read-output.ts
1463
- var parseStudioTarget = (input, entity) => {
1464
- if (/^[a-z][a-z0-9+.-]*:/i.test(input)) {
1465
- let url;
1466
- try {
1467
- url = new URL(input);
1468
- } catch {
1469
- throw new StudioReadError("invalid_target", "Invalid Studio URL.");
1470
- }
1471
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
1472
- throw new StudioReadError(
1473
- "invalid_target",
1474
- "Use an HTTP(S) Studio entity URL without embedded credentials."
1475
- );
1476
- }
1477
- const match = url.pathname.match(
1478
- /\/(runs|sessions|interrupts)\/([^/]+)\/?$/
1479
- );
1480
- if (!match)
1481
- throw new StudioReadError(
1482
- "invalid_target",
1483
- "Expected a Studio /runs/<id>, /sessions/<id>, or /interrupts/<id> URL."
1484
- );
1485
- if (entity && match[1] !== entity)
1486
- throw new StudioReadError(
1487
- "invalid_target",
1488
- "URL entity type does not match the command."
1489
- );
1490
- let id;
1491
- try {
1492
- id = decodeURIComponent(match[2] ?? "");
1493
- } catch {
1494
- throw new StudioReadError(
1495
- "invalid_target",
1496
- "Invalid entity ID encoding."
1497
- );
1498
- }
1499
- validateId(id);
1500
- const selection = {};
1501
- for (const key of [
1502
- "tab",
1503
- "sessionTab",
1504
- "call",
1505
- "branch",
1506
- "node",
1507
- "event",
1508
- "trace",
1509
- "detailView"
1510
- ]) {
1511
- const value = url.searchParams.get(key);
1512
- if (value) selection[key] = value;
1513
- }
1514
- url.search = "";
1515
- url.hash = "";
1516
- return {
1517
- entity: match[1],
1518
- id,
1519
- url: url.toString(),
1520
- selection
1521
- };
1522
- }
1523
- if (!entity)
1524
- throw new StudioReadError(
1525
- "invalid_target",
1526
- "inspect requires a Studio entity URL. Use runs/sessions/interrupts get for a bare ID."
1527
- );
1528
- validateId(input);
1529
- return { entity, id: input };
1530
- };
1531
- var validateId = (id) => {
1532
- if (!id || id.length > 512 || id === "." || id === ".." || /[\s/?#\\]/.test(id) || [...id].some(
1533
- (char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127
1534
- )) {
1535
- throw new StudioReadError("invalid_target", "Invalid Studio entity ID.");
1536
- }
1537
- };
1538
- var secretKey = /(?:password|secret|authorization|cookie|apikey|credential|resumetoken|accesstoken|refreshtoken|bearertoken|sessiontoken|authtoken)/i;
1539
- var contentKey = /^(?:input|output|inputs|outputs|inputmessages|outputmessages|messages|prompt|systemprompt|question|response|result|latestresult|arguments|args|content|text|options)$/i;
1540
- var sanitizeStudioData = (value, includeContent, key = "") => {
1541
- const normalized = key.replace(/[^a-z0-9]/gi, "");
1542
- const keyPolicy = normalized.toLowerCase() === "apikey" && value && typeof value === "object" && "scopes" in value && "mode" in value;
1543
- if (secretKey.test(normalized) && !keyPolicy) return "[REDACTED]";
1544
- if (!includeContent && contentKey.test(normalized))
1545
- return "[CONTENT OMITTED: use --include-content]";
1546
- if (typeof value === "string")
1547
- return value.replace(/ktyx_(?:test|live)_[^\s"'<>]+/g, "[REDACTED]").replace(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]");
1548
- if (Array.isArray(value))
1549
- return value.map((item) => sanitizeStudioData(item, includeContent));
1550
- if (value && typeof value === "object")
1551
- return Object.fromEntries(
1552
- Object.entries(value).map(([name, item]) => [
1553
- name,
1554
- sanitizeStudioData(item, includeContent, name)
1555
- ])
1556
- );
1557
- return value;
1558
- };
1559
- var summarizeEvidence = (events) => {
1560
- const findings = events.flatMap((event) => {
1561
- const payload = event.payload;
1562
- const error = payload.error && typeof payload.error === "object" && !Array.isArray(payload.error) ? payload.error : {};
1563
- const suspension = event.type === "span.failed" && (error.controlFlow === true || ["GraphInterrupt", "NodeInterrupt", "ParallelChildWaiting"].includes(
1564
- String(error.name)
1565
- ));
1566
- const failed = !suspension && (/(?:failed|error|fault)/i.test(event.type) || payload.outcome === "fault" || payload.status === "failed");
1567
- const warning = suspension || /(?:retry|cancel|interrupt|limit_reached|denied|suspended|waiting)/i.test(
1568
- event.type
1569
- ) || ["denied", "cancelled"].includes(String(payload.outcome));
1570
- if (!failed && !warning) return [];
1571
- return [
1572
- {
1573
- eventId: event.id,
1574
- runId: event.runId,
1575
- nodeId: event.nodeId,
1576
- type: event.type,
1577
- occurredAt: event.occurredAt,
1578
- severity: failed ? "error" : "context",
1579
- evidence: payload
1580
- }
1581
- ];
1582
- });
1583
- return {
1584
- findings,
1585
- note: "Evidence, not an automated root-cause verdict. Interrupts, cancellations, and retries can be expected; correlate with the final run outcome. Missing telemetry does not prove an action did not occur."
1586
- };
1587
- };
1588
- var formatReadOutput = (value, json) => {
1589
- if (json) return JSON.stringify(value);
1590
- return JSON.stringify(value, null, 2);
1591
- };
1592
-
1593
2660
  // src/studio/read-command.ts
1594
- var integer = (min, max) => (value) => {
2661
+ var integer3 = (min, max) => (value) => {
1595
2662
  if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > max) {
1596
- throw new import_commander2.InvalidArgumentError(
2663
+ throw new import_commander4.InvalidArgumentError(
1597
2664
  `Expected an integer between ${min} and ${max}.`
1598
2665
  );
1599
2666
  }
@@ -1749,7 +2816,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
1749
2816
  const detailOptions = (command) => common(command).option(
1750
2817
  "--event-limit <count>",
1751
2818
  "Maximum latest events and diagnostic findings (1\u201310000).",
1752
- integer(1, 1e4),
2819
+ integer3(1, 1e4),
1753
2820
  100
1754
2821
  ).option(
1755
2822
  "--focus-selection",
@@ -1802,7 +2869,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
1802
2869
  "--environment <name>",
1803
2870
  "Environment filter; overrides the connection default."
1804
2871
  ).addOption(
1805
- new import_commander2.Option(
2872
+ new import_commander4.Option(
1806
2873
  "--range <range>",
1807
2874
  "Studio time range: 1h, 24h, 7d, 30d, all (or Studio preset name)."
1808
2875
  ).choices([
@@ -1847,7 +2914,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
1847
2914
  ).option(
1848
2915
  "--event-limit <count>",
1849
2916
  "Maximum timeline steps returned per run (1\u201310000).",
1850
- integer(1, 1e4),
2917
+ integer3(1, 1e4),
1851
2918
  100
1852
2919
  ).action(
1853
2920
  async (leftInput, rightInput, options) => {
@@ -1947,13 +3014,13 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
1947
3014
  }
1948
3015
  const list = filters(
1949
3016
  group.command("list").description("Read a bounded, filtered page.")
1950
- ).option("--limit <count>", "Page size (1\u2013100).", integer(1, 100), 25).option(
3017
+ ).option("--limit <count>", "Page size (1\u2013100).", integer3(1, 100), 25).option(
1951
3018
  "--cursor <offset>",
1952
3019
  "Numeric offset returned as nextCursor.",
1953
- integer(0, Number.MAX_SAFE_INTEGER),
3020
+ integer3(0, Number.MAX_SAFE_INTEGER),
1954
3021
  0
1955
3022
  ).option("--status <status>", "Entity status filter.").option("--session <id>", "Session filter.").option("--user <id>", "User filter.").option("--tenant <id>", "Tenant filter.").option("--model <name>", "Model filter.").option("--node <id>", "Node filter.").option("--tool <name>", "Tool name filter.").addOption(
1956
- new import_commander2.Option("--tool-outcome <outcome>", "Tool outcome filter.").choices([
3023
+ new import_commander4.Option("--tool-outcome <outcome>", "Tool outcome filter.").choices([
1957
3024
  "success",
1958
3025
  "denied",
1959
3026
  "fault",
@@ -2082,14 +3149,14 @@ var defaultStudioRuntime = {
2082
3149
  var portParser = (value) => {
2083
3150
  const port = Number(value);
2084
3151
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
2085
- throw new import_commander3.InvalidArgumentError(
3152
+ throw new import_commander5.InvalidArgumentError(
2086
3153
  "Port must be an integer between 1 and 65535."
2087
3154
  );
2088
3155
  }
2089
3156
  return port;
2090
3157
  };
2091
3158
  var withHome = (command) => command.addOption(
2092
- new import_commander3.Option("--home <path>", "Local Studio state directory.").default(
3159
+ new import_commander5.Option("--home <path>", "Local Studio state directory.").default(
2093
3160
  defaultStudioHome()
2094
3161
  )
2095
3162
  );
@@ -2099,7 +3166,7 @@ var configureOutput = (command, runtime) => command.configureOutput({
2099
3166
  });
2100
3167
  var createStudioCommand = (runtime = defaultStudioRuntime) => {
2101
3168
  const studio = configureOutput(
2102
- new import_commander3.Command("studio").description("Run and manage Kortyx Studio.").showHelpAfterError().addHelpText(
3169
+ new import_commander5.Command("studio").description("Run and manage Kortyx Studio.").showHelpAfterError().addHelpText(
2103
3170
  "after",
2104
3171
  "\nLocal state and generated credentials are stored in ~/.kortyx/studio by default."
2105
3172
  ),
@@ -2142,7 +3209,7 @@ var createStudioCommand = (runtime = defaultStudioRuntime) => {
2142
3209
  "Generate unpersisted credentials for a self-hosted deployment.",
2143
3210
  false
2144
3211
  ).addOption(
2145
- new import_commander3.Option("--format <format>", "Credential output format.").choices(["human", "dotenv"]).default("human")
3212
+ new import_commander5.Option("--format <format>", "Credential output format.").choices(["human", "dotenv"]).default("human")
2146
3213
  ).option(
2147
3214
  "--service-name <name>",
2148
3215
  "SDK service name used by dotenv output.",
@@ -2184,20 +3251,29 @@ var createStudioCommand = (runtime = defaultStudioRuntime) => {
2184
3251
  ).action(async ({ home, confirm }) => {
2185
3252
  await resetStudio(home, confirm, runtime);
2186
3253
  });
2187
- registerStudioReadCommands(studio, (message) => runtime.log(message));
3254
+ registerStudioReadCommands(
3255
+ studio,
3256
+ (message) => runtime.log(message),
3257
+ runtime.request
3258
+ );
3259
+ registerStudioEvalCommands(
3260
+ studio,
3261
+ (message) => runtime.log(message),
3262
+ runtime.request
3263
+ );
2188
3264
  return studio;
2189
3265
  };
2190
3266
 
2191
3267
  // src/workflow-calls.ts
2192
- var import_node_path3 = require("path");
3268
+ var import_node_path5 = require("path");
2193
3269
  var import_typescript = __toESM(require("typescript"));
2194
3270
  function discoverWorkflowCalls(entry, snapshots) {
2195
- const configPath = import_typescript.default.findConfigFile((0, import_node_path3.dirname)(entry), import_typescript.default.sys.fileExists);
3271
+ const configPath = import_typescript.default.findConfigFile((0, import_node_path5.dirname)(entry), import_typescript.default.sys.fileExists);
2196
3272
  const config = configPath ? import_typescript.default.readConfigFile(configPath, import_typescript.default.sys.readFile).config : {};
2197
3273
  const parsed = import_typescript.default.parseJsonConfigFileContent(
2198
3274
  config ?? {},
2199
3275
  import_typescript.default.sys,
2200
- configPath ? (0, import_node_path3.dirname)(configPath) : (0, import_node_path3.dirname)(entry)
3276
+ configPath ? (0, import_node_path5.dirname)(configPath) : (0, import_node_path5.dirname)(entry)
2201
3277
  );
2202
3278
  const program = import_typescript.default.createProgram([entry], {
2203
3279
  ...parsed.options,
@@ -2238,7 +3314,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2238
3314
  );
2239
3315
  };
2240
3316
  const factoryBindings = /* @__PURE__ */ new WeakMap();
2241
- function resolve4(e, bindings, seen = /* @__PURE__ */ new Set()) {
3317
+ function resolve6(e, bindings, seen = /* @__PURE__ */ new Set()) {
2242
3318
  if (!e) return;
2243
3319
  e = unwrap(e);
2244
3320
  if (seen.has(e)) return;
@@ -2246,18 +3322,18 @@ function discoverWorkflowCalls(entry, snapshots) {
2246
3322
  if (import_typescript.default.isIdentifier(e)) {
2247
3323
  const s = symbol(e);
2248
3324
  const bound = s && bindings.get(s);
2249
- if (bound) return resolve4(bound, bindings, seen);
3325
+ if (bound) return resolve6(bound, bindings, seen);
2250
3326
  const d = s?.valueDeclaration;
2251
3327
  if (d && (import_typescript.default.isVariableDeclaration(d) || import_typescript.default.isPropertyAssignment(d)) && d.initializer && (!import_typescript.default.isVariableDeclaration(d) || import_typescript.default.isVariableDeclarationList(d.parent) && (d.parent.flags & import_typescript.default.NodeFlags.Const) !== 0))
2252
- return resolve4(d.initializer, bindings, seen);
3328
+ return resolve6(d.initializer, bindings, seen);
2253
3329
  }
2254
3330
  if (import_typescript.default.isPropertyAccessExpression(e)) {
2255
3331
  const found = property(e.expression, e.name.text, bindings, seen);
2256
- if (found) return resolve4(found, bindings, seen);
3332
+ if (found) return resolve6(found, bindings, seen);
2257
3333
  const owner = symbol(e.expression);
2258
3334
  const declaration = symbol(e.name)?.valueDeclaration;
2259
3335
  if (owner && owner.flags & import_typescript.default.SymbolFlags.Module && declaration && import_typescript.default.isVariableDeclaration(declaration))
2260
- return resolve4(declaration.initializer, bindings, seen);
3336
+ return resolve6(declaration.initializer, bindings, seen);
2261
3337
  return e;
2262
3338
  }
2263
3339
  if (import_typescript.default.isElementAccessExpression(e) && e.argumentExpression && import_typescript.default.isStringLiteralLike(e.argumentExpression)) {
@@ -2267,7 +3343,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2267
3343
  bindings,
2268
3344
  seen
2269
3345
  );
2270
- return found ? resolve4(found, bindings, seen) : e;
3346
+ return found ? resolve6(found, bindings, seen) : e;
2271
3347
  }
2272
3348
  if (import_typescript.default.isCallExpression(e)) {
2273
3349
  const d = symbol(
@@ -2281,12 +3357,12 @@ function discoverWorkflowCalls(entry, snapshots) {
2281
3357
  const key = symbol(p.name);
2282
3358
  const arg = e.arguments[i] ?? p.initializer;
2283
3359
  if (key && arg)
2284
- bound.set(key, resolve4(arg, bindings, new Set(seen)) ?? arg);
3360
+ bound.set(key, resolve6(arg, bindings, new Set(seen)) ?? arg);
2285
3361
  }
2286
3362
  });
2287
3363
  const returned = import_typescript.default.isBlock(fn.body) ? fn.body.statements.filter(import_typescript.default.isReturnStatement).map((r) => r.expression) : [fn.body];
2288
3364
  if (returned.length === 1 && returned[0]) {
2289
- const value = resolve4(returned[0], bound, seen);
3365
+ const value = resolve6(returned[0], bound, seen);
2290
3366
  if (value) factoryBindings.set(value, bound);
2291
3367
  return value;
2292
3368
  }
@@ -2295,30 +3371,30 @@ function discoverWorkflowCalls(entry, snapshots) {
2295
3371
  return e;
2296
3372
  }
2297
3373
  function property(e, key, bindings, seen = /* @__PURE__ */ new Set()) {
2298
- const obj = resolve4(e, bindings, seen);
3374
+ const obj = resolve6(e, bindings, seen);
2299
3375
  if (!obj || !import_typescript.default.isObjectLiteralExpression(obj)) return;
2300
3376
  const scoped = factoryBindings.get(obj) ?? bindings;
2301
3377
  for (const p of [...obj.properties].reverse()) {
2302
3378
  if (import_typescript.default.isSpreadAssignment(p)) {
2303
- const spread = resolve4(p.expression, scoped, new Set(seen));
3379
+ const spread = resolve6(p.expression, scoped, new Set(seen));
2304
3380
  if (!spread || !import_typescript.default.isObjectLiteralExpression(spread)) return void 0;
2305
3381
  const found = property(p.expression, key, scoped, new Set(seen));
2306
3382
  if (found) return found;
2307
3383
  } else if (p.name && (import_typescript.default.isIdentifier(p.name) || import_typescript.default.isStringLiteralLike(p.name)) && p.name.text === key) {
2308
3384
  if (import_typescript.default.isPropertyAssignment(p))
2309
- return resolve4(p.initializer, scoped, new Set(seen));
3385
+ return resolve6(p.initializer, scoped, new Set(seen));
2310
3386
  if (import_typescript.default.isShorthandPropertyAssignment(p))
2311
- return resolve4(p.name, scoped, new Set(seen));
3387
+ return resolve6(p.name, scoped, new Set(seen));
2312
3388
  }
2313
3389
  }
2314
3390
  return void 0;
2315
3391
  }
2316
3392
  function literal(e, bindings) {
2317
- const value = resolve4(e, bindings);
3393
+ const value = resolve6(e, bindings);
2318
3394
  return value && import_typescript.default.isStringLiteralLike(value) ? value.text : void 0;
2319
3395
  }
2320
3396
  function workflowId(e, bindings) {
2321
- const value = resolve4(e, bindings);
3397
+ const value = resolve6(e, bindings);
2322
3398
  if (!value) return;
2323
3399
  if (import_typescript.default.isStringLiteralLike(value)) return value.text;
2324
3400
  if (import_typescript.default.isCallExpression(value) && api(value.expression, "defineWorkflow"))
@@ -2334,19 +3410,19 @@ function discoverWorkflowCalls(entry, snapshots) {
2334
3410
  if (d && import_typescript.default.isBindingElement(d) && (d.propertyName?.getText() ?? d.name.getText()) === "useWorkflow") {
2335
3411
  const decl = d.parent.parent;
2336
3412
  if (import_typescript.default.isVariableDeclaration(decl) && decl.initializer) {
2337
- const value2 = resolve4(decl.initializer, /* @__PURE__ */ new Map());
3413
+ const value2 = resolve6(decl.initializer, /* @__PURE__ */ new Map());
2338
3414
  return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
2339
3415
  }
2340
3416
  }
2341
3417
  if (import_typescript.default.isPropertyAccessExpression(e) && e.name.text === "useWorkflow") {
2342
- const value2 = resolve4(e.expression, /* @__PURE__ */ new Map());
3418
+ const value2 = resolve6(e.expression, /* @__PURE__ */ new Map());
2343
3419
  return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
2344
3420
  }
2345
- const value = resolve4(e, /* @__PURE__ */ new Map());
3421
+ const value = resolve6(e, /* @__PURE__ */ new Map());
2346
3422
  return !!value && value !== e && api(value, "useWorkflow");
2347
3423
  }
2348
3424
  function functionFor(e) {
2349
- const value = resolve4(e, /* @__PURE__ */ new Map());
3425
+ const value = resolve6(e, /* @__PURE__ */ new Map());
2350
3426
  if (value && (import_typescript.default.isArrowFunction(value) || import_typescript.default.isFunctionExpression(value)))
2351
3427
  return value;
2352
3428
  const d = symbol(
@@ -2369,7 +3445,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2369
3445
  target.toolDiscovery.warnings.push(warning);
2370
3446
  warnings.add(warning);
2371
3447
  };
2372
- const value = resolve4(expression, bindings);
3448
+ const value = resolve6(expression, bindings);
2373
3449
  if (!value || seen.has(value)) {
2374
3450
  unresolved();
2375
3451
  return;
@@ -2414,8 +3490,8 @@ function discoverWorkflowCalls(entry, snapshots) {
2414
3490
  bindings
2415
3491
  );
2416
3492
  const schema = property(value, "inputSchema", bindings);
2417
- const fields = resolve4(property(schema, "properties", bindings), bindings);
2418
- const requiredValue = resolve4(
3493
+ const fields = resolve6(property(schema, "properties", bindings), bindings);
3494
+ const requiredValue = resolve6(
2419
3495
  property(schema, "required", bindings),
2420
3496
  bindings
2421
3497
  );
@@ -2504,7 +3580,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2504
3580
  if (!arg) return;
2505
3581
  if (import_typescript.default.isIdentifier(p.name)) {
2506
3582
  const s = symbol(p.name);
2507
- if (s) bound.set(s, resolve4(arg, bindings) ?? arg);
3583
+ if (s) bound.set(s, resolve6(arg, bindings) ?? arg);
2508
3584
  } else if (import_typescript.default.isObjectBindingPattern(p.name)) {
2509
3585
  for (const b of p.name.elements) {
2510
3586
  const value = property(
@@ -2514,7 +3590,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2514
3590
  );
2515
3591
  const s = symbol(b.name);
2516
3592
  if (s && value)
2517
- bound.set(s, resolve4(value, bindings) ?? value);
3593
+ bound.set(s, resolve6(value, bindings) ?? value);
2518
3594
  }
2519
3595
  }
2520
3596
  });
@@ -2529,7 +3605,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2529
3605
  for (const source of program.getSourceFiles().filter(local)) {
2530
3606
  const walk = (n) => {
2531
3607
  if (import_typescript.default.isCallExpression(n) && api(n.expression, "defineWorkflow")) {
2532
- const obj = resolve4(n.arguments[0], /* @__PURE__ */ new Map());
3608
+ const obj = resolve6(n.arguments[0], /* @__PURE__ */ new Map());
2533
3609
  const id = obj && literal(property(obj, "id", /* @__PURE__ */ new Map()), /* @__PURE__ */ new Map());
2534
3610
  if (id && known.has(id) && obj && import_typescript.default.isObjectLiteralExpression(obj))
2535
3611
  definitions.set(id, [...definitions.get(id) ?? [], obj]);
@@ -2599,24 +3675,24 @@ var readPackageName = async (cwd) => {
2599
3675
  let current = cwd;
2600
3676
  for (; ; ) {
2601
3677
  try {
2602
- const raw = await (0, import_promises4.readFile)((0, import_node_path4.join)(current, "package.json"), "utf8");
3678
+ const raw = await (0, import_promises5.readFile)((0, import_node_path6.join)(current, "package.json"), "utf8");
2603
3679
  const parsed = JSON.parse(raw);
2604
3680
  if (isRecord(parsed) && typeof parsed.name === "string") {
2605
3681
  return parsed.name;
2606
3682
  }
2607
3683
  } catch {
2608
3684
  }
2609
- const parent = (0, import_node_path4.dirname)(current);
3685
+ const parent = (0, import_node_path6.dirname)(current);
2610
3686
  if (parent === current) return void 0;
2611
3687
  current = parent;
2612
3688
  }
2613
3689
  };
2614
3690
  var loadEnvFiles = async (cwd) => {
2615
- for (const filename of [".env", ".env.local"]) {
2616
- const path = (0, import_node_path4.join)(cwd, filename);
3691
+ for (const filename of [".env.local", ".env"]) {
3692
+ const path = (0, import_node_path6.join)(cwd, filename);
2617
3693
  let raw;
2618
3694
  try {
2619
- raw = await (0, import_promises4.readFile)(path, "utf8");
3695
+ raw = await (0, import_promises5.readFile)(path, "utf8");
2620
3696
  } catch {
2621
3697
  continue;
2622
3698
  }
@@ -2642,7 +3718,7 @@ var resolveOptions = async (options) => {
2642
3718
  if (!options.entry) {
2643
3719
  throw new Error("Missing required option: --entry <path>.");
2644
3720
  }
2645
- const entry = (0, import_node_path4.isAbsolute)(options.entry) ? options.entry : (0, import_node_path4.resolve)(options.cwd, options.entry);
3721
+ const entry = (0, import_node_path6.isAbsolute)(options.entry) ? options.entry : (0, import_node_path6.resolve)(options.cwd, options.entry);
2646
3722
  const environment = options.environment ?? process.env.KORTYX_TELEMETRY_ENVIRONMENT ?? process.env.NODE_ENV ?? "development";
2647
3723
  const serviceName = options.serviceName ?? process.env.KORTYX_TELEMETRY_SERVICE_NAME ?? await readPackageName(options.cwd) ?? "kortyx-app";
2648
3724
  return {
@@ -2656,19 +3732,19 @@ var resolveOptions = async (options) => {
2656
3732
  };
2657
3733
  };
2658
3734
  var createFrameworkMarkerStubs = async () => {
2659
- const root = await (0, import_promises4.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "kortyx-cli-stubs-"));
3735
+ const root = await (0, import_promises5.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "kortyx-cli-stubs-"));
2660
3736
  for (const moduleName of ["server-only", "client-only"]) {
2661
- const directory = (0, import_node_path4.join)(root, moduleName);
2662
- await (0, import_promises4.mkdir)(directory, { recursive: true });
2663
- await (0, import_promises4.writeFile)((0, import_node_path4.join)(directory, "index.js"), "", "utf8");
2664
- await (0, import_promises4.writeFile)(
2665
- (0, import_node_path4.join)(directory, "package.json"),
3737
+ const directory = (0, import_node_path6.join)(root, moduleName);
3738
+ await (0, import_promises5.mkdir)(directory, { recursive: true });
3739
+ await (0, import_promises5.writeFile)((0, import_node_path6.join)(directory, "index.js"), "", "utf8");
3740
+ await (0, import_promises5.writeFile)(
3741
+ (0, import_node_path6.join)(directory, "package.json"),
2666
3742
  JSON.stringify({ name: moduleName, version: "0.0.0", main: "index.js" }),
2667
3743
  "utf8"
2668
3744
  );
2669
3745
  }
2670
3746
  const previousNodePath = process.env.NODE_PATH;
2671
- process.env.NODE_PATH = previousNodePath ? `${root}${import_node_path4.delimiter}${previousNodePath}` : root;
3747
+ process.env.NODE_PATH = previousNodePath ? `${root}${import_node_path6.delimiter}${previousNodePath}` : root;
2672
3748
  const moduleWithPrivateInit = import_node_module.default;
2673
3749
  moduleWithPrivateInit._initPaths?.();
2674
3750
  return {
@@ -2676,48 +3752,71 @@ var createFrameworkMarkerStubs = async () => {
2676
3752
  if (previousNodePath === void 0) delete process.env.NODE_PATH;
2677
3753
  else process.env.NODE_PATH = previousNodePath;
2678
3754
  moduleWithPrivateInit._initPaths?.();
2679
- await (0, import_promises4.rm)(root, { recursive: true, force: true });
3755
+ await (0, import_promises5.rm)(root, { recursive: true, force: true });
2680
3756
  }
2681
3757
  };
2682
3758
  };
2683
- var importEntry = async (entry) => {
3759
+ var openEntry = async (entry) => {
2684
3760
  const stubs = await createFrameworkMarkerStubs();
2685
3761
  let unregisterPaths;
3762
+ let unregisterEsm;
3763
+ const unregisterTsx = (0, import_api.register)();
3764
+ const release = async () => {
3765
+ unregisterPaths?.();
3766
+ unregisterTsx();
3767
+ await unregisterEsm?.();
3768
+ await stubs.cleanup();
3769
+ };
2686
3770
  try {
2687
- const tsconfig = await findNearestFile((0, import_node_path4.dirname)(entry), "tsconfig.json");
3771
+ const tsconfig = await findNearestFile((0, import_node_path6.dirname)(entry), "tsconfig.json");
3772
+ unregisterEsm = (0, import_api2.register)(tsconfig ? { tsconfig } : {});
2688
3773
  unregisterPaths = tsconfig ? await registerTsconfigPaths(tsconfig) : void 0;
2689
- const module2 = (0, import_api.require)(entry, (0, import_node_path4.join)((0, import_node_path4.dirname)(entry), "kortyx-cli.cjs"));
3774
+ const module2 = (0, import_node_module.createRequire)((0, import_node_path6.join)((0, import_node_path6.dirname)(entry), "kortyx-cli.cjs"))(entry);
2690
3775
  if (!isRecord(module2)) {
2691
3776
  throw new Error(`Entry module did not export an object: ${entry}`);
2692
3777
  }
2693
- return module2;
3778
+ return { module: module2, release };
3779
+ } catch (error) {
3780
+ await release();
3781
+ throw error;
3782
+ }
3783
+ };
3784
+ var importEntry = async (entry) => {
3785
+ const loaded = await openEntry(entry);
3786
+ try {
3787
+ return loaded.module;
2694
3788
  } finally {
2695
- unregisterPaths?.();
2696
- await stubs.cleanup();
3789
+ await loaded.release();
2697
3790
  }
2698
3791
  };
2699
3792
  var findNearestFile = async (startDirectory, filename) => {
2700
3793
  let current = startDirectory;
2701
3794
  for (; ; ) {
2702
- const candidate = (0, import_node_path4.join)(current, filename);
3795
+ const candidate = (0, import_node_path6.join)(current, filename);
2703
3796
  try {
2704
- await (0, import_promises4.access)(candidate);
3797
+ await (0, import_promises5.access)(candidate);
2705
3798
  return candidate;
2706
3799
  } catch {
2707
- const parent = (0, import_node_path4.dirname)(current);
3800
+ const parent = (0, import_node_path6.dirname)(current);
2708
3801
  if (parent === current) return void 0;
2709
3802
  current = parent;
2710
3803
  }
2711
3804
  }
2712
3805
  };
2713
3806
  var registerTsconfigPaths = async (tsconfigPath) => {
2714
- const raw = JSON.parse(await (0, import_promises4.readFile)(tsconfigPath, "utf8"));
3807
+ const parsed = import_typescript2.default.parseConfigFileTextToJson(
3808
+ tsconfigPath,
3809
+ await (0, import_promises5.readFile)(tsconfigPath, "utf8")
3810
+ );
3811
+ if (parsed.error)
3812
+ throw new Error(`Invalid TypeScript configuration: ${tsconfigPath}`);
3813
+ const raw = parsed.config;
2715
3814
  if (!isRecord(raw)) return () => void 0;
2716
3815
  const compilerOptions = isRecord(raw.compilerOptions) ? raw.compilerOptions : void 0;
2717
3816
  const paths = isRecord(compilerOptions?.paths) ? compilerOptions.paths : void 0;
2718
3817
  if (!paths) return () => void 0;
2719
- const tsconfigDirectory = (0, import_node_path4.dirname)(tsconfigPath);
2720
- const baseUrl = typeof compilerOptions?.baseUrl === "string" ? (0, import_node_path4.resolve)(tsconfigDirectory, compilerOptions.baseUrl) : tsconfigDirectory;
3818
+ const tsconfigDirectory = (0, import_node_path6.dirname)(tsconfigPath);
3819
+ const baseUrl = typeof compilerOptions?.baseUrl === "string" ? (0, import_node_path6.resolve)(tsconfigDirectory, compilerOptions.baseUrl) : tsconfigDirectory;
2721
3820
  const mappings = Object.entries(paths).flatMap(([pattern, rawTargets]) => {
2722
3821
  if (!Array.isArray(rawTargets)) return [];
2723
3822
  return rawTargets.filter((target) => typeof target === "string").map((target) => ({ pattern, target }));
@@ -2746,7 +3845,7 @@ var registerTsconfigPaths = async (tsconfigPath) => {
2746
3845
  request.length - mapping.patternSuffix.length
2747
3846
  ) : "";
2748
3847
  const mapped = mapping.wildcard ? `${mapping.targetPrefix}${wildcardValue}${mapping.targetSuffix}` : mapping.target;
2749
- for (const candidate of candidateModulePaths((0, import_node_path4.resolve)(baseUrl, mapped))) {
3848
+ for (const candidate of candidateModulePaths((0, import_node_path6.resolve)(baseUrl, mapped))) {
2750
3849
  try {
2751
3850
  return originalResolve.call(this, candidate, parent, isMain, options);
2752
3851
  } catch {
@@ -2767,10 +3866,10 @@ var candidateModulePaths = (basePath) => [
2767
3866
  `${basePath}.cts`,
2768
3867
  `${basePath}.js`,
2769
3868
  `${basePath}.jsx`,
2770
- (0, import_node_path4.join)(basePath, "index.ts"),
2771
- (0, import_node_path4.join)(basePath, "index.tsx"),
2772
- (0, import_node_path4.join)(basePath, "index.js"),
2773
- (0, import_node_path4.join)(basePath, "index.jsx")
3869
+ (0, import_node_path6.join)(basePath, "index.ts"),
3870
+ (0, import_node_path6.join)(basePath, "index.tsx"),
3871
+ (0, import_node_path6.join)(basePath, "index.js"),
3872
+ (0, import_node_path6.join)(basePath, "index.jsx")
2774
3873
  ];
2775
3874
  var valueFromModule = (module2, exportName) => {
2776
3875
  if (exportName) {
@@ -2800,7 +3899,7 @@ var projectSnapshots = async (value, options) => {
2800
3899
  }
2801
3900
  const knownWorkflowIds = workflows.map((workflow) => workflow.id);
2802
3901
  return workflows.map(
2803
- (workflow) => (0, import_agent.projectWorkflowTopology)({
3902
+ (workflow) => (0, import_agent2.projectWorkflowTopology)({
2804
3903
  workflow,
2805
3904
  environment: options.environment,
2806
3905
  service: options.service,
@@ -2928,12 +4027,30 @@ var runTopologyPush = async (rawOptions) => {
2928
4027
  printResults(results, options);
2929
4028
  };
2930
4029
  var createCliProgram = () => {
2931
- const program = new import_commander4.Command().name("kortyx").description("Build, observe, and operate Kortyx projects.").showHelpAfterError().addHelpText(
4030
+ const program = new import_commander6.Command().name("kortyx").description("Build, observe, and operate Kortyx projects.").showHelpAfterError().addHelpText(
2932
4031
  "after",
2933
4032
  '\nRun "kortyx <command> --help" for detailed command options.'
2934
4033
  );
2935
4034
  program.addCommand(createStudioCommand());
2936
4035
  program.addCommand(createConnectionsCommand());
4036
+ let releaseEvalEntry;
4037
+ program.addCommand(
4038
+ createLocalEvalsCommand({
4039
+ load: async (entry, envFiles) => {
4040
+ Object.assign(
4041
+ process.env,
4042
+ await loadEvalEnvironment(process.cwd(), envFiles)
4043
+ );
4044
+ const loaded = await openEntry(entry);
4045
+ releaseEvalEntry = loaded.release;
4046
+ return loaded.module;
4047
+ },
4048
+ release: async () => {
4049
+ await releaseEvalEntry?.();
4050
+ releaseEvalEntry = void 0;
4051
+ }
4052
+ })
4053
+ );
2937
4054
  const topology = program.command("topology").description("Publish deterministic workflow topology to Studio.");
2938
4055
  topology.command("push").description("Project and publish workflow topology.").requiredOption(
2939
4056
  "--entry <path>",
@@ -2971,7 +4088,7 @@ var main = async () => {
2971
4088
  try {
2972
4089
  await program.parseAsync(process.argv);
2973
4090
  } catch (error) {
2974
- if (error instanceof import_commander4.CommanderError && error.exitCode === 0) return;
4091
+ if (error instanceof import_commander6.CommanderError && error.exitCode === 0) return;
2975
4092
  if (error instanceof StudioReadError) {
2976
4093
  console.error(
2977
4094
  process.argv.includes("--json") ? JSON.stringify(error.toJSON()) : `[${error.code}] ${error.message}`