@kortyx/cli 0.9.0 → 0.10.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,21 +23,179 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
 
25
25
  // src/index.ts
26
- var import_promises3 = require("fs/promises");
26
+ var import_promises4 = require("fs/promises");
27
27
  var import_node_module = __toESM(require("module"));
28
- var import_node_os2 = require("os");
29
- var import_node_path3 = require("path");
28
+ var import_node_os3 = require("os");
29
+ var import_node_path4 = require("path");
30
30
  var import_agent = require("@kortyx/agent");
31
31
  var import_errors = require("@kortyx/core/errors");
32
- var import_telemetry_contracts = require("@kortyx/telemetry-contracts");
33
- var import_commander2 = require("commander");
32
+ var import_telemetry_contracts4 = require("@kortyx/telemetry-contracts");
33
+ var import_commander4 = require("commander");
34
34
  var import_api = require("tsx/cjs/api");
35
35
 
36
- // src/studio/command.ts
36
+ // src/connections-command.ts
37
+ var import_telemetry_contracts = require("@kortyx/telemetry-contracts");
37
38
  var import_commander = require("commander");
38
39
 
39
- // src/studio/local-stack.ts
40
+ // src/connections.ts
41
+ var import_node_crypto = require("crypto");
40
42
  var import_promises2 = require("fs/promises");
43
+ var import_node_os2 = require("os");
44
+ var import_node_path2 = require("path");
45
+ var import_zod2 = require("zod");
46
+
47
+ // src/studio/read-client.ts
48
+ var StudioReadError = class extends Error {
49
+ constructor(code, message, status, requestId) {
50
+ super(message);
51
+ this.code = code;
52
+ this.status = status;
53
+ this.requestId = requestId;
54
+ this.name = "StudioReadError";
55
+ }
56
+ code;
57
+ status;
58
+ requestId;
59
+ toJSON() {
60
+ return {
61
+ error: {
62
+ code: this.code,
63
+ message: this.message,
64
+ status: this.status,
65
+ requestId: this.requestId
66
+ }
67
+ };
68
+ }
69
+ };
70
+ var normalizeConnectionUrl = (input) => {
71
+ let url;
72
+ try {
73
+ url = new URL(input);
74
+ } catch {
75
+ throw new StudioReadError(
76
+ "invalid_url",
77
+ "Provide an absolute HTTP(S) connection URL."
78
+ );
79
+ }
80
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
81
+ if (url.username || url.password || url.search || url.hash || !(url.protocol === "https:" || url.protocol === "http:" && loopback)) {
82
+ throw new StudioReadError(
83
+ "invalid_url",
84
+ "Connection URLs require HTTPS (HTTP is allowed on loopback), without credentials, query, or fragment."
85
+ );
86
+ }
87
+ return url.toString().replace(/\/+$/, "");
88
+ };
89
+ var StudioReadClient = class {
90
+ constructor(apiUrl, apiKey, request = fetch) {
91
+ this.apiKey = apiKey;
92
+ this.request = request;
93
+ this.apiUrl = normalizeConnectionUrl(apiUrl);
94
+ if (!/^ktyx_(?:test|live)_[^_]+_\S+$/.test(apiKey)) {
95
+ throw new StudioReadError(
96
+ "invalid_key",
97
+ "Expected a Kortyx API key with studio:read permission."
98
+ );
99
+ }
100
+ }
101
+ apiKey;
102
+ request;
103
+ apiUrl;
104
+ async get(path, schema, query = {}) {
105
+ if (!/^\/v1\/studio\/(?:context|catalogs|workflows|(?:runs|sessions|interrupts)(?:\/[^/?#]+)?)$/.test(
106
+ path
107
+ ) || /(?:^|\/)(?:\.|\.\.|%2e(?:%2e)?)(?:\/|$)/i.test(path)) {
108
+ throw new StudioReadError(
109
+ "invalid_endpoint",
110
+ "Only supported Studio read endpoints are allowed."
111
+ );
112
+ }
113
+ const url = new URL(`${this.apiUrl}${path}`);
114
+ for (const [key, value2] of Object.entries(query))
115
+ url.searchParams.set(key, value2);
116
+ let response;
117
+ try {
118
+ response = await this.request(url, {
119
+ method: "GET",
120
+ headers: {
121
+ authorization: `Bearer ${this.apiKey}`,
122
+ accept: "application/json"
123
+ },
124
+ redirect: "error",
125
+ signal: AbortSignal.timeout(15e3)
126
+ });
127
+ } catch {
128
+ throw new StudioReadError(
129
+ "connection_failed",
130
+ "Studio API request failed or timed out. Check the URL, network/VPN, and TLS. Redirects are not followed."
131
+ );
132
+ }
133
+ const requestId = response.headers.get("x-request-id") ?? void 0;
134
+ if (!response.ok) {
135
+ await response.body?.cancel();
136
+ 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}.`;
137
+ throw new StudioReadError(
138
+ "api_error",
139
+ message,
140
+ response.status,
141
+ requestId
142
+ );
143
+ }
144
+ let value;
145
+ const reader = response.body?.getReader();
146
+ if (!reader)
147
+ throw new StudioReadError(
148
+ "invalid_response",
149
+ "Studio returned an empty response."
150
+ );
151
+ try {
152
+ const decoder = new TextDecoder();
153
+ let body = "";
154
+ let bytes = 0;
155
+ for (; ; ) {
156
+ const chunk = await reader.read();
157
+ if (chunk.done) break;
158
+ bytes += chunk.value.byteLength;
159
+ if (bytes > 20 * 1024 * 1024) {
160
+ await reader.cancel();
161
+ throw new StudioReadError(
162
+ "response_too_large",
163
+ "Studio response exceeded the 20 MiB safety limit. Use a smaller query or inspect an individual run."
164
+ );
165
+ }
166
+ body += decoder.decode(chunk.value, { stream: true });
167
+ }
168
+ body += decoder.decode();
169
+ value = JSON.parse(body);
170
+ } catch (error) {
171
+ if (error instanceof StudioReadError) throw error;
172
+ throw new StudioReadError(
173
+ "invalid_response",
174
+ "Could not read Studio JSON response.",
175
+ response.status,
176
+ requestId
177
+ );
178
+ } finally {
179
+ reader.releaseLock();
180
+ }
181
+ const result = schema.safeParse(value);
182
+ if (!result.success) {
183
+ throw new StudioReadError(
184
+ "schema_mismatch",
185
+ "Studio response does not match this CLI's contracts. Check API/CLI version compatibility.",
186
+ response.status,
187
+ requestId
188
+ );
189
+ }
190
+ return result.data;
191
+ }
192
+ };
193
+
194
+ // src/studio/state.ts
195
+ var import_promises = require("fs/promises");
196
+ var import_node_os = require("os");
197
+ var import_node_path = require("path");
198
+ var import_zod = require("zod");
41
199
 
42
200
  // src/studio/compose.ts
43
201
  var STUDIO_COMPOSE_FILE = `name: \${KORTYX_COMPOSE_PROJECT_NAME:-kortyx-studio}
@@ -158,10 +316,6 @@ function studioComposeFile(updater) {
158
316
  }
159
317
 
160
318
  // src/studio/state.ts
161
- var import_promises = require("fs/promises");
162
- var import_node_os = require("os");
163
- var import_node_path = require("path");
164
- var import_zod = require("zod");
165
319
  var CONFIG_VERSION = 1;
166
320
  var DEFAULT_API_PORT = 6400;
167
321
  var DEFAULT_STUDIO_PORT = 6300;
@@ -381,7 +535,309 @@ var requireStudioConfig = async (home) => {
381
535
  return config;
382
536
  };
383
537
 
538
+ // src/connections.ts
539
+ var ProfileSchema = import_zod2.z.object({
540
+ name: import_zod2.z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/),
541
+ apiUrl: import_zod2.z.string(),
542
+ studioUrl: import_zod2.z.string().optional(),
543
+ apiKeyEnv: import_zod2.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/),
544
+ environment: import_zod2.z.string().min(1).optional(),
545
+ project: import_zod2.z.string(),
546
+ organization: import_zod2.z.string()
547
+ }).strict();
548
+ var ConfigSchema = import_zod2.z.object({
549
+ version: import_zod2.z.literal(1),
550
+ current: import_zod2.z.string().optional(),
551
+ profiles: import_zod2.z.array(ProfileSchema)
552
+ }).strict().superRefine((config, ctx) => {
553
+ const names = config.profiles.map((profile) => profile.name);
554
+ if (names.includes("local") || new Set(names).size !== names.length || config.current && config.current !== "local" && !names.includes(config.current)) {
555
+ ctx.addIssue({
556
+ code: "custom",
557
+ message: "Invalid connection names or selection."
558
+ });
559
+ }
560
+ });
561
+ var defaultConnectionsHome = () => (0, import_node_path2.resolve)(process.env.KORTYX_CONFIG_HOME ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kortyx"));
562
+ var readConnections = async (home = defaultConnectionsHome()) => {
563
+ try {
564
+ const result = ConfigSchema.safeParse(
565
+ JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.join)(home, "connections.json"), "utf8"))
566
+ );
567
+ if (!result.success) throw new Error("invalid");
568
+ for (const profile of result.data.profiles) {
569
+ normalizeConnectionUrl(profile.apiUrl);
570
+ if (profile.studioUrl) normalizeConnectionUrl(profile.studioUrl);
571
+ }
572
+ return result.data;
573
+ } catch (error) {
574
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
575
+ return { version: 1, profiles: [] };
576
+ throw new StudioReadError(
577
+ "invalid_config",
578
+ "Invalid or unreadable connections.json. Fix the configuration; it will not be overwritten."
579
+ );
580
+ }
581
+ };
582
+ var saveConnections = async (config, home = defaultConnectionsHome()) => {
583
+ const result = ConfigSchema.safeParse(config);
584
+ if (!result.success)
585
+ throw new StudioReadError(
586
+ "invalid_config",
587
+ "Invalid connection name or configuration."
588
+ );
589
+ await (0, import_promises2.mkdir)(home, { recursive: true, mode: 448 });
590
+ const temporary = (0, import_node_path2.join)(home, `.connections-${(0, import_node_crypto.randomUUID)()}.tmp`);
591
+ try {
592
+ await (0, import_promises2.writeFile)(temporary, `${JSON.stringify(result.data, null, 2)}
593
+ `, {
594
+ mode: 384,
595
+ flag: "wx"
596
+ });
597
+ await (0, import_promises2.rename)(temporary, (0, import_node_path2.join)(home, "connections.json"));
598
+ } finally {
599
+ await (0, import_promises2.unlink)(temporary).catch((error) => {
600
+ if (error.code !== "ENOENT") throw error;
601
+ });
602
+ }
603
+ };
604
+ var urlMatches = (target, base) => {
605
+ const url = new URL(base);
606
+ const prefix = url.pathname.replace(/\/+$/, "");
607
+ return target.origin === url.origin && (target.pathname === prefix || target.pathname.startsWith(`${prefix}/`));
608
+ };
609
+ var resolveConnection = async (options, targetUrl) => {
610
+ const config = await readConnections(options.configHome);
611
+ const local = await readStudioConfig(options.home ?? defaultStudioHome());
612
+ const localLocator = local ? {
613
+ name: "local",
614
+ apiUrl: `http://localhost:${local.apiPort}`,
615
+ studioUrl: `http://localhost:${local.studioPort}`
616
+ } : void 0;
617
+ let selected = options.connection ?? (process.env.KORTYX_CONNECTION || void 0);
618
+ if (options.apiUrl || options.apiKeyEnv) {
619
+ if (selected || !options.apiUrl || !options.apiKeyEnv) {
620
+ throw new StudioReadError(
621
+ "invalid_connection",
622
+ "Direct connections require both --api-url and --api-key-env, without --connection/KORTYX_CONNECTION. Keys are never reused for URL overrides."
623
+ );
624
+ }
625
+ if (targetUrl)
626
+ throw new StudioReadError(
627
+ "unmapped_url",
628
+ "Register a connection with --studio-url before inspecting a URL. Direct connections accept entity IDs only."
629
+ );
630
+ return {
631
+ name: "direct",
632
+ apiUrl: normalizeConnectionUrl(options.apiUrl),
633
+ apiKey: readKeyEnv(options.apiKeyEnv)
634
+ };
635
+ }
636
+ const locators = [
637
+ ...config.profiles,
638
+ ...localLocator ? [localLocator] : []
639
+ ];
640
+ if (targetUrl) {
641
+ const target = new URL(targetUrl);
642
+ const matches = locators.filter(
643
+ (profile2) => urlMatches(target, profile2.apiUrl) || profile2.studioUrl && urlMatches(target, profile2.studioUrl)
644
+ );
645
+ if (selected) {
646
+ if (!matches.some((profile2) => profile2.name === selected)) {
647
+ throw new StudioReadError(
648
+ "connection_mismatch",
649
+ "The supplied URL does not match the selected connection's API or Studio URL."
650
+ );
651
+ }
652
+ } else if (matches.length === 1) selected = matches[0]?.name;
653
+ else
654
+ throw new StudioReadError(
655
+ "unmapped_url",
656
+ matches.length ? "URL matches multiple projects. Specify --connection explicitly." : "URL does not match a configured connection. Add one with the correct --studio-url."
657
+ );
658
+ }
659
+ selected ??= config.current ?? "local";
660
+ if (selected === "local") {
661
+ if (!localLocator)
662
+ throw new StudioReadError(
663
+ "not_configured",
664
+ "Local Studio is not configured. Run kortyx studio start or select a remote connection."
665
+ );
666
+ const environment = await readStudioEnvironment(
667
+ options.home ?? defaultStudioHome()
668
+ );
669
+ return { ...localLocator, apiKey: environment.KORTYX_STUDIO_API_KEY };
670
+ }
671
+ const profile = config.profiles.find((item) => item.name === selected);
672
+ if (!profile)
673
+ throw new StudioReadError(
674
+ "unknown_connection",
675
+ "Connection not found. Run kortyx connections list."
676
+ );
677
+ return {
678
+ name: profile.name,
679
+ apiUrl: profile.apiUrl,
680
+ studioUrl: profile.studioUrl,
681
+ environment: profile.environment,
682
+ apiKey: readKeyEnv(profile.apiKeyEnv)
683
+ };
684
+ };
685
+ var readKeyEnv = (name) => {
686
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
687
+ throw new StudioReadError(
688
+ "invalid_key_env",
689
+ "Provide a valid environment variable name."
690
+ );
691
+ const value = process.env[name];
692
+ if (!value)
693
+ throw new StudioReadError(
694
+ "missing_key",
695
+ `Set ${name} to a project-scoped Studio read key. Do not use the telemetry write key.`
696
+ );
697
+ return value;
698
+ };
699
+
700
+ // src/connections-command.ts
701
+ var createConnectionsCommand = (log = console.log, request = fetch) => {
702
+ const command = new import_commander.Command("connections").description(
703
+ "Manage Studio connection profiles (no remote mutations or stored secrets)."
704
+ );
705
+ const common = (child) => child.option(
706
+ "--config-home <path>",
707
+ "Connection configuration directory.",
708
+ defaultConnectionsHome()
709
+ ).option(
710
+ "--home <path>",
711
+ "Managed local Studio directory.",
712
+ defaultStudioHome()
713
+ ).option("--json", "Print machine-readable JSON.");
714
+ const print = (value, options) => log(JSON.stringify(value, null, options.json ? 0 : 2));
715
+ common(
716
+ command.command("list").description("List profiles without reading or displaying their keys.")
717
+ ).action(async (options) => {
718
+ const config = await readConnections(options.configHome);
719
+ const local = await readStudioConfig(options.home);
720
+ print(
721
+ {
722
+ current: config.current ?? "local",
723
+ connections: [
724
+ ...local ? [
725
+ {
726
+ name: "local",
727
+ apiUrl: `http://localhost:${local.apiPort}`,
728
+ studioUrl: `http://localhost:${local.studioPort}`,
729
+ credential: "managed-local"
730
+ }
731
+ ] : [],
732
+ ...config.profiles
733
+ ]
734
+ },
735
+ options
736
+ );
737
+ });
738
+ common(
739
+ command.command("add <name>").description(
740
+ "Validate a read key and save URL locators plus an environment-variable reference."
741
+ ).requiredOption(
742
+ "--api-url <url>",
743
+ "Studio API base URL (not the browser URL)."
744
+ ).requiredOption(
745
+ "--api-key-env <name>",
746
+ "Environment variable containing the project-scoped read key."
747
+ ).option(
748
+ "--studio-url <url>",
749
+ "Studio browser base URL, used to resolve pasted links."
750
+ ).option("--environment <name>", "Default environment filter for lists.").option("--replace", "Explicitly replace an existing named connection.")
751
+ ).action(
752
+ async (name, options) => {
753
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name) || name === "local") {
754
+ throw new StudioReadError(
755
+ "invalid_name",
756
+ "Use a connection name of 1\u201364 letters, digits, hyphens or underscores; local is reserved."
757
+ );
758
+ }
759
+ const config = await readConnections(options.configHome);
760
+ if (config.profiles.some((profile2) => profile2.name === name) && !options.replace) {
761
+ throw new StudioReadError(
762
+ "connection_exists",
763
+ "Connection already exists. Use --replace to update it."
764
+ );
765
+ }
766
+ const apiUrl = normalizeConnectionUrl(options.apiUrl);
767
+ const studioUrl = options.studioUrl ? normalizeConnectionUrl(options.studioUrl) : void 0;
768
+ const client = new StudioReadClient(
769
+ apiUrl,
770
+ readKeyEnv(options.apiKeyEnv),
771
+ request
772
+ );
773
+ const context = await client.get(
774
+ "/v1/studio/context",
775
+ import_telemetry_contracts.StudioContextResponseSchema
776
+ );
777
+ if (!context.apiKey.scopes.includes("studio:read"))
778
+ throw new StudioReadError(
779
+ "missing_scope",
780
+ "The API key lacks studio:read permission."
781
+ );
782
+ const profile = {
783
+ name,
784
+ apiUrl,
785
+ apiKeyEnv: options.apiKeyEnv,
786
+ project: context.project.name,
787
+ organization: context.organization.name,
788
+ ...studioUrl ? { studioUrl } : {},
789
+ ...options.environment ? { environment: options.environment } : {}
790
+ };
791
+ config.profiles = [
792
+ ...config.profiles.filter((item) => item.name !== name),
793
+ profile
794
+ ];
795
+ await saveConnections(config, options.configHome);
796
+ print({ connection: profile, verified: true }, options);
797
+ }
798
+ );
799
+ common(
800
+ command.command("use <name>").description(
801
+ "Select the default connection; agents should prefer per-command --connection."
802
+ )
803
+ ).action(async (name, options) => {
804
+ const config = await readConnections(options.configHome);
805
+ if (name === "local" ? !await readStudioConfig(options.home) : !config.profiles.some((profile) => profile.name === name)) {
806
+ throw new StudioReadError(
807
+ "unknown_connection",
808
+ "Connection not found. Run kortyx connections list."
809
+ );
810
+ }
811
+ config.current = name;
812
+ await saveConnections(config, options.configHome);
813
+ print({ current: name }, options);
814
+ });
815
+ common(
816
+ command.command("remove <name>").description(
817
+ "Remove a saved locator, not its credential or remote project."
818
+ )
819
+ ).action(async (name, options) => {
820
+ const config = await readConnections(options.configHome);
821
+ if (!config.profiles.some((profile) => profile.name === name))
822
+ throw new StudioReadError(
823
+ "unknown_connection",
824
+ "Saved connection not found; managed local cannot be removed."
825
+ );
826
+ config.profiles = config.profiles.filter(
827
+ (profile) => profile.name !== name
828
+ );
829
+ if (config.current === name) delete config.current;
830
+ await saveConnections(config, options.configHome);
831
+ print({ removed: name }, options);
832
+ });
833
+ return command;
834
+ };
835
+
836
+ // src/studio/command.ts
837
+ var import_commander3 = require("commander");
838
+
384
839
  // src/studio/local-stack.ts
840
+ var import_promises3 = require("fs/promises");
385
841
  var composeArgs = (home, args) => [
386
842
  "compose",
387
843
  "--env-file",
@@ -543,7 +999,7 @@ var startStudio = async (options, runtime) => {
543
999
  ],
544
1000
  { inherit: false }
545
1001
  );
546
- await (0, import_promises2.writeFile)(
1002
+ await (0, import_promises3.writeFile)(
547
1003
  studioComposePath(options.home),
548
1004
  studioComposeFile(
549
1005
  process.platform !== "win32" && capability.stdout.trim() === "supported"
@@ -615,8 +1071,878 @@ var resetStudio = async (home, confirm, runtime) => {
615
1071
  );
616
1072
  };
617
1073
 
1074
+ // src/studio/read-command.ts
1075
+ var import_telemetry_contracts3 = require("@kortyx/telemetry-contracts");
1076
+ var import_commander2 = require("commander");
1077
+
1078
+ // src/studio/read-analysis.ts
1079
+ var import_telemetry_contracts2 = require("@kortyx/telemetry-contracts");
1080
+ var stringValue = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
1081
+ var numberValue = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
1082
+ var recordValue = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
1083
+ var toolTerminalTypes = /* @__PURE__ */ new Set([
1084
+ "tool.completed",
1085
+ "tool.failed",
1086
+ "tool.denied",
1087
+ "tool.cancelled",
1088
+ "tool.reused"
1089
+ ]);
1090
+ var buildStudioTimeline = (events, interrupts = []) => {
1091
+ const interruptById = new Map(interrupts.map((item) => [item.id, item]));
1092
+ const items = [];
1093
+ const ordered = [...events].sort(
1094
+ (left, right) => Date.parse(left.occurredAt) - Date.parse(right.occurredAt) || Date.parse(left.receivedAt) - Date.parse(right.receivedAt) || left.id.localeCompare(right.id)
1095
+ );
1096
+ for (const event of ordered) {
1097
+ const payload = event.payload;
1098
+ const base = {
1099
+ eventId: event.id,
1100
+ at: event.occurredAt,
1101
+ nodeId: event.nodeId,
1102
+ workflowId: event.workflowId
1103
+ };
1104
+ if (event.type === "generation.completed") {
1105
+ items.push({
1106
+ ...base,
1107
+ kind: "model",
1108
+ ...numberValue(payload.durationMs) !== void 0 ? { durationMs: numberValue(payload.durationMs) } : {},
1109
+ ...stringValue(payload.provider) ? { provider: stringValue(payload.provider) } : {},
1110
+ ...stringValue(payload.model) ? { model: stringValue(payload.model) } : {},
1111
+ ...payload.finishReason !== void 0 ? { finishReason: payload.finishReason } : {}
1112
+ });
1113
+ continue;
1114
+ }
1115
+ if (toolTerminalTypes.has(event.type)) {
1116
+ const fallbackOutcome = event.type.slice("tool.".length);
1117
+ items.push({
1118
+ ...base,
1119
+ kind: "tool",
1120
+ ...numberValue(payload.durationMs) !== void 0 ? { durationMs: numberValue(payload.durationMs) } : {},
1121
+ ...stringValue(payload.name) || stringValue(payload.tool) ? { toolName: stringValue(payload.name) ?? stringValue(payload.tool) } : {},
1122
+ toolOutcome: stringValue(payload.outcome) ?? (fallbackOutcome === "completed" ? "success" : fallbackOutcome),
1123
+ ...stringValue(payload.toolCallId) ? { toolCallId: stringValue(payload.toolCallId) } : {},
1124
+ ...payload.input !== void 0 ? { input: payload.input } : {},
1125
+ ...payload.output !== void 0 ? { output: payload.output } : {}
1126
+ });
1127
+ continue;
1128
+ }
1129
+ if (event.type === "interrupt.created") {
1130
+ const id = stringValue(payload.interruptId);
1131
+ const interrupt = id ? interruptById.get(id) : void 0;
1132
+ items.push({
1133
+ ...base,
1134
+ kind: "interrupt",
1135
+ ...id ? { interruptId: id } : {},
1136
+ ...interrupt?.type || stringValue(payload.kind) ? { interruptType: interrupt?.type ?? stringValue(payload.kind) } : {},
1137
+ ...interrupt?.interactionMode || stringValue(payload.interactionMode) ? {
1138
+ interactionMode: interrupt?.interactionMode ?? stringValue(payload.interactionMode)
1139
+ } : {},
1140
+ ...interrupt?.question !== void 0 && interrupt.question !== null ? { question: interrupt.question } : payload.question !== void 0 ? { question: payload.question } : {}
1141
+ });
1142
+ continue;
1143
+ }
1144
+ if (event.type === "interrupt.resolved") {
1145
+ items.push({
1146
+ ...base,
1147
+ kind: "resume",
1148
+ ...stringValue(payload.interruptId) ? { interruptId: stringValue(payload.interruptId) } : {},
1149
+ ...stringValue(payload.resumeOutcome) ? { resumeOutcome: stringValue(payload.resumeOutcome) } : {}
1150
+ });
1151
+ continue;
1152
+ }
1153
+ if (event.type === "run.limit_reached") {
1154
+ items.push({
1155
+ ...base,
1156
+ kind: "limit",
1157
+ ...stringValue(payload.limit) ? { limit: stringValue(payload.limit) } : {},
1158
+ ...payload.consumed !== void 0 ? { consumed: payload.consumed } : {},
1159
+ ...payload.maximum !== void 0 ? { maximum: payload.maximum } : {}
1160
+ });
1161
+ }
1162
+ }
1163
+ return items.map((item, index) => ({ step: index + 1, ...item }));
1164
+ };
1165
+ var selectedValue = (event, key) => {
1166
+ const payload = event.payload;
1167
+ if (key === "branch")
1168
+ return [stringValue(payload.branchId)].filter(
1169
+ (value) => Boolean(value)
1170
+ );
1171
+ return [
1172
+ stringValue(payload.callId),
1173
+ stringValue(payload.invocationId)
1174
+ ].filter((value) => Boolean(value));
1175
+ };
1176
+ var focusStudioEvents = (events, target) => {
1177
+ const selection = target.selection ?? {};
1178
+ const applied = Object.fromEntries(
1179
+ ["event", "trace", "node", "branch", "call"].filter((key) => Boolean(selection[key])).map((key) => [key, selection[key]])
1180
+ );
1181
+ if (Object.keys(applied).length === 0)
1182
+ throw new StudioReadError(
1183
+ "selection_not_focusable",
1184
+ "The URL has no event, trace, node, branch, or call selector to focus."
1185
+ );
1186
+ const callEventIds = new Set(
1187
+ selection.call ? (0, import_telemetry_contracts2.projectWorkflowCalls)(events).filter(
1188
+ (call) => call.id === selection.call || call.callId === selection.call || call.invocationId === selection.call
1189
+ ).flatMap((call) => call.events.map((event) => event.id)) : []
1190
+ );
1191
+ const focused = events.filter(
1192
+ (event) => Object.entries(applied).every(([key, value]) => {
1193
+ if (key === "event") return event.id === value;
1194
+ if (key === "trace") return event.traceId === value;
1195
+ if (key === "node") return event.nodeId === value;
1196
+ if (key === "branch")
1197
+ return selectedValue(event, "branch").includes(value);
1198
+ return callEventIds.has(event.id) || selectedValue(event, "call").includes(value);
1199
+ })
1200
+ );
1201
+ if (focused.length === 0)
1202
+ throw new StudioReadError(
1203
+ "selection_not_found",
1204
+ "The pasted URL selector did not match any returned Studio events."
1205
+ );
1206
+ return { events: focused, applied };
1207
+ };
1208
+ var stableValue = (value) => {
1209
+ if (Array.isArray(value)) return value.map(stableValue);
1210
+ if (value && typeof value === "object")
1211
+ return Object.fromEntries(
1212
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)])
1213
+ );
1214
+ return value;
1215
+ };
1216
+ var analyzeStudioDiagnostics = (events, run2, interrupts = []) => {
1217
+ const findings = [];
1218
+ const terminalTools = events.filter(
1219
+ (event) => toolTerminalTypes.has(event.type)
1220
+ );
1221
+ for (let index = 1; index < terminalTools.length; index++) {
1222
+ const previous = terminalTools[index - 1];
1223
+ const current = terminalTools[index];
1224
+ if (!previous || !current) continue;
1225
+ const previousName = stringValue(previous.payload.name) ?? stringValue(previous.payload.tool);
1226
+ const currentName = stringValue(current.payload.name) ?? stringValue(current.payload.tool);
1227
+ if (previousName && previousName === currentName && previous.payload.input !== void 0 && current.payload.input !== void 0 && JSON.stringify(stableValue(previous.payload.input)) === JSON.stringify(stableValue(current.payload.input))) {
1228
+ findings.push({
1229
+ code: "repeated_tool_input",
1230
+ severity: "warning",
1231
+ message: `Tool ${currentName} was called consecutively with the same captured input.`,
1232
+ eventIds: [previous.id, current.id],
1233
+ evidence: { toolName: currentName, input: current.payload.input }
1234
+ });
1235
+ }
1236
+ }
1237
+ let lastInterrupt;
1238
+ for (const event of events) {
1239
+ if (toolTerminalTypes.has(event.type)) lastInterrupt = void 0;
1240
+ if (event.type !== "interrupt.created") continue;
1241
+ if (lastInterrupt)
1242
+ findings.push({
1243
+ code: "consecutive_human_interrupts",
1244
+ severity: "warning",
1245
+ message: "Human input was requested again without an intervening terminal tool event.",
1246
+ eventIds: [lastInterrupt.id, event.id]
1247
+ });
1248
+ lastInterrupt = event;
1249
+ }
1250
+ for (const event of events.filter(
1251
+ (item) => item.type === "run.limit_reached"
1252
+ ))
1253
+ findings.push({
1254
+ code: "tool_step_limit_reached",
1255
+ severity: "warning",
1256
+ message: "The runtime reported that an execution limit was reached.",
1257
+ eventIds: [event.id],
1258
+ evidence: event.payload
1259
+ });
1260
+ const schemaFailures = events.filter((event) => {
1261
+ if (event.type !== "span.failed") return false;
1262
+ const error = recordValue(event.payload.error);
1263
+ const failure = recordValue(event.payload.failure);
1264
+ const haystack = [
1265
+ error.code,
1266
+ error.message,
1267
+ failure.code,
1268
+ failure.message,
1269
+ event.payload.code,
1270
+ event.payload.message
1271
+ ].filter((value) => typeof value === "string").join(" ");
1272
+ return /MODEL_OUTPUT_SCHEMA|INVALID_OUTPUT|output (?:schema|validation)|schema validation/i.test(
1273
+ haystack
1274
+ );
1275
+ });
1276
+ for (const failure of schemaFailures) {
1277
+ const laterGeneration = events.find(
1278
+ (event) => event.type === "generation.completed" && Date.parse(event.occurredAt) > Date.parse(failure.occurredAt)
1279
+ );
1280
+ findings.push({
1281
+ code: laterGeneration ? "output_schema_retry" : "output_schema_failure",
1282
+ severity: laterGeneration ? "warning" : "error",
1283
+ message: laterGeneration ? "An output-schema failure was followed by another model generation." : "The runtime reported an output-schema validation failure.",
1284
+ eventIds: [failure.id, ...laterGeneration ? [laterGeneration.id] : []],
1285
+ evidence: failure.payload
1286
+ });
1287
+ }
1288
+ const pending = interrupts.filter((item) => item.status === "pending");
1289
+ if (run2?.status === "interrupted" && pending.length > 0)
1290
+ findings.push({
1291
+ code: "unresolved_interrupt",
1292
+ severity: "warning",
1293
+ message: "The workflow is suspended with unresolved human input.",
1294
+ eventIds: events.filter(
1295
+ (event) => event.type === "interrupt.created" && pending.some((item) => item.id === event.payload.interruptId)
1296
+ ).map((event) => event.id),
1297
+ evidence: { interruptIds: pending.map((item) => item.id) }
1298
+ });
1299
+ return findings;
1300
+ };
1301
+ var compareCatalogRuntime = (run2, workflows) => {
1302
+ const rootReferences = run2.workflowRefs.filter(
1303
+ (reference) => reference.workflowId === run2.workflowId
1304
+ );
1305
+ const rows = rootReferences.length ? rootReferences : [
1306
+ {
1307
+ workflowId: run2.workflowId,
1308
+ workflowRevisionId: run2.workflowRevisionId,
1309
+ declaredVersion: run2.declaredVersion
1310
+ }
1311
+ ];
1312
+ const compared = rows.map((reference) => {
1313
+ const workflow = workflows.find((item) => item.id === reference.workflowId);
1314
+ return {
1315
+ workflowId: reference.workflowId,
1316
+ executedRevisionId: reference.workflowRevisionId,
1317
+ executedVersion: reference.declaredVersion,
1318
+ activeRevisionId: workflow?.activeRevisionId ?? null,
1319
+ activeVersion: workflow?.activeVersion ?? null,
1320
+ drift: Boolean(
1321
+ workflow?.activeRevisionId && reference.workflowRevisionId && workflow.activeRevisionId !== reference.workflowRevisionId || !reference.workflowRevisionId && workflow?.activeVersion && reference.declaredVersion && workflow.activeVersion !== reference.declaredVersion
1322
+ )
1323
+ };
1324
+ });
1325
+ const published = compared.some((item) => item.activeRevisionId);
1326
+ return {
1327
+ status: !published ? "not-published" : compared.some((item) => item.drift) ? "drift" : "current",
1328
+ workflows: compared
1329
+ };
1330
+ };
1331
+ var compareRunAnalysis = (left, right) => {
1332
+ const metadataFields = [
1333
+ "workflowId",
1334
+ "workflowRevisionId",
1335
+ "declaredVersion",
1336
+ "environment",
1337
+ "provider",
1338
+ "model",
1339
+ "status",
1340
+ "result"
1341
+ ];
1342
+ const metadata = metadataFields.flatMap(
1343
+ (field) => JSON.stringify(left.run[field]) === JSON.stringify(right.run[field]) ? [] : [{ field, left: left.run[field], right: right.run[field] }]
1344
+ );
1345
+ if (JSON.stringify(left.deploymentRefs ?? []) !== JSON.stringify(right.deploymentRefs ?? []))
1346
+ metadata.push({
1347
+ field: "deploymentRefs",
1348
+ left: left.deploymentRefs ?? [],
1349
+ right: right.deploymentRefs ?? []
1350
+ });
1351
+ const signature = (item) => `${item.kind}:${item.toolName ?? item.interruptType ?? item.limit ?? ""}`;
1352
+ const length = Math.max(left.timeline.length, right.timeline.length);
1353
+ const comparable = (item) => {
1354
+ const { step: _step, eventId: _eventId, at: _at, ...rest } = item;
1355
+ return rest;
1356
+ };
1357
+ const timeline = Array.from({ length }, (_, index) => {
1358
+ const leftItem = left.timeline[index] ?? null;
1359
+ const rightItem = right.timeline[index] ?? null;
1360
+ return {
1361
+ index: index + 1,
1362
+ status: leftItem && rightItem ? JSON.stringify(comparable(leftItem)) === JSON.stringify(comparable(rightItem)) ? "same" : signature(leftItem) === signature(rightItem) ? "changed" : "diverged" : leftItem ? "removed" : "added",
1363
+ left: leftItem,
1364
+ right: rightItem
1365
+ };
1366
+ });
1367
+ return {
1368
+ metadata,
1369
+ timeline,
1370
+ firstDivergence: timeline.find((item) => item.status !== "same")?.index ?? null,
1371
+ different: metadata.length > 0 || timeline.some((item) => item.status !== "same")
1372
+ };
1373
+ };
1374
+
1375
+ // src/studio/read-output.ts
1376
+ var parseStudioTarget = (input, entity) => {
1377
+ if (/^[a-z][a-z0-9+.-]*:/i.test(input)) {
1378
+ let url;
1379
+ try {
1380
+ url = new URL(input);
1381
+ } catch {
1382
+ throw new StudioReadError("invalid_target", "Invalid Studio URL.");
1383
+ }
1384
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
1385
+ throw new StudioReadError(
1386
+ "invalid_target",
1387
+ "Use an HTTP(S) Studio entity URL without embedded credentials."
1388
+ );
1389
+ }
1390
+ const match = url.pathname.match(
1391
+ /\/(runs|sessions|interrupts)\/([^/]+)\/?$/
1392
+ );
1393
+ if (!match)
1394
+ throw new StudioReadError(
1395
+ "invalid_target",
1396
+ "Expected a Studio /runs/<id>, /sessions/<id>, or /interrupts/<id> URL."
1397
+ );
1398
+ if (entity && match[1] !== entity)
1399
+ throw new StudioReadError(
1400
+ "invalid_target",
1401
+ "URL entity type does not match the command."
1402
+ );
1403
+ let id;
1404
+ try {
1405
+ id = decodeURIComponent(match[2] ?? "");
1406
+ } catch {
1407
+ throw new StudioReadError(
1408
+ "invalid_target",
1409
+ "Invalid entity ID encoding."
1410
+ );
1411
+ }
1412
+ validateId(id);
1413
+ const selection = {};
1414
+ for (const key of [
1415
+ "tab",
1416
+ "sessionTab",
1417
+ "call",
1418
+ "branch",
1419
+ "node",
1420
+ "event",
1421
+ "trace",
1422
+ "detailView"
1423
+ ]) {
1424
+ const value = url.searchParams.get(key);
1425
+ if (value) selection[key] = value;
1426
+ }
1427
+ url.search = "";
1428
+ url.hash = "";
1429
+ return {
1430
+ entity: match[1],
1431
+ id,
1432
+ url: url.toString(),
1433
+ selection
1434
+ };
1435
+ }
1436
+ if (!entity)
1437
+ throw new StudioReadError(
1438
+ "invalid_target",
1439
+ "inspect requires a Studio entity URL. Use runs/sessions/interrupts get for a bare ID."
1440
+ );
1441
+ validateId(input);
1442
+ return { entity, id: input };
1443
+ };
1444
+ var validateId = (id) => {
1445
+ if (!id || id.length > 512 || id === "." || id === ".." || /[\s/?#\\]/.test(id) || [...id].some(
1446
+ (char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127
1447
+ )) {
1448
+ throw new StudioReadError("invalid_target", "Invalid Studio entity ID.");
1449
+ }
1450
+ };
1451
+ var secretKey = /(?:password|secret|authorization|cookie|apikey|credential|resumetoken|accesstoken|refreshtoken|bearertoken|sessiontoken|authtoken)/i;
1452
+ var contentKey = /^(?:input|output|inputs|outputs|inputmessages|outputmessages|messages|prompt|systemprompt|question|response|result|latestresult|arguments|args|content|text|options)$/i;
1453
+ var sanitizeStudioData = (value, includeContent, key = "") => {
1454
+ const normalized = key.replace(/[^a-z0-9]/gi, "");
1455
+ const keyPolicy = normalized.toLowerCase() === "apikey" && value && typeof value === "object" && "scopes" in value && "mode" in value;
1456
+ if (secretKey.test(normalized) && !keyPolicy) return "[REDACTED]";
1457
+ if (!includeContent && contentKey.test(normalized))
1458
+ return "[CONTENT OMITTED: use --include-content]";
1459
+ if (typeof value === "string")
1460
+ return value.replace(/ktyx_(?:test|live)_[^\s"'<>]+/g, "[REDACTED]").replace(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]");
1461
+ if (Array.isArray(value))
1462
+ return value.map((item) => sanitizeStudioData(item, includeContent));
1463
+ if (value && typeof value === "object")
1464
+ return Object.fromEntries(
1465
+ Object.entries(value).map(([name, item]) => [
1466
+ name,
1467
+ sanitizeStudioData(item, includeContent, name)
1468
+ ])
1469
+ );
1470
+ return value;
1471
+ };
1472
+ var summarizeEvidence = (events) => {
1473
+ const findings = events.flatMap((event) => {
1474
+ const payload = event.payload;
1475
+ const failed = /(?:failed|error|fault)/i.test(event.type) || payload.outcome === "fault" || payload.status === "failed";
1476
+ const warning = /(?:retry|cancel|interrupt|limit_reached|denied|suspended|waiting)/i.test(
1477
+ event.type
1478
+ ) || ["denied", "cancelled"].includes(String(payload.outcome));
1479
+ if (!failed && !warning) return [];
1480
+ return [
1481
+ {
1482
+ eventId: event.id,
1483
+ runId: event.runId,
1484
+ nodeId: event.nodeId,
1485
+ type: event.type,
1486
+ occurredAt: event.occurredAt,
1487
+ severity: failed ? "error" : "context",
1488
+ evidence: payload
1489
+ }
1490
+ ];
1491
+ });
1492
+ return {
1493
+ findings,
1494
+ 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."
1495
+ };
1496
+ };
1497
+ var formatReadOutput = (value, json) => {
1498
+ if (json) return JSON.stringify(value);
1499
+ return JSON.stringify(value, null, 2);
1500
+ };
1501
+
1502
+ // src/studio/read-command.ts
1503
+ var integer = (min, max) => (value) => {
1504
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > max) {
1505
+ throw new import_commander2.InvalidArgumentError(
1506
+ `Expected an integer between ${min} and ${max}.`
1507
+ );
1508
+ }
1509
+ return Number(value);
1510
+ };
1511
+ var rangeAliases = {
1512
+ "1h": "Last hour",
1513
+ "24h": "24 hours",
1514
+ "7d": "7 days",
1515
+ "30d": "30 days",
1516
+ all: "All time"
1517
+ };
1518
+ var registerStudioReadCommands = (studio, log, request = fetch) => {
1519
+ const common = (command) => command.option(
1520
+ "--connection <name>",
1521
+ "Named project/URL connection (or KORTYX_CONNECTION)."
1522
+ ).option(
1523
+ "--config-home <path>",
1524
+ "Connection configuration directory.",
1525
+ defaultConnectionsHome()
1526
+ ).option(
1527
+ "--home <path>",
1528
+ "Managed local Studio directory.",
1529
+ defaultStudioHome()
1530
+ ).option(
1531
+ "--api-url <url>",
1532
+ "Direct API URL; requires --api-key-env. Never reuses a profile key."
1533
+ ).option(
1534
+ "--api-key-env <name>",
1535
+ "Environment variable holding a direct connection read key."
1536
+ ).option("--json", "Print stable machine-readable JSON.").option(
1537
+ "--include-content",
1538
+ "Include captured input/output and prompts (may contain sensitive data)."
1539
+ );
1540
+ const print = (value, options) => log(
1541
+ formatReadOutput(
1542
+ sanitizeStudioData(value, options.includeContent ?? false),
1543
+ options.json ?? false
1544
+ )
1545
+ );
1546
+ const clientFor = async (options, targetUrl) => {
1547
+ const connection = await resolveConnection(options, targetUrl);
1548
+ return {
1549
+ connection,
1550
+ client: new StudioReadClient(
1551
+ connection.apiUrl,
1552
+ connection.apiKey,
1553
+ request
1554
+ )
1555
+ };
1556
+ };
1557
+ const inspect = async (input, options, entity) => {
1558
+ const target = parseStudioTarget(input, entity);
1559
+ const { connection, client } = await clientFor(options, target.url);
1560
+ const context = await client.get(
1561
+ "/v1/studio/context",
1562
+ import_telemetry_contracts3.StudioContextResponseSchema
1563
+ );
1564
+ const path = `/v1/studio/${target.entity}/${encodeURIComponent(target.id)}`;
1565
+ const detail = target.entity === "runs" ? await client.get(path, import_telemetry_contracts3.StudioRunDetailResponseSchema) : target.entity === "sessions" ? await client.get(path, import_telemetry_contracts3.StudioSessionDetailResponseSchema) : await client.get(path, import_telemetry_contracts3.StudioInterruptDetailResponseSchema);
1566
+ const limit = options.eventLimit;
1567
+ const events = [...detail.events].sort(
1568
+ (a, b) => Date.parse(a.occurredAt) - Date.parse(b.occurredAt) || Date.parse(a.receivedAt) - Date.parse(b.receivedAt) || a.id.localeCompare(b.id)
1569
+ );
1570
+ const focus = options.focusSelection ? focusStudioEvents(events, target) : { events, applied: {} };
1571
+ const selectedEvents = focus.events;
1572
+ const evidence = summarizeEvidence(selectedEvents);
1573
+ const run2 = "run" in detail ? detail.run : null;
1574
+ const interrupts = "interrupts" in detail ? detail.interrupts : "interrupt" in detail ? [detail.interrupt] : [];
1575
+ const analyzedFindings = analyzeStudioDiagnostics(
1576
+ selectedEvents,
1577
+ run2,
1578
+ interrupts
1579
+ );
1580
+ const supersededEventIds = new Set(
1581
+ analyzedFindings.flatMap((finding) => finding.eventIds)
1582
+ );
1583
+ const findings = [
1584
+ ...evidence.findings.filter(
1585
+ (finding) => !supersededEventIds.has(finding.eventId)
1586
+ ),
1587
+ ...analyzedFindings
1588
+ ];
1589
+ const calls = (0, import_telemetry_contracts3.projectWorkflowCalls)(selectedEvents);
1590
+ let catalog;
1591
+ if (target.entity === "runs" && run2) {
1592
+ try {
1593
+ const workflows2 = await client.get(
1594
+ "/v1/studio/workflows",
1595
+ import_telemetry_contracts3.StudioWorkflowsResponseSchema,
1596
+ { range: "All time", workflow: run2.workflowId }
1597
+ );
1598
+ catalog = compareCatalogRuntime(run2, workflows2.workflows);
1599
+ } catch (error) {
1600
+ catalog = {
1601
+ status: "unavailable",
1602
+ workflows: [],
1603
+ reason: error instanceof StudioReadError ? error.code : "catalog_comparison_failed"
1604
+ };
1605
+ }
1606
+ }
1607
+ const catalogFinding = catalog?.status === "drift" ? [
1608
+ {
1609
+ code: "catalog_runtime_drift",
1610
+ severity: "warning",
1611
+ message: "The executed workflow revision differs from the currently active catalog revision.",
1612
+ eventIds: [],
1613
+ evidence: catalog.workflows
1614
+ }
1615
+ ] : [];
1616
+ const allFindings = [...findings, ...catalogFinding];
1617
+ print(
1618
+ {
1619
+ schemaVersion: 1,
1620
+ connection: {
1621
+ name: connection.name,
1622
+ apiUrl: connection.apiUrl,
1623
+ context
1624
+ },
1625
+ target,
1626
+ detail: { ...detail, events: selectedEvents.slice(-limit) },
1627
+ timeline: buildStudioTimeline(selectedEvents, interrupts).slice(-limit),
1628
+ calls: calls.slice(-limit).map(({ events: callEvents, ...call }) => ({
1629
+ ...call,
1630
+ eventIds: callEvents.map((event) => event.id)
1631
+ })),
1632
+ diagnostics: {
1633
+ ...evidence,
1634
+ findings: allFindings.slice(-limit),
1635
+ findingCount: allFindings.length,
1636
+ findingsOmitted: Math.max(0, allFindings.length - limit)
1637
+ },
1638
+ ...catalog ? { catalog } : {},
1639
+ coverage: {
1640
+ returnedEvents: Math.min(selectedEvents.length, limit),
1641
+ availableEvents: selectedEvents.length,
1642
+ omittedEvents: Math.max(0, selectedEvents.length - limit),
1643
+ eventWindow: "latest",
1644
+ selectionFocus: options.focusSelection ? {
1645
+ applied: focus.applied,
1646
+ matchedEvents: selectedEvents.length,
1647
+ totalEvents: events.length
1648
+ } : null,
1649
+ contentIncluded: options.includeContent ?? false,
1650
+ availableCalls: calls.length,
1651
+ omittedCalls: Math.max(0, calls.length - limit),
1652
+ note: "Available events are those returned by Studio, not a guarantee of complete runtime history. Content capture is opt-in; secrets are redacted on a best-effort basis even with --include-content."
1653
+ }
1654
+ },
1655
+ options
1656
+ );
1657
+ };
1658
+ const detailOptions = (command) => common(command).option(
1659
+ "--event-limit <count>",
1660
+ "Maximum latest events and diagnostic findings (1\u201310000).",
1661
+ integer(1, 1e4),
1662
+ 100
1663
+ ).option(
1664
+ "--focus-selection",
1665
+ "Focus events using call/event/branch/node/trace selectors from the URL."
1666
+ );
1667
+ detailOptions(
1668
+ studio.command("inspect <url>").description(
1669
+ "Inspect a pasted run/session/interrupt URL using a matching configured connection."
1670
+ )
1671
+ ).action((input, options) => inspect(input, options));
1672
+ common(
1673
+ studio.command("doctor").description(
1674
+ "Verify the read credential, project, environments, and API compatibility."
1675
+ )
1676
+ ).action(async (options) => {
1677
+ const { connection, client } = await clientFor(options);
1678
+ const context = await client.get(
1679
+ "/v1/studio/context",
1680
+ import_telemetry_contracts3.StudioContextResponseSchema
1681
+ );
1682
+ print(
1683
+ {
1684
+ schemaVersion: 1,
1685
+ connection: connection.name,
1686
+ apiUrl: connection.apiUrl,
1687
+ context
1688
+ },
1689
+ options
1690
+ );
1691
+ });
1692
+ common(
1693
+ studio.command("catalogs").description(
1694
+ "Read available environment/provider/model/workflow/tag filters."
1695
+ )
1696
+ ).action(async (options) => {
1697
+ const { connection, client } = await clientFor(options);
1698
+ print(
1699
+ {
1700
+ schemaVersion: 1,
1701
+ connection: connection.name,
1702
+ data: await client.get(
1703
+ "/v1/studio/catalogs",
1704
+ import_telemetry_contracts3.StudioCatalogsResponseSchema
1705
+ )
1706
+ },
1707
+ options
1708
+ );
1709
+ });
1710
+ const filters = (command) => common(command).option(
1711
+ "--environment <name>",
1712
+ "Environment filter; overrides the connection default."
1713
+ ).addOption(
1714
+ new import_commander2.Option(
1715
+ "--range <range>",
1716
+ "Studio time range: 1h, 24h, 7d, 30d, all (or Studio preset name)."
1717
+ ).choices([
1718
+ ...Object.keys(rangeAliases),
1719
+ ...import_telemetry_contracts3.StudioTimeRangeSchema.options
1720
+ ]).default("24h")
1721
+ ).option(
1722
+ "--started-after <iso>",
1723
+ "Explicit start boundary (requires --started-before)."
1724
+ ).option(
1725
+ "--started-before <iso>",
1726
+ "Explicit end boundary (requires --started-after)."
1727
+ ).option("--workflow <id>", "Workflow filter.");
1728
+ const timeQuery = (options) => {
1729
+ const custom = Boolean(options.startedAfter || options.startedBefore);
1730
+ const range = custom ? "Custom range" : rangeAliases[options.range] ?? options.range;
1731
+ const resolution = (0, import_telemetry_contracts3.resolveStudioTimeRange)({
1732
+ range,
1733
+ startedAfter: options.startedAfter,
1734
+ startedBefore: options.startedBefore
1735
+ });
1736
+ if ("error" in resolution)
1737
+ throw new StudioReadError("invalid_time_range", resolution.error);
1738
+ return custom ? {
1739
+ range,
1740
+ startedAfter: String(resolution.value.startedAfter),
1741
+ startedBefore: String(resolution.value.startedBefore)
1742
+ } : { range };
1743
+ };
1744
+ for (const entity of ["runs", "sessions", "interrupts"]) {
1745
+ const group = studio.command(entity).description(`Read Studio ${entity}.`);
1746
+ detailOptions(
1747
+ group.command("get <id-or-url>").description("Read detail, events, and diagnostic evidence.")
1748
+ ).action(
1749
+ (input, options) => inspect(input, options, entity)
1750
+ );
1751
+ if (entity === "runs") {
1752
+ common(
1753
+ group.command("compare <left-id-or-url> <right-id-or-url>").description(
1754
+ "Compare two runs' versions, outcomes, and compact execution timelines."
1755
+ )
1756
+ ).option(
1757
+ "--event-limit <count>",
1758
+ "Maximum timeline steps returned per run (1\u201310000).",
1759
+ integer(1, 1e4),
1760
+ 100
1761
+ ).action(
1762
+ async (leftInput, rightInput, options) => {
1763
+ const leftTarget = parseStudioTarget(leftInput, "runs");
1764
+ const rightTarget = parseStudioTarget(rightInput, "runs");
1765
+ const { connection, client } = await clientFor(
1766
+ options,
1767
+ leftTarget.url
1768
+ );
1769
+ await resolveConnection(
1770
+ { ...options, connection: connection.name },
1771
+ rightTarget.url
1772
+ );
1773
+ const context = await client.get(
1774
+ "/v1/studio/context",
1775
+ import_telemetry_contracts3.StudioContextResponseSchema
1776
+ );
1777
+ const [leftDetail, rightDetail] = await Promise.all([
1778
+ client.get(
1779
+ `/v1/studio/runs/${encodeURIComponent(leftTarget.id)}`,
1780
+ import_telemetry_contracts3.StudioRunDetailResponseSchema
1781
+ ),
1782
+ client.get(
1783
+ `/v1/studio/runs/${encodeURIComponent(rightTarget.id)}`,
1784
+ import_telemetry_contracts3.StudioRunDetailResponseSchema
1785
+ )
1786
+ ]);
1787
+ const catalogFor = async (detail) => {
1788
+ try {
1789
+ const response = await client.get(
1790
+ "/v1/studio/workflows",
1791
+ import_telemetry_contracts3.StudioWorkflowsResponseSchema,
1792
+ { range: "All time", workflow: detail.run.workflowId }
1793
+ );
1794
+ return compareCatalogRuntime(detail.run, response.workflows);
1795
+ } catch (error) {
1796
+ return {
1797
+ status: "unavailable",
1798
+ workflows: [],
1799
+ reason: error instanceof StudioReadError ? error.code : "catalog_comparison_failed"
1800
+ };
1801
+ }
1802
+ };
1803
+ const [leftCatalog, rightCatalog] = await Promise.all([
1804
+ catalogFor(leftDetail),
1805
+ catalogFor(rightDetail)
1806
+ ]);
1807
+ const side = (target, detail, catalog) => {
1808
+ const ordered = [...detail.events].sort(
1809
+ (a, b) => Date.parse(a.occurredAt) - Date.parse(b.occurredAt) || Date.parse(a.receivedAt) - Date.parse(b.receivedAt) || a.id.localeCompare(b.id)
1810
+ );
1811
+ const timeline = buildStudioTimeline(
1812
+ ordered,
1813
+ detail.interrupts
1814
+ ).slice(-options.eventLimit);
1815
+ return {
1816
+ target,
1817
+ run: detail.run,
1818
+ timeline,
1819
+ catalog,
1820
+ deploymentRefs: [
1821
+ ...new Set(
1822
+ ordered.map((event) => event.deploymentRef).filter((value) => Boolean(value))
1823
+ )
1824
+ ],
1825
+ diagnostics: analyzeStudioDiagnostics(
1826
+ ordered,
1827
+ detail.run,
1828
+ detail.interrupts
1829
+ ),
1830
+ coverage: {
1831
+ returnedTimelineSteps: timeline.length,
1832
+ availableEvents: ordered.length,
1833
+ contentIncluded: options.includeContent ?? false
1834
+ }
1835
+ };
1836
+ };
1837
+ const left = side(leftTarget, leftDetail, leftCatalog);
1838
+ const right = side(rightTarget, rightDetail, rightCatalog);
1839
+ print(
1840
+ {
1841
+ schemaVersion: 1,
1842
+ connection: {
1843
+ name: connection.name,
1844
+ apiUrl: connection.apiUrl,
1845
+ context
1846
+ },
1847
+ left,
1848
+ right,
1849
+ comparison: compareRunAnalysis(left, right),
1850
+ note: "Comparison reports observed differences, not an automated root-cause verdict. Missing or omitted content cannot prove equivalence."
1851
+ },
1852
+ options
1853
+ );
1854
+ }
1855
+ );
1856
+ }
1857
+ const list = filters(
1858
+ group.command("list").description("Read a bounded, filtered page.")
1859
+ ).option("--limit <count>", "Page size (1\u2013100).", integer(1, 100), 25).option(
1860
+ "--cursor <offset>",
1861
+ "Numeric offset returned as nextCursor.",
1862
+ integer(0, Number.MAX_SAFE_INTEGER),
1863
+ 0
1864
+ ).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(
1865
+ new import_commander2.Option("--tool-outcome <outcome>", "Tool outcome filter.").choices([
1866
+ "success",
1867
+ "denied",
1868
+ "fault",
1869
+ "cancelled",
1870
+ "reused"
1871
+ ])
1872
+ ).option("--q <text>", "Search filter.").option("--include-children", "Include child workflow runs.");
1873
+ list.action(async (options) => {
1874
+ const query = timeQuery(options);
1875
+ const { connection, client } = await clientFor(options);
1876
+ const environment = options.environment ?? connection.environment;
1877
+ if (environment) query.env = environment;
1878
+ for (const key of [
1879
+ "status",
1880
+ "workflow",
1881
+ "session",
1882
+ "user",
1883
+ "tenant",
1884
+ "model",
1885
+ "node",
1886
+ "tool",
1887
+ "toolOutcome",
1888
+ "q"
1889
+ ]) {
1890
+ const value = options[key];
1891
+ if (value) query[key] = value;
1892
+ }
1893
+ query.pageSize = String(options.limit);
1894
+ query.cursor = String(options.cursor);
1895
+ if (options.includeChildren) query.includeChildren = "true";
1896
+ const path = `/v1/studio/${entity}`;
1897
+ const data = entity === "runs" ? await client.get(path, import_telemetry_contracts3.StudioRunsResponseSchema, query) : entity === "sessions" ? await client.get(path, import_telemetry_contracts3.StudioSessionsResponseSchema, query) : await client.get(path, import_telemetry_contracts3.StudioInterruptsResponseSchema, query);
1898
+ const items = "runs" in data ? data.runs : "sessions" in data ? data.sessions : data.interrupts;
1899
+ const next = options.cursor + items.length;
1900
+ print(
1901
+ {
1902
+ schemaVersion: 1,
1903
+ connection: connection.name,
1904
+ data,
1905
+ page: {
1906
+ cursor: options.cursor,
1907
+ limit: options.limit,
1908
+ nextCursor: items.length && next < data.totalCount ? next : null
1909
+ }
1910
+ },
1911
+ options
1912
+ );
1913
+ });
1914
+ }
1915
+ const workflows = studio.command("workflows").description("Read declared topology and observed workflow metrics.");
1916
+ filters(
1917
+ workflows.command("list").description("Read workflow topology in a time cohort.")
1918
+ ).option("--version <version>", "Declared version filter.").action(async (options) => {
1919
+ if (options.environment)
1920
+ throw new StudioReadError(
1921
+ "unsupported_filter",
1922
+ "The workflow API does not currently support environment filtering."
1923
+ );
1924
+ const query = timeQuery(options);
1925
+ if (options.workflow) query.workflow = options.workflow;
1926
+ if (options.version) query.version = options.version;
1927
+ const { connection, client } = await clientFor(options);
1928
+ print(
1929
+ {
1930
+ schemaVersion: 1,
1931
+ connection: connection.name,
1932
+ data: await client.get(
1933
+ "/v1/studio/workflows",
1934
+ import_telemetry_contracts3.StudioWorkflowsResponseSchema,
1935
+ query
1936
+ ),
1937
+ note: "Workflow metrics span environments; connection environment defaults are not applied by this API."
1938
+ },
1939
+ options
1940
+ );
1941
+ });
1942
+ };
1943
+
618
1944
  // src/studio/runtime.ts
619
- var import_node_crypto = require("crypto");
1945
+ var import_node_crypto2 = require("crypto");
620
1946
  var import_node_net = require("net");
621
1947
  var stringOutput = (value) => typeof value === "string" ? value : "";
622
1948
  var isCommandNotFound = (error) => error instanceof Error && ("code" in error && error.code === "ENOENT" || error.message.includes("ENOENT"));
@@ -655,32 +1981,33 @@ var defaultStudioRuntime = {
655
1981
  run,
656
1982
  portAvailable,
657
1983
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
658
- random: (bytes) => (0, import_node_crypto.randomBytes)(bytes).toString("base64url"),
659
- log: (message = "") => console.log(message)
1984
+ random: (bytes) => (0, import_node_crypto2.randomBytes)(bytes).toString("base64url"),
1985
+ log: (message = "") => console.log(message),
1986
+ error: (message = "") => console.error(message)
660
1987
  };
661
1988
 
662
1989
  // src/studio/command.ts
663
1990
  var portParser = (value) => {
664
1991
  const port = Number(value);
665
1992
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
666
- throw new import_commander.InvalidArgumentError(
1993
+ throw new import_commander3.InvalidArgumentError(
667
1994
  "Port must be an integer between 1 and 65535."
668
1995
  );
669
1996
  }
670
1997
  return port;
671
1998
  };
672
1999
  var withHome = (command) => command.addOption(
673
- new import_commander.Option("--home <path>", "Local Studio state directory.").default(
2000
+ new import_commander3.Option("--home <path>", "Local Studio state directory.").default(
674
2001
  defaultStudioHome()
675
2002
  )
676
2003
  );
677
2004
  var configureOutput = (command, runtime) => command.configureOutput({
678
2005
  writeOut: (output) => runtime.log(output.trimEnd()),
679
- writeErr: (output) => runtime.log(output.trimEnd())
2006
+ writeErr: (output) => (runtime.error ?? runtime.log)(output.trimEnd())
680
2007
  });
681
2008
  var createStudioCommand = (runtime = defaultStudioRuntime) => {
682
2009
  const studio = configureOutput(
683
- new import_commander.Command("studio").description("Run and manage Kortyx Studio.").showHelpAfterError().addHelpText(
2010
+ new import_commander3.Command("studio").description("Run and manage Kortyx Studio.").showHelpAfterError().addHelpText(
684
2011
  "after",
685
2012
  "\nLocal state and generated credentials are stored in ~/.kortyx/studio by default."
686
2013
  ),
@@ -723,7 +2050,7 @@ var createStudioCommand = (runtime = defaultStudioRuntime) => {
723
2050
  "Generate unpersisted credentials for a self-hosted deployment.",
724
2051
  false
725
2052
  ).addOption(
726
- new import_commander.Option("--format <format>", "Credential output format.").choices(["human", "dotenv"]).default("human")
2053
+ new import_commander3.Option("--format <format>", "Credential output format.").choices(["human", "dotenv"]).default("human")
727
2054
  ).option(
728
2055
  "--service-name <name>",
729
2056
  "SDK service name used by dotenv output.",
@@ -765,19 +2092,20 @@ var createStudioCommand = (runtime = defaultStudioRuntime) => {
765
2092
  ).action(async ({ home, confirm }) => {
766
2093
  await resetStudio(home, confirm, runtime);
767
2094
  });
2095
+ registerStudioReadCommands(studio, (message) => runtime.log(message));
768
2096
  return studio;
769
2097
  };
770
2098
 
771
2099
  // src/workflow-calls.ts
772
- var import_node_path2 = require("path");
2100
+ var import_node_path3 = require("path");
773
2101
  var import_typescript = __toESM(require("typescript"));
774
2102
  function discoverWorkflowCalls(entry, snapshots) {
775
- const configPath = import_typescript.default.findConfigFile((0, import_node_path2.dirname)(entry), import_typescript.default.sys.fileExists);
2103
+ const configPath = import_typescript.default.findConfigFile((0, import_node_path3.dirname)(entry), import_typescript.default.sys.fileExists);
776
2104
  const config = configPath ? import_typescript.default.readConfigFile(configPath, import_typescript.default.sys.readFile).config : {};
777
2105
  const parsed = import_typescript.default.parseJsonConfigFileContent(
778
2106
  config ?? {},
779
2107
  import_typescript.default.sys,
780
- configPath ? (0, import_node_path2.dirname)(configPath) : (0, import_node_path2.dirname)(entry)
2108
+ configPath ? (0, import_node_path3.dirname)(configPath) : (0, import_node_path3.dirname)(entry)
781
2109
  );
782
2110
  const program = import_typescript.default.createProgram([entry], {
783
2111
  ...parsed.options,
@@ -818,7 +2146,7 @@ function discoverWorkflowCalls(entry, snapshots) {
818
2146
  );
819
2147
  };
820
2148
  const factoryBindings = /* @__PURE__ */ new WeakMap();
821
- function resolve3(e, bindings, seen = /* @__PURE__ */ new Set()) {
2149
+ function resolve4(e, bindings, seen = /* @__PURE__ */ new Set()) {
822
2150
  if (!e) return;
823
2151
  e = unwrap(e);
824
2152
  if (seen.has(e)) return;
@@ -826,18 +2154,18 @@ function discoverWorkflowCalls(entry, snapshots) {
826
2154
  if (import_typescript.default.isIdentifier(e)) {
827
2155
  const s = symbol(e);
828
2156
  const bound = s && bindings.get(s);
829
- if (bound) return resolve3(bound, bindings, seen);
2157
+ if (bound) return resolve4(bound, bindings, seen);
830
2158
  const d = s?.valueDeclaration;
831
2159
  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))
832
- return resolve3(d.initializer, bindings, seen);
2160
+ return resolve4(d.initializer, bindings, seen);
833
2161
  }
834
2162
  if (import_typescript.default.isPropertyAccessExpression(e)) {
835
2163
  const found = property(e.expression, e.name.text, bindings, seen);
836
- if (found) return resolve3(found, bindings, seen);
2164
+ if (found) return resolve4(found, bindings, seen);
837
2165
  const owner = symbol(e.expression);
838
2166
  const declaration = symbol(e.name)?.valueDeclaration;
839
2167
  if (owner && owner.flags & import_typescript.default.SymbolFlags.Module && declaration && import_typescript.default.isVariableDeclaration(declaration))
840
- return resolve3(declaration.initializer, bindings, seen);
2168
+ return resolve4(declaration.initializer, bindings, seen);
841
2169
  return e;
842
2170
  }
843
2171
  if (import_typescript.default.isElementAccessExpression(e) && e.argumentExpression && import_typescript.default.isStringLiteralLike(e.argumentExpression)) {
@@ -847,7 +2175,7 @@ function discoverWorkflowCalls(entry, snapshots) {
847
2175
  bindings,
848
2176
  seen
849
2177
  );
850
- return found ? resolve3(found, bindings, seen) : e;
2178
+ return found ? resolve4(found, bindings, seen) : e;
851
2179
  }
852
2180
  if (import_typescript.default.isCallExpression(e)) {
853
2181
  const d = symbol(
@@ -861,12 +2189,12 @@ function discoverWorkflowCalls(entry, snapshots) {
861
2189
  const key = symbol(p.name);
862
2190
  const arg = e.arguments[i] ?? p.initializer;
863
2191
  if (key && arg)
864
- bound.set(key, resolve3(arg, bindings, new Set(seen)) ?? arg);
2192
+ bound.set(key, resolve4(arg, bindings, new Set(seen)) ?? arg);
865
2193
  }
866
2194
  });
867
2195
  const returned = import_typescript.default.isBlock(fn.body) ? fn.body.statements.filter(import_typescript.default.isReturnStatement).map((r) => r.expression) : [fn.body];
868
2196
  if (returned.length === 1 && returned[0]) {
869
- const value = resolve3(returned[0], bound, seen);
2197
+ const value = resolve4(returned[0], bound, seen);
870
2198
  if (value) factoryBindings.set(value, bound);
871
2199
  return value;
872
2200
  }
@@ -875,30 +2203,30 @@ function discoverWorkflowCalls(entry, snapshots) {
875
2203
  return e;
876
2204
  }
877
2205
  function property(e, key, bindings, seen = /* @__PURE__ */ new Set()) {
878
- const obj = resolve3(e, bindings, seen);
2206
+ const obj = resolve4(e, bindings, seen);
879
2207
  if (!obj || !import_typescript.default.isObjectLiteralExpression(obj)) return;
880
2208
  const scoped = factoryBindings.get(obj) ?? bindings;
881
2209
  for (const p of [...obj.properties].reverse()) {
882
2210
  if (import_typescript.default.isSpreadAssignment(p)) {
883
- const spread = resolve3(p.expression, scoped, new Set(seen));
2211
+ const spread = resolve4(p.expression, scoped, new Set(seen));
884
2212
  if (!spread || !import_typescript.default.isObjectLiteralExpression(spread)) return void 0;
885
2213
  const found = property(p.expression, key, scoped, new Set(seen));
886
2214
  if (found) return found;
887
2215
  } else if (p.name && (import_typescript.default.isIdentifier(p.name) || import_typescript.default.isStringLiteralLike(p.name)) && p.name.text === key) {
888
2216
  if (import_typescript.default.isPropertyAssignment(p))
889
- return resolve3(p.initializer, scoped, new Set(seen));
2217
+ return resolve4(p.initializer, scoped, new Set(seen));
890
2218
  if (import_typescript.default.isShorthandPropertyAssignment(p))
891
- return resolve3(p.name, scoped, new Set(seen));
2219
+ return resolve4(p.name, scoped, new Set(seen));
892
2220
  }
893
2221
  }
894
2222
  return void 0;
895
2223
  }
896
2224
  function literal(e, bindings) {
897
- const value = resolve3(e, bindings);
2225
+ const value = resolve4(e, bindings);
898
2226
  return value && import_typescript.default.isStringLiteralLike(value) ? value.text : void 0;
899
2227
  }
900
2228
  function workflowId(e, bindings) {
901
- const value = resolve3(e, bindings);
2229
+ const value = resolve4(e, bindings);
902
2230
  if (!value) return;
903
2231
  if (import_typescript.default.isStringLiteralLike(value)) return value.text;
904
2232
  if (import_typescript.default.isCallExpression(value) && api(value.expression, "defineWorkflow"))
@@ -914,19 +2242,19 @@ function discoverWorkflowCalls(entry, snapshots) {
914
2242
  if (d && import_typescript.default.isBindingElement(d) && (d.propertyName?.getText() ?? d.name.getText()) === "useWorkflow") {
915
2243
  const decl = d.parent.parent;
916
2244
  if (import_typescript.default.isVariableDeclaration(decl) && decl.initializer) {
917
- const value2 = resolve3(decl.initializer, /* @__PURE__ */ new Map());
2245
+ const value2 = resolve4(decl.initializer, /* @__PURE__ */ new Map());
918
2246
  return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
919
2247
  }
920
2248
  }
921
2249
  if (import_typescript.default.isPropertyAccessExpression(e) && e.name.text === "useWorkflow") {
922
- const value2 = resolve3(e.expression, /* @__PURE__ */ new Map());
2250
+ const value2 = resolve4(e.expression, /* @__PURE__ */ new Map());
923
2251
  return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
924
2252
  }
925
- const value = resolve3(e, /* @__PURE__ */ new Map());
2253
+ const value = resolve4(e, /* @__PURE__ */ new Map());
926
2254
  return !!value && value !== e && api(value, "useWorkflow");
927
2255
  }
928
2256
  function functionFor(e) {
929
- const value = resolve3(e, /* @__PURE__ */ new Map());
2257
+ const value = resolve4(e, /* @__PURE__ */ new Map());
930
2258
  if (value && (import_typescript.default.isArrowFunction(value) || import_typescript.default.isFunctionExpression(value)))
931
2259
  return value;
932
2260
  const d = symbol(
@@ -949,7 +2277,7 @@ function discoverWorkflowCalls(entry, snapshots) {
949
2277
  target.toolDiscovery.warnings.push(warning);
950
2278
  warnings.add(warning);
951
2279
  };
952
- const value = resolve3(expression, bindings);
2280
+ const value = resolve4(expression, bindings);
953
2281
  if (!value || seen.has(value)) {
954
2282
  unresolved();
955
2283
  return;
@@ -994,8 +2322,8 @@ function discoverWorkflowCalls(entry, snapshots) {
994
2322
  bindings
995
2323
  );
996
2324
  const schema = property(value, "inputSchema", bindings);
997
- const fields = resolve3(property(schema, "properties", bindings), bindings);
998
- const requiredValue = resolve3(
2325
+ const fields = resolve4(property(schema, "properties", bindings), bindings);
2326
+ const requiredValue = resolve4(
999
2327
  property(schema, "required", bindings),
1000
2328
  bindings
1001
2329
  );
@@ -1084,7 +2412,7 @@ function discoverWorkflowCalls(entry, snapshots) {
1084
2412
  if (!arg) return;
1085
2413
  if (import_typescript.default.isIdentifier(p.name)) {
1086
2414
  const s = symbol(p.name);
1087
- if (s) bound.set(s, resolve3(arg, bindings) ?? arg);
2415
+ if (s) bound.set(s, resolve4(arg, bindings) ?? arg);
1088
2416
  } else if (import_typescript.default.isObjectBindingPattern(p.name)) {
1089
2417
  for (const b of p.name.elements) {
1090
2418
  const value = property(
@@ -1094,7 +2422,7 @@ function discoverWorkflowCalls(entry, snapshots) {
1094
2422
  );
1095
2423
  const s = symbol(b.name);
1096
2424
  if (s && value)
1097
- bound.set(s, resolve3(value, bindings) ?? value);
2425
+ bound.set(s, resolve4(value, bindings) ?? value);
1098
2426
  }
1099
2427
  }
1100
2428
  });
@@ -1109,7 +2437,7 @@ function discoverWorkflowCalls(entry, snapshots) {
1109
2437
  for (const source of program.getSourceFiles().filter(local)) {
1110
2438
  const walk = (n) => {
1111
2439
  if (import_typescript.default.isCallExpression(n) && api(n.expression, "defineWorkflow")) {
1112
- const obj = resolve3(n.arguments[0], /* @__PURE__ */ new Map());
2440
+ const obj = resolve4(n.arguments[0], /* @__PURE__ */ new Map());
1113
2441
  const id = obj && literal(property(obj, "id", /* @__PURE__ */ new Map()), /* @__PURE__ */ new Map());
1114
2442
  if (id && known.has(id) && obj && import_typescript.default.isObjectLiteralExpression(obj))
1115
2443
  definitions.set(id, [...definitions.get(id) ?? [], obj]);
@@ -1179,24 +2507,24 @@ var readPackageName = async (cwd) => {
1179
2507
  let current = cwd;
1180
2508
  for (; ; ) {
1181
2509
  try {
1182
- const raw = await (0, import_promises3.readFile)((0, import_node_path3.join)(current, "package.json"), "utf8");
2510
+ const raw = await (0, import_promises4.readFile)((0, import_node_path4.join)(current, "package.json"), "utf8");
1183
2511
  const parsed = JSON.parse(raw);
1184
2512
  if (isRecord(parsed) && typeof parsed.name === "string") {
1185
2513
  return parsed.name;
1186
2514
  }
1187
2515
  } catch {
1188
2516
  }
1189
- const parent = (0, import_node_path3.dirname)(current);
2517
+ const parent = (0, import_node_path4.dirname)(current);
1190
2518
  if (parent === current) return void 0;
1191
2519
  current = parent;
1192
2520
  }
1193
2521
  };
1194
2522
  var loadEnvFiles = async (cwd) => {
1195
2523
  for (const filename of [".env", ".env.local"]) {
1196
- const path = (0, import_node_path3.join)(cwd, filename);
2524
+ const path = (0, import_node_path4.join)(cwd, filename);
1197
2525
  let raw;
1198
2526
  try {
1199
- raw = await (0, import_promises3.readFile)(path, "utf8");
2527
+ raw = await (0, import_promises4.readFile)(path, "utf8");
1200
2528
  } catch {
1201
2529
  continue;
1202
2530
  }
@@ -1222,7 +2550,7 @@ var resolveOptions = async (options) => {
1222
2550
  if (!options.entry) {
1223
2551
  throw new Error("Missing required option: --entry <path>.");
1224
2552
  }
1225
- const entry = (0, import_node_path3.isAbsolute)(options.entry) ? options.entry : (0, import_node_path3.resolve)(options.cwd, options.entry);
2553
+ const entry = (0, import_node_path4.isAbsolute)(options.entry) ? options.entry : (0, import_node_path4.resolve)(options.cwd, options.entry);
1226
2554
  const environment = options.environment ?? process.env.KORTYX_TELEMETRY_ENVIRONMENT ?? process.env.NODE_ENV ?? "development";
1227
2555
  const serviceName = options.serviceName ?? process.env.KORTYX_TELEMETRY_SERVICE_NAME ?? await readPackageName(options.cwd) ?? "kortyx-app";
1228
2556
  return {
@@ -1236,19 +2564,19 @@ var resolveOptions = async (options) => {
1236
2564
  };
1237
2565
  };
1238
2566
  var createFrameworkMarkerStubs = async () => {
1239
- const root = await (0, import_promises3.mkdtemp)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "kortyx-cli-stubs-"));
2567
+ const root = await (0, import_promises4.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "kortyx-cli-stubs-"));
1240
2568
  for (const moduleName of ["server-only", "client-only"]) {
1241
- const directory = (0, import_node_path3.join)(root, moduleName);
1242
- await (0, import_promises3.mkdir)(directory, { recursive: true });
1243
- await (0, import_promises3.writeFile)((0, import_node_path3.join)(directory, "index.js"), "", "utf8");
1244
- await (0, import_promises3.writeFile)(
1245
- (0, import_node_path3.join)(directory, "package.json"),
2569
+ const directory = (0, import_node_path4.join)(root, moduleName);
2570
+ await (0, import_promises4.mkdir)(directory, { recursive: true });
2571
+ await (0, import_promises4.writeFile)((0, import_node_path4.join)(directory, "index.js"), "", "utf8");
2572
+ await (0, import_promises4.writeFile)(
2573
+ (0, import_node_path4.join)(directory, "package.json"),
1246
2574
  JSON.stringify({ name: moduleName, version: "0.0.0", main: "index.js" }),
1247
2575
  "utf8"
1248
2576
  );
1249
2577
  }
1250
2578
  const previousNodePath = process.env.NODE_PATH;
1251
- process.env.NODE_PATH = previousNodePath ? `${root}${import_node_path3.delimiter}${previousNodePath}` : root;
2579
+ process.env.NODE_PATH = previousNodePath ? `${root}${import_node_path4.delimiter}${previousNodePath}` : root;
1252
2580
  const moduleWithPrivateInit = import_node_module.default;
1253
2581
  moduleWithPrivateInit._initPaths?.();
1254
2582
  return {
@@ -1256,7 +2584,7 @@ var createFrameworkMarkerStubs = async () => {
1256
2584
  if (previousNodePath === void 0) delete process.env.NODE_PATH;
1257
2585
  else process.env.NODE_PATH = previousNodePath;
1258
2586
  moduleWithPrivateInit._initPaths?.();
1259
- await (0, import_promises3.rm)(root, { recursive: true, force: true });
2587
+ await (0, import_promises4.rm)(root, { recursive: true, force: true });
1260
2588
  }
1261
2589
  };
1262
2590
  };
@@ -1264,9 +2592,9 @@ var importEntry = async (entry) => {
1264
2592
  const stubs = await createFrameworkMarkerStubs();
1265
2593
  let unregisterPaths;
1266
2594
  try {
1267
- const tsconfig = await findNearestFile((0, import_node_path3.dirname)(entry), "tsconfig.json");
2595
+ const tsconfig = await findNearestFile((0, import_node_path4.dirname)(entry), "tsconfig.json");
1268
2596
  unregisterPaths = tsconfig ? await registerTsconfigPaths(tsconfig) : void 0;
1269
- const module2 = (0, import_api.require)(entry, (0, import_node_path3.join)((0, import_node_path3.dirname)(entry), "kortyx-cli.cjs"));
2597
+ const module2 = (0, import_api.require)(entry, (0, import_node_path4.join)((0, import_node_path4.dirname)(entry), "kortyx-cli.cjs"));
1270
2598
  if (!isRecord(module2)) {
1271
2599
  throw new Error(`Entry module did not export an object: ${entry}`);
1272
2600
  }
@@ -1279,25 +2607,25 @@ var importEntry = async (entry) => {
1279
2607
  var findNearestFile = async (startDirectory, filename) => {
1280
2608
  let current = startDirectory;
1281
2609
  for (; ; ) {
1282
- const candidate = (0, import_node_path3.join)(current, filename);
2610
+ const candidate = (0, import_node_path4.join)(current, filename);
1283
2611
  try {
1284
- await (0, import_promises3.access)(candidate);
2612
+ await (0, import_promises4.access)(candidate);
1285
2613
  return candidate;
1286
2614
  } catch {
1287
- const parent = (0, import_node_path3.dirname)(current);
2615
+ const parent = (0, import_node_path4.dirname)(current);
1288
2616
  if (parent === current) return void 0;
1289
2617
  current = parent;
1290
2618
  }
1291
2619
  }
1292
2620
  };
1293
2621
  var registerTsconfigPaths = async (tsconfigPath) => {
1294
- const raw = JSON.parse(await (0, import_promises3.readFile)(tsconfigPath, "utf8"));
2622
+ const raw = JSON.parse(await (0, import_promises4.readFile)(tsconfigPath, "utf8"));
1295
2623
  if (!isRecord(raw)) return () => void 0;
1296
2624
  const compilerOptions = isRecord(raw.compilerOptions) ? raw.compilerOptions : void 0;
1297
2625
  const paths = isRecord(compilerOptions?.paths) ? compilerOptions.paths : void 0;
1298
2626
  if (!paths) return () => void 0;
1299
- const tsconfigDirectory = (0, import_node_path3.dirname)(tsconfigPath);
1300
- const baseUrl = typeof compilerOptions?.baseUrl === "string" ? (0, import_node_path3.resolve)(tsconfigDirectory, compilerOptions.baseUrl) : tsconfigDirectory;
2627
+ const tsconfigDirectory = (0, import_node_path4.dirname)(tsconfigPath);
2628
+ const baseUrl = typeof compilerOptions?.baseUrl === "string" ? (0, import_node_path4.resolve)(tsconfigDirectory, compilerOptions.baseUrl) : tsconfigDirectory;
1301
2629
  const mappings = Object.entries(paths).flatMap(([pattern, rawTargets]) => {
1302
2630
  if (!Array.isArray(rawTargets)) return [];
1303
2631
  return rawTargets.filter((target) => typeof target === "string").map((target) => ({ pattern, target }));
@@ -1326,7 +2654,7 @@ var registerTsconfigPaths = async (tsconfigPath) => {
1326
2654
  request.length - mapping.patternSuffix.length
1327
2655
  ) : "";
1328
2656
  const mapped = mapping.wildcard ? `${mapping.targetPrefix}${wildcardValue}${mapping.targetSuffix}` : mapping.target;
1329
- for (const candidate of candidateModulePaths((0, import_node_path3.resolve)(baseUrl, mapped))) {
2657
+ for (const candidate of candidateModulePaths((0, import_node_path4.resolve)(baseUrl, mapped))) {
1330
2658
  try {
1331
2659
  return originalResolve.call(this, candidate, parent, isMain, options);
1332
2660
  } catch {
@@ -1347,10 +2675,10 @@ var candidateModulePaths = (basePath) => [
1347
2675
  `${basePath}.cts`,
1348
2676
  `${basePath}.js`,
1349
2677
  `${basePath}.jsx`,
1350
- (0, import_node_path3.join)(basePath, "index.ts"),
1351
- (0, import_node_path3.join)(basePath, "index.tsx"),
1352
- (0, import_node_path3.join)(basePath, "index.js"),
1353
- (0, import_node_path3.join)(basePath, "index.jsx")
2678
+ (0, import_node_path4.join)(basePath, "index.ts"),
2679
+ (0, import_node_path4.join)(basePath, "index.tsx"),
2680
+ (0, import_node_path4.join)(basePath, "index.js"),
2681
+ (0, import_node_path4.join)(basePath, "index.jsx")
1354
2682
  ];
1355
2683
  var valueFromModule = (module2, exportName) => {
1356
2684
  if (exportName) {
@@ -1412,7 +2740,7 @@ var pushSnapshot = async (apiUrl, apiKey, snapshot) => {
1412
2740
  }
1413
2741
  );
1414
2742
  }
1415
- return import_telemetry_contracts.EnsureWorkflowTopologyResponseSchema.parse(await response.json());
2743
+ return import_telemetry_contracts4.EnsureWorkflowTopologyResponseSchema.parse(await response.json());
1416
2744
  };
1417
2745
  var printResults = (results, options) => {
1418
2746
  if (options.json) {
@@ -1492,7 +2820,7 @@ var runTopologyPush = async (rawOptions) => {
1492
2820
  }
1493
2821
  }
1494
2822
  const parsedSnapshots = snapshots.map(
1495
- (snapshot) => import_telemetry_contracts.EnsureWorkflowTopologyRequestSchema.parse(snapshot)
2823
+ (snapshot) => import_telemetry_contracts4.EnsureWorkflowTopologyRequestSchema.parse(snapshot)
1496
2824
  );
1497
2825
  const results = [];
1498
2826
  for (const snapshot of parsedSnapshots) {
@@ -1508,11 +2836,12 @@ var runTopologyPush = async (rawOptions) => {
1508
2836
  printResults(results, options);
1509
2837
  };
1510
2838
  var createCliProgram = () => {
1511
- const program = new import_commander2.Command().name("kortyx").description("Build, observe, and operate Kortyx projects.").showHelpAfterError().addHelpText(
2839
+ const program = new import_commander4.Command().name("kortyx").description("Build, observe, and operate Kortyx projects.").showHelpAfterError().addHelpText(
1512
2840
  "after",
1513
2841
  '\nRun "kortyx <command> --help" for detailed command options.'
1514
2842
  );
1515
2843
  program.addCommand(createStudioCommand());
2844
+ program.addCommand(createConnectionsCommand());
1516
2845
  const topology = program.command("topology").description("Publish deterministic workflow topology to Studio.");
1517
2846
  topology.command("push").description("Project and publish workflow topology.").requiredOption(
1518
2847
  "--entry <path>",
@@ -1550,7 +2879,14 @@ var main = async () => {
1550
2879
  try {
1551
2880
  await program.parseAsync(process.argv);
1552
2881
  } catch (error) {
1553
- if (error instanceof import_commander2.CommanderError && error.exitCode === 0) return;
2882
+ if (error instanceof import_commander4.CommanderError && error.exitCode === 0) return;
2883
+ if (error instanceof StudioReadError) {
2884
+ console.error(
2885
+ process.argv.includes("--json") ? JSON.stringify(error.toJSON()) : `[${error.code}] ${error.message}`
2886
+ );
2887
+ process.exitCode = 1;
2888
+ return;
2889
+ }
1554
2890
  const failure = (0, import_errors.serializeFailure)(error);
1555
2891
  console.error(
1556
2892
  error instanceof import_errors.KortyxError ? `[${failure.code}] ${failure.message}` : error instanceof Error ? error.message.slice(0, 1024) : failure.message