@kortyx/cli 0.9.0 → 0.10.1
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 +17 -0
- package/README.md +127 -0
- package/dist/index.js +1413 -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,
|
|
@@ -818,7 +2146,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
818
2146
|
);
|
|
819
2147
|
};
|
|
820
2148
|
const factoryBindings = /* @__PURE__ */ new WeakMap();
|
|
821
|
-
function
|
|
2149
|
+
function resolve4(e, bindings, seen = /* @__PURE__ */ new Set()) {
|
|
822
2150
|
if (!e) return;
|
|
823
2151
|
e = unwrap(e);
|
|
824
2152
|
if (seen.has(e)) return;
|
|
@@ -826,18 +2154,18 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
826
2154
|
if (import_typescript.default.isIdentifier(e)) {
|
|
827
2155
|
const s = symbol(e);
|
|
828
2156
|
const bound = s && bindings.get(s);
|
|
829
|
-
if (bound) return
|
|
2157
|
+
if (bound) return resolve4(bound, bindings, seen);
|
|
830
2158
|
const d = s?.valueDeclaration;
|
|
831
2159
|
if (d && (import_typescript.default.isVariableDeclaration(d) || import_typescript.default.isPropertyAssignment(d)) && d.initializer && (!import_typescript.default.isVariableDeclaration(d) || import_typescript.default.isVariableDeclarationList(d.parent) && (d.parent.flags & import_typescript.default.NodeFlags.Const) !== 0))
|
|
832
|
-
return
|
|
2160
|
+
return resolve4(d.initializer, bindings, seen);
|
|
833
2161
|
}
|
|
834
2162
|
if (import_typescript.default.isPropertyAccessExpression(e)) {
|
|
835
2163
|
const found = property(e.expression, e.name.text, bindings, seen);
|
|
836
|
-
if (found) return
|
|
2164
|
+
if (found) return resolve4(found, bindings, seen);
|
|
837
2165
|
const owner = symbol(e.expression);
|
|
838
2166
|
const declaration = symbol(e.name)?.valueDeclaration;
|
|
839
2167
|
if (owner && owner.flags & import_typescript.default.SymbolFlags.Module && declaration && import_typescript.default.isVariableDeclaration(declaration))
|
|
840
|
-
return
|
|
2168
|
+
return resolve4(declaration.initializer, bindings, seen);
|
|
841
2169
|
return e;
|
|
842
2170
|
}
|
|
843
2171
|
if (import_typescript.default.isElementAccessExpression(e) && e.argumentExpression && import_typescript.default.isStringLiteralLike(e.argumentExpression)) {
|
|
@@ -847,7 +2175,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
847
2175
|
bindings,
|
|
848
2176
|
seen
|
|
849
2177
|
);
|
|
850
|
-
return found ?
|
|
2178
|
+
return found ? resolve4(found, bindings, seen) : e;
|
|
851
2179
|
}
|
|
852
2180
|
if (import_typescript.default.isCallExpression(e)) {
|
|
853
2181
|
const d = symbol(
|
|
@@ -861,12 +2189,12 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
861
2189
|
const key = symbol(p.name);
|
|
862
2190
|
const arg = e.arguments[i] ?? p.initializer;
|
|
863
2191
|
if (key && arg)
|
|
864
|
-
bound.set(key,
|
|
2192
|
+
bound.set(key, resolve4(arg, bindings, new Set(seen)) ?? arg);
|
|
865
2193
|
}
|
|
866
2194
|
});
|
|
867
2195
|
const returned = import_typescript.default.isBlock(fn.body) ? fn.body.statements.filter(import_typescript.default.isReturnStatement).map((r) => r.expression) : [fn.body];
|
|
868
2196
|
if (returned.length === 1 && returned[0]) {
|
|
869
|
-
const value =
|
|
2197
|
+
const value = resolve4(returned[0], bound, seen);
|
|
870
2198
|
if (value) factoryBindings.set(value, bound);
|
|
871
2199
|
return value;
|
|
872
2200
|
}
|
|
@@ -875,30 +2203,30 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
875
2203
|
return e;
|
|
876
2204
|
}
|
|
877
2205
|
function property(e, key, bindings, seen = /* @__PURE__ */ new Set()) {
|
|
878
|
-
const obj =
|
|
2206
|
+
const obj = resolve4(e, bindings, seen);
|
|
879
2207
|
if (!obj || !import_typescript.default.isObjectLiteralExpression(obj)) return;
|
|
880
2208
|
const scoped = factoryBindings.get(obj) ?? bindings;
|
|
881
2209
|
for (const p of [...obj.properties].reverse()) {
|
|
882
2210
|
if (import_typescript.default.isSpreadAssignment(p)) {
|
|
883
|
-
const spread =
|
|
2211
|
+
const spread = resolve4(p.expression, scoped, new Set(seen));
|
|
884
2212
|
if (!spread || !import_typescript.default.isObjectLiteralExpression(spread)) return void 0;
|
|
885
2213
|
const found = property(p.expression, key, scoped, new Set(seen));
|
|
886
2214
|
if (found) return found;
|
|
887
2215
|
} else if (p.name && (import_typescript.default.isIdentifier(p.name) || import_typescript.default.isStringLiteralLike(p.name)) && p.name.text === key) {
|
|
888
2216
|
if (import_typescript.default.isPropertyAssignment(p))
|
|
889
|
-
return
|
|
2217
|
+
return resolve4(p.initializer, scoped, new Set(seen));
|
|
890
2218
|
if (import_typescript.default.isShorthandPropertyAssignment(p))
|
|
891
|
-
return
|
|
2219
|
+
return resolve4(p.name, scoped, new Set(seen));
|
|
892
2220
|
}
|
|
893
2221
|
}
|
|
894
2222
|
return void 0;
|
|
895
2223
|
}
|
|
896
2224
|
function literal(e, bindings) {
|
|
897
|
-
const value =
|
|
2225
|
+
const value = resolve4(e, bindings);
|
|
898
2226
|
return value && import_typescript.default.isStringLiteralLike(value) ? value.text : void 0;
|
|
899
2227
|
}
|
|
900
2228
|
function workflowId(e, bindings) {
|
|
901
|
-
const value =
|
|
2229
|
+
const value = resolve4(e, bindings);
|
|
902
2230
|
if (!value) return;
|
|
903
2231
|
if (import_typescript.default.isStringLiteralLike(value)) return value.text;
|
|
904
2232
|
if (import_typescript.default.isCallExpression(value) && api(value.expression, "defineWorkflow"))
|
|
@@ -914,19 +2242,19 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
914
2242
|
if (d && import_typescript.default.isBindingElement(d) && (d.propertyName?.getText() ?? d.name.getText()) === "useWorkflow") {
|
|
915
2243
|
const decl = d.parent.parent;
|
|
916
2244
|
if (import_typescript.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
917
|
-
const value2 =
|
|
2245
|
+
const value2 = resolve4(decl.initializer, /* @__PURE__ */ new Map());
|
|
918
2246
|
return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
|
|
919
2247
|
}
|
|
920
2248
|
}
|
|
921
2249
|
if (import_typescript.default.isPropertyAccessExpression(e) && e.name.text === "useWorkflow") {
|
|
922
|
-
const value2 =
|
|
2250
|
+
const value2 = resolve4(e.expression, /* @__PURE__ */ new Map());
|
|
923
2251
|
return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
|
|
924
2252
|
}
|
|
925
|
-
const value =
|
|
2253
|
+
const value = resolve4(e, /* @__PURE__ */ new Map());
|
|
926
2254
|
return !!value && value !== e && api(value, "useWorkflow");
|
|
927
2255
|
}
|
|
928
2256
|
function functionFor(e) {
|
|
929
|
-
const value =
|
|
2257
|
+
const value = resolve4(e, /* @__PURE__ */ new Map());
|
|
930
2258
|
if (value && (import_typescript.default.isArrowFunction(value) || import_typescript.default.isFunctionExpression(value)))
|
|
931
2259
|
return value;
|
|
932
2260
|
const d = symbol(
|
|
@@ -949,7 +2277,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
949
2277
|
target.toolDiscovery.warnings.push(warning);
|
|
950
2278
|
warnings.add(warning);
|
|
951
2279
|
};
|
|
952
|
-
const value =
|
|
2280
|
+
const value = resolve4(expression, bindings);
|
|
953
2281
|
if (!value || seen.has(value)) {
|
|
954
2282
|
unresolved();
|
|
955
2283
|
return;
|
|
@@ -994,8 +2322,8 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
994
2322
|
bindings
|
|
995
2323
|
);
|
|
996
2324
|
const schema = property(value, "inputSchema", bindings);
|
|
997
|
-
const fields =
|
|
998
|
-
const requiredValue =
|
|
2325
|
+
const fields = resolve4(property(schema, "properties", bindings), bindings);
|
|
2326
|
+
const requiredValue = resolve4(
|
|
999
2327
|
property(schema, "required", bindings),
|
|
1000
2328
|
bindings
|
|
1001
2329
|
);
|
|
@@ -1084,7 +2412,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
1084
2412
|
if (!arg) return;
|
|
1085
2413
|
if (import_typescript.default.isIdentifier(p.name)) {
|
|
1086
2414
|
const s = symbol(p.name);
|
|
1087
|
-
if (s) bound.set(s,
|
|
2415
|
+
if (s) bound.set(s, resolve4(arg, bindings) ?? arg);
|
|
1088
2416
|
} else if (import_typescript.default.isObjectBindingPattern(p.name)) {
|
|
1089
2417
|
for (const b of p.name.elements) {
|
|
1090
2418
|
const value = property(
|
|
@@ -1094,7 +2422,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
1094
2422
|
);
|
|
1095
2423
|
const s = symbol(b.name);
|
|
1096
2424
|
if (s && value)
|
|
1097
|
-
bound.set(s,
|
|
2425
|
+
bound.set(s, resolve4(value, bindings) ?? value);
|
|
1098
2426
|
}
|
|
1099
2427
|
}
|
|
1100
2428
|
});
|
|
@@ -1109,7 +2437,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
1109
2437
|
for (const source of program.getSourceFiles().filter(local)) {
|
|
1110
2438
|
const walk = (n) => {
|
|
1111
2439
|
if (import_typescript.default.isCallExpression(n) && api(n.expression, "defineWorkflow")) {
|
|
1112
|
-
const obj =
|
|
2440
|
+
const obj = resolve4(n.arguments[0], /* @__PURE__ */ new Map());
|
|
1113
2441
|
const id = obj && literal(property(obj, "id", /* @__PURE__ */ new Map()), /* @__PURE__ */ new Map());
|
|
1114
2442
|
if (id && known.has(id) && obj && import_typescript.default.isObjectLiteralExpression(obj))
|
|
1115
2443
|
definitions.set(id, [...definitions.get(id) ?? [], obj]);
|
|
@@ -1179,24 +2507,24 @@ var readPackageName = async (cwd) => {
|
|
|
1179
2507
|
let current = cwd;
|
|
1180
2508
|
for (; ; ) {
|
|
1181
2509
|
try {
|
|
1182
|
-
const raw = await (0,
|
|
2510
|
+
const raw = await (0, import_promises4.readFile)((0, import_node_path4.join)(current, "package.json"), "utf8");
|
|
1183
2511
|
const parsed = JSON.parse(raw);
|
|
1184
2512
|
if (isRecord(parsed) && typeof parsed.name === "string") {
|
|
1185
2513
|
return parsed.name;
|
|
1186
2514
|
}
|
|
1187
2515
|
} catch {
|
|
1188
2516
|
}
|
|
1189
|
-
const parent = (0,
|
|
2517
|
+
const parent = (0, import_node_path4.dirname)(current);
|
|
1190
2518
|
if (parent === current) return void 0;
|
|
1191
2519
|
current = parent;
|
|
1192
2520
|
}
|
|
1193
2521
|
};
|
|
1194
2522
|
var loadEnvFiles = async (cwd) => {
|
|
1195
2523
|
for (const filename of [".env", ".env.local"]) {
|
|
1196
|
-
const path = (0,
|
|
2524
|
+
const path = (0, import_node_path4.join)(cwd, filename);
|
|
1197
2525
|
let raw;
|
|
1198
2526
|
try {
|
|
1199
|
-
raw = await (0,
|
|
2527
|
+
raw = await (0, import_promises4.readFile)(path, "utf8");
|
|
1200
2528
|
} catch {
|
|
1201
2529
|
continue;
|
|
1202
2530
|
}
|
|
@@ -1222,7 +2550,7 @@ var resolveOptions = async (options) => {
|
|
|
1222
2550
|
if (!options.entry) {
|
|
1223
2551
|
throw new Error("Missing required option: --entry <path>.");
|
|
1224
2552
|
}
|
|
1225
|
-
const entry = (0,
|
|
2553
|
+
const entry = (0, import_node_path4.isAbsolute)(options.entry) ? options.entry : (0, import_node_path4.resolve)(options.cwd, options.entry);
|
|
1226
2554
|
const environment = options.environment ?? process.env.KORTYX_TELEMETRY_ENVIRONMENT ?? process.env.NODE_ENV ?? "development";
|
|
1227
2555
|
const serviceName = options.serviceName ?? process.env.KORTYX_TELEMETRY_SERVICE_NAME ?? await readPackageName(options.cwd) ?? "kortyx-app";
|
|
1228
2556
|
return {
|
|
@@ -1236,19 +2564,19 @@ var resolveOptions = async (options) => {
|
|
|
1236
2564
|
};
|
|
1237
2565
|
};
|
|
1238
2566
|
var createFrameworkMarkerStubs = async () => {
|
|
1239
|
-
const root = await (0,
|
|
2567
|
+
const root = await (0, import_promises4.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "kortyx-cli-stubs-"));
|
|
1240
2568
|
for (const moduleName of ["server-only", "client-only"]) {
|
|
1241
|
-
const directory = (0,
|
|
1242
|
-
await (0,
|
|
1243
|
-
await (0,
|
|
1244
|
-
await (0,
|
|
1245
|
-
(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"),
|
|
1246
2574
|
JSON.stringify({ name: moduleName, version: "0.0.0", main: "index.js" }),
|
|
1247
2575
|
"utf8"
|
|
1248
2576
|
);
|
|
1249
2577
|
}
|
|
1250
2578
|
const previousNodePath = process.env.NODE_PATH;
|
|
1251
|
-
process.env.NODE_PATH = previousNodePath ? `${root}${
|
|
2579
|
+
process.env.NODE_PATH = previousNodePath ? `${root}${import_node_path4.delimiter}${previousNodePath}` : root;
|
|
1252
2580
|
const moduleWithPrivateInit = import_node_module.default;
|
|
1253
2581
|
moduleWithPrivateInit._initPaths?.();
|
|
1254
2582
|
return {
|
|
@@ -1256,7 +2584,7 @@ var createFrameworkMarkerStubs = async () => {
|
|
|
1256
2584
|
if (previousNodePath === void 0) delete process.env.NODE_PATH;
|
|
1257
2585
|
else process.env.NODE_PATH = previousNodePath;
|
|
1258
2586
|
moduleWithPrivateInit._initPaths?.();
|
|
1259
|
-
await (0,
|
|
2587
|
+
await (0, import_promises4.rm)(root, { recursive: true, force: true });
|
|
1260
2588
|
}
|
|
1261
2589
|
};
|
|
1262
2590
|
};
|
|
@@ -1264,9 +2592,9 @@ var importEntry = async (entry) => {
|
|
|
1264
2592
|
const stubs = await createFrameworkMarkerStubs();
|
|
1265
2593
|
let unregisterPaths;
|
|
1266
2594
|
try {
|
|
1267
|
-
const tsconfig = await findNearestFile((0,
|
|
2595
|
+
const tsconfig = await findNearestFile((0, import_node_path4.dirname)(entry), "tsconfig.json");
|
|
1268
2596
|
unregisterPaths = tsconfig ? await registerTsconfigPaths(tsconfig) : void 0;
|
|
1269
|
-
const module2 = (0, import_api.require)(entry, (0,
|
|
2597
|
+
const module2 = (0, import_api.require)(entry, (0, import_node_path4.join)((0, import_node_path4.dirname)(entry), "kortyx-cli.cjs"));
|
|
1270
2598
|
if (!isRecord(module2)) {
|
|
1271
2599
|
throw new Error(`Entry module did not export an object: ${entry}`);
|
|
1272
2600
|
}
|
|
@@ -1279,25 +2607,25 @@ var importEntry = async (entry) => {
|
|
|
1279
2607
|
var findNearestFile = async (startDirectory, filename) => {
|
|
1280
2608
|
let current = startDirectory;
|
|
1281
2609
|
for (; ; ) {
|
|
1282
|
-
const candidate = (0,
|
|
2610
|
+
const candidate = (0, import_node_path4.join)(current, filename);
|
|
1283
2611
|
try {
|
|
1284
|
-
await (0,
|
|
2612
|
+
await (0, import_promises4.access)(candidate);
|
|
1285
2613
|
return candidate;
|
|
1286
2614
|
} catch {
|
|
1287
|
-
const parent = (0,
|
|
2615
|
+
const parent = (0, import_node_path4.dirname)(current);
|
|
1288
2616
|
if (parent === current) return void 0;
|
|
1289
2617
|
current = parent;
|
|
1290
2618
|
}
|
|
1291
2619
|
}
|
|
1292
2620
|
};
|
|
1293
2621
|
var registerTsconfigPaths = async (tsconfigPath) => {
|
|
1294
|
-
const raw = JSON.parse(await (0,
|
|
2622
|
+
const raw = JSON.parse(await (0, import_promises4.readFile)(tsconfigPath, "utf8"));
|
|
1295
2623
|
if (!isRecord(raw)) return () => void 0;
|
|
1296
2624
|
const compilerOptions = isRecord(raw.compilerOptions) ? raw.compilerOptions : void 0;
|
|
1297
2625
|
const paths = isRecord(compilerOptions?.paths) ? compilerOptions.paths : void 0;
|
|
1298
2626
|
if (!paths) return () => void 0;
|
|
1299
|
-
const tsconfigDirectory = (0,
|
|
1300
|
-
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;
|
|
1301
2629
|
const mappings = Object.entries(paths).flatMap(([pattern, rawTargets]) => {
|
|
1302
2630
|
if (!Array.isArray(rawTargets)) return [];
|
|
1303
2631
|
return rawTargets.filter((target) => typeof target === "string").map((target) => ({ pattern, target }));
|
|
@@ -1326,7 +2654,7 @@ var registerTsconfigPaths = async (tsconfigPath) => {
|
|
|
1326
2654
|
request.length - mapping.patternSuffix.length
|
|
1327
2655
|
) : "";
|
|
1328
2656
|
const mapped = mapping.wildcard ? `${mapping.targetPrefix}${wildcardValue}${mapping.targetSuffix}` : mapping.target;
|
|
1329
|
-
for (const candidate of candidateModulePaths((0,
|
|
2657
|
+
for (const candidate of candidateModulePaths((0, import_node_path4.resolve)(baseUrl, mapped))) {
|
|
1330
2658
|
try {
|
|
1331
2659
|
return originalResolve.call(this, candidate, parent, isMain, options);
|
|
1332
2660
|
} catch {
|
|
@@ -1347,10 +2675,10 @@ var candidateModulePaths = (basePath) => [
|
|
|
1347
2675
|
`${basePath}.cts`,
|
|
1348
2676
|
`${basePath}.js`,
|
|
1349
2677
|
`${basePath}.jsx`,
|
|
1350
|
-
(0,
|
|
1351
|
-
(0,
|
|
1352
|
-
(0,
|
|
1353
|
-
(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")
|
|
1354
2682
|
];
|
|
1355
2683
|
var valueFromModule = (module2, exportName) => {
|
|
1356
2684
|
if (exportName) {
|
|
@@ -1412,7 +2740,7 @@ var pushSnapshot = async (apiUrl, apiKey, snapshot) => {
|
|
|
1412
2740
|
}
|
|
1413
2741
|
);
|
|
1414
2742
|
}
|
|
1415
|
-
return
|
|
2743
|
+
return import_telemetry_contracts4.EnsureWorkflowTopologyResponseSchema.parse(await response.json());
|
|
1416
2744
|
};
|
|
1417
2745
|
var printResults = (results, options) => {
|
|
1418
2746
|
if (options.json) {
|
|
@@ -1492,7 +2820,7 @@ var runTopologyPush = async (rawOptions) => {
|
|
|
1492
2820
|
}
|
|
1493
2821
|
}
|
|
1494
2822
|
const parsedSnapshots = snapshots.map(
|
|
1495
|
-
(snapshot) =>
|
|
2823
|
+
(snapshot) => import_telemetry_contracts4.EnsureWorkflowTopologyRequestSchema.parse(snapshot)
|
|
1496
2824
|
);
|
|
1497
2825
|
const results = [];
|
|
1498
2826
|
for (const snapshot of parsedSnapshots) {
|
|
@@ -1508,11 +2836,12 @@ var runTopologyPush = async (rawOptions) => {
|
|
|
1508
2836
|
printResults(results, options);
|
|
1509
2837
|
};
|
|
1510
2838
|
var createCliProgram = () => {
|
|
1511
|
-
const program = new
|
|
2839
|
+
const program = new import_commander4.Command().name("kortyx").description("Build, observe, and operate Kortyx projects.").showHelpAfterError().addHelpText(
|
|
1512
2840
|
"after",
|
|
1513
2841
|
'\nRun "kortyx <command> --help" for detailed command options.'
|
|
1514
2842
|
);
|
|
1515
2843
|
program.addCommand(createStudioCommand());
|
|
2844
|
+
program.addCommand(createConnectionsCommand());
|
|
1516
2845
|
const topology = program.command("topology").description("Publish deterministic workflow topology to Studio.");
|
|
1517
2846
|
topology.command("push").description("Project and publish workflow topology.").requiredOption(
|
|
1518
2847
|
"--entry <path>",
|
|
@@ -1550,7 +2879,14 @@ var main = async () => {
|
|
|
1550
2879
|
try {
|
|
1551
2880
|
await program.parseAsync(process.argv);
|
|
1552
2881
|
} catch (error) {
|
|
1553
|
-
if (error instanceof
|
|
2882
|
+
if (error instanceof import_commander4.CommanderError && error.exitCode === 0) return;
|
|
2883
|
+
if (error instanceof StudioReadError) {
|
|
2884
|
+
console.error(
|
|
2885
|
+
process.argv.includes("--json") ? JSON.stringify(error.toJSON()) : `[${error.code}] ${error.message}`
|
|
2886
|
+
);
|
|
2887
|
+
process.exitCode = 1;
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
1554
2890
|
const failure = (0, import_errors.serializeFailure)(error);
|
|
1555
2891
|
console.error(
|
|
1556
2892
|
error instanceof import_errors.KortyxError ? `[${failure.code}] ${failure.message}` : error instanceof Error ? error.message.slice(0, 1024) : failure.message
|