@kortyx/cli 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,900 @@
1
+ #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/index.ts
26
+ var import_promises2 = require("fs/promises");
27
+ var import_node_module = __toESM(require("module"));
28
+ var import_node_os2 = require("os");
29
+ var import_node_path2 = require("path");
30
+ var import_agent = require("@kortyx/agent");
31
+ var import_telemetry_contracts = require("@kortyx/telemetry-contracts");
32
+ var import_commander2 = require("commander");
33
+ var import_api = require("tsx/cjs/api");
34
+
35
+ // src/studio/command.ts
36
+ var import_commander = require("commander");
37
+
38
+ // src/studio/state.ts
39
+ var import_promises = require("fs/promises");
40
+ var import_node_os = require("os");
41
+ var import_node_path = require("path");
42
+ var import_zod = require("zod");
43
+
44
+ // src/studio/compose.ts
45
+ var STUDIO_COMPOSE_FILE = `name: \${KORTYX_COMPOSE_PROJECT_NAME:-kortyx-studio}
46
+
47
+ x-api-env: &api-env
48
+ DATABASE_URL: postgres://kortyx:\${POSTGRES_PASSWORD}@postgres:5432/kortyx
49
+ KORTYX_API_KEY_PEPPER: \${KORTYX_API_KEY_PEPPER}
50
+
51
+ x-bootstrap-keys: &bootstrap-keys
52
+ KORTYX_TELEMETRY_API_KEY: \${KORTYX_TELEMETRY_API_KEY}
53
+ KORTYX_STUDIO_API_KEY: \${KORTYX_STUDIO_API_KEY}
54
+
55
+ services:
56
+ postgres:
57
+ image: postgres:17-alpine
58
+ environment:
59
+ POSTGRES_USER: kortyx
60
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
61
+ POSTGRES_DB: kortyx
62
+ volumes:
63
+ - postgres-data:/var/lib/postgresql/data
64
+ healthcheck:
65
+ test: ["CMD-SHELL", "pg_isready -U kortyx -d kortyx"]
66
+ interval: 2s
67
+ timeout: 5s
68
+ retries: 30
69
+
70
+ db-init:
71
+ image: \${KORTYX_API_IMAGE:-ghcr.io/kortyx-io/kortyx-api}:\${KORTYX_STUDIO_IMAGE_TAG:-latest}
72
+ pull_policy: \${KORTYX_STUDIO_PULL_POLICY:-always}
73
+ environment:
74
+ <<: [*api-env, *bootstrap-keys]
75
+ command: >
76
+ sh -lc "pnpm --filter @kortyx/telemetry-db db:migrate &&
77
+ pnpm --filter @kortyx/telemetry-db db:bootstrap"
78
+ depends_on:
79
+ postgres:
80
+ condition: service_healthy
81
+ restart: "no"
82
+
83
+ api:
84
+ image: \${KORTYX_API_IMAGE:-ghcr.io/kortyx-io/kortyx-api}:\${KORTYX_STUDIO_IMAGE_TAG:-latest}
85
+ pull_policy: \${KORTYX_STUDIO_PULL_POLICY:-always}
86
+ environment:
87
+ <<: *api-env
88
+ NODE_ENV: production
89
+ API_HOST: 0.0.0.0
90
+ API_PORT: 6400
91
+ ports:
92
+ - "127.0.0.1:\${API_PORT:-6400}:6400"
93
+ depends_on:
94
+ db-init:
95
+ condition: service_completed_successfully
96
+ healthcheck:
97
+ test:
98
+ [
99
+ "CMD-SHELL",
100
+ "node -e \\"fetch('http://localhost:6400/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\\"",
101
+ ]
102
+ interval: 5s
103
+ timeout: 5s
104
+ retries: 30
105
+
106
+ studio:
107
+ image: \${KORTYX_STUDIO_IMAGE:-ghcr.io/kortyx-io/kortyx-studio}:\${KORTYX_STUDIO_IMAGE_TAG:-latest}
108
+ pull_policy: \${KORTYX_STUDIO_PULL_POLICY:-always}
109
+ environment:
110
+ NODE_ENV: production
111
+ PORT: 6300
112
+ KORTYX_API_URL: http://api:6400
113
+ KORTYX_STUDIO_API_KEY: \${KORTYX_STUDIO_API_KEY}
114
+ KORTYX_STUDIO_AUTH_MODE: basic
115
+ KORTYX_STUDIO_BASIC_AUTH_USERNAME: \${KORTYX_STUDIO_BASIC_AUTH_USERNAME}
116
+ KORTYX_STUDIO_BASIC_AUTH_PASSWORD: \${KORTYX_STUDIO_BASIC_AUTH_PASSWORD}
117
+ ports:
118
+ - "127.0.0.1:\${STUDIO_PORT:-6300}:6300"
119
+ depends_on:
120
+ api:
121
+ condition: service_healthy
122
+ healthcheck:
123
+ test:
124
+ [
125
+ "CMD-SHELL",
126
+ "node -e \\"fetch('http://localhost:6300').then((r)=>process.exit(r.status<500?0:1)).catch(()=>process.exit(1))\\"",
127
+ ]
128
+ interval: 5s
129
+ timeout: 5s
130
+ retries: 30
131
+
132
+ volumes:
133
+ postgres-data:
134
+ `;
135
+
136
+ // src/studio/state.ts
137
+ var CONFIG_VERSION = 1;
138
+ var DEFAULT_API_PORT = 6400;
139
+ var DEFAULT_STUDIO_PORT = 6300;
140
+ var DEFAULT_IMAGE_TAG = "latest";
141
+ var DEFAULT_USERNAME = "admin";
142
+ var DEFAULT_PROJECT_NAME = "kortyx-studio";
143
+ var PortSchema = import_zod.z.number().int().min(1).max(65535);
144
+ var StudioConfigSchema = import_zod.z.object({
145
+ version: import_zod.z.literal(CONFIG_VERSION),
146
+ apiPort: PortSchema,
147
+ studioPort: PortSchema,
148
+ imageTag: import_zod.z.string().regex(
149
+ /^[A-Za-z0-9][A-Za-z0-9._-]*$/,
150
+ "Image tag contains unsupported characters."
151
+ ),
152
+ username: import_zod.z.string().regex(/^[A-Za-z0-9._@-]+$/, "Username contains unsupported characters."),
153
+ projectName: import_zod.z.string().regex(
154
+ /^[a-z0-9][a-z0-9_-]*$/,
155
+ "Docker Compose project name contains unsupported characters."
156
+ ),
157
+ createdAt: import_zod.z.iso.datetime()
158
+ }).refine((config) => config.apiPort !== config.studioPort, {
159
+ message: "Studio and API ports must be different.",
160
+ path: ["apiPort"]
161
+ });
162
+ var ApiKeySchema = import_zod.z.string().regex(/^ktyx_(?:test|live)_[^_]+_.+$/, "Invalid Kortyx API key.");
163
+ var StudioEnvironmentSchema = import_zod.z.object({
164
+ KORTYX_COMPOSE_PROJECT_NAME: import_zod.z.string().min(1),
165
+ KORTYX_STUDIO_IMAGE_TAG: import_zod.z.string().min(1),
166
+ API_PORT: import_zod.z.string().regex(/^\d+$/),
167
+ STUDIO_PORT: import_zod.z.string().regex(/^\d+$/),
168
+ POSTGRES_PASSWORD: import_zod.z.string().min(16),
169
+ KORTYX_API_KEY_PEPPER: import_zod.z.string().min(16),
170
+ KORTYX_TELEMETRY_API_KEY: ApiKeySchema,
171
+ KORTYX_STUDIO_API_KEY: ApiKeySchema,
172
+ KORTYX_STUDIO_BASIC_AUTH_USERNAME: import_zod.z.string().min(1),
173
+ KORTYX_STUDIO_BASIC_AUTH_PASSWORD: import_zod.z.string().min(12)
174
+ }).passthrough();
175
+ var defaultStudioHome = () => (0, import_node_path.resolve)(
176
+ process.env.KORTYX_STUDIO_HOME ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kortyx", "studio")
177
+ );
178
+ var studioConfigPath = (home) => (0, import_node_path.join)(home, "config.json");
179
+ var studioEnvPath = (home) => (0, import_node_path.join)(home, ".env");
180
+ var studioComposePath = (home) => (0, import_node_path.join)(home, "compose.yml");
181
+ var isMissingFile = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
182
+ var invalidStateError = (label, path, error) => new Error(
183
+ `Unsupported or invalid Studio ${label} at ${path}.
184
+ ${import_zod.z.prettifyError(error)}`
185
+ );
186
+ var readStudioConfig = async (home) => {
187
+ try {
188
+ const parsed = JSON.parse(
189
+ await (0, import_promises.readFile)(studioConfigPath(home), "utf8")
190
+ );
191
+ const result = StudioConfigSchema.safeParse(parsed);
192
+ if (!result.success) {
193
+ throw invalidStateError("config", studioConfigPath(home), result.error);
194
+ }
195
+ return result.data;
196
+ } catch (error) {
197
+ if (isMissingFile(error)) return void 0;
198
+ throw error;
199
+ }
200
+ };
201
+ var resolveConfig = (existing, options, runtime) => {
202
+ const result = StudioConfigSchema.safeParse({
203
+ version: CONFIG_VERSION,
204
+ apiPort: options.apiPort ?? existing?.apiPort ?? DEFAULT_API_PORT,
205
+ studioPort: options.studioPort ?? existing?.studioPort ?? DEFAULT_STUDIO_PORT,
206
+ imageTag: options.imageTag ?? existing?.imageTag ?? DEFAULT_IMAGE_TAG,
207
+ username: options.username ?? existing?.username ?? DEFAULT_USERNAME,
208
+ projectName: options.projectName ?? existing?.projectName ?? DEFAULT_PROJECT_NAME,
209
+ createdAt: existing?.createdAt ?? runtime.now()
210
+ });
211
+ if (!result.success) {
212
+ throw new Error(
213
+ `Invalid Studio configuration.
214
+ ${import_zod.z.prettifyError(result.error)}`
215
+ );
216
+ }
217
+ return result.data;
218
+ };
219
+ var createApiKey = (purpose, runtime) => `ktyx_live_${purpose}${runtime.random(6).toLowerCase()}_${runtime.random(32)}`;
220
+ var createEnvironment = (config, runtime) => StudioEnvironmentSchema.parse({
221
+ KORTYX_COMPOSE_PROJECT_NAME: config.projectName,
222
+ KORTYX_STUDIO_IMAGE_TAG: config.imageTag,
223
+ API_PORT: String(config.apiPort),
224
+ STUDIO_PORT: String(config.studioPort),
225
+ POSTGRES_PASSWORD: runtime.random(32),
226
+ KORTYX_API_KEY_PEPPER: runtime.random(48),
227
+ KORTYX_TELEMETRY_API_KEY: createApiKey("telemetry", runtime),
228
+ KORTYX_STUDIO_API_KEY: createApiKey("studio", runtime),
229
+ KORTYX_STUDIO_BASIC_AUTH_USERNAME: config.username,
230
+ KORTYX_STUDIO_BASIC_AUTH_PASSWORD: runtime.random(24)
231
+ });
232
+ var parseEnvironmentFile = (raw, path) => {
233
+ const values = {};
234
+ for (const line of raw.split(/\r?\n/)) {
235
+ if (!line || line.startsWith("#")) continue;
236
+ const separator = line.indexOf("=");
237
+ if (separator <= 0) {
238
+ throw new Error(`Invalid environment entry in ${path}: ${line}`);
239
+ }
240
+ values[line.slice(0, separator)] = line.slice(separator + 1);
241
+ }
242
+ const result = StudioEnvironmentSchema.safeParse(values);
243
+ if (!result.success) {
244
+ throw invalidStateError("environment", path, result.error);
245
+ }
246
+ return result.data;
247
+ };
248
+ var serializeEnvironment = (environment) => `${Object.entries(environment).map(([key, value]) => `${key}=${value}`).join("\n")}
249
+ `;
250
+ var readStudioEnvironment = async (home) => {
251
+ const path = studioEnvPath(home);
252
+ return parseEnvironmentFile(await (0, import_promises.readFile)(path, "utf8"), path);
253
+ };
254
+ var ensureStudioState = async (options, runtime) => {
255
+ await (0, import_promises.mkdir)(options.home, { recursive: true, mode: 448 });
256
+ await (0, import_promises.chmod)(options.home, 448);
257
+ const existing = await readStudioConfig(options.home);
258
+ const config = resolveConfig(existing, options, runtime);
259
+ await (0, import_promises.writeFile)(
260
+ studioConfigPath(options.home),
261
+ `${JSON.stringify(config, null, 2)}
262
+ `,
263
+ { mode: 384 }
264
+ );
265
+ await (0, import_promises.chmod)(studioConfigPath(options.home), 384);
266
+ await (0, import_promises.writeFile)(studioComposePath(options.home), STUDIO_COMPOSE_FILE, {
267
+ mode: 384
268
+ });
269
+ await (0, import_promises.chmod)(studioComposePath(options.home), 384);
270
+ let environment;
271
+ try {
272
+ environment = await readStudioEnvironment(options.home);
273
+ } catch (error) {
274
+ if (!isMissingFile(error)) throw error;
275
+ environment = createEnvironment(config, runtime);
276
+ }
277
+ const updatedEnvironment = StudioEnvironmentSchema.parse({
278
+ ...environment,
279
+ KORTYX_COMPOSE_PROJECT_NAME: config.projectName,
280
+ KORTYX_STUDIO_IMAGE_TAG: config.imageTag,
281
+ API_PORT: String(config.apiPort),
282
+ STUDIO_PORT: String(config.studioPort),
283
+ KORTYX_STUDIO_BASIC_AUTH_USERNAME: config.username
284
+ });
285
+ await (0, import_promises.writeFile)(
286
+ studioEnvPath(options.home),
287
+ serializeEnvironment(updatedEnvironment),
288
+ { mode: 384 }
289
+ );
290
+ await (0, import_promises.chmod)(studioEnvPath(options.home), 384);
291
+ return config;
292
+ };
293
+ var requireStudioConfig = async (home) => {
294
+ const config = await readStudioConfig(home);
295
+ if (!config) {
296
+ throw new Error(
297
+ `Kortyx Studio is not initialized at ${home}. Run "kortyx studio start" first.`
298
+ );
299
+ }
300
+ return config;
301
+ };
302
+
303
+ // src/studio/local-stack.ts
304
+ var composeArgs = (home, args) => [
305
+ "compose",
306
+ "--env-file",
307
+ studioEnvPath(home),
308
+ "-f",
309
+ studioComposePath(home),
310
+ ...args
311
+ ];
312
+ var runCompose = async (home, args, runtime, inherit = true) => await runtime.run("docker", composeArgs(home, args), {
313
+ cwd: home,
314
+ inherit
315
+ });
316
+ var preflightDocker = async (runtime) => {
317
+ await runtime.run("docker", ["version", "--format", "{{.Server.Version}}"], {
318
+ inherit: false
319
+ });
320
+ await runtime.run("docker", ["compose", "version", "--short"], {
321
+ inherit: false
322
+ });
323
+ };
324
+ var hasRunningServices = async (home, runtime) => {
325
+ const result = await runCompose(
326
+ home,
327
+ ["ps", "--status", "running", "--services"],
328
+ runtime,
329
+ false
330
+ );
331
+ return result.stdout.trim().length > 0;
332
+ };
333
+ var assertPortsAvailable = async (config, runtime) => {
334
+ for (const [label, port] of [
335
+ ["Studio", config.studioPort],
336
+ ["API", config.apiPort]
337
+ ]) {
338
+ if (!await runtime.portAvailable(port)) {
339
+ throw new Error(
340
+ `${label} port ${port} is already in use. Choose another with --${label.toLowerCase()}-port ${port + 1}.`
341
+ );
342
+ }
343
+ }
344
+ };
345
+ var printStudioConnection = async (home, config, runtime) => {
346
+ const environment = await readStudioEnvironment(home);
347
+ runtime.log();
348
+ runtime.log("Kortyx Studio connection");
349
+ runtime.log(` Studio: http://localhost:${config.studioPort}`);
350
+ runtime.log(` API: http://localhost:${config.apiPort}`);
351
+ runtime.log(` Username: ${environment.KORTYX_STUDIO_BASIC_AUTH_USERNAME}`);
352
+ runtime.log(` Password: ${environment.KORTYX_STUDIO_BASIC_AUTH_PASSWORD}`);
353
+ runtime.log();
354
+ runtime.log("Add these server-side variables to your Kortyx SDK project:");
355
+ runtime.log(` KORTYX_TELEMETRY_API_URL=http://localhost:${config.apiPort}`);
356
+ runtime.log(
357
+ ` KORTYX_TELEMETRY_API_KEY=${environment.KORTYX_TELEMETRY_API_KEY}`
358
+ );
359
+ runtime.log(" KORTYX_TELEMETRY_ENVIRONMENT=development");
360
+ runtime.log(" KORTYX_TELEMETRY_SERVICE_NAME=my-agent");
361
+ runtime.log();
362
+ runtime.log(`Local state: ${home}`);
363
+ };
364
+ var startStudio = async (options, runtime) => {
365
+ await preflightDocker(runtime);
366
+ const config = await ensureStudioState(options, runtime);
367
+ if (!await hasRunningServices(options.home, runtime)) {
368
+ await assertPortsAvailable(config, runtime);
369
+ }
370
+ runtime.log(
371
+ "Starting Kortyx Studio. The first image pull can take a few minutes."
372
+ );
373
+ await runCompose(
374
+ options.home,
375
+ ["up", "-d", "--wait", "--wait-timeout", "180"],
376
+ runtime
377
+ );
378
+ runtime.log("Kortyx Studio is ready.");
379
+ await printStudioConnection(options.home, config, runtime);
380
+ };
381
+ var stopStudio = async (home, runtime) => {
382
+ await preflightDocker(runtime);
383
+ await requireStudioConfig(home);
384
+ await runCompose(home, ["stop"], runtime);
385
+ runtime.log(
386
+ "Kortyx Studio stopped. Local data and credentials were preserved."
387
+ );
388
+ };
389
+ var restartStudio = async (home, runtime) => {
390
+ await preflightDocker(runtime);
391
+ const config = await requireStudioConfig(home);
392
+ await runCompose(
393
+ home,
394
+ ["up", "-d", "--force-recreate", "--wait", "--wait-timeout", "180"],
395
+ runtime
396
+ );
397
+ runtime.log("Kortyx Studio restarted.");
398
+ await printStudioConnection(home, config, runtime);
399
+ };
400
+ var showStudioStatus = async (home, runtime) => {
401
+ await preflightDocker(runtime);
402
+ const config = await requireStudioConfig(home);
403
+ await runCompose(home, ["ps"], runtime);
404
+ runtime.log();
405
+ runtime.log(`Studio: http://localhost:${config.studioPort}`);
406
+ runtime.log(`API: http://localhost:${config.apiPort}`);
407
+ runtime.log(`State: ${home}`);
408
+ };
409
+ var showStudioLogs = async (home, follow, runtime) => {
410
+ await preflightDocker(runtime);
411
+ await requireStudioConfig(home);
412
+ await runCompose(
413
+ home,
414
+ ["logs", ...follow ? ["--follow"] : [], "--tail", "200"],
415
+ runtime
416
+ );
417
+ };
418
+ var resetStudio = async (home, confirm, runtime) => {
419
+ if (!confirm) {
420
+ throw new Error(
421
+ 'Reset permanently deletes the local Studio database. Re-run with "kortyx studio reset --confirm".'
422
+ );
423
+ }
424
+ await preflightDocker(runtime);
425
+ await requireStudioConfig(home);
426
+ await runCompose(home, ["down", "--volumes", "--remove-orphans"], runtime);
427
+ runtime.log(
428
+ 'Kortyx Studio data was deleted. Credentials were preserved; run "kortyx studio start" to create a fresh database.'
429
+ );
430
+ };
431
+
432
+ // src/studio/runtime.ts
433
+ var import_node_crypto = require("crypto");
434
+ var import_node_net = require("net");
435
+ var stringOutput = (value) => typeof value === "string" ? value : "";
436
+ var isCommandNotFound = (error) => error instanceof Error && ("code" in error && error.code === "ENOENT" || error.message.includes("ENOENT"));
437
+ var run = async (command, args, options) => {
438
+ try {
439
+ const { execa } = await import("execa");
440
+ const result = await execa(command, args, {
441
+ ...options.cwd ? { cwd: options.cwd } : {},
442
+ stdin: "inherit",
443
+ stdout: options.inherit ? "inherit" : "pipe",
444
+ stderr: options.inherit ? "inherit" : "pipe"
445
+ });
446
+ return {
447
+ stdout: stringOutput(result.stdout),
448
+ stderr: stringOutput(result.stderr)
449
+ };
450
+ } catch (error) {
451
+ if (isCommandNotFound(error)) {
452
+ throw new Error(
453
+ `Could not run ${command}. Install and start Docker Desktop, then try again.`,
454
+ { cause: error }
455
+ );
456
+ }
457
+ throw error;
458
+ }
459
+ };
460
+ var portAvailable = async (port) => await new Promise((resolvePromise) => {
461
+ const server = (0, import_node_net.createServer)();
462
+ server.unref();
463
+ server.once("error", () => resolvePromise(false));
464
+ server.listen({ host: "127.0.0.1", port }, () => {
465
+ server.close(() => resolvePromise(true));
466
+ });
467
+ });
468
+ var defaultStudioRuntime = {
469
+ run,
470
+ portAvailable,
471
+ now: () => (/* @__PURE__ */ new Date()).toISOString(),
472
+ random: (bytes) => (0, import_node_crypto.randomBytes)(bytes).toString("base64url"),
473
+ log: (message = "") => console.log(message)
474
+ };
475
+
476
+ // src/studio/command.ts
477
+ var portParser = (value) => {
478
+ const port = Number(value);
479
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
480
+ throw new import_commander.InvalidArgumentError(
481
+ "Port must be an integer between 1 and 65535."
482
+ );
483
+ }
484
+ return port;
485
+ };
486
+ var withHome = (command) => command.addOption(
487
+ new import_commander.Option("--home <path>", "Local Studio state directory.").default(
488
+ defaultStudioHome()
489
+ )
490
+ );
491
+ var configureOutput = (command, runtime) => command.configureOutput({
492
+ writeOut: (output) => runtime.log(output.trimEnd()),
493
+ writeErr: (output) => runtime.log(output.trimEnd())
494
+ });
495
+ var createStudioCommand = (runtime = defaultStudioRuntime) => {
496
+ const studio = configureOutput(
497
+ new import_commander.Command("studio").description("Run and manage Kortyx Studio.").showHelpAfterError().addHelpText(
498
+ "after",
499
+ "\nLocal state and generated credentials are stored in ~/.kortyx/studio by default."
500
+ ),
501
+ runtime
502
+ );
503
+ withHome(
504
+ studio.command("start").description(
505
+ "Create or update the local stack and wait until it is healthy."
506
+ ).option("--studio-port <port>", "Studio host port.", portParser).option("--api-port <port>", "Telemetry API host port.", portParser).option("--image-tag <tag>", "Published Studio image tag.").option("--username <name>", "Local Studio username.").option("--project-name <name>", "Docker Compose project name.")
507
+ ).action(async (options) => {
508
+ await startStudio(options, runtime);
509
+ });
510
+ withHome(
511
+ studio.command("stop").description("Stop containers while preserving local data.")
512
+ ).action(async ({ home }) => {
513
+ await stopStudio(home, runtime);
514
+ });
515
+ withHome(
516
+ studio.command("restart").description("Recreate the stack and wait until it is healthy.")
517
+ ).action(async ({ home }) => {
518
+ await restartStudio(home, runtime);
519
+ });
520
+ withHome(
521
+ studio.command("status").description("Show container health and local endpoints.")
522
+ ).action(async ({ home }) => {
523
+ await showStudioStatus(home, runtime);
524
+ });
525
+ withHome(
526
+ studio.command("logs").description("Show API, Studio, bootstrap, and database logs.").option("--no-follow", "Print recent logs without following.")
527
+ ).action(async ({ home, follow }) => {
528
+ await showStudioLogs(home, follow, runtime);
529
+ });
530
+ withHome(
531
+ studio.command("credentials").description("Print Studio sign-in and SDK telemetry configuration.")
532
+ ).action(async ({ home }) => {
533
+ await printStudioConnection(home, await requireStudioConfig(home), runtime);
534
+ });
535
+ withHome(
536
+ studio.command("reset").description("Delete the local Studio database volume.").option("--confirm", "Confirm permanent deletion.", false)
537
+ ).action(async ({ home, confirm }) => {
538
+ await resetStudio(home, confirm, runtime);
539
+ });
540
+ return studio;
541
+ };
542
+
543
+ // src/index.ts
544
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
545
+ var isWorkflowDefinition = (value) => isRecord(value) && typeof value.id === "string" && isRecord(value.nodes) && Array.isArray(value.edges);
546
+ var isAgent = (value) => isRecord(value) && typeof value.projectTopology === "function";
547
+ var readPackageName = async (cwd) => {
548
+ let current = cwd;
549
+ for (; ; ) {
550
+ try {
551
+ const raw = await (0, import_promises2.readFile)((0, import_node_path2.join)(current, "package.json"), "utf8");
552
+ const parsed = JSON.parse(raw);
553
+ if (isRecord(parsed) && typeof parsed.name === "string") {
554
+ return parsed.name;
555
+ }
556
+ } catch {
557
+ }
558
+ const parent = (0, import_node_path2.dirname)(current);
559
+ if (parent === current) return void 0;
560
+ current = parent;
561
+ }
562
+ };
563
+ var loadEnvFiles = async (cwd) => {
564
+ for (const filename of [".env", ".env.local"]) {
565
+ const path = (0, import_node_path2.join)(cwd, filename);
566
+ let raw;
567
+ try {
568
+ raw = await (0, import_promises2.readFile)(path, "utf8");
569
+ } catch {
570
+ continue;
571
+ }
572
+ for (const line of raw.split(/\r?\n/)) {
573
+ const trimmed = line.trim();
574
+ if (!trimmed || trimmed.startsWith("#")) continue;
575
+ const separator = trimmed.indexOf("=");
576
+ if (separator <= 0) continue;
577
+ const key = trimmed.slice(0, separator).trim();
578
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
579
+ if (process.env[key] !== void 0) continue;
580
+ process.env[key] = unquoteEnvValue(trimmed.slice(separator + 1).trim());
581
+ }
582
+ }
583
+ };
584
+ var unquoteEnvValue = (value) => {
585
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
586
+ return value.slice(1, -1);
587
+ }
588
+ return value;
589
+ };
590
+ var resolveOptions = async (options) => {
591
+ if (!options.entry) {
592
+ throw new Error("Missing required option: --entry <path>.");
593
+ }
594
+ const entry = (0, import_node_path2.isAbsolute)(options.entry) ? options.entry : (0, import_node_path2.resolve)(options.cwd, options.entry);
595
+ const environment = options.environment ?? process.env.KORTYX_TELEMETRY_ENVIRONMENT ?? process.env.NODE_ENV ?? "development";
596
+ const serviceName = options.serviceName ?? process.env.KORTYX_TELEMETRY_SERVICE_NAME ?? await readPackageName(options.cwd) ?? "kortyx-app";
597
+ return {
598
+ ...options,
599
+ entry,
600
+ environment,
601
+ serviceName,
602
+ apiUrl: options.apiUrl ?? process.env.KORTYX_TELEMETRY_API_URL ?? process.env.KORTYX_API_URL,
603
+ apiKey: options.apiKey ?? process.env.KORTYX_TELEMETRY_API_KEY,
604
+ deploymentRef: options.deploymentRef ?? process.env.KORTYX_TELEMETRY_DEPLOYMENT_REF ?? process.env.GITHUB_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA
605
+ };
606
+ };
607
+ var createFrameworkMarkerStubs = async () => {
608
+ const root = await (0, import_promises2.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "kortyx-cli-stubs-"));
609
+ for (const moduleName of ["server-only", "client-only"]) {
610
+ const directory = (0, import_node_path2.join)(root, moduleName);
611
+ await (0, import_promises2.mkdir)(directory, { recursive: true });
612
+ await (0, import_promises2.writeFile)((0, import_node_path2.join)(directory, "index.js"), "", "utf8");
613
+ await (0, import_promises2.writeFile)(
614
+ (0, import_node_path2.join)(directory, "package.json"),
615
+ JSON.stringify({ name: moduleName, version: "0.0.0", main: "index.js" }),
616
+ "utf8"
617
+ );
618
+ }
619
+ const previousNodePath = process.env.NODE_PATH;
620
+ process.env.NODE_PATH = previousNodePath ? `${root}${import_node_path2.delimiter}${previousNodePath}` : root;
621
+ const moduleWithPrivateInit = import_node_module.default;
622
+ moduleWithPrivateInit._initPaths?.();
623
+ return {
624
+ cleanup: async () => {
625
+ if (previousNodePath === void 0) delete process.env.NODE_PATH;
626
+ else process.env.NODE_PATH = previousNodePath;
627
+ moduleWithPrivateInit._initPaths?.();
628
+ await (0, import_promises2.rm)(root, { recursive: true, force: true });
629
+ }
630
+ };
631
+ };
632
+ var importEntry = async (entry) => {
633
+ const stubs = await createFrameworkMarkerStubs();
634
+ let unregisterPaths;
635
+ try {
636
+ const tsconfig = await findNearestFile((0, import_node_path2.dirname)(entry), "tsconfig.json");
637
+ unregisterPaths = tsconfig ? await registerTsconfigPaths(tsconfig) : void 0;
638
+ const module2 = (0, import_api.require)(entry, (0, import_node_path2.join)((0, import_node_path2.dirname)(entry), "kortyx-cli.cjs"));
639
+ if (!isRecord(module2)) {
640
+ throw new Error(`Entry module did not export an object: ${entry}`);
641
+ }
642
+ return module2;
643
+ } finally {
644
+ unregisterPaths?.();
645
+ await stubs.cleanup();
646
+ }
647
+ };
648
+ var findNearestFile = async (startDirectory, filename) => {
649
+ let current = startDirectory;
650
+ for (; ; ) {
651
+ const candidate = (0, import_node_path2.join)(current, filename);
652
+ try {
653
+ await (0, import_promises2.access)(candidate);
654
+ return candidate;
655
+ } catch {
656
+ const parent = (0, import_node_path2.dirname)(current);
657
+ if (parent === current) return void 0;
658
+ current = parent;
659
+ }
660
+ }
661
+ };
662
+ var registerTsconfigPaths = async (tsconfigPath) => {
663
+ const raw = JSON.parse(await (0, import_promises2.readFile)(tsconfigPath, "utf8"));
664
+ if (!isRecord(raw)) return () => void 0;
665
+ const compilerOptions = isRecord(raw.compilerOptions) ? raw.compilerOptions : void 0;
666
+ const paths = isRecord(compilerOptions?.paths) ? compilerOptions.paths : void 0;
667
+ if (!paths) return () => void 0;
668
+ const tsconfigDirectory = (0, import_node_path2.dirname)(tsconfigPath);
669
+ const baseUrl = typeof compilerOptions?.baseUrl === "string" ? (0, import_node_path2.resolve)(tsconfigDirectory, compilerOptions.baseUrl) : tsconfigDirectory;
670
+ const mappings = Object.entries(paths).flatMap(([pattern, rawTargets]) => {
671
+ if (!Array.isArray(rawTargets)) return [];
672
+ return rawTargets.filter((target) => typeof target === "string").map((target) => ({ pattern, target }));
673
+ }).map(({ pattern, target }) => {
674
+ const [patternPrefix, patternSuffix = ""] = pattern.split("*");
675
+ const [targetPrefix, targetSuffix = ""] = target.split("*");
676
+ return {
677
+ pattern,
678
+ target,
679
+ patternPrefix,
680
+ patternSuffix,
681
+ targetPrefix,
682
+ targetSuffix,
683
+ wildcard: pattern.includes("*")
684
+ };
685
+ });
686
+ if (mappings.length === 0) return () => void 0;
687
+ const moduleWithResolver = import_node_module.default;
688
+ const originalResolve = moduleWithResolver._resolveFilename;
689
+ moduleWithResolver._resolveFilename = function resolveWithTsconfigPaths(request, parent, isMain, options) {
690
+ for (const mapping of mappings) {
691
+ const matched = mapping.wildcard ? request.startsWith(mapping.patternPrefix ?? "") && request.endsWith(mapping.patternSuffix) : request === mapping.pattern;
692
+ if (!matched) continue;
693
+ const wildcardValue = mapping.wildcard ? request.slice(
694
+ mapping.patternPrefix?.length ?? 0,
695
+ request.length - mapping.patternSuffix.length
696
+ ) : "";
697
+ const mapped = mapping.wildcard ? `${mapping.targetPrefix}${wildcardValue}${mapping.targetSuffix}` : mapping.target;
698
+ for (const candidate of candidateModulePaths((0, import_node_path2.resolve)(baseUrl, mapped))) {
699
+ try {
700
+ return originalResolve.call(this, candidate, parent, isMain, options);
701
+ } catch {
702
+ }
703
+ }
704
+ }
705
+ return originalResolve.call(this, request, parent, isMain, options);
706
+ };
707
+ return () => {
708
+ moduleWithResolver._resolveFilename = originalResolve;
709
+ };
710
+ };
711
+ var candidateModulePaths = (basePath) => [
712
+ basePath,
713
+ `${basePath}.ts`,
714
+ `${basePath}.tsx`,
715
+ `${basePath}.mts`,
716
+ `${basePath}.cts`,
717
+ `${basePath}.js`,
718
+ `${basePath}.jsx`,
719
+ (0, import_node_path2.join)(basePath, "index.ts"),
720
+ (0, import_node_path2.join)(basePath, "index.tsx"),
721
+ (0, import_node_path2.join)(basePath, "index.js"),
722
+ (0, import_node_path2.join)(basePath, "index.jsx")
723
+ ];
724
+ var valueFromModule = (module2, exportName) => {
725
+ if (exportName) {
726
+ if (!(exportName in module2)) {
727
+ throw new Error(`Entry module does not export "${exportName}".`);
728
+ }
729
+ return module2[exportName];
730
+ }
731
+ if (isAgent(module2.agent)) return module2.agent;
732
+ if (Array.isArray(module2.workflows)) return module2.workflows;
733
+ if (isAgent(module2.default)) return module2.default;
734
+ if (Array.isArray(module2.default)) return module2.default;
735
+ const workflowExports = Object.values(module2).filter(isWorkflowDefinition);
736
+ if (workflowExports.length > 0) return workflowExports;
737
+ throw new Error(
738
+ "Entry must export an agent with projectTopology(), a workflows array, or named workflow definitions. Use --export when needed."
739
+ );
740
+ };
741
+ var projectSnapshots = async (value, options) => {
742
+ if (isAgent(value)) {
743
+ const snapshots = await value.projectTopology?.(options);
744
+ return snapshots ?? [];
745
+ }
746
+ const workflows = Array.isArray(value) ? value.filter(isWorkflowDefinition) : isWorkflowDefinition(value) ? [value] : [];
747
+ if (workflows.length === 0) {
748
+ throw new Error("No workflow definitions found in the selected export.");
749
+ }
750
+ const knownWorkflowIds = workflows.map((workflow) => workflow.id);
751
+ return workflows.map(
752
+ (workflow) => (0, import_agent.projectWorkflowTopology)({
753
+ workflow,
754
+ environment: options.environment,
755
+ service: options.service,
756
+ knownWorkflowIds
757
+ })
758
+ );
759
+ };
760
+ var endpointFor = (apiUrl, path) => `${apiUrl.replace(/\/$/, "")}${path}`;
761
+ var pushSnapshot = async (apiUrl, apiKey, snapshot) => {
762
+ const response = await fetch(
763
+ endpointFor(apiUrl, "/v1/telemetry/workflow-revisions:ensure"),
764
+ {
765
+ method: "POST",
766
+ headers: {
767
+ authorization: `Bearer ${apiKey}`,
768
+ "content-type": "application/json"
769
+ },
770
+ body: JSON.stringify(snapshot)
771
+ }
772
+ );
773
+ if (!response.ok) {
774
+ const text = await response.text().catch(() => "");
775
+ throw new Error(
776
+ `Kortyx API rejected topology ${snapshot.workflow.id} (${response.status}): ${text}`
777
+ );
778
+ }
779
+ return import_telemetry_contracts.EnsureWorkflowTopologyResponseSchema.parse(await response.json());
780
+ };
781
+ var printResults = (results, options) => {
782
+ if (options.json) {
783
+ console.log(
784
+ JSON.stringify(
785
+ {
786
+ workflows: results.map(({ snapshot, response }) => ({
787
+ workflowId: snapshot.workflow.id,
788
+ declaredVersion: snapshot.workflow.declaredVersion,
789
+ topologyHash: snapshot.workflow.topologyHash,
790
+ transitionCount: snapshot.workflow.transitions?.length ?? 0,
791
+ workflowRevisionId: response?.workflowRevisionId,
792
+ created: response?.created
793
+ }))
794
+ },
795
+ null,
796
+ 2
797
+ )
798
+ );
799
+ return;
800
+ }
801
+ const verb = options.dryRun ? "Projected" : "Pushed";
802
+ console.log(`${verb} ${results.length} workflow topology snapshot(s).`);
803
+ for (const { snapshot, response } of results) {
804
+ const revision = response ? ` revision=${response.workflowRevisionId} created=${response.created}` : "";
805
+ console.log(
806
+ `- ${snapshot.workflow.id}@${snapshot.workflow.declaredVersion} hash=${snapshot.workflow.topologyHash.slice(
807
+ 0,
808
+ 12
809
+ )} transitions=${snapshot.workflow.transitions?.length ?? 0}${revision}`
810
+ );
811
+ }
812
+ };
813
+ var runTopologyPush = async (rawOptions) => {
814
+ const options = await resolveOptions(rawOptions);
815
+ if (!options.dryRun && !options.apiUrl) {
816
+ throw new Error(
817
+ "Missing Kortyx API URL. Pass --api-url or set KORTYX_TELEMETRY_API_URL."
818
+ );
819
+ }
820
+ if (!options.dryRun && !options.apiKey) {
821
+ throw new Error(
822
+ "Missing telemetry API key. Pass --api-key or set KORTYX_TELEMETRY_API_KEY."
823
+ );
824
+ }
825
+ const module2 = await importEntry(options.entry);
826
+ const selected = valueFromModule(module2, options.exportName);
827
+ const service = {
828
+ name: options.serviceName,
829
+ ...options.deploymentRef ? { deploymentRef: options.deploymentRef } : {}
830
+ };
831
+ const snapshots = await projectSnapshots(selected, {
832
+ environment: options.environment,
833
+ service
834
+ });
835
+ const parsedSnapshots = snapshots.map(
836
+ (snapshot) => import_telemetry_contracts.EnsureWorkflowTopologyRequestSchema.parse(snapshot)
837
+ );
838
+ const results = [];
839
+ for (const snapshot of parsedSnapshots) {
840
+ let response;
841
+ if (!options.dryRun) {
842
+ if (!options.apiUrl || !options.apiKey) {
843
+ throw new Error("Missing Kortyx API URL or telemetry API key.");
844
+ }
845
+ response = await pushSnapshot(options.apiUrl, options.apiKey, snapshot);
846
+ }
847
+ results.push({ snapshot, ...response ? { response } : {} });
848
+ }
849
+ printResults(results, options);
850
+ };
851
+ var createCliProgram = () => {
852
+ const program = new import_commander2.Command().name("kortyx").description("Build, observe, and operate Kortyx projects.").showHelpAfterError().addHelpText(
853
+ "after",
854
+ '\nRun "kortyx <command> --help" for detailed command options.'
855
+ );
856
+ program.addCommand(createStudioCommand());
857
+ const topology = program.command("topology").description("Publish deterministic workflow topology to Studio.");
858
+ topology.command("push").description("Project and publish workflow topology.").requiredOption(
859
+ "--entry <path>",
860
+ "Module exporting an agent or workflows array."
861
+ ).option("--export <name>", "Named export to load from the entry module.").option(
862
+ "--api-url <url>",
863
+ "Kortyx API URL. Defaults to KORTYX_TELEMETRY_API_URL."
864
+ ).option(
865
+ "--api-key <key>",
866
+ "Telemetry API key. Defaults to KORTYX_TELEMETRY_API_KEY."
867
+ ).option("--environment <name>", "Telemetry environment.").option("--service-name <name>", "Telemetry service name.").option("--deployment-ref <ref>", "Deployment reference.").option("--dry-run", "Print topology without publishing.", false).option("--json", "Print machine-readable JSON.", false).action(async (options) => {
868
+ const cliOptions = {
869
+ entry: options.entry,
870
+ exportName: options.export,
871
+ apiUrl: options.apiUrl,
872
+ apiKey: options.apiKey,
873
+ environment: options.environment,
874
+ serviceName: options.serviceName,
875
+ deploymentRef: options.deploymentRef,
876
+ dryRun: options.dryRun ?? false,
877
+ json: options.json ?? false,
878
+ cwd: process.cwd()
879
+ };
880
+ await loadEnvFiles(cliOptions.cwd);
881
+ await runTopologyPush(cliOptions);
882
+ });
883
+ return program;
884
+ };
885
+ var main = async () => {
886
+ const program = createCliProgram().exitOverride();
887
+ if (process.argv.length === 2) {
888
+ program.outputHelp();
889
+ return;
890
+ }
891
+ try {
892
+ await program.parseAsync(process.argv);
893
+ } catch (error) {
894
+ if (error instanceof import_commander2.CommanderError && error.exitCode === 0) return;
895
+ console.error(error instanceof Error ? error.message : String(error));
896
+ process.exitCode = 1;
897
+ }
898
+ };
899
+ void main();
900
+ //# sourceMappingURL=index.js.map