@kortyx/cli 0.8.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,
@@ -793,6 +2121,7 @@ function discoverWorkflowCalls(entry, snapshots) {
793
2121
  );
794
2122
  const calls = /* @__PURE__ */ new Map();
795
2123
  const warnings = /* @__PURE__ */ new Set();
2124
+ const attachedTools = /* @__PURE__ */ new Map();
796
2125
  const definitions = /* @__PURE__ */ new Map();
797
2126
  const local = (n) => !n.getSourceFile().isDeclarationFile && !n.getSourceFile().fileName.includes("node_modules");
798
2127
  const symbol = (n) => {
@@ -816,7 +2145,8 @@ function discoverWorkflowCalls(entry, snapshots) {
816
2145
  )
817
2146
  );
818
2147
  };
819
- function resolve3(e, bindings, seen = /* @__PURE__ */ new Set()) {
2148
+ const factoryBindings = /* @__PURE__ */ new WeakMap();
2149
+ function resolve4(e, bindings, seen = /* @__PURE__ */ new Set()) {
820
2150
  if (!e) return;
821
2151
  e = unwrap(e);
822
2152
  if (seen.has(e)) return;
@@ -824,18 +2154,18 @@ function discoverWorkflowCalls(entry, snapshots) {
824
2154
  if (import_typescript.default.isIdentifier(e)) {
825
2155
  const s = symbol(e);
826
2156
  const bound = s && bindings.get(s);
827
- if (bound) return resolve3(bound, bindings, seen);
2157
+ if (bound) return resolve4(bound, bindings, seen);
828
2158
  const d = s?.valueDeclaration;
829
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))
830
- return resolve3(d.initializer, bindings, seen);
2160
+ return resolve4(d.initializer, bindings, seen);
831
2161
  }
832
2162
  if (import_typescript.default.isPropertyAccessExpression(e)) {
833
2163
  const found = property(e.expression, e.name.text, bindings, seen);
834
- if (found) return resolve3(found, bindings, seen);
2164
+ if (found) return resolve4(found, bindings, seen);
835
2165
  const owner = symbol(e.expression);
836
2166
  const declaration = symbol(e.name)?.valueDeclaration;
837
2167
  if (owner && owner.flags & import_typescript.default.SymbolFlags.Module && declaration && import_typescript.default.isVariableDeclaration(declaration))
838
- return resolve3(declaration.initializer, bindings, seen);
2168
+ return resolve4(declaration.initializer, bindings, seen);
839
2169
  return e;
840
2170
  }
841
2171
  if (import_typescript.default.isElementAccessExpression(e) && e.argumentExpression && import_typescript.default.isStringLiteralLike(e.argumentExpression)) {
@@ -845,32 +2175,58 @@ function discoverWorkflowCalls(entry, snapshots) {
845
2175
  bindings,
846
2176
  seen
847
2177
  );
848
- return found ? resolve3(found, bindings, seen) : e;
2178
+ return found ? resolve4(found, bindings, seen) : e;
2179
+ }
2180
+ if (import_typescript.default.isCallExpression(e)) {
2181
+ const d = symbol(
2182
+ import_typescript.default.isPropertyAccessExpression(e.expression) ? e.expression.name : e.expression
2183
+ )?.valueDeclaration;
2184
+ const fn = d && import_typescript.default.isFunctionDeclaration(d) ? d : d && import_typescript.default.isVariableDeclaration(d) && d.initializer && (import_typescript.default.isArrowFunction(d.initializer) || import_typescript.default.isFunctionExpression(d.initializer)) ? d.initializer : void 0;
2185
+ if (fn?.body && local(fn)) {
2186
+ const bound = new Map(bindings);
2187
+ fn.parameters.forEach((p, i) => {
2188
+ if (import_typescript.default.isIdentifier(p.name)) {
2189
+ const key = symbol(p.name);
2190
+ const arg = e.arguments[i] ?? p.initializer;
2191
+ if (key && arg)
2192
+ bound.set(key, resolve4(arg, bindings, new Set(seen)) ?? arg);
2193
+ }
2194
+ });
2195
+ const returned = import_typescript.default.isBlock(fn.body) ? fn.body.statements.filter(import_typescript.default.isReturnStatement).map((r) => r.expression) : [fn.body];
2196
+ if (returned.length === 1 && returned[0]) {
2197
+ const value = resolve4(returned[0], bound, seen);
2198
+ if (value) factoryBindings.set(value, bound);
2199
+ return value;
2200
+ }
2201
+ }
849
2202
  }
850
2203
  return e;
851
2204
  }
852
2205
  function property(e, key, bindings, seen = /* @__PURE__ */ new Set()) {
853
- const obj = resolve3(e, bindings, seen);
2206
+ const obj = resolve4(e, bindings, seen);
854
2207
  if (!obj || !import_typescript.default.isObjectLiteralExpression(obj)) return;
2208
+ const scoped = factoryBindings.get(obj) ?? bindings;
855
2209
  for (const p of [...obj.properties].reverse()) {
856
2210
  if (import_typescript.default.isSpreadAssignment(p)) {
857
- const spread = resolve3(p.expression, bindings, new Set(seen));
2211
+ const spread = resolve4(p.expression, scoped, new Set(seen));
858
2212
  if (!spread || !import_typescript.default.isObjectLiteralExpression(spread)) return void 0;
859
- const found = property(p.expression, key, bindings, new Set(seen));
2213
+ const found = property(p.expression, key, scoped, new Set(seen));
860
2214
  if (found) return found;
861
2215
  } else if (p.name && (import_typescript.default.isIdentifier(p.name) || import_typescript.default.isStringLiteralLike(p.name)) && p.name.text === key) {
862
- if (import_typescript.default.isPropertyAssignment(p)) return p.initializer;
863
- if (import_typescript.default.isShorthandPropertyAssignment(p)) return p.name;
2216
+ if (import_typescript.default.isPropertyAssignment(p))
2217
+ return resolve4(p.initializer, scoped, new Set(seen));
2218
+ if (import_typescript.default.isShorthandPropertyAssignment(p))
2219
+ return resolve4(p.name, scoped, new Set(seen));
864
2220
  }
865
2221
  }
866
2222
  return void 0;
867
2223
  }
868
2224
  function literal(e, bindings) {
869
- const value = resolve3(e, bindings);
2225
+ const value = resolve4(e, bindings);
870
2226
  return value && import_typescript.default.isStringLiteralLike(value) ? value.text : void 0;
871
2227
  }
872
2228
  function workflowId(e, bindings) {
873
- const value = resolve3(e, bindings);
2229
+ const value = resolve4(e, bindings);
874
2230
  if (!value) return;
875
2231
  if (import_typescript.default.isStringLiteralLike(value)) return value.text;
876
2232
  if (import_typescript.default.isCallExpression(value) && api(value.expression, "defineWorkflow"))
@@ -886,19 +2242,19 @@ function discoverWorkflowCalls(entry, snapshots) {
886
2242
  if (d && import_typescript.default.isBindingElement(d) && (d.propertyName?.getText() ?? d.name.getText()) === "useWorkflow") {
887
2243
  const decl = d.parent.parent;
888
2244
  if (import_typescript.default.isVariableDeclaration(decl) && decl.initializer) {
889
- const value2 = resolve3(decl.initializer, /* @__PURE__ */ new Map());
2245
+ const value2 = resolve4(decl.initializer, /* @__PURE__ */ new Map());
890
2246
  return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
891
2247
  }
892
2248
  }
893
2249
  if (import_typescript.default.isPropertyAccessExpression(e) && e.name.text === "useWorkflow") {
894
- const value2 = resolve3(e.expression, /* @__PURE__ */ new Map());
2250
+ const value2 = resolve4(e.expression, /* @__PURE__ */ new Map());
895
2251
  return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
896
2252
  }
897
- const value = resolve3(e, /* @__PURE__ */ new Map());
2253
+ const value = resolve4(e, /* @__PURE__ */ new Map());
898
2254
  return !!value && value !== e && api(value, "useWorkflow");
899
2255
  }
900
2256
  function functionFor(e) {
901
- const value = resolve3(e, /* @__PURE__ */ new Map());
2257
+ const value = resolve4(e, /* @__PURE__ */ new Map());
902
2258
  if (value && (import_typescript.default.isArrowFunction(value) || import_typescript.default.isFunctionExpression(value)))
903
2259
  return value;
904
2260
  const d = symbol(
@@ -906,12 +2262,110 @@ function discoverWorkflowCalls(entry, snapshots) {
906
2262
  )?.valueDeclaration;
907
2263
  return d && (import_typescript.default.isFunctionDeclaration(d) || import_typescript.default.isMethodDeclaration(d)) ? d : void 0;
908
2264
  }
2265
+ function recordTools(expression, mode, bindings, workflow, node, list = false, seen = /* @__PURE__ */ new Set()) {
2266
+ const byNode = attachedTools.get(workflow) ?? /* @__PURE__ */ new Map();
2267
+ attachedTools.set(workflow, byNode);
2268
+ const target = byNode.get(node) ?? {
2269
+ tools: [],
2270
+ toolDiscovery: { status: "complete", warnings: [] }
2271
+ };
2272
+ byNode.set(node, target);
2273
+ const unresolved = () => {
2274
+ target.toolDiscovery.status = "unresolved";
2275
+ const warning = `${workflow}/${node}: tool attachment is not statically resolved; real execution supplies observed tools.`;
2276
+ if (!target.toolDiscovery.warnings.includes(warning))
2277
+ target.toolDiscovery.warnings.push(warning);
2278
+ warnings.add(warning);
2279
+ };
2280
+ const value = resolve4(expression, bindings);
2281
+ if (!value || seen.has(value)) {
2282
+ unresolved();
2283
+ return;
2284
+ }
2285
+ seen.add(value);
2286
+ if (list && import_typescript.default.isArrayLiteralExpression(value)) {
2287
+ for (const item of value.elements) {
2288
+ if (import_typescript.default.isSpreadElement(item))
2289
+ recordTools(
2290
+ item.expression,
2291
+ mode,
2292
+ bindings,
2293
+ workflow,
2294
+ node,
2295
+ true,
2296
+ new Set(seen)
2297
+ );
2298
+ else
2299
+ recordTools(
2300
+ item,
2301
+ mode,
2302
+ bindings,
2303
+ workflow,
2304
+ node,
2305
+ false,
2306
+ new Set(seen)
2307
+ );
2308
+ }
2309
+ return;
2310
+ }
2311
+ if (list) {
2312
+ unresolved();
2313
+ return;
2314
+ }
2315
+ const name = literal(property(value, "name", bindings), bindings);
2316
+ if (!name) {
2317
+ unresolved();
2318
+ return;
2319
+ }
2320
+ const description = literal(
2321
+ property(value, "description", bindings),
2322
+ bindings
2323
+ );
2324
+ const schema = property(value, "inputSchema", bindings);
2325
+ const fields = resolve4(property(schema, "properties", bindings), bindings);
2326
+ const requiredValue = resolve4(
2327
+ property(schema, "required", bindings),
2328
+ bindings
2329
+ );
2330
+ const required = requiredValue && import_typescript.default.isArrayLiteralExpression(requiredValue) ? requiredValue.elements.map((entry2) => literal(entry2, bindings)) : [];
2331
+ const inputFields = fields && import_typescript.default.isObjectLiteralExpression(fields) ? fields.properties.flatMap((field) => {
2332
+ if (!import_typescript.default.isPropertyAssignment(field) || !(import_typescript.default.isIdentifier(field.name) || import_typescript.default.isStringLiteralLike(field.name)))
2333
+ return [];
2334
+ return [
2335
+ {
2336
+ name: field.name.text,
2337
+ type: literal(
2338
+ property(field.initializer, "type", bindings),
2339
+ bindings
2340
+ ) ?? "unknown",
2341
+ required: required.includes(field.name.text)
2342
+ }
2343
+ ];
2344
+ }) : [];
2345
+ if (!target.tools.some(
2346
+ (tool) => tool.name === name && tool.callingMode === mode
2347
+ ))
2348
+ target.tools.push({
2349
+ name,
2350
+ callingMode: mode,
2351
+ provenance: "source",
2352
+ ...description ? { description } : {},
2353
+ ...inputFields.length ? { inputFields } : {}
2354
+ });
2355
+ }
909
2356
  function scanFunction(fn, bindings, workflow, node, stack) {
910
2357
  if (!fn.body || !local(fn) || stack.has(fn)) return;
911
2358
  if (stack.size >= 32) {
912
2359
  warnings.add(
913
2360
  `${workflow}/${node}: custom-hook discovery depth exceeded.`
914
2361
  );
2362
+ const attached = attachedTools.get(workflow)?.get(node);
2363
+ if (attached) {
2364
+ attached.toolDiscovery.status = "unresolved";
2365
+ attached.toolDiscovery.warnings.push(
2366
+ "Custom-hook discovery depth exceeded."
2367
+ );
2368
+ }
915
2369
  return;
916
2370
  }
917
2371
  const next = new Set(stack).add(fn);
@@ -920,7 +2374,18 @@ function discoverWorkflowCalls(entry, snapshots) {
920
2374
  if (import_typescript.default.isVariableDeclaration(n) && n.initializer && (import_typescript.default.isArrowFunction(n.initializer) || import_typescript.default.isFunctionExpression(n.initializer)))
921
2375
  return;
922
2376
  if (import_typescript.default.isCallExpression(n)) {
923
- if (isHook(n.expression)) {
2377
+ if (api(n.expression, "useTool")) {
2378
+ recordTools(
2379
+ property(n.arguments[0], "tool", bindings),
2380
+ "direct",
2381
+ bindings,
2382
+ workflow,
2383
+ node
2384
+ );
2385
+ } else if (api(n.expression, "useReason")) {
2386
+ const list = property(n.arguments[0], "tools", bindings);
2387
+ if (list) recordTools(list, "model", bindings, workflow, node, true);
2388
+ } else if (isHook(n.expression)) {
924
2389
  const target = workflowId(
925
2390
  property(n.arguments[0], "workflow", bindings),
926
2391
  bindings
@@ -947,7 +2412,7 @@ function discoverWorkflowCalls(entry, snapshots) {
947
2412
  if (!arg) return;
948
2413
  if (import_typescript.default.isIdentifier(p.name)) {
949
2414
  const s = symbol(p.name);
950
- if (s) bound.set(s, resolve3(arg, bindings) ?? arg);
2415
+ if (s) bound.set(s, resolve4(arg, bindings) ?? arg);
951
2416
  } else if (import_typescript.default.isObjectBindingPattern(p.name)) {
952
2417
  for (const b of p.name.elements) {
953
2418
  const value = property(
@@ -957,7 +2422,7 @@ function discoverWorkflowCalls(entry, snapshots) {
957
2422
  );
958
2423
  const s = symbol(b.name);
959
2424
  if (s && value)
960
- bound.set(s, resolve3(value, bindings) ?? value);
2425
+ bound.set(s, resolve4(value, bindings) ?? value);
961
2426
  }
962
2427
  }
963
2428
  });
@@ -972,7 +2437,7 @@ function discoverWorkflowCalls(entry, snapshots) {
972
2437
  for (const source of program.getSourceFiles().filter(local)) {
973
2438
  const walk = (n) => {
974
2439
  if (import_typescript.default.isCallExpression(n) && api(n.expression, "defineWorkflow")) {
975
- const obj = resolve3(n.arguments[0], /* @__PURE__ */ new Map());
2440
+ const obj = resolve4(n.arguments[0], /* @__PURE__ */ new Map());
976
2441
  const id = obj && literal(property(obj, "id", /* @__PURE__ */ new Map()), /* @__PURE__ */ new Map());
977
2442
  if (id && known.has(id) && obj && import_typescript.default.isObjectLiteralExpression(obj))
978
2443
  definitions.set(id, [...definitions.get(id) ?? [], obj]);
@@ -982,11 +2447,26 @@ function discoverWorkflowCalls(entry, snapshots) {
982
2447
  walk(source);
983
2448
  }
984
2449
  for (const [id, nodes] of known) {
2450
+ attachedTools.set(
2451
+ id,
2452
+ new Map(
2453
+ [...nodes].map((node) => [
2454
+ node,
2455
+ { tools: [], toolDiscovery: { status: "complete", warnings: [] } }
2456
+ ])
2457
+ )
2458
+ );
985
2459
  const defs = definitions.get(id) ?? [];
986
2460
  if (defs.length !== 1) {
987
2461
  warnings.add(
988
2462
  `${id}: ${defs.length ? "ambiguous" : "unavailable"} workflow source; child-call discovery skipped.`
989
2463
  );
2464
+ for (const node of attachedTools.get(id)?.values() ?? []) {
2465
+ node.toolDiscovery.status = "unresolved";
2466
+ node.toolDiscovery.warnings.push(
2467
+ "Workflow source unavailable or ambiguous."
2468
+ );
2469
+ }
990
2470
  continue;
991
2471
  }
992
2472
  calls.set(id, []);
@@ -996,16 +2476,27 @@ function discoverWorkflowCalls(entry, snapshots) {
996
2476
  const run2 = property(property(obj, node, bindings), "run", bindings);
997
2477
  const fn = run2 && functionFor(run2);
998
2478
  if (fn) scanFunction(fn, bindings, id, node, /* @__PURE__ */ new Set());
999
- else
2479
+ else {
2480
+ const tools = attachedTools.get(id)?.get(node);
2481
+ if (tools) {
2482
+ tools.toolDiscovery.status = "unresolved";
2483
+ tools.toolDiscovery.warnings.push("Node source unavailable.");
2484
+ }
1000
2485
  warnings.add(
1001
2486
  `${id}/${node}: node source unavailable; child-call discovery skipped.`
1002
2487
  );
2488
+ }
1003
2489
  }
1004
2490
  calls.get(id)?.sort(
1005
2491
  (a, b) => a.sourceNodeId.localeCompare(b.sourceNodeId) || a.targetWorkflowId.localeCompare(b.targetWorkflowId)
1006
2492
  );
1007
2493
  }
1008
- return { calls, warnings: [...warnings].sort() };
2494
+ for (const nodes of attachedTools.values())
2495
+ for (const node of nodes.values())
2496
+ node.tools.sort(
2497
+ (a, b) => a.name.localeCompare(b.name) || a.callingMode.localeCompare(b.callingMode)
2498
+ );
2499
+ return { calls, tools: attachedTools, warnings: [...warnings].sort() };
1009
2500
  }
1010
2501
 
1011
2502
  // src/index.ts
@@ -1016,24 +2507,24 @@ var readPackageName = async (cwd) => {
1016
2507
  let current = cwd;
1017
2508
  for (; ; ) {
1018
2509
  try {
1019
- 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");
1020
2511
  const parsed = JSON.parse(raw);
1021
2512
  if (isRecord(parsed) && typeof parsed.name === "string") {
1022
2513
  return parsed.name;
1023
2514
  }
1024
2515
  } catch {
1025
2516
  }
1026
- const parent = (0, import_node_path3.dirname)(current);
2517
+ const parent = (0, import_node_path4.dirname)(current);
1027
2518
  if (parent === current) return void 0;
1028
2519
  current = parent;
1029
2520
  }
1030
2521
  };
1031
2522
  var loadEnvFiles = async (cwd) => {
1032
2523
  for (const filename of [".env", ".env.local"]) {
1033
- const path = (0, import_node_path3.join)(cwd, filename);
2524
+ const path = (0, import_node_path4.join)(cwd, filename);
1034
2525
  let raw;
1035
2526
  try {
1036
- raw = await (0, import_promises3.readFile)(path, "utf8");
2527
+ raw = await (0, import_promises4.readFile)(path, "utf8");
1037
2528
  } catch {
1038
2529
  continue;
1039
2530
  }
@@ -1059,7 +2550,7 @@ var resolveOptions = async (options) => {
1059
2550
  if (!options.entry) {
1060
2551
  throw new Error("Missing required option: --entry <path>.");
1061
2552
  }
1062
- 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);
1063
2554
  const environment = options.environment ?? process.env.KORTYX_TELEMETRY_ENVIRONMENT ?? process.env.NODE_ENV ?? "development";
1064
2555
  const serviceName = options.serviceName ?? process.env.KORTYX_TELEMETRY_SERVICE_NAME ?? await readPackageName(options.cwd) ?? "kortyx-app";
1065
2556
  return {
@@ -1073,19 +2564,19 @@ var resolveOptions = async (options) => {
1073
2564
  };
1074
2565
  };
1075
2566
  var createFrameworkMarkerStubs = async () => {
1076
- 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-"));
1077
2568
  for (const moduleName of ["server-only", "client-only"]) {
1078
- const directory = (0, import_node_path3.join)(root, moduleName);
1079
- await (0, import_promises3.mkdir)(directory, { recursive: true });
1080
- await (0, import_promises3.writeFile)((0, import_node_path3.join)(directory, "index.js"), "", "utf8");
1081
- await (0, import_promises3.writeFile)(
1082
- (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"),
1083
2574
  JSON.stringify({ name: moduleName, version: "0.0.0", main: "index.js" }),
1084
2575
  "utf8"
1085
2576
  );
1086
2577
  }
1087
2578
  const previousNodePath = process.env.NODE_PATH;
1088
- 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;
1089
2580
  const moduleWithPrivateInit = import_node_module.default;
1090
2581
  moduleWithPrivateInit._initPaths?.();
1091
2582
  return {
@@ -1093,7 +2584,7 @@ var createFrameworkMarkerStubs = async () => {
1093
2584
  if (previousNodePath === void 0) delete process.env.NODE_PATH;
1094
2585
  else process.env.NODE_PATH = previousNodePath;
1095
2586
  moduleWithPrivateInit._initPaths?.();
1096
- await (0, import_promises3.rm)(root, { recursive: true, force: true });
2587
+ await (0, import_promises4.rm)(root, { recursive: true, force: true });
1097
2588
  }
1098
2589
  };
1099
2590
  };
@@ -1101,9 +2592,9 @@ var importEntry = async (entry) => {
1101
2592
  const stubs = await createFrameworkMarkerStubs();
1102
2593
  let unregisterPaths;
1103
2594
  try {
1104
- 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");
1105
2596
  unregisterPaths = tsconfig ? await registerTsconfigPaths(tsconfig) : void 0;
1106
- 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"));
1107
2598
  if (!isRecord(module2)) {
1108
2599
  throw new Error(`Entry module did not export an object: ${entry}`);
1109
2600
  }
@@ -1116,25 +2607,25 @@ var importEntry = async (entry) => {
1116
2607
  var findNearestFile = async (startDirectory, filename) => {
1117
2608
  let current = startDirectory;
1118
2609
  for (; ; ) {
1119
- const candidate = (0, import_node_path3.join)(current, filename);
2610
+ const candidate = (0, import_node_path4.join)(current, filename);
1120
2611
  try {
1121
- await (0, import_promises3.access)(candidate);
2612
+ await (0, import_promises4.access)(candidate);
1122
2613
  return candidate;
1123
2614
  } catch {
1124
- const parent = (0, import_node_path3.dirname)(current);
2615
+ const parent = (0, import_node_path4.dirname)(current);
1125
2616
  if (parent === current) return void 0;
1126
2617
  current = parent;
1127
2618
  }
1128
2619
  }
1129
2620
  };
1130
2621
  var registerTsconfigPaths = async (tsconfigPath) => {
1131
- const raw = JSON.parse(await (0, import_promises3.readFile)(tsconfigPath, "utf8"));
2622
+ const raw = JSON.parse(await (0, import_promises4.readFile)(tsconfigPath, "utf8"));
1132
2623
  if (!isRecord(raw)) return () => void 0;
1133
2624
  const compilerOptions = isRecord(raw.compilerOptions) ? raw.compilerOptions : void 0;
1134
2625
  const paths = isRecord(compilerOptions?.paths) ? compilerOptions.paths : void 0;
1135
2626
  if (!paths) return () => void 0;
1136
- const tsconfigDirectory = (0, import_node_path3.dirname)(tsconfigPath);
1137
- 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;
1138
2629
  const mappings = Object.entries(paths).flatMap(([pattern, rawTargets]) => {
1139
2630
  if (!Array.isArray(rawTargets)) return [];
1140
2631
  return rawTargets.filter((target) => typeof target === "string").map((target) => ({ pattern, target }));
@@ -1163,7 +2654,7 @@ var registerTsconfigPaths = async (tsconfigPath) => {
1163
2654
  request.length - mapping.patternSuffix.length
1164
2655
  ) : "";
1165
2656
  const mapped = mapping.wildcard ? `${mapping.targetPrefix}${wildcardValue}${mapping.targetSuffix}` : mapping.target;
1166
- 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))) {
1167
2658
  try {
1168
2659
  return originalResolve.call(this, candidate, parent, isMain, options);
1169
2660
  } catch {
@@ -1184,10 +2675,10 @@ var candidateModulePaths = (basePath) => [
1184
2675
  `${basePath}.cts`,
1185
2676
  `${basePath}.js`,
1186
2677
  `${basePath}.jsx`,
1187
- (0, import_node_path3.join)(basePath, "index.ts"),
1188
- (0, import_node_path3.join)(basePath, "index.tsx"),
1189
- (0, import_node_path3.join)(basePath, "index.js"),
1190
- (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")
1191
2682
  ];
1192
2683
  var valueFromModule = (module2, exportName) => {
1193
2684
  if (exportName) {
@@ -1249,7 +2740,7 @@ var pushSnapshot = async (apiUrl, apiKey, snapshot) => {
1249
2740
  }
1250
2741
  );
1251
2742
  }
1252
- return import_telemetry_contracts.EnsureWorkflowTopologyResponseSchema.parse(await response.json());
2743
+ return import_telemetry_contracts4.EnsureWorkflowTopologyResponseSchema.parse(await response.json());
1253
2744
  };
1254
2745
  var printResults = (results, options) => {
1255
2746
  if (options.json) {
@@ -1262,6 +2753,11 @@ var printResults = (results, options) => {
1262
2753
  topologyHash: snapshot.workflow.topologyHash,
1263
2754
  transitionCount: snapshot.workflow.transitions?.length ?? 0,
1264
2755
  calls: snapshot.workflow.calls ?? [],
2756
+ tools: snapshot.workflow.nodes.map((node) => ({
2757
+ nodeId: node.id,
2758
+ tools: node.tools ?? [],
2759
+ discovery: node.toolDiscovery
2760
+ })),
1265
2761
  workflowRevisionId: response?.workflowRevisionId,
1266
2762
  created: response?.created
1267
2763
  }))
@@ -1280,7 +2776,7 @@ var printResults = (results, options) => {
1280
2776
  `- ${snapshot.workflow.id}@${snapshot.workflow.declaredVersion} hash=${snapshot.workflow.topologyHash.slice(
1281
2777
  0,
1282
2778
  12
1283
- )} transitions=${snapshot.workflow.transitions?.length ?? 0} calls=${snapshot.workflow.calls?.length ?? 0}${revision}`
2779
+ )} transitions=${snapshot.workflow.transitions?.length ?? 0} calls=${snapshot.workflow.calls?.length ?? 0} tools=${snapshot.workflow.nodes.reduce((sum, node) => sum + (node.tools?.length ?? 0), 0)}${revision}`
1284
2780
  );
1285
2781
  }
1286
2782
  };
@@ -1312,9 +2808,19 @@ var runTopologyPush = async (rawOptions) => {
1312
2808
  for (const snapshot of snapshots) {
1313
2809
  const calls = discovered.calls.get(snapshot.workflow.id);
1314
2810
  if (calls) snapshot.workflow.calls = calls;
2811
+ for (const node of snapshot.workflow.nodes) {
2812
+ const discoveredTools = discovered.tools.get(snapshot.workflow.id)?.get(node.id);
2813
+ if (discoveredTools)
2814
+ Object.assign(node, discoveredTools, {
2815
+ toolDiscovery: {
2816
+ ...discoveredTools.toolDiscovery,
2817
+ publishedAt: (/* @__PURE__ */ new Date()).toISOString()
2818
+ }
2819
+ });
2820
+ }
1315
2821
  }
1316
2822
  const parsedSnapshots = snapshots.map(
1317
- (snapshot) => import_telemetry_contracts.EnsureWorkflowTopologyRequestSchema.parse(snapshot)
2823
+ (snapshot) => import_telemetry_contracts4.EnsureWorkflowTopologyRequestSchema.parse(snapshot)
1318
2824
  );
1319
2825
  const results = [];
1320
2826
  for (const snapshot of parsedSnapshots) {
@@ -1330,11 +2836,12 @@ var runTopologyPush = async (rawOptions) => {
1330
2836
  printResults(results, options);
1331
2837
  };
1332
2838
  var createCliProgram = () => {
1333
- 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(
1334
2840
  "after",
1335
2841
  '\nRun "kortyx <command> --help" for detailed command options.'
1336
2842
  );
1337
2843
  program.addCommand(createStudioCommand());
2844
+ program.addCommand(createConnectionsCommand());
1338
2845
  const topology = program.command("topology").description("Publish deterministic workflow topology to Studio.");
1339
2846
  topology.command("push").description("Project and publish workflow topology.").requiredOption(
1340
2847
  "--entry <path>",
@@ -1372,7 +2879,14 @@ var main = async () => {
1372
2879
  try {
1373
2880
  await program.parseAsync(process.argv);
1374
2881
  } catch (error) {
1375
- 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
+ }
1376
2890
  const failure = (0, import_errors.serializeFailure)(error);
1377
2891
  console.error(
1378
2892
  error instanceof import_errors.KortyxError ? `[${failure.code}] ${failure.message}` : error instanceof Error ? error.message.slice(0, 1024) : failure.message