@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/CHANGELOG.md +22 -0
- package/README.md +133 -0
- package/dist/index.js +1591 -77
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
26
|
+
var import_promises4 = require("fs/promises");
|
|
27
27
|
var import_node_module = __toESM(require("module"));
|
|
28
|
-
var
|
|
29
|
-
var
|
|
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
|
|
33
|
-
var
|
|
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/
|
|
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/
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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,
|
|
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))
|
|
863
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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,
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
2524
|
+
const path = (0, import_node_path4.join)(cwd, filename);
|
|
1034
2525
|
let raw;
|
|
1035
2526
|
try {
|
|
1036
|
-
raw = await (0,
|
|
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,
|
|
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,
|
|
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,
|
|
1079
|
-
await (0,
|
|
1080
|
-
await (0,
|
|
1081
|
-
await (0,
|
|
1082
|
-
(0,
|
|
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}${
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
2610
|
+
const candidate = (0, import_node_path4.join)(current, filename);
|
|
1120
2611
|
try {
|
|
1121
|
-
await (0,
|
|
2612
|
+
await (0, import_promises4.access)(candidate);
|
|
1122
2613
|
return candidate;
|
|
1123
2614
|
} catch {
|
|
1124
|
-
const parent = (0,
|
|
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,
|
|
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,
|
|
1137
|
-
const baseUrl = typeof compilerOptions?.baseUrl === "string" ? (0,
|
|
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,
|
|
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,
|
|
1188
|
-
(0,
|
|
1189
|
-
(0,
|
|
1190
|
-
(0,
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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
|