@kortyx/cli 0.11.4 → 0.12.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:
@@ -265,6 +275,12 @@ services:
265
275
  NODE_ENV: production
266
276
  API_HOST: 0.0.0.0
267
277
  API_PORT: 6400
278
+ KORTYX_EVAL_JUDGE_MODEL: \${KORTYX_EVAL_JUDGE_MODEL:-}
279
+ KORTYX_EVAL_JUDGE_API_KEY: \${KORTYX_EVAL_JUDGE_API_KEY:-}
280
+ KORTYX_EVAL_JUDGE_API: \${KORTYX_EVAL_JUDGE_API:-responses}
281
+ KORTYX_EVAL_JUDGE_BASE_URL: \${KORTYX_EVAL_JUDGE_BASE_URL:-}
282
+ KORTYX_EVAL_JUDGE_ID: \${KORTYX_EVAL_JUDGE_ID:-}
283
+ KORTYX_EVAL_JUDGE_VERSION: \${KORTYX_EVAL_JUDGE_VERSION:-}
268
284
  KORTYX_STUDIO_RELEASE: \${KORTYX_STUDIO_IMAGE_TAG:-latest}
269
285
  ports:
270
286
  - "127.0.0.1:\${API_PORT:-6400}:6400"
@@ -838,48 +854,1028 @@ var createConnectionsCommand = (log = console.log, request = fetch) => {
838
854
  print({ connection: profile, verified: true }, options);
839
855
  }
840
856
  );
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)) {
857
+ common(
858
+ command.command("use <name>").description(
859
+ "Select the default connection; agents should prefer per-command --connection."
860
+ )
861
+ ).action(async (name, options) => {
862
+ const config = await readConnections(options.configHome);
863
+ if (name === "local" ? !await readStudioConfig(options.home) : !config.profiles.some((profile) => profile.name === name)) {
864
+ throw new StudioReadError(
865
+ "unknown_connection",
866
+ "Connection not found. Run kortyx connections list."
867
+ );
868
+ }
869
+ config.current = name;
870
+ await saveConnections(config, options.configHome);
871
+ print({ current: name }, options);
872
+ });
873
+ common(
874
+ command.command("remove <name>").description(
875
+ "Remove a saved locator, not its credential or remote project."
876
+ )
877
+ ).action(async (name, options) => {
878
+ const config = await readConnections(options.configHome);
879
+ if (!config.profiles.some((profile) => profile.name === name))
880
+ throw new StudioReadError(
881
+ "unknown_connection",
882
+ "Saved connection not found; managed local cannot be removed."
883
+ );
884
+ config.profiles = config.profiles.filter(
885
+ (profile) => profile.name !== name
886
+ );
887
+ if (config.current === name) delete config.current;
888
+ await saveConnections(config, options.configHome);
889
+ print({ removed: name }, options);
890
+ });
891
+ return command;
892
+ };
893
+
894
+ // src/evals/command.ts
895
+ var import_node_path3 = require("path");
896
+ var import_agent = require("@kortyx/agent");
897
+ var import_commander2 = require("commander");
898
+
899
+ // src/evals/reporter.ts
900
+ var text = (value) => value.replace(/[\p{Cc}\p{Cf}]/gu, " ").trim();
901
+ var duration = (ms) => ms < 1e3 ? `${Math.round(ms)} ms` : `${(ms / 1e3).toFixed(2)} s`;
902
+ var wide = /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe6f\uff01-\uff60\uffe0-\uffe6]|\p{Extended_Pictographic}/u;
903
+ var fit = (value, columns) => {
904
+ const clean = value.replace(/[\p{Cc}\p{Cf}]/gu, " ").trimEnd();
905
+ let used = 0;
906
+ let result = "";
907
+ for (const char of clean) {
908
+ used += /\p{Mark}/u.test(char) ? 0 : wide.test(char) ? 2 : 1;
909
+ if (used > columns - 2) return `${result}\u2026`;
910
+ result += char;
911
+ }
912
+ return result;
913
+ };
914
+ var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
915
+ var phaseNames = {
916
+ params: "Preparing inputs",
917
+ setup: "Setting up test",
918
+ execute: "Running workflow",
919
+ responder: "Resolving human input",
920
+ reference: "Loading reference",
921
+ grading: "Checking expectations",
922
+ cleanup: "Cleaning up",
923
+ reporting: "Reporting results"
924
+ };
925
+ function createEvalTerminalReporter(write, color, options = {}) {
926
+ const now = options.now ?? Date.now;
927
+ const columns = () => Math.max(12, options.columns?.() ?? process.stdout.columns ?? 100);
928
+ const paint = (code, value) => color ? `\x1B[${code}m${value}\x1B[0m` : value;
929
+ const active = /* @__PURE__ */ new Map();
930
+ let names = /* @__PURE__ */ new Map();
931
+ let steps = /* @__PURE__ */ new Map();
932
+ let completed = 0;
933
+ let total = 0;
934
+ let started = 0;
935
+ let frame = 0;
936
+ let liveLines = 0;
937
+ let paused = false;
938
+ let running = false;
939
+ let timer;
940
+ const key = (id, repetition) => JSON.stringify([id, repetition]);
941
+ const clear = () => {
942
+ if (liveLines) write(`\x1B[${liveLines}A\r\x1B[0J`);
943
+ liveLines = 0;
944
+ };
945
+ const line = (value = "") => {
946
+ clear();
947
+ write(`${value}
948
+ `);
949
+ };
950
+ const progressBar = (done, count, width, pulse = false) => {
951
+ const filled = count ? Math.min(width, Math.floor(done / count * width)) : 0;
952
+ const remaining = width - filled;
953
+ const pending = pulse && remaining > 0 ? Array.from(
954
+ { length: remaining },
955
+ (_, index) => index === frame % remaining ? "\u2578" : "\u2591"
956
+ ).join("") : "\u2591".repeat(remaining);
957
+ return `${"\u2501".repeat(filled)}${pending}`;
958
+ };
959
+ const render = () => {
960
+ if (!color || !running || paused) return;
961
+ clear();
962
+ const width = columns();
963
+ const barWidth = Math.max(6, Math.min(22, width - 60));
964
+ const percent = total ? Math.floor(completed / total * 100) : 0;
965
+ const lines = [
966
+ paint(
967
+ 36,
968
+ fit(
969
+ ` RUN ${progressBar(completed, total, barWidth)} ${completed}/${total || "?"} \xB7 ${percent}%`,
970
+ width
971
+ )
972
+ ),
973
+ paint(
974
+ 90,
975
+ fit(
976
+ ` ${active.size} running \xB7 ${Math.max(0, total - completed - active.size)} queued \xB7 ${((now() - started) / 1e3).toFixed(1)}s elapsed`,
977
+ width
978
+ )
979
+ )
980
+ ];
981
+ for (const item of [...active.values()].slice(0, 6)) {
982
+ const name = names.get(item.caseId) ?? item.caseId;
983
+ lines.push(
984
+ paint(
985
+ 33,
986
+ fit(
987
+ ` ${frames[frame % frames.length]} ${name} #${item.repetition}`,
988
+ width
989
+ )
990
+ )
991
+ );
992
+ const stepCount = steps.get(item.caseId) ?? 1;
993
+ const step = item.stepIndex === void 0 ? "" : `Step ${item.stepIndex + 1}/${stepCount} \xB7 `;
994
+ lines.push(
995
+ paint(
996
+ item.phase === "grading" ? 35 : 90,
997
+ fit(
998
+ ` ${progressBar(item.completedSteps, stepCount, 6, true)} ${step}${phaseNames[item.phase]} \xB7 ${((now() - item.started) / 1e3).toFixed(1)}s`,
999
+ width
1000
+ )
1001
+ )
1002
+ );
1003
+ }
1004
+ if (active.size > 6)
1005
+ lines.push(paint(90, ` +${active.size - 6} more running`));
1006
+ write(`${lines.join("\n")}
1007
+ `);
1008
+ liveLines = lines.length;
1009
+ frame++;
1010
+ };
1011
+ const stop = () => {
1012
+ if (timer) clearInterval(timer);
1013
+ timer = void 0;
1014
+ running = false;
1015
+ clear();
1016
+ };
1017
+ return {
1018
+ start(suite) {
1019
+ stop();
1020
+ names = new Map(
1021
+ suite.cases.map((item) => [item.id, item.name ?? item.id])
1022
+ );
1023
+ steps = new Map(suite.cases.map((item) => [item.id, item.steps.length]));
1024
+ active.clear();
1025
+ completed = total = frame = 0;
1026
+ paused = false;
1027
+ started = now();
1028
+ line();
1029
+ if (color)
1030
+ line(paint(35, " \u2501".padEnd(Math.min(columns() - 1, 64), "\u2501")));
1031
+ line(paint("1;35", `\u25C6 Kortyx Evals \xB7 ${text(suite.name ?? suite.id)}`));
1032
+ line(paint(90, ` ${text(suite.id)} \xB7 local execution`));
1033
+ line();
1034
+ running = true;
1035
+ if (color) {
1036
+ timer = setInterval(render, 100);
1037
+ timer.unref();
1038
+ }
1039
+ },
1040
+ progress(event) {
1041
+ if (event.type === "run-started") {
1042
+ total = event.caseIds.length * event.repetitions;
1043
+ line(
1044
+ paint(
1045
+ 90,
1046
+ ` ${event.caseIds.length} ${event.caseIds.length === 1 ? "test" : "tests"} \xB7 ${total} ${total === 1 ? "attempt" : "attempts"} \xB7 concurrency ${event.concurrency}`
1047
+ )
1048
+ );
1049
+ line();
1050
+ } else if (event.type === "case-started") {
1051
+ active.set(key(event.caseId, event.repetition), {
1052
+ caseId: event.caseId,
1053
+ repetition: event.repetition,
1054
+ started: now(),
1055
+ phase: "params",
1056
+ completedSteps: 0
1057
+ });
1058
+ if (!color)
1059
+ line(
1060
+ ` \u2192 ${text(names.get(event.caseId) ?? event.caseId)} [${text(event.caseId)}] #${event.repetition} RUNNING`
1061
+ );
1062
+ } else if (event.type === "case-progress") {
1063
+ const item = active.get(key(event.caseId, event.repetition));
1064
+ if (item) {
1065
+ item.phase = event.phase;
1066
+ item.stepIndex = event.stepIndex;
1067
+ }
1068
+ } else if (event.type === "step-completed") {
1069
+ const item = active.get(key(event.caseId, event.repetition));
1070
+ if (item) item.completedSteps = event.step.index + 1;
1071
+ const passed = event.step.status === "passed";
1072
+ const criterionCount = event.step.criteria.length;
1073
+ const detail = criterionCount ? ` \xB7 ${event.step.criteria.filter((criterion) => criterion.passed).length}/${criterionCount} criteria passed` : "";
1074
+ line(
1075
+ ` ${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}`
1076
+ );
1077
+ } else {
1078
+ const item = event.result;
1079
+ active.delete(key(item.caseId, item.repetition));
1080
+ completed++;
1081
+ const passed = item.status === "passed";
1082
+ const cancelled = item.status === "cancelled";
1083
+ const code = passed ? 32 : cancelled || item.status === "ungraded" ? 33 : 31;
1084
+ const symbol = passed ? "\u2713" : cancelled ? "\u2212" : item.status === "ungraded" ? "\u25C7" : "\u2717";
1085
+ line(
1086
+ ` ${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())}`
1087
+ );
1088
+ line();
1089
+ }
1090
+ render();
1091
+ },
1092
+ finish(result) {
1093
+ stop();
1094
+ for (const item of result.cases) {
1095
+ for (const step of item.steps) {
1096
+ if (step.status === "passed") continue;
1097
+ line();
1098
+ line(
1099
+ paint(
1100
+ 31,
1101
+ ` ${text(names.get(item.caseId) ?? item.caseId)} \u203A attempt ${item.repetition} \u203A step ${step.index + 1}`
1102
+ )
1103
+ );
1104
+ line(
1105
+ ` Expected ${step.expectation.type} \xB7 observed ${step.observation.type}`
1106
+ );
1107
+ if (step.reason) line(` ${text(step.reason)}`);
1108
+ for (const criterion of step.criteria.filter(
1109
+ (value) => !value.passed
1110
+ )) {
1111
+ line(` \u2717 ${text(criterion.text)}`);
1112
+ line(` ${text(criterion.reason)}`);
1113
+ for (const evidence of criterion.evidence)
1114
+ line(paint(90, ` Evidence: ${text(evidence)}`));
1115
+ }
1116
+ }
1117
+ for (const issue of item.errors)
1118
+ line(
1119
+ ` ${text(item.caseId)} \xB7 ${issue.phase} [${issue.code}] ${text(issue.message)}`
1120
+ );
1121
+ }
1122
+ for (const issue of result.errors)
1123
+ line(` ${issue.phase} [${issue.code}] ${text(issue.message)}`);
1124
+ const successful = result.status === "passed";
1125
+ const code = successful ? 32 : result.status === "cancelled" || result.status === "ungraded" ? 33 : 31;
1126
+ line();
1127
+ if (color) {
1128
+ line(
1129
+ paint(
1130
+ code,
1131
+ ` ${progressBar(result.cases.length, result.cases.length, Math.min(22, columns() - 3))} ${result.cases.length}/${result.cases.length}`
1132
+ )
1133
+ );
1134
+ line(
1135
+ paint(
1136
+ `${code};1`,
1137
+ ` ${successful ? "\u2713 SUITE PASSED" : `\u25C7 SUITE ${result.status.toUpperCase()}`}`
1138
+ )
1139
+ );
1140
+ }
1141
+ line(
1142
+ ` ${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`)}` : ""}`
1143
+ );
1144
+ line(
1145
+ paint(
1146
+ 90,
1147
+ ` Duration ${duration(result.durationMs)} \xB7 ${result.cases.length} ${result.cases.length === 1 ? "attempt" : "attempts"}`
1148
+ )
1149
+ );
1150
+ line();
1151
+ },
1152
+ pause() {
1153
+ paused = true;
1154
+ clear();
1155
+ },
1156
+ resume() {
1157
+ paused = false;
1158
+ render();
1159
+ },
1160
+ close: stop
1161
+ };
1162
+ }
1163
+
1164
+ // src/evals/command.ts
1165
+ var integer = (value) => {
1166
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1)
1167
+ throw new import_commander2.InvalidArgumentError("Use a positive integer.");
1168
+ return Number(value);
1169
+ };
1170
+ var isRunner = (value) => value !== null && typeof value === "object" && "run" in value && typeof value.run === "function" && "describe" in value && typeof value.describe === "function";
1171
+ function createLocalEvalsCommand(runtime) {
1172
+ const write = runtime.write ?? process.stdout.write.bind(process.stdout);
1173
+ const exitCode = runtime.setExitCode ?? ((value) => {
1174
+ process.exitCode = value;
1175
+ });
1176
+ const color = runtime.color ?? Boolean(
1177
+ process.stdout.isTTY && process.env.NO_COLOR === void 0 && process.env.FORCE_COLOR !== "0"
1178
+ );
1179
+ const load = async (options) => {
1180
+ const module2 = await runtime.load(
1181
+ (0, import_node_path3.resolve)(runtime.cwd ?? process.cwd(), options.entry),
1182
+ options.env
1183
+ );
1184
+ const selected = options.export ? module2[options.export] : module2.evals ?? module2.default;
1185
+ if (!isRunner(selected))
1186
+ throw new Error(
1187
+ `Export ${options.export ?? '"evals" or default'} must be a createEvals instance.`
1188
+ );
1189
+ const suites = selected.describe().suites.map((suite) => (0, import_agent.parseEvalSuite)(suite));
1190
+ if (!suites.length) throw new Error("No eval suites are configured.");
1191
+ return { runner: selected, suites };
1192
+ };
1193
+ const withRunner = async (options, action) => {
1194
+ try {
1195
+ await action(await load(options));
1196
+ } finally {
1197
+ await runtime.release?.();
1198
+ }
1199
+ };
1200
+ const entryOptions = (command) => command.requiredOption(
1201
+ "--entry <path>",
1202
+ "Module exporting your createEvals instance (TypeScript or JavaScript)."
1203
+ ).option("--export <name>", "Named export; defaults to evals or default.").option(
1204
+ "--env <path>",
1205
+ "Load an env file before the entry; repeat for overlays. Replaces automatic env loading.",
1206
+ (value, previous) => [...previous, value],
1207
+ []
1208
+ ).option(
1209
+ "--json",
1210
+ "Print machine-readable JSON instead of the terminal report."
1211
+ );
1212
+ const root = new import_commander2.Command("evals").description(
1213
+ "Run application evals locally, without Studio."
1214
+ );
1215
+ entryOptions(
1216
+ root.command("list").description("List suites exported by an application module.")
1217
+ ).action(
1218
+ (options) => withRunner(options, async ({ suites }) => {
1219
+ write(
1220
+ options.json ? `${JSON.stringify(suites)}
1221
+ ` : suites.map(
1222
+ (suite) => `${suite.id} ${suite.name ?? suite.id} ${suite.cases.length} cases
1223
+ `
1224
+ ).join("")
1225
+ );
1226
+ })
1227
+ );
1228
+ entryOptions(
1229
+ root.command("run").description(
1230
+ "Execute suites and report progress, failures and results locally."
1231
+ )
1232
+ ).option(
1233
+ "--suite <id>",
1234
+ "Run one suite; omitted runs all suites in definition order."
1235
+ ).option(
1236
+ "--case <id...>",
1237
+ "Select cases within --suite (space-separated IDs)."
1238
+ ).option("--repetitions <count>", "Override attempts per case.", integer).option(
1239
+ "--concurrency <count>",
1240
+ "Override concurrent cases within each suite.",
1241
+ integer
1242
+ ).option("--no-color", "Disable terminal colors and live progress.").option("--color", "Force terminal colors and live progress.").action(
1243
+ (options, command) => withRunner(options, async ({ runner, suites }) => {
1244
+ const selected = options.suite ? suites.filter((suite) => suite.id === options.suite) : suites;
1245
+ if (!selected.length)
1246
+ throw new Error(`Unknown eval suite: ${options.suite}`);
1247
+ if (options.case && (!options.suite || new Set(options.case).size !== options.case.length || options.case.some(
1248
+ (id) => !selected[0]?.cases.some((item) => item.id === id)
1249
+ )))
1250
+ throw new Error(
1251
+ "--case requires --suite and unique existing case IDs."
1252
+ );
1253
+ const controller = new AbortController();
1254
+ const removeInterrupt = (runtime.onInterrupt ?? ((abort) => {
1255
+ process.once("SIGINT", abort);
1256
+ return () => {
1257
+ process.removeListener("SIGINT", abort);
1258
+ };
1259
+ }))(() => controller.abort());
1260
+ const interactive = options.color && !options.json && (color || command.getOptionValueSource("color") === "cli");
1261
+ const reporter = createEvalTerminalReporter(write, interactive);
1262
+ const restoreOutput = [];
1263
+ if (!runtime.write && interactive) {
1264
+ for (const stream of [process.stdout, process.stderr]) {
1265
+ const original = stream.write;
1266
+ const wrapped = new Proxy(original, {
1267
+ apply(target, receiver, args) {
1268
+ reporter.pause();
1269
+ const written = Reflect.apply(target, receiver, args);
1270
+ if (String(args[0]).endsWith("\n")) reporter.resume();
1271
+ return written;
1272
+ }
1273
+ });
1274
+ stream.write = wrapped;
1275
+ restoreOutput.push(() => {
1276
+ if (stream.write === wrapped) stream.write = original;
1277
+ });
1278
+ }
1279
+ }
1280
+ const runs = [];
1281
+ try {
1282
+ for (const suite of selected) {
1283
+ if (controller.signal.aborted) break;
1284
+ if (!options.json) reporter.start(suite);
1285
+ const result = await runner.run({
1286
+ suiteId: suite.id,
1287
+ signal: controller.signal,
1288
+ includeActivity: !options.json,
1289
+ ...options.case ? { caseIds: options.case } : {},
1290
+ ...options.repetitions !== void 0 ? { repetitions: options.repetitions } : {},
1291
+ ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {},
1292
+ ...!options.json ? { onProgress: reporter.progress } : {}
1293
+ });
1294
+ runs.push(result);
1295
+ if (!options.json) reporter.finish(result);
1296
+ }
1297
+ const counts = { passed: 0, failed: 0, error: 0, cancelled: 0 };
1298
+ for (const run2 of runs)
1299
+ for (const key of Object.keys(counts))
1300
+ counts[key] += run2.counts[key];
1301
+ const cancelled = controller.signal.aborted || counts.cancelled > 0;
1302
+ const passed = runs.length === selected.length && runs.every((run2) => run2.status === "passed");
1303
+ const status = cancelled ? "cancelled" : counts.error || runs.some((run2) => run2.status === "error") ? "error" : passed ? "passed" : "failed";
1304
+ if (options.json)
1305
+ write(
1306
+ `${JSON.stringify({ schemaVersion: 1, status, counts, runs })}
1307
+ `
1308
+ );
1309
+ else if (selected.length > 1)
1310
+ write(
1311
+ `Overall ${status.toUpperCase()} \xB7 ${runs.length}/${selected.length} suites \xB7 ${counts.passed} passed \xB7 ${counts.failed} failed \xB7 ${counts.error} errors
1312
+ `
1313
+ );
1314
+ exitCode(cancelled ? 130 : passed ? 0 : 1);
1315
+ } finally {
1316
+ reporter.close();
1317
+ for (const restore of restoreOutput) restore();
1318
+ removeInterrupt();
1319
+ }
1320
+ })
1321
+ );
1322
+ return root;
1323
+ }
1324
+
1325
+ // src/evals/environment.ts
1326
+ var import_promises3 = require("fs/promises");
1327
+ var import_node_path4 = require("path");
1328
+ var import_node_util = require("util");
1329
+ var exists = async (file) => {
1330
+ try {
1331
+ await (0, import_promises3.lstat)(file);
1332
+ return true;
1333
+ } catch (error) {
1334
+ if (error.code === "ENOENT") return false;
1335
+ throw new Error(`Cannot read eval configuration: ${file}`);
1336
+ }
1337
+ };
1338
+ var findConfiguration = async (cwd) => {
1339
+ let directory = (0, import_node_path4.resolve)(cwd);
1340
+ const directories = [];
1341
+ for (; ; ) {
1342
+ directories.unshift(directory);
1343
+ const profile = (0, import_node_path4.join)(directory, ".env.evals.json");
1344
+ if (await exists(profile)) return { profile, directories };
1345
+ if (await exists((0, import_node_path4.join)(directory, ".git")) || await exists((0, import_node_path4.join)(directory, "pnpm-workspace.yaml")))
1346
+ return { directories };
1347
+ if ((0, import_node_path4.dirname)(directory) === directory)
1348
+ return { directories: [(0, import_node_path4.resolve)(cwd)] };
1349
+ directory = (0, import_node_path4.dirname)(directory);
1350
+ }
1351
+ };
1352
+ var read = async (file, privateFile) => {
1353
+ let stat;
1354
+ try {
1355
+ stat = await (0, import_promises3.lstat)(file);
1356
+ } catch {
1357
+ throw new Error(`Configured eval environment file is unavailable: ${file}`);
1358
+ }
1359
+ if (!stat.isFile() || privateFile && process.platform !== "win32" && ((stat.mode & 63) !== 0 || typeof process.getuid === "function" && stat.uid !== process.getuid()))
1360
+ throw new Error(
1361
+ `Eval configuration must be ${privateFile ? "an owner-only" : "a"} regular file: ${file}`
1362
+ );
1363
+ try {
1364
+ return await (0, import_promises3.readFile)(file, "utf8");
1365
+ } catch {
1366
+ throw new Error(`Cannot read eval configuration: ${file}`);
1367
+ }
1368
+ };
1369
+ async function loadEvalEnvironment(cwd, envFiles, shell = process.env) {
1370
+ let files;
1371
+ let privateFiles = false;
1372
+ if (envFiles?.length) {
1373
+ files = envFiles.map((file) => (0, import_node_path4.resolve)(cwd, file));
1374
+ } else {
1375
+ const { profile, directories } = await findConfiguration(cwd);
1376
+ if (profile) {
1377
+ let value;
1378
+ try {
1379
+ value = JSON.parse(await read(profile, true));
1380
+ } catch (error) {
1381
+ if (error instanceof SyntaxError)
1382
+ throw new Error(`Invalid eval environment profile: ${profile}`);
1383
+ throw error;
1384
+ }
1385
+ 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(
1386
+ (file) => typeof file !== "string" || !file.trim()
1387
+ ))
1388
+ throw new Error(`Invalid eval environment profile: ${profile}`);
1389
+ files = value.envFiles.map(
1390
+ (file) => (0, import_node_path4.resolve)((0, import_node_path4.dirname)(profile), file)
1391
+ );
1392
+ privateFiles = true;
1393
+ } else {
1394
+ files = [];
1395
+ for (const directory of directories)
1396
+ for (const name of [".env", ".env.local"]) {
1397
+ const file = (0, import_node_path4.resolve)(directory, name);
1398
+ if (await exists(file)) files.push(file);
1399
+ }
1400
+ }
1401
+ }
1402
+ const environment = {};
1403
+ for (const file of files)
1404
+ Object.assign(environment, (0, import_node_util.parseEnv)(await read(file, privateFiles)));
1405
+ return { ...environment, ...shell };
1406
+ }
1407
+
1408
+ // src/studio/command.ts
1409
+ var import_commander5 = require("commander");
1410
+
1411
+ // src/studio/eval-command.ts
1412
+ var import_commander3 = require("commander");
1413
+
1414
+ // src/studio/eval-client.ts
1415
+ var import_evals = require("@kortyx/agent/evals");
1416
+ var import_zod3 = require("zod");
1417
+ var StartRequestSchema = import_evals.StudioEvalStartRequestSchema;
1418
+ var parseEvalRunTarget = (input) => {
1419
+ let id = input;
1420
+ let url;
1421
+ if (/^[a-z][a-z0-9+.-]*:/i.test(input)) {
1422
+ let locator;
1423
+ try {
1424
+ locator = new URL(input);
1425
+ } catch {
1426
+ throw new StudioReadError("invalid_target", "Invalid eval run URL.");
1427
+ }
1428
+ const match = locator.pathname.match(/\/evals\/runs\/([^/]+)\/?$/);
1429
+ if (!match || !["http:", "https:"].includes(locator.protocol) || locator.username || locator.password)
1430
+ throw new StudioReadError(
1431
+ "invalid_target",
1432
+ "Expected an HTTP(S) Studio /evals/runs/<uuid> URL without credentials."
1433
+ );
1434
+ id = match[1] ?? "";
1435
+ locator.search = "";
1436
+ locator.hash = "";
1437
+ url = locator.toString();
1438
+ }
1439
+ if (!import_zod3.z.uuid().safeParse(id).success)
1440
+ throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
1441
+ return { id, url };
1442
+ };
1443
+ var StudioEvalClient = class extends StudioApiTransport {
1444
+ targets() {
1445
+ return this.requestJson(
1446
+ "GET",
1447
+ "/v1/studio/evals/targets",
1448
+ import_evals.StudioEvalTargetsResponseSchema
1449
+ );
1450
+ }
1451
+ runs() {
1452
+ return this.requestJson(
1453
+ "GET",
1454
+ "/v1/studio/evals/runs",
1455
+ import_evals.StudioEvalHistorySchema
1456
+ );
1457
+ }
1458
+ run(id) {
1459
+ if (!import_zod3.z.uuid().safeParse(id).success)
1460
+ throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
1461
+ return this.requestJson(
1462
+ "GET",
1463
+ `/v1/studio/evals/runs/${id}`,
1464
+ import_evals.StudioEvalDetailSchema
1465
+ );
1466
+ }
1467
+ start(input) {
1468
+ const parsed = StartRequestSchema.safeParse(input);
1469
+ if (!parsed.success)
1470
+ throw new StudioReadError(
1471
+ "invalid_eval_request",
1472
+ "Invalid suite revision, case selection, or execution limits."
1473
+ );
1474
+ return this.requestJson(
1475
+ "POST",
1476
+ "/v1/studio/evals/runs",
1477
+ import_zod3.z.object({ id: import_zod3.z.uuid() }),
1478
+ {},
1479
+ parsed.data
1480
+ );
1481
+ }
1482
+ cancel(id) {
1483
+ if (!import_zod3.z.uuid().safeParse(id).success)
1484
+ throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
1485
+ return this.requestJson(
1486
+ "POST",
1487
+ `/v1/studio/evals/runs/${id}/cancel`,
1488
+ import_zod3.z.object({ ok: import_zod3.z.literal(true) })
1489
+ );
1490
+ }
1491
+ };
1492
+
1493
+ // src/studio/read-output.ts
1494
+ var parseStudioTarget = (input, entity) => {
1495
+ if (/^[a-z][a-z0-9+.-]*:/i.test(input)) {
1496
+ let url;
1497
+ try {
1498
+ url = new URL(input);
1499
+ } catch {
1500
+ throw new StudioReadError("invalid_target", "Invalid Studio URL.");
1501
+ }
1502
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
1503
+ throw new StudioReadError(
1504
+ "invalid_target",
1505
+ "Use an HTTP(S) Studio entity URL without embedded credentials."
1506
+ );
1507
+ }
1508
+ const match = url.pathname.match(
1509
+ /\/(runs|sessions|interrupts)\/([^/]+)\/?$/
1510
+ );
1511
+ if (!match)
1512
+ throw new StudioReadError(
1513
+ "invalid_target",
1514
+ "Expected a Studio /runs/<id>, /sessions/<id>, or /interrupts/<id> URL."
1515
+ );
1516
+ if (entity && match[1] !== entity)
1517
+ throw new StudioReadError(
1518
+ "invalid_target",
1519
+ "URL entity type does not match the command."
1520
+ );
1521
+ let id;
1522
+ try {
1523
+ id = decodeURIComponent(match[2] ?? "");
1524
+ } catch {
1525
+ throw new StudioReadError(
1526
+ "invalid_target",
1527
+ "Invalid entity ID encoding."
1528
+ );
1529
+ }
1530
+ validateId(id);
1531
+ const selection = {};
1532
+ for (const key of [
1533
+ "tab",
1534
+ "sessionTab",
1535
+ "call",
1536
+ "branch",
1537
+ "node",
1538
+ "event",
1539
+ "trace",
1540
+ "detailView"
1541
+ ]) {
1542
+ const value = url.searchParams.get(key);
1543
+ if (value) selection[key] = value;
1544
+ }
1545
+ url.search = "";
1546
+ url.hash = "";
1547
+ return {
1548
+ entity: match[1],
1549
+ id,
1550
+ url: url.toString(),
1551
+ selection
1552
+ };
1553
+ }
1554
+ if (!entity)
1555
+ throw new StudioReadError(
1556
+ "invalid_target",
1557
+ "inspect requires a Studio entity URL. Use runs/sessions/interrupts get for a bare ID."
1558
+ );
1559
+ validateId(input);
1560
+ return { entity, id: input };
1561
+ };
1562
+ var validateId = (id) => {
1563
+ if (!id || id.length > 512 || id === "." || id === ".." || /[\s/?#\\]/.test(id) || [...id].some(
1564
+ (char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127
1565
+ )) {
1566
+ throw new StudioReadError("invalid_target", "Invalid Studio entity ID.");
1567
+ }
1568
+ };
1569
+ var secretKey = /(?:password|secret|authorization|cookie|apikey|credential|resumetoken|accesstoken|refreshtoken|bearertoken|sessiontoken|authtoken)/i;
1570
+ var contentKey = /^(?:input|output|inputs|outputs|inputmessages|outputmessages|messages|prompt|systemprompt|question|response|result|latestresult|arguments|args|content|text|options)$/i;
1571
+ var sanitizeStudioData = (value, includeContent, key = "") => {
1572
+ const normalized = key.replace(/[^a-z0-9]/gi, "");
1573
+ const keyPolicy = normalized.toLowerCase() === "apikey" && value && typeof value === "object" && "scopes" in value && "mode" in value;
1574
+ if (secretKey.test(normalized) && !keyPolicy) return "[REDACTED]";
1575
+ if (!includeContent && contentKey.test(normalized))
1576
+ return "[CONTENT OMITTED: use --include-content]";
1577
+ if (typeof value === "string")
1578
+ return value.replace(/ktyx_(?:test|live)_[^\s"'<>]+/g, "[REDACTED]").replace(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]");
1579
+ if (Array.isArray(value))
1580
+ return value.map((item) => sanitizeStudioData(item, includeContent));
1581
+ if (value && typeof value === "object")
1582
+ return Object.fromEntries(
1583
+ Object.entries(value).map(([name, item]) => [
1584
+ name,
1585
+ sanitizeStudioData(item, includeContent, name)
1586
+ ])
1587
+ );
1588
+ return value;
1589
+ };
1590
+ var summarizeEvidence = (events) => {
1591
+ const findings = events.flatMap((event) => {
1592
+ const payload = event.payload;
1593
+ const error = payload.error && typeof payload.error === "object" && !Array.isArray(payload.error) ? payload.error : {};
1594
+ const suspension = event.type === "span.failed" && (error.controlFlow === true || ["GraphInterrupt", "NodeInterrupt", "ParallelChildWaiting"].includes(
1595
+ String(error.name)
1596
+ ));
1597
+ const failed = !suspension && (/(?:failed|error|fault)/i.test(event.type) || payload.outcome === "fault" || payload.status === "failed");
1598
+ const warning = suspension || /(?:retry|cancel|interrupt|limit_reached|denied|suspended|waiting)/i.test(
1599
+ event.type
1600
+ ) || ["denied", "cancelled"].includes(String(payload.outcome));
1601
+ if (!failed && !warning) return [];
1602
+ return [
1603
+ {
1604
+ eventId: event.id,
1605
+ runId: event.runId,
1606
+ nodeId: event.nodeId,
1607
+ type: event.type,
1608
+ occurredAt: event.occurredAt,
1609
+ severity: failed ? "error" : "context",
1610
+ evidence: payload
1611
+ }
1612
+ ];
1613
+ });
1614
+ return {
1615
+ findings,
1616
+ 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."
1617
+ };
1618
+ };
1619
+ var formatReadOutput = (value, json) => {
1620
+ if (json) return JSON.stringify(value);
1621
+ return JSON.stringify(value, null, 2);
1622
+ };
1623
+
1624
+ // src/studio/eval-command.ts
1625
+ var integer2 = (max) => (value) => {
1626
+ if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > max)
1627
+ throw new import_commander3.InvalidArgumentError(`Expected an integer between 1 and ${max}.`);
1628
+ return Number(value);
1629
+ };
1630
+ var summary = ({ error: _error, ...run2 }) => run2;
1631
+ var compactDetail = (run2) => ({
1632
+ ...summary(run2),
1633
+ suite: void 0,
1634
+ events: void 0,
1635
+ result: void 0,
1636
+ caseResults: run2.result?.cases.map(
1637
+ ({ caseId, repetition, status, durationMs, steps }) => ({
1638
+ caseId,
1639
+ repetition,
1640
+ status,
1641
+ durationMs,
1642
+ steps: steps.map(({ index, status: status2, criteria }) => ({
1643
+ index,
1644
+ status: status2,
1645
+ criteria: criteria.map(({ id, passed }) => ({ id, passed }))
1646
+ }))
1647
+ })
1648
+ )
1649
+ });
1650
+ function registerStudioEvalCommands(studio, log, request = fetch) {
1651
+ const common = (command) => command.option(
1652
+ "--connection <name>",
1653
+ "Named project/API connection (or KORTYX_CONNECTION)."
1654
+ ).option(
1655
+ "--config-home <path>",
1656
+ "Connection configuration directory.",
1657
+ defaultConnectionsHome()
1658
+ ).option(
1659
+ "--home <path>",
1660
+ "Managed local Studio directory.",
1661
+ defaultStudioHome()
1662
+ ).option("--api-url <url>", "Direct API URL; requires --api-key-env.").option(
1663
+ "--api-key-env <name>",
1664
+ "Environment variable holding a Studio key."
1665
+ ).option("--json", "Print stable machine-readable JSON.").option(
1666
+ "--include-content",
1667
+ "Include suite definitions and captured execution content."
1668
+ );
1669
+ const print = (value, options) => log(
1670
+ formatReadOutput(
1671
+ sanitizeStudioData(
1672
+ { schemaVersion: 1, ...value },
1673
+ options.includeContent ?? false
1674
+ ),
1675
+ options.json ?? false
1676
+ )
1677
+ );
1678
+ const clientFor = async (options, url) => {
1679
+ const connection = await resolveConnection(options, url);
1680
+ return {
1681
+ connection,
1682
+ client: new StudioEvalClient(
1683
+ connection.apiUrl,
1684
+ connection.apiKey,
1685
+ request
1686
+ )
1687
+ };
1688
+ };
1689
+ const evals = studio.command("evals").description(
1690
+ "Discover suites, enqueue evals, inspect saved results and cancel runs."
1691
+ );
1692
+ const suites = evals.command("suites").description("Discover application-owned eval suites.");
1693
+ const discover = async (options) => {
1694
+ const { connection, client } = await clientFor(options);
1695
+ const data = await client.targets();
1696
+ const environment = options.environment ?? connection.environment;
1697
+ const targets = data.targets.filter(
1698
+ (target) => (!options.target || target.id === options.target) && (!environment || target.environment === environment)
1699
+ );
1700
+ return {
1701
+ connection,
1702
+ client,
1703
+ targets,
1704
+ canRun: data.canRun,
1705
+ studioJudge: data.studioJudge ?? null
1706
+ };
1707
+ };
1708
+ const selectionOptions = (command) => common(command).option("--target <id>", "Application eval target ID.").option(
1709
+ "--environment <name>",
1710
+ "Filter targets; defaults to the connection environment."
1711
+ );
1712
+ selectionOptions(
1713
+ suites.command("list").description("List targets, suites, revisions and case IDs.")
1714
+ ).action(async (options) => {
1715
+ const { connection, targets, canRun, studioJudge } = await discover(options);
1716
+ print(
1717
+ {
1718
+ connection: connection.name,
1719
+ canRun,
1720
+ studioJudge,
1721
+ targets: targets.map((target) => ({
1722
+ id: target.id,
1723
+ name: target.name,
1724
+ environment: target.environment,
1725
+ available: target.manifest !== null,
1726
+ appJudge: target.manifest?.judge ?? null,
1727
+ studioJudging: Boolean(target.manifest?.studioJudging),
1728
+ suites: target.manifest?.suites.map((suite) => ({
1729
+ id: suite.id,
1730
+ name: suite.name,
1731
+ revision: target.revisions[suite.id],
1732
+ caseIds: suite.cases.map((item) => item.id)
1733
+ })) ?? []
1734
+ }))
1735
+ },
1736
+ options
1737
+ );
1738
+ });
1739
+ const select = async (suiteId, options) => {
1740
+ const data = await discover(options);
1741
+ const matches = data.targets.flatMap((target) => {
1742
+ const suite = target.manifest?.suites.find((item) => item.id === suiteId);
1743
+ return suite ? [{ target, suite }] : [];
1744
+ });
1745
+ const match = matches[0];
1746
+ if (matches.length !== 1 || !match)
848
1747
  throw new StudioReadError(
849
- "unknown_connection",
850
- "Connection not found. Run kortyx connections list."
1748
+ "invalid_suite_selection",
1749
+ matches.length ? "Suite exists on multiple targets. Select --target and/or --environment." : "Suite unavailable. List suites and check the target connection."
851
1750
  );
852
- }
853
- config.current = name;
854
- await saveConnections(config, options.configHome);
855
- print({ current: name }, options);
1751
+ return { ...data, ...match };
1752
+ };
1753
+ selectionOptions(
1754
+ suites.command("get <suite-id>").description(
1755
+ "Read a suite; --include-content returns its full definition."
1756
+ )
1757
+ ).action(async (suiteId, options) => {
1758
+ const { connection, target, suite } = await select(suiteId, options);
1759
+ print(
1760
+ {
1761
+ connection: connection.name,
1762
+ targetId: target.id,
1763
+ revision: target.revisions[suite.id],
1764
+ suite: options.includeContent ? suite : {
1765
+ id: suite.id,
1766
+ name: suite.name,
1767
+ caseIds: suite.cases.map((item) => item.id)
1768
+ }
1769
+ },
1770
+ options
1771
+ );
856
1772
  });
857
- common(
858
- command.command("remove <name>").description(
859
- "Remove a saved locator, not its credential or remote project."
1773
+ const runs = evals.command("runs").description("Start and inspect saved eval runs.");
1774
+ selectionOptions(
1775
+ runs.command("start <suite-id>").description(
1776
+ "Enqueue once and return immediately; does not wait for grades."
860
1777
  )
861
- ).action(async (name, options) => {
862
- const config = await readConnections(options.configHome);
863
- if (!config.profiles.some((profile) => profile.name === name))
1778
+ ).option(
1779
+ "--case <id>",
1780
+ "Select a case; repeat for multiple cases.",
1781
+ (value, previous) => [...previous, value],
1782
+ []
1783
+ ).option(
1784
+ "--repetitions <count>",
1785
+ "Attempts per selected case (1\u201320).",
1786
+ integer2(20),
1787
+ 1
1788
+ ).option("--concurrency <count>", "Parallel attempts (1\u20134).", integer2(4), 1).option(
1789
+ "--judge <location>",
1790
+ "Judge selection: studio (default) or app.",
1791
+ (value) => {
1792
+ if (value !== "studio" && value !== "app")
1793
+ throw new import_commander3.InvalidArgumentError("Expected studio or app.");
1794
+ return value;
1795
+ },
1796
+ "studio"
1797
+ ).action(async (suiteId, options) => {
1798
+ const { connection, client, target, suite, canRun } = await select(
1799
+ suiteId,
1800
+ options
1801
+ );
1802
+ if (!canRun)
864
1803
  throw new StudioReadError(
865
- "unknown_connection",
866
- "Saved connection not found; managed local cannot be removed."
1804
+ "eval_forbidden",
1805
+ "This Studio key requires eval:run to execute suites."
867
1806
  );
868
- config.profiles = config.profiles.filter(
869
- (profile) => profile.name !== name
1807
+ const caseIds = options.case ?? [];
1808
+ 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)
1809
+ throw new StudioReadError(
1810
+ "invalid_case_selection",
1811
+ "Select unique existing case IDs and at most 100 total attempts."
1812
+ );
1813
+ const run2 = await client.start({
1814
+ targetId: target.id,
1815
+ judge: options.judge ?? "studio",
1816
+ suiteId,
1817
+ suiteRevision: target.revisions[suiteId] ?? "",
1818
+ repetitions: options.repetitions,
1819
+ concurrency: options.concurrency,
1820
+ ...caseIds.length ? { caseIds } : {}
1821
+ });
1822
+ print(
1823
+ {
1824
+ connection: connection.name,
1825
+ id: run2.id,
1826
+ status: "queued",
1827
+ studioUrl: connection.studioUrl ? `${connection.studioUrl}/evals/runs/${run2.id}` : null
1828
+ },
1829
+ options
870
1830
  );
871
- if (config.current === name) delete config.current;
872
- await saveConnections(config, options.configHome);
873
- print({ removed: name }, options);
874
1831
  });
875
- return command;
876
- };
877
-
878
- // src/studio/command.ts
879
- var import_commander3 = require("commander");
1832
+ common(
1833
+ runs.command("list").description("List the latest 100 saved runs in this project.")
1834
+ ).option(
1835
+ "--environment <name>",
1836
+ "Filter results; defaults to the connection environment."
1837
+ ).action(async (options) => {
1838
+ const { connection, client } = await clientFor(options);
1839
+ const data = await client.runs();
1840
+ const environment = options.environment ?? connection.environment;
1841
+ print(
1842
+ {
1843
+ connection: connection.name,
1844
+ runs: data.runs.filter((run2) => !environment || run2.environment === environment).map(summary)
1845
+ },
1846
+ options
1847
+ );
1848
+ });
1849
+ common(
1850
+ runs.command("get <id-or-url>").description(
1851
+ "Read status, counts and criterion verdicts; content is opt-in."
1852
+ )
1853
+ ).action(async (input, options) => {
1854
+ const target = parseEvalRunTarget(input);
1855
+ const { connection, client } = await clientFor(options, target.url);
1856
+ const { run: run2 } = await client.run(target.id);
1857
+ print(
1858
+ {
1859
+ connection: connection.name,
1860
+ run: options.includeContent ? run2 : compactDetail(run2)
1861
+ },
1862
+ options
1863
+ );
1864
+ });
1865
+ common(
1866
+ runs.command("cancel <id-or-url>").description(
1867
+ "Request cooperative cancellation of a queued or running eval."
1868
+ )
1869
+ ).action(async (input, options) => {
1870
+ const target = parseEvalRunTarget(input);
1871
+ const { connection, client } = await clientFor(options, target.url);
1872
+ const data = await client.cancel(target.id);
1873
+ print({ connection: connection.name, id: target.id, ...data }, options);
1874
+ });
1875
+ }
880
1876
 
881
1877
  // src/studio/local-stack.ts
882
- var import_promises3 = require("fs/promises");
1878
+ var import_promises4 = require("fs/promises");
883
1879
  var import_telemetry_contracts3 = require("@kortyx/telemetry-contracts");
884
1880
  var composeArgs = (home, args) => [
885
1881
  "compose",
@@ -1033,9 +2029,20 @@ var startStudio = async (options, runtime) => {
1033
2029
  const studioImage = String(
1034
2030
  environment.KORTYX_STUDIO_IMAGE_REF ?? `${environment.KORTYX_STUDIO_IMAGE ?? "ghcr.io/kortyx-io/kortyx-studio"}:${config.imageTag}`
1035
2031
  );
1036
- await runtime.run("docker", ["pull", apiImage], { inherit: true });
1037
- if (studioImage !== apiImage)
1038
- await runtime.run("docker", ["pull", studioImage], { inherit: true });
2032
+ const pullPolicy = process.env.KORTYX_STUDIO_PULL_POLICY ?? "always";
2033
+ for (const image of /* @__PURE__ */ new Set([apiImage, studioImage])) {
2034
+ if (pullPolicy === "never") continue;
2035
+ if (pullPolicy === "missing") {
2036
+ try {
2037
+ await runtime.run("docker", ["image", "inspect", image], {
2038
+ inherit: false
2039
+ });
2040
+ continue;
2041
+ } catch {
2042
+ }
2043
+ }
2044
+ await runtime.run("docker", ["pull", image], { inherit: true });
2045
+ }
1039
2046
  const capability = await runtime.run(
1040
2047
  "docker",
1041
2048
  [
@@ -1049,7 +2056,7 @@ var startStudio = async (options, runtime) => {
1049
2056
  ],
1050
2057
  { inherit: false }
1051
2058
  );
1052
- await (0, import_promises3.writeFile)(
2059
+ await (0, import_promises4.writeFile)(
1053
2060
  studioComposePath(options.home),
1054
2061
  studioComposeFile(
1055
2062
  process.platform !== "win32" && capability.stdout.trim() === "supported"
@@ -1152,7 +2159,7 @@ var resetStudio = async (home, confirm, runtime) => {
1152
2159
 
1153
2160
  // src/studio/read-command.ts
1154
2161
  var import_telemetry_contracts5 = require("@kortyx/telemetry-contracts");
1155
- var import_commander2 = require("commander");
2162
+ var import_commander4 = require("commander");
1156
2163
 
1157
2164
  // src/studio/read-analysis.ts
1158
2165
  var import_telemetry_contracts4 = require("@kortyx/telemetry-contracts");
@@ -1459,141 +2466,10 @@ var compareRunAnalysis = (left, right) => {
1459
2466
  };
1460
2467
  };
1461
2468
 
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
2469
  // src/studio/read-command.ts
1594
- var integer = (min, max) => (value) => {
2470
+ var integer3 = (min, max) => (value) => {
1595
2471
  if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > max) {
1596
- throw new import_commander2.InvalidArgumentError(
2472
+ throw new import_commander4.InvalidArgumentError(
1597
2473
  `Expected an integer between ${min} and ${max}.`
1598
2474
  );
1599
2475
  }
@@ -1749,7 +2625,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
1749
2625
  const detailOptions = (command) => common(command).option(
1750
2626
  "--event-limit <count>",
1751
2627
  "Maximum latest events and diagnostic findings (1\u201310000).",
1752
- integer(1, 1e4),
2628
+ integer3(1, 1e4),
1753
2629
  100
1754
2630
  ).option(
1755
2631
  "--focus-selection",
@@ -1802,7 +2678,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
1802
2678
  "--environment <name>",
1803
2679
  "Environment filter; overrides the connection default."
1804
2680
  ).addOption(
1805
- new import_commander2.Option(
2681
+ new import_commander4.Option(
1806
2682
  "--range <range>",
1807
2683
  "Studio time range: 1h, 24h, 7d, 30d, all (or Studio preset name)."
1808
2684
  ).choices([
@@ -1847,7 +2723,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
1847
2723
  ).option(
1848
2724
  "--event-limit <count>",
1849
2725
  "Maximum timeline steps returned per run (1\u201310000).",
1850
- integer(1, 1e4),
2726
+ integer3(1, 1e4),
1851
2727
  100
1852
2728
  ).action(
1853
2729
  async (leftInput, rightInput, options) => {
@@ -1947,13 +2823,13 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
1947
2823
  }
1948
2824
  const list = filters(
1949
2825
  group.command("list").description("Read a bounded, filtered page.")
1950
- ).option("--limit <count>", "Page size (1\u2013100).", integer(1, 100), 25).option(
2826
+ ).option("--limit <count>", "Page size (1\u2013100).", integer3(1, 100), 25).option(
1951
2827
  "--cursor <offset>",
1952
2828
  "Numeric offset returned as nextCursor.",
1953
- integer(0, Number.MAX_SAFE_INTEGER),
2829
+ integer3(0, Number.MAX_SAFE_INTEGER),
1954
2830
  0
1955
2831
  ).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([
2832
+ new import_commander4.Option("--tool-outcome <outcome>", "Tool outcome filter.").choices([
1957
2833
  "success",
1958
2834
  "denied",
1959
2835
  "fault",
@@ -2082,14 +2958,14 @@ var defaultStudioRuntime = {
2082
2958
  var portParser = (value) => {
2083
2959
  const port = Number(value);
2084
2960
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
2085
- throw new import_commander3.InvalidArgumentError(
2961
+ throw new import_commander5.InvalidArgumentError(
2086
2962
  "Port must be an integer between 1 and 65535."
2087
2963
  );
2088
2964
  }
2089
2965
  return port;
2090
2966
  };
2091
2967
  var withHome = (command) => command.addOption(
2092
- new import_commander3.Option("--home <path>", "Local Studio state directory.").default(
2968
+ new import_commander5.Option("--home <path>", "Local Studio state directory.").default(
2093
2969
  defaultStudioHome()
2094
2970
  )
2095
2971
  );
@@ -2099,7 +2975,7 @@ var configureOutput = (command, runtime) => command.configureOutput({
2099
2975
  });
2100
2976
  var createStudioCommand = (runtime = defaultStudioRuntime) => {
2101
2977
  const studio = configureOutput(
2102
- new import_commander3.Command("studio").description("Run and manage Kortyx Studio.").showHelpAfterError().addHelpText(
2978
+ new import_commander5.Command("studio").description("Run and manage Kortyx Studio.").showHelpAfterError().addHelpText(
2103
2979
  "after",
2104
2980
  "\nLocal state and generated credentials are stored in ~/.kortyx/studio by default."
2105
2981
  ),
@@ -2142,7 +3018,7 @@ var createStudioCommand = (runtime = defaultStudioRuntime) => {
2142
3018
  "Generate unpersisted credentials for a self-hosted deployment.",
2143
3019
  false
2144
3020
  ).addOption(
2145
- new import_commander3.Option("--format <format>", "Credential output format.").choices(["human", "dotenv"]).default("human")
3021
+ new import_commander5.Option("--format <format>", "Credential output format.").choices(["human", "dotenv"]).default("human")
2146
3022
  ).option(
2147
3023
  "--service-name <name>",
2148
3024
  "SDK service name used by dotenv output.",
@@ -2184,20 +3060,29 @@ var createStudioCommand = (runtime = defaultStudioRuntime) => {
2184
3060
  ).action(async ({ home, confirm }) => {
2185
3061
  await resetStudio(home, confirm, runtime);
2186
3062
  });
2187
- registerStudioReadCommands(studio, (message) => runtime.log(message));
3063
+ registerStudioReadCommands(
3064
+ studio,
3065
+ (message) => runtime.log(message),
3066
+ runtime.request
3067
+ );
3068
+ registerStudioEvalCommands(
3069
+ studio,
3070
+ (message) => runtime.log(message),
3071
+ runtime.request
3072
+ );
2188
3073
  return studio;
2189
3074
  };
2190
3075
 
2191
3076
  // src/workflow-calls.ts
2192
- var import_node_path3 = require("path");
3077
+ var import_node_path5 = require("path");
2193
3078
  var import_typescript = __toESM(require("typescript"));
2194
3079
  function discoverWorkflowCalls(entry, snapshots) {
2195
- const configPath = import_typescript.default.findConfigFile((0, import_node_path3.dirname)(entry), import_typescript.default.sys.fileExists);
3080
+ const configPath = import_typescript.default.findConfigFile((0, import_node_path5.dirname)(entry), import_typescript.default.sys.fileExists);
2196
3081
  const config = configPath ? import_typescript.default.readConfigFile(configPath, import_typescript.default.sys.readFile).config : {};
2197
3082
  const parsed = import_typescript.default.parseJsonConfigFileContent(
2198
3083
  config ?? {},
2199
3084
  import_typescript.default.sys,
2200
- configPath ? (0, import_node_path3.dirname)(configPath) : (0, import_node_path3.dirname)(entry)
3085
+ configPath ? (0, import_node_path5.dirname)(configPath) : (0, import_node_path5.dirname)(entry)
2201
3086
  );
2202
3087
  const program = import_typescript.default.createProgram([entry], {
2203
3088
  ...parsed.options,
@@ -2238,7 +3123,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2238
3123
  );
2239
3124
  };
2240
3125
  const factoryBindings = /* @__PURE__ */ new WeakMap();
2241
- function resolve4(e, bindings, seen = /* @__PURE__ */ new Set()) {
3126
+ function resolve6(e, bindings, seen = /* @__PURE__ */ new Set()) {
2242
3127
  if (!e) return;
2243
3128
  e = unwrap(e);
2244
3129
  if (seen.has(e)) return;
@@ -2246,18 +3131,18 @@ function discoverWorkflowCalls(entry, snapshots) {
2246
3131
  if (import_typescript.default.isIdentifier(e)) {
2247
3132
  const s = symbol(e);
2248
3133
  const bound = s && bindings.get(s);
2249
- if (bound) return resolve4(bound, bindings, seen);
3134
+ if (bound) return resolve6(bound, bindings, seen);
2250
3135
  const d = s?.valueDeclaration;
2251
3136
  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);
3137
+ return resolve6(d.initializer, bindings, seen);
2253
3138
  }
2254
3139
  if (import_typescript.default.isPropertyAccessExpression(e)) {
2255
3140
  const found = property(e.expression, e.name.text, bindings, seen);
2256
- if (found) return resolve4(found, bindings, seen);
3141
+ if (found) return resolve6(found, bindings, seen);
2257
3142
  const owner = symbol(e.expression);
2258
3143
  const declaration = symbol(e.name)?.valueDeclaration;
2259
3144
  if (owner && owner.flags & import_typescript.default.SymbolFlags.Module && declaration && import_typescript.default.isVariableDeclaration(declaration))
2260
- return resolve4(declaration.initializer, bindings, seen);
3145
+ return resolve6(declaration.initializer, bindings, seen);
2261
3146
  return e;
2262
3147
  }
2263
3148
  if (import_typescript.default.isElementAccessExpression(e) && e.argumentExpression && import_typescript.default.isStringLiteralLike(e.argumentExpression)) {
@@ -2267,7 +3152,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2267
3152
  bindings,
2268
3153
  seen
2269
3154
  );
2270
- return found ? resolve4(found, bindings, seen) : e;
3155
+ return found ? resolve6(found, bindings, seen) : e;
2271
3156
  }
2272
3157
  if (import_typescript.default.isCallExpression(e)) {
2273
3158
  const d = symbol(
@@ -2281,12 +3166,12 @@ function discoverWorkflowCalls(entry, snapshots) {
2281
3166
  const key = symbol(p.name);
2282
3167
  const arg = e.arguments[i] ?? p.initializer;
2283
3168
  if (key && arg)
2284
- bound.set(key, resolve4(arg, bindings, new Set(seen)) ?? arg);
3169
+ bound.set(key, resolve6(arg, bindings, new Set(seen)) ?? arg);
2285
3170
  }
2286
3171
  });
2287
3172
  const returned = import_typescript.default.isBlock(fn.body) ? fn.body.statements.filter(import_typescript.default.isReturnStatement).map((r) => r.expression) : [fn.body];
2288
3173
  if (returned.length === 1 && returned[0]) {
2289
- const value = resolve4(returned[0], bound, seen);
3174
+ const value = resolve6(returned[0], bound, seen);
2290
3175
  if (value) factoryBindings.set(value, bound);
2291
3176
  return value;
2292
3177
  }
@@ -2295,30 +3180,30 @@ function discoverWorkflowCalls(entry, snapshots) {
2295
3180
  return e;
2296
3181
  }
2297
3182
  function property(e, key, bindings, seen = /* @__PURE__ */ new Set()) {
2298
- const obj = resolve4(e, bindings, seen);
3183
+ const obj = resolve6(e, bindings, seen);
2299
3184
  if (!obj || !import_typescript.default.isObjectLiteralExpression(obj)) return;
2300
3185
  const scoped = factoryBindings.get(obj) ?? bindings;
2301
3186
  for (const p of [...obj.properties].reverse()) {
2302
3187
  if (import_typescript.default.isSpreadAssignment(p)) {
2303
- const spread = resolve4(p.expression, scoped, new Set(seen));
3188
+ const spread = resolve6(p.expression, scoped, new Set(seen));
2304
3189
  if (!spread || !import_typescript.default.isObjectLiteralExpression(spread)) return void 0;
2305
3190
  const found = property(p.expression, key, scoped, new Set(seen));
2306
3191
  if (found) return found;
2307
3192
  } else if (p.name && (import_typescript.default.isIdentifier(p.name) || import_typescript.default.isStringLiteralLike(p.name)) && p.name.text === key) {
2308
3193
  if (import_typescript.default.isPropertyAssignment(p))
2309
- return resolve4(p.initializer, scoped, new Set(seen));
3194
+ return resolve6(p.initializer, scoped, new Set(seen));
2310
3195
  if (import_typescript.default.isShorthandPropertyAssignment(p))
2311
- return resolve4(p.name, scoped, new Set(seen));
3196
+ return resolve6(p.name, scoped, new Set(seen));
2312
3197
  }
2313
3198
  }
2314
3199
  return void 0;
2315
3200
  }
2316
3201
  function literal(e, bindings) {
2317
- const value = resolve4(e, bindings);
3202
+ const value = resolve6(e, bindings);
2318
3203
  return value && import_typescript.default.isStringLiteralLike(value) ? value.text : void 0;
2319
3204
  }
2320
3205
  function workflowId(e, bindings) {
2321
- const value = resolve4(e, bindings);
3206
+ const value = resolve6(e, bindings);
2322
3207
  if (!value) return;
2323
3208
  if (import_typescript.default.isStringLiteralLike(value)) return value.text;
2324
3209
  if (import_typescript.default.isCallExpression(value) && api(value.expression, "defineWorkflow"))
@@ -2334,19 +3219,19 @@ function discoverWorkflowCalls(entry, snapshots) {
2334
3219
  if (d && import_typescript.default.isBindingElement(d) && (d.propertyName?.getText() ?? d.name.getText()) === "useWorkflow") {
2335
3220
  const decl = d.parent.parent;
2336
3221
  if (import_typescript.default.isVariableDeclaration(decl) && decl.initializer) {
2337
- const value2 = resolve4(decl.initializer, /* @__PURE__ */ new Map());
3222
+ const value2 = resolve6(decl.initializer, /* @__PURE__ */ new Map());
2338
3223
  return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
2339
3224
  }
2340
3225
  }
2341
3226
  if (import_typescript.default.isPropertyAccessExpression(e) && e.name.text === "useWorkflow") {
2342
- const value2 = resolve4(e.expression, /* @__PURE__ */ new Map());
3227
+ const value2 = resolve6(e.expression, /* @__PURE__ */ new Map());
2343
3228
  return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
2344
3229
  }
2345
- const value = resolve4(e, /* @__PURE__ */ new Map());
3230
+ const value = resolve6(e, /* @__PURE__ */ new Map());
2346
3231
  return !!value && value !== e && api(value, "useWorkflow");
2347
3232
  }
2348
3233
  function functionFor(e) {
2349
- const value = resolve4(e, /* @__PURE__ */ new Map());
3234
+ const value = resolve6(e, /* @__PURE__ */ new Map());
2350
3235
  if (value && (import_typescript.default.isArrowFunction(value) || import_typescript.default.isFunctionExpression(value)))
2351
3236
  return value;
2352
3237
  const d = symbol(
@@ -2369,7 +3254,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2369
3254
  target.toolDiscovery.warnings.push(warning);
2370
3255
  warnings.add(warning);
2371
3256
  };
2372
- const value = resolve4(expression, bindings);
3257
+ const value = resolve6(expression, bindings);
2373
3258
  if (!value || seen.has(value)) {
2374
3259
  unresolved();
2375
3260
  return;
@@ -2414,8 +3299,8 @@ function discoverWorkflowCalls(entry, snapshots) {
2414
3299
  bindings
2415
3300
  );
2416
3301
  const schema = property(value, "inputSchema", bindings);
2417
- const fields = resolve4(property(schema, "properties", bindings), bindings);
2418
- const requiredValue = resolve4(
3302
+ const fields = resolve6(property(schema, "properties", bindings), bindings);
3303
+ const requiredValue = resolve6(
2419
3304
  property(schema, "required", bindings),
2420
3305
  bindings
2421
3306
  );
@@ -2504,7 +3389,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2504
3389
  if (!arg) return;
2505
3390
  if (import_typescript.default.isIdentifier(p.name)) {
2506
3391
  const s = symbol(p.name);
2507
- if (s) bound.set(s, resolve4(arg, bindings) ?? arg);
3392
+ if (s) bound.set(s, resolve6(arg, bindings) ?? arg);
2508
3393
  } else if (import_typescript.default.isObjectBindingPattern(p.name)) {
2509
3394
  for (const b of p.name.elements) {
2510
3395
  const value = property(
@@ -2514,7 +3399,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2514
3399
  );
2515
3400
  const s = symbol(b.name);
2516
3401
  if (s && value)
2517
- bound.set(s, resolve4(value, bindings) ?? value);
3402
+ bound.set(s, resolve6(value, bindings) ?? value);
2518
3403
  }
2519
3404
  }
2520
3405
  });
@@ -2529,7 +3414,7 @@ function discoverWorkflowCalls(entry, snapshots) {
2529
3414
  for (const source of program.getSourceFiles().filter(local)) {
2530
3415
  const walk = (n) => {
2531
3416
  if (import_typescript.default.isCallExpression(n) && api(n.expression, "defineWorkflow")) {
2532
- const obj = resolve4(n.arguments[0], /* @__PURE__ */ new Map());
3417
+ const obj = resolve6(n.arguments[0], /* @__PURE__ */ new Map());
2533
3418
  const id = obj && literal(property(obj, "id", /* @__PURE__ */ new Map()), /* @__PURE__ */ new Map());
2534
3419
  if (id && known.has(id) && obj && import_typescript.default.isObjectLiteralExpression(obj))
2535
3420
  definitions.set(id, [...definitions.get(id) ?? [], obj]);
@@ -2599,24 +3484,24 @@ var readPackageName = async (cwd) => {
2599
3484
  let current = cwd;
2600
3485
  for (; ; ) {
2601
3486
  try {
2602
- const raw = await (0, import_promises4.readFile)((0, import_node_path4.join)(current, "package.json"), "utf8");
3487
+ const raw = await (0, import_promises5.readFile)((0, import_node_path6.join)(current, "package.json"), "utf8");
2603
3488
  const parsed = JSON.parse(raw);
2604
3489
  if (isRecord(parsed) && typeof parsed.name === "string") {
2605
3490
  return parsed.name;
2606
3491
  }
2607
3492
  } catch {
2608
3493
  }
2609
- const parent = (0, import_node_path4.dirname)(current);
3494
+ const parent = (0, import_node_path6.dirname)(current);
2610
3495
  if (parent === current) return void 0;
2611
3496
  current = parent;
2612
3497
  }
2613
3498
  };
2614
3499
  var loadEnvFiles = async (cwd) => {
2615
- for (const filename of [".env", ".env.local"]) {
2616
- const path = (0, import_node_path4.join)(cwd, filename);
3500
+ for (const filename of [".env.local", ".env"]) {
3501
+ const path = (0, import_node_path6.join)(cwd, filename);
2617
3502
  let raw;
2618
3503
  try {
2619
- raw = await (0, import_promises4.readFile)(path, "utf8");
3504
+ raw = await (0, import_promises5.readFile)(path, "utf8");
2620
3505
  } catch {
2621
3506
  continue;
2622
3507
  }
@@ -2642,7 +3527,7 @@ var resolveOptions = async (options) => {
2642
3527
  if (!options.entry) {
2643
3528
  throw new Error("Missing required option: --entry <path>.");
2644
3529
  }
2645
- const entry = (0, import_node_path4.isAbsolute)(options.entry) ? options.entry : (0, import_node_path4.resolve)(options.cwd, options.entry);
3530
+ const entry = (0, import_node_path6.isAbsolute)(options.entry) ? options.entry : (0, import_node_path6.resolve)(options.cwd, options.entry);
2646
3531
  const environment = options.environment ?? process.env.KORTYX_TELEMETRY_ENVIRONMENT ?? process.env.NODE_ENV ?? "development";
2647
3532
  const serviceName = options.serviceName ?? process.env.KORTYX_TELEMETRY_SERVICE_NAME ?? await readPackageName(options.cwd) ?? "kortyx-app";
2648
3533
  return {
@@ -2656,19 +3541,19 @@ var resolveOptions = async (options) => {
2656
3541
  };
2657
3542
  };
2658
3543
  var createFrameworkMarkerStubs = async () => {
2659
- const root = await (0, import_promises4.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "kortyx-cli-stubs-"));
3544
+ const root = await (0, import_promises5.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "kortyx-cli-stubs-"));
2660
3545
  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"),
3546
+ const directory = (0, import_node_path6.join)(root, moduleName);
3547
+ await (0, import_promises5.mkdir)(directory, { recursive: true });
3548
+ await (0, import_promises5.writeFile)((0, import_node_path6.join)(directory, "index.js"), "", "utf8");
3549
+ await (0, import_promises5.writeFile)(
3550
+ (0, import_node_path6.join)(directory, "package.json"),
2666
3551
  JSON.stringify({ name: moduleName, version: "0.0.0", main: "index.js" }),
2667
3552
  "utf8"
2668
3553
  );
2669
3554
  }
2670
3555
  const previousNodePath = process.env.NODE_PATH;
2671
- process.env.NODE_PATH = previousNodePath ? `${root}${import_node_path4.delimiter}${previousNodePath}` : root;
3556
+ process.env.NODE_PATH = previousNodePath ? `${root}${import_node_path6.delimiter}${previousNodePath}` : root;
2672
3557
  const moduleWithPrivateInit = import_node_module.default;
2673
3558
  moduleWithPrivateInit._initPaths?.();
2674
3559
  return {
@@ -2676,48 +3561,71 @@ var createFrameworkMarkerStubs = async () => {
2676
3561
  if (previousNodePath === void 0) delete process.env.NODE_PATH;
2677
3562
  else process.env.NODE_PATH = previousNodePath;
2678
3563
  moduleWithPrivateInit._initPaths?.();
2679
- await (0, import_promises4.rm)(root, { recursive: true, force: true });
3564
+ await (0, import_promises5.rm)(root, { recursive: true, force: true });
2680
3565
  }
2681
3566
  };
2682
3567
  };
2683
- var importEntry = async (entry) => {
3568
+ var openEntry = async (entry) => {
2684
3569
  const stubs = await createFrameworkMarkerStubs();
2685
3570
  let unregisterPaths;
3571
+ let unregisterEsm;
3572
+ const unregisterTsx = (0, import_api.register)();
3573
+ const release = async () => {
3574
+ unregisterPaths?.();
3575
+ unregisterTsx();
3576
+ await unregisterEsm?.();
3577
+ await stubs.cleanup();
3578
+ };
2686
3579
  try {
2687
- const tsconfig = await findNearestFile((0, import_node_path4.dirname)(entry), "tsconfig.json");
3580
+ const tsconfig = await findNearestFile((0, import_node_path6.dirname)(entry), "tsconfig.json");
3581
+ unregisterEsm = (0, import_api2.register)(tsconfig ? { tsconfig } : {});
2688
3582
  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"));
3583
+ const module2 = (0, import_node_module.createRequire)((0, import_node_path6.join)((0, import_node_path6.dirname)(entry), "kortyx-cli.cjs"))(entry);
2690
3584
  if (!isRecord(module2)) {
2691
3585
  throw new Error(`Entry module did not export an object: ${entry}`);
2692
3586
  }
2693
- return module2;
3587
+ return { module: module2, release };
3588
+ } catch (error) {
3589
+ await release();
3590
+ throw error;
3591
+ }
3592
+ };
3593
+ var importEntry = async (entry) => {
3594
+ const loaded = await openEntry(entry);
3595
+ try {
3596
+ return loaded.module;
2694
3597
  } finally {
2695
- unregisterPaths?.();
2696
- await stubs.cleanup();
3598
+ await loaded.release();
2697
3599
  }
2698
3600
  };
2699
3601
  var findNearestFile = async (startDirectory, filename) => {
2700
3602
  let current = startDirectory;
2701
3603
  for (; ; ) {
2702
- const candidate = (0, import_node_path4.join)(current, filename);
3604
+ const candidate = (0, import_node_path6.join)(current, filename);
2703
3605
  try {
2704
- await (0, import_promises4.access)(candidate);
3606
+ await (0, import_promises5.access)(candidate);
2705
3607
  return candidate;
2706
3608
  } catch {
2707
- const parent = (0, import_node_path4.dirname)(current);
3609
+ const parent = (0, import_node_path6.dirname)(current);
2708
3610
  if (parent === current) return void 0;
2709
3611
  current = parent;
2710
3612
  }
2711
3613
  }
2712
3614
  };
2713
3615
  var registerTsconfigPaths = async (tsconfigPath) => {
2714
- const raw = JSON.parse(await (0, import_promises4.readFile)(tsconfigPath, "utf8"));
3616
+ const parsed = import_typescript2.default.parseConfigFileTextToJson(
3617
+ tsconfigPath,
3618
+ await (0, import_promises5.readFile)(tsconfigPath, "utf8")
3619
+ );
3620
+ if (parsed.error)
3621
+ throw new Error(`Invalid TypeScript configuration: ${tsconfigPath}`);
3622
+ const raw = parsed.config;
2715
3623
  if (!isRecord(raw)) return () => void 0;
2716
3624
  const compilerOptions = isRecord(raw.compilerOptions) ? raw.compilerOptions : void 0;
2717
3625
  const paths = isRecord(compilerOptions?.paths) ? compilerOptions.paths : void 0;
2718
3626
  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;
3627
+ const tsconfigDirectory = (0, import_node_path6.dirname)(tsconfigPath);
3628
+ const baseUrl = typeof compilerOptions?.baseUrl === "string" ? (0, import_node_path6.resolve)(tsconfigDirectory, compilerOptions.baseUrl) : tsconfigDirectory;
2721
3629
  const mappings = Object.entries(paths).flatMap(([pattern, rawTargets]) => {
2722
3630
  if (!Array.isArray(rawTargets)) return [];
2723
3631
  return rawTargets.filter((target) => typeof target === "string").map((target) => ({ pattern, target }));
@@ -2746,7 +3654,7 @@ var registerTsconfigPaths = async (tsconfigPath) => {
2746
3654
  request.length - mapping.patternSuffix.length
2747
3655
  ) : "";
2748
3656
  const mapped = mapping.wildcard ? `${mapping.targetPrefix}${wildcardValue}${mapping.targetSuffix}` : mapping.target;
2749
- for (const candidate of candidateModulePaths((0, import_node_path4.resolve)(baseUrl, mapped))) {
3657
+ for (const candidate of candidateModulePaths((0, import_node_path6.resolve)(baseUrl, mapped))) {
2750
3658
  try {
2751
3659
  return originalResolve.call(this, candidate, parent, isMain, options);
2752
3660
  } catch {
@@ -2767,10 +3675,10 @@ var candidateModulePaths = (basePath) => [
2767
3675
  `${basePath}.cts`,
2768
3676
  `${basePath}.js`,
2769
3677
  `${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")
3678
+ (0, import_node_path6.join)(basePath, "index.ts"),
3679
+ (0, import_node_path6.join)(basePath, "index.tsx"),
3680
+ (0, import_node_path6.join)(basePath, "index.js"),
3681
+ (0, import_node_path6.join)(basePath, "index.jsx")
2774
3682
  ];
2775
3683
  var valueFromModule = (module2, exportName) => {
2776
3684
  if (exportName) {
@@ -2800,7 +3708,7 @@ var projectSnapshots = async (value, options) => {
2800
3708
  }
2801
3709
  const knownWorkflowIds = workflows.map((workflow) => workflow.id);
2802
3710
  return workflows.map(
2803
- (workflow) => (0, import_agent.projectWorkflowTopology)({
3711
+ (workflow) => (0, import_agent2.projectWorkflowTopology)({
2804
3712
  workflow,
2805
3713
  environment: options.environment,
2806
3714
  service: options.service,
@@ -2928,12 +3836,30 @@ var runTopologyPush = async (rawOptions) => {
2928
3836
  printResults(results, options);
2929
3837
  };
2930
3838
  var createCliProgram = () => {
2931
- const program = new import_commander4.Command().name("kortyx").description("Build, observe, and operate Kortyx projects.").showHelpAfterError().addHelpText(
3839
+ const program = new import_commander6.Command().name("kortyx").description("Build, observe, and operate Kortyx projects.").showHelpAfterError().addHelpText(
2932
3840
  "after",
2933
3841
  '\nRun "kortyx <command> --help" for detailed command options.'
2934
3842
  );
2935
3843
  program.addCommand(createStudioCommand());
2936
3844
  program.addCommand(createConnectionsCommand());
3845
+ let releaseEvalEntry;
3846
+ program.addCommand(
3847
+ createLocalEvalsCommand({
3848
+ load: async (entry, envFiles) => {
3849
+ Object.assign(
3850
+ process.env,
3851
+ await loadEvalEnvironment(process.cwd(), envFiles)
3852
+ );
3853
+ const loaded = await openEntry(entry);
3854
+ releaseEvalEntry = loaded.release;
3855
+ return loaded.module;
3856
+ },
3857
+ release: async () => {
3858
+ await releaseEvalEntry?.();
3859
+ releaseEvalEntry = void 0;
3860
+ }
3861
+ })
3862
+ );
2937
3863
  const topology = program.command("topology").description("Publish deterministic workflow topology to Studio.");
2938
3864
  topology.command("push").description("Project and publish workflow topology.").requiredOption(
2939
3865
  "--entry <path>",
@@ -2971,7 +3897,7 @@ var main = async () => {
2971
3897
  try {
2972
3898
  await program.parseAsync(process.argv);
2973
3899
  } catch (error) {
2974
- if (error instanceof import_commander4.CommanderError && error.exitCode === 0) return;
3900
+ if (error instanceof import_commander6.CommanderError && error.exitCode === 0) return;
2975
3901
  if (error instanceof StudioReadError) {
2976
3902
  console.error(
2977
3903
  process.argv.includes("--json") ? JSON.stringify(error.toJSON()) : `[${error.code}] ${error.message}`