@toolforge-js/sdk 0.8.0 → 0.8.2

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/cli/index.js CHANGED
@@ -1,21 +1,26 @@
1
- import { C as __toESM, S as __require, _ as startAgentMessageSchema, a as AGENT_TAG_NAME, b as stopToolMessageSchema, c as getErrorMessage, d as runWithRetries, f as ackMessageSchema, g as runnerId, h as initCommunicationMessageSchema, i as AGENT_STEP_TAG_NAME, l as invariant, m as heartbeatAckMessageSchema, n as TOOL_TAG_NAME, o as Agent, p as baseMessageSchema, r as Tool, s as convertToWords, t as TOOL_HANDLER_TAG_NAME, u as exponentialBackoff, v as startToolMessageSchema, x as __commonJS, y as stopAgentMessageSchema } from "../tool-UVAGtnlr.js";
2
- import { t as toolForgeConfigSchema } from "../config-schema-CcWOtgOv.js";
1
+ import { C as __toESM, S as __require, _ as stopToolMessageSchema, a as AGENT_TAG_NAME, b as invariant, c as runWithRetries, d as heartbeatAckMessageSchema, f as initCommunicationMessageSchema, g as stopAgentMessageSchema, h as startToolMessageSchema, i as AGENT_STEP_TAG_NAME, l as ackMessageSchema, m as startAgentMessageSchema, n as TOOL_TAG_NAME, o as Agent, p as runnerId, r as Tool, s as exponentialBackoff, t as TOOL_HANDLER_TAG_NAME, u as baseMessageSchema, v as convertToWords, x as __commonJS, y as getErrorMessage } from "../tool-DDDEH8M3.js";
2
+ import { n as toolForgeConfigSchema, r as TOKEN_PREFIX, t as TF_CONFIG_TAG_NAME } from "../config-schema-DcLVggqh.js";
3
3
  import * as z$1 from "zod";
4
4
  import z from "zod";
5
- import { readFileSync } from "node:fs";
5
+ import { createReadStream, readFileSync } from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import { EventEmitter } from "node:events";
8
8
  import ora from "ora";
9
9
  import { pino } from "pino";
10
- import { P, match } from "ts-pattern";
11
- import { setTimeout as setTimeout$1 } from "node:timers/promises";
12
- import chokidar from "chokidar";
13
- import { nanoid } from "nanoid";
14
- import picocolors from "picocolors";
10
+ import * as crypto from "node:crypto";
15
11
  import * as fs from "node:fs/promises";
12
+ import { create } from "tar";
16
13
  import * as os from "node:os";
17
14
  import { camelCase, capitalize, kebabCase } from "es-toolkit/string";
18
15
  import esbuild from "esbuild";
16
+ import { nanoid } from "nanoid";
17
+ import { P, match } from "ts-pattern";
18
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
19
+ import * as prompts from "@clack/prompts";
20
+ import picocolors from "picocolors";
21
+ import { createAuthClient } from "better-auth/client";
22
+ import { deviceAuthorizationClient } from "better-auth/client/plugins";
23
+ import chokidar from "chokidar";
19
24
 
20
25
  //#region ../../node_modules/.bun/commander@14.0.2/node_modules/commander/lib/error.js
21
26
  var require_error = /* @__PURE__ */ __commonJS({ "../../node_modules/.bun/commander@14.0.2/node_modules/commander/lib/error.js": ((exports) => {
@@ -3066,13 +3071,17 @@ async function gatherForgeItems(toolsDir, mode, logger) {
3066
3071
  return await import(bundleFilePath);
3067
3072
  }
3068
3073
  const { forgeItems, toolEntries, agentEntries } = await gatherToolAndAgentEntries(toolsDir, mode, logger);
3069
- if (forgeItems.length || toolEntries.length || agentEntries.length) return await import(`${await bundleForge({
3070
- toolEntries,
3071
- agentEntries,
3072
- forgeItems,
3073
- toolsDir,
3074
- logger
3075
- })}?t=${Date.now()}`);
3074
+ if (forgeItems.length || toolEntries.length || agentEntries.length) {
3075
+ const { bundleFilePath } = await bundleForge({
3076
+ toolEntries,
3077
+ agentEntries,
3078
+ forgeItems,
3079
+ toolsDir,
3080
+ logger
3081
+ });
3082
+ logger.debug("loading bundled tools from %s", bundleFilePath);
3083
+ return await import(`${bundleFilePath}?t=${Date.now()}`);
3084
+ }
3076
3085
  return {
3077
3086
  forgeItems: [],
3078
3087
  tools: {},
@@ -3196,11 +3205,11 @@ ${agentEntries.map(({ variableName, id }) => ` '${id}': agent_${variableName},`
3196
3205
  }
3197
3206
  const entryFileContent = entryFileContentGroups.join("\n\n");
3198
3207
  logger.debug(`generate file content\n${"-".repeat(70)}\n%s\n${"-".repeat(70)}`, entryFileContent);
3199
- const outDir = path.resolve(process.cwd(), TOOL_FORGE_BUILD_DIR);
3200
- const bundleFilePath = path.resolve(outDir, TOOL_FORGE_BUILD_BUNDLE_FILE);
3208
+ const bundleDirPath = path.resolve(process.cwd(), TOOL_FORGE_BUILD_DIR);
3209
+ const bundleFilePath = path.resolve(bundleDirPath, TOOL_FORGE_BUILD_BUNDLE_FILE);
3201
3210
  logger.debug("bundling tools to %s", bundleFilePath);
3202
- await setupOutputDir(outDir);
3203
- logger.debug("ensured output directory exists at %s", outDir);
3211
+ await setupOutputDir(bundleDirPath);
3212
+ logger.debug("ensured output directory exists at %s", bundleDirPath);
3204
3213
  logger.debug("starting esbuild bundling...");
3205
3214
  await esbuild.build({
3206
3215
  stdin: {
@@ -3212,11 +3221,20 @@ ${agentEntries.map(({ variableName, id }) => ` '${id}': agent_${variableName},`
3212
3221
  platform: "node",
3213
3222
  outfile: bundleFilePath,
3214
3223
  logLevel: "error",
3215
- format: "esm"
3224
+ format: "esm",
3225
+ banner: { js: [
3226
+ "/* eslint-disable */",
3227
+ "// @ts-nocheck",
3228
+ "// noinspection JSUnusedGlobalSymbols",
3229
+ "// This file is auto-generated by ToolForge. Do not edit manually."
3230
+ ].join("\n\n") }
3216
3231
  });
3217
3232
  logger.debug("esbuild bundling completed");
3218
- return bundleFilePath;
3219
- }
3233
+ return {
3234
+ bundleFilePath,
3235
+ bundleDirPath
3236
+ };
3237
+ } else throw new Error("no tools or agents found to bundle");
3220
3238
  }
3221
3239
  function isToolDefinition(obj) {
3222
3240
  return "__tf__tag__name__" in obj && obj.__tf__tag__name__ === TOOL_TAG_NAME && "handler" in obj && typeof obj.handler === "function" && "__tf__tag__name__" in obj.handler && obj.handler.__tf__tag__name__ === TOOL_HANDLER_TAG_NAME;
@@ -3260,6 +3278,417 @@ function getMemoryUsage() {
3260
3278
  };
3261
3279
  }
3262
3280
 
3281
+ //#endregion
3282
+ //#region src/cli/actions/utils.ts
3283
+ async function getToolForgeConfig(configRelPath) {
3284
+ try {
3285
+ const configPath = path.resolve(process.cwd(), configRelPath);
3286
+ return {
3287
+ config: toolForgeConfigSchema.extend({ __tf__tag__name__: z$1.symbol(TF_CONFIG_TAG_NAME.description) }).parse(await import(configPath).then((mod) => mod.default)),
3288
+ configPath
3289
+ };
3290
+ } catch (error) {
3291
+ if (error instanceof z$1.ZodError) throw new Error("Invalid tool forge config.\n\nPlease refer the documentation [https://docs.tool-forge.ai/sdk/configuration] for the correct configuration schema");
3292
+ throw error;
3293
+ }
3294
+ }
3295
+ /**
3296
+ * Opens the given URL in the default web browser.
3297
+ */
3298
+ function openInBrowser(url) {
3299
+ const platform = process.platform;
3300
+ let command;
3301
+ if (platform === "win32") command = `start '${url}'`;
3302
+ else if (platform === "darwin") command = `open '${url}'`;
3303
+ else command = `xdg-open '${url}'`;
3304
+ Bun.spawn({
3305
+ cmd: [
3306
+ "sh",
3307
+ "-c",
3308
+ command
3309
+ ],
3310
+ stdout: "ignore",
3311
+ stderr: "ignore"
3312
+ });
3313
+ }
3314
+ const TOOL_FORGE_CLI_DOMAIN = "ai.tool-forge.cli";
3315
+ const TOOL_FORGE_ACCESS_TOKEN_KEY = "access-token";
3316
+ async function buildForge(config, logger) {
3317
+ const toolsDir = path.resolve(process.cwd(), config.toolsDir);
3318
+ const { toolEntries, agentEntries, forgeItems } = await gatherToolAndAgentEntries(toolsDir, "production", logger);
3319
+ if (forgeItems.length === 0) {
3320
+ logger.warn("no tools or agents found to build");
3321
+ process.exit(0);
3322
+ }
3323
+ logger.info("building %d tools and agents...", forgeItems.length);
3324
+ const { bundleFilePath, bundleDirPath } = await bundleForge({
3325
+ toolEntries,
3326
+ agentEntries,
3327
+ forgeItems,
3328
+ toolsDir,
3329
+ logger
3330
+ });
3331
+ logger.info("tools built successfully");
3332
+ return {
3333
+ bundleFilePath,
3334
+ bundleDirPath
3335
+ };
3336
+ }
3337
+ const FILES_TO_COPY = [
3338
+ "package.json",
3339
+ "toolforge.config.ts",
3340
+ "tsconfig.json",
3341
+ "bun.lock",
3342
+ "bun.lockb"
3343
+ ];
3344
+ const DIRS_TO_COPY = ["prisma"];
3345
+ async function createCompleteBundle({ bundleDirPath, rootPath }, logger) {
3346
+ for (const file of FILES_TO_COPY) {
3347
+ const srcPath = path.resolve(rootPath, file);
3348
+ if (!await fs.exists(srcPath)) continue;
3349
+ if (file === "package.json") {
3350
+ logger.debug("cleaning package.json before adding to bundle");
3351
+ const cleanedPackageJSON = await cleanPackageJSON(srcPath, logger);
3352
+ const destPath = path.resolve(bundleDirPath, "package.json");
3353
+ logger.debug("cleaned package.json: %o", cleanedPackageJSON);
3354
+ logger.debug("writing cleaned package.json to %s", destPath);
3355
+ try {
3356
+ await fs.writeFile(destPath, JSON.stringify(cleanedPackageJSON, null, 2), "utf-8");
3357
+ } catch (error) {
3358
+ logger.warn("failed to write cleaned package.json: %s", error.message);
3359
+ }
3360
+ } else {
3361
+ const srcPath$1 = path.resolve(rootPath, file);
3362
+ const destPath = path.resolve(bundleDirPath, file);
3363
+ logger.debug("copying %s to %s", srcPath$1, destPath);
3364
+ try {
3365
+ await fs.copyFile(srcPath$1, destPath);
3366
+ } catch (error) {
3367
+ logger.warn("failed to copy %s: %s", srcPath$1, error.message);
3368
+ }
3369
+ }
3370
+ }
3371
+ for (const dir of DIRS_TO_COPY) {
3372
+ const srcPath = path.join(rootPath, dir);
3373
+ const destPath = path.join(bundleDirPath, dir);
3374
+ logger.debug("copying %s to %s", srcPath, destPath);
3375
+ try {
3376
+ if (!(await fs.stat(srcPath)).isDirectory()) continue;
3377
+ await fs.cp(srcPath, destPath, { recursive: true });
3378
+ } catch (error) {
3379
+ logger.warn("failed to copy %s: %s", srcPath, error.message);
3380
+ }
3381
+ }
3382
+ const tarballPath = path.resolve(rootPath, ".toolforge-bundle.tar.gz");
3383
+ logger.info("creating tarball at %s", tarballPath);
3384
+ await create({
3385
+ gzip: true,
3386
+ file: tarballPath,
3387
+ cwd: bundleDirPath
3388
+ }, ["."]);
3389
+ const sha256 = await getFileSha256(tarballPath);
3390
+ logger.debug("tarball sha256: %s", sha256);
3391
+ logger.info("tarball created successfully");
3392
+ return {
3393
+ tarballPath,
3394
+ sha256
3395
+ };
3396
+ }
3397
+ const VALID_TOOL_FORGE_PACKAGES = ["@toolforge-js/sdk"];
3398
+ /**
3399
+ * Cleans the package.json file by removing toolforge internal dependencies
3400
+ * and updating other workspace dependencies to their latest versions.
3401
+ */
3402
+ async function cleanPackageJSON(filePath, logger) {
3403
+ const packageJSON = z$1.object({
3404
+ devDependencies: z$1.record(z$1.string(), z$1.string()).optional().default({}),
3405
+ dependencies: z$1.record(z$1.string(), z$1.string()).optional().default({})
3406
+ }).loose().parse(JSON.parse(await fs.readFile(filePath, "utf-8")));
3407
+ for (const key of ["devDependencies", "dependencies"]) {
3408
+ const deps = packageJSON[key];
3409
+ for (const dep of Object.keys(deps)) {
3410
+ logger.trace("processing dependency %s", dep);
3411
+ if (!dep.startsWith("@toolforge-js/")) {
3412
+ logger.trace("skipping non-toolforge package %s", dep);
3413
+ continue;
3414
+ }
3415
+ if (!VALID_TOOL_FORGE_PACKAGES.includes(dep)) {
3416
+ logger.trace("removing invalid toolforge package %s", dep);
3417
+ delete deps[dep];
3418
+ } else {
3419
+ if (deps[dep].startsWith("workspace:")) {
3420
+ const latestVersion = await getPackageLatestVersion(dep);
3421
+ if (latestVersion) deps[dep] = latestVersion;
3422
+ }
3423
+ logger.trace("updated dependency %s to version %s", dep, deps[dep]);
3424
+ }
3425
+ }
3426
+ }
3427
+ return packageJSON;
3428
+ }
3429
+ /**
3430
+ * Gets the latest version of a package from npm registry.
3431
+ */
3432
+ async function getPackageLatestVersion(packageName) {
3433
+ const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
3434
+ return z$1.object({
3435
+ version: z$1.string(),
3436
+ name: z$1.literal(packageName)
3437
+ }).parse(await res.json()).version;
3438
+ }
3439
+ /**
3440
+ * Generates the SHA-256 hash of a file.
3441
+ */
3442
+ function getFileSha256(filePath) {
3443
+ return new Promise((resolve, reject) => {
3444
+ const hash = crypto.createHash("sha256");
3445
+ const stream = createReadStream(filePath);
3446
+ stream.on("error", reject);
3447
+ stream.on("data", (chunk) => {
3448
+ hash.update(chunk);
3449
+ });
3450
+ stream.on("end", () => {
3451
+ resolve(hash.digest("hex"));
3452
+ });
3453
+ });
3454
+ }
3455
+
3456
+ //#endregion
3457
+ //#region src/cli/actions/build.ts
3458
+ async function build(configRelPath, debug) {
3459
+ const logger = pino({
3460
+ level: debug ? "debug" : "info",
3461
+ transport: { target: "pino-pretty" }
3462
+ });
3463
+ try {
3464
+ const spinner = ora("loading configuration...").start();
3465
+ const { config, configPath } = await getToolForgeConfig(configRelPath);
3466
+ spinner.stop();
3467
+ logger.debug({ config }, "loaded config from %s", configPath);
3468
+ await buildForge(config, logger);
3469
+ } catch (error) {
3470
+ logger.error(error, "failed to build tools");
3471
+ process.exit(1);
3472
+ }
3473
+ }
3474
+
3475
+ //#endregion
3476
+ //#region src/cli/actions/deploy.ts
3477
+ /**
3478
+ * Action to deploy the tools to Tool Forge platform.
3479
+ */
3480
+ async function deploy(configRelPath, debug) {
3481
+ const logFilePath = path.resolve(process.cwd(), ".tf-logs", `deploy-${Date.now()}.log`);
3482
+ const logger = pino({
3483
+ level: debug ? "debug" : "info",
3484
+ transport: {
3485
+ target: "pino/file",
3486
+ options: {
3487
+ destination: logFilePath,
3488
+ mkdir: true
3489
+ }
3490
+ }
3491
+ });
3492
+ prompts.intro("Deploying to Tool Forge");
3493
+ prompts.log.info(`Saving logs to ${logFilePath}`);
3494
+ const spinner = prompts.spinner();
3495
+ spinner.start("Checking authentication...");
3496
+ const key = await Bun.secrets.get({
3497
+ service: TOOL_FORGE_CLI_DOMAIN,
3498
+ name: TOOL_FORGE_ACCESS_TOKEN_KEY
3499
+ });
3500
+ spinner.stop("Authentication checked.");
3501
+ if (!key) {
3502
+ prompts.outro("You are not logged in. Please run \"tool-forge login\" to login.");
3503
+ process.exit(1);
3504
+ }
3505
+ const loginSpinner = prompts.spinner();
3506
+ loginSpinner.start("Loading configuration...");
3507
+ const { config } = await getToolForgeConfig(configRelPath).then((data) => {
3508
+ loginSpinner.stop("Configuration loaded.");
3509
+ return data;
3510
+ }).catch((error) => {
3511
+ loginSpinner.stop("Failed to load configuration.");
3512
+ prompts.outro(getErrorMessage(error));
3513
+ process.exit(1);
3514
+ });
3515
+ const envSpinner = prompts.spinner();
3516
+ envSpinner.start("Fetching production environments...");
3517
+ const { environments } = await fetch(new URL("/api/cli/production-environments", config.appServerUrl), { headers: { "x-tf-token": key } }).then(async (res) => {
3518
+ if (!res.ok) throw new Error(`Failed to fetch production environments: ${res.status} ${res.statusText}`);
3519
+ const data = z$1.object({ environments: z$1.object({
3520
+ id: z$1.string(),
3521
+ environmentName: z$1.string(),
3522
+ workspaceName: z$1.string()
3523
+ }).array() }).parse(await res.json());
3524
+ envSpinner.stop("Production environments fetched.");
3525
+ return data;
3526
+ }).catch((error) => {
3527
+ envSpinner.stop("Failed to fetch production environments.");
3528
+ prompts.outro(getErrorMessage(error));
3529
+ process.exit(1);
3530
+ });
3531
+ const environmentSelect = await prompts.select({
3532
+ message: "Select the production environment to deploy to:",
3533
+ options: environments.map((env) => ({
3534
+ label: `${env.environmentName} (Workspace: ${env.workspaceName})`,
3535
+ value: env.id
3536
+ }))
3537
+ });
3538
+ if (prompts.isCancel(environmentSelect)) {
3539
+ prompts.outro("Deployment cancelled.");
3540
+ process.exit(0);
3541
+ }
3542
+ const name = await prompts.text({
3543
+ message: "Enter a name for this deployment:",
3544
+ validate(value) {
3545
+ if (value.length === 0) return "Deployment name cannot be empty";
3546
+ }
3547
+ });
3548
+ if (prompts.isCancel(name)) {
3549
+ prompts.outro("Deployment cancelled.");
3550
+ process.exit(0);
3551
+ }
3552
+ const buildSpinner = prompts.spinner();
3553
+ buildSpinner.start("Building tools...");
3554
+ const { tarballPath, sha256 } = await buildForge(config, logger).then(async ({ bundleDirPath }) => {
3555
+ buildSpinner.stop("Tools built successfully.");
3556
+ const { tarballPath: tarballPath$1, sha256: sha256$1 } = await createCompleteBundle({
3557
+ bundleDirPath,
3558
+ rootPath: process.cwd()
3559
+ }, logger);
3560
+ return {
3561
+ tarballPath: tarballPath$1,
3562
+ sha256: sha256$1
3563
+ };
3564
+ }).catch((error) => {
3565
+ buildSpinner.stop("Failed to build tools.");
3566
+ prompts.outro(getErrorMessage(error));
3567
+ process.exit(1);
3568
+ });
3569
+ const deploySpinner = prompts.spinner();
3570
+ const environmentName = environments.find((env) => env.id === environmentSelect)?.environmentName;
3571
+ deploySpinner.start(`Deploying tools to ${environmentName}...`);
3572
+ const formData = new FormData();
3573
+ formData.append("bundle", new File([await Bun.file(tarballPath).arrayBuffer()], path.basename(tarballPath), { type: "application/gzip" }));
3574
+ formData.append("environmentId", environmentSelect);
3575
+ formData.append("sha256", sha256);
3576
+ formData.append("name", name);
3577
+ const { url } = await fetch(new URL("/api/cli/initialize-deployment", config.appServerUrl), {
3578
+ method: "POST",
3579
+ headers: { "x-tf-token": key },
3580
+ body: formData
3581
+ }).then(async (res) => {
3582
+ if (!res.ok) {
3583
+ const errorData = await res.json().catch(() => null);
3584
+ logger.error("deployment failed: %s %s %s", res.status, res.statusText, JSON.stringify(errorData));
3585
+ throw new Error(`Deployment failed: ${res.status} ${res.statusText}`);
3586
+ }
3587
+ const data = z$1.object({
3588
+ deploymentId: z$1.uuid(),
3589
+ url: z$1.string()
3590
+ }).parse(await res.json());
3591
+ deploySpinner.stop("Tools deployed successfully.");
3592
+ return data;
3593
+ }).catch((error) => {
3594
+ deploySpinner.stop("Failed to deploy tools.");
3595
+ prompts.outro(getErrorMessage(error));
3596
+ process.exit(1);
3597
+ });
3598
+ const deploymentUrl = new URL(url, config.appUrl).toString();
3599
+ prompts.outro(`Deployment initiated successfully!\n\nYou can view the deployment status at:\n${picocolors.green(deploymentUrl)}`);
3600
+ }
3601
+
3602
+ //#endregion
3603
+ //#region src/cli/actions/login.ts
3604
+ async function login(configRelPath) {
3605
+ prompts.intro("Logging in to Tool Forge");
3606
+ const spinner = prompts.spinner();
3607
+ spinner.start("Loading configuration...");
3608
+ try {
3609
+ const { config } = await getToolForgeConfig(configRelPath);
3610
+ spinner.stop("Configuration loaded.");
3611
+ const authClient = createAuthClient({
3612
+ baseURL: config.appServerUrl,
3613
+ plugins: [deviceAuthorizationClient()]
3614
+ });
3615
+ const deviceLabel = getDeviceLabel();
3616
+ const clientId = `toolforge-cli:${deviceLabel}`;
3617
+ const { data: deviceCodeData, error } = await authClient.device.code({
3618
+ client_id: clientId,
3619
+ scope: "deployments:read deployments:write"
3620
+ });
3621
+ if (error || !deviceCodeData) {
3622
+ prompts.outro(`failed to initiate device authorization flow: ${error.error_description}`);
3623
+ process.exit(1);
3624
+ }
3625
+ const verificationUrl = new URL("/cli/login", config.appUrl);
3626
+ verificationUrl.searchParams.set("deviceLabel", deviceLabel);
3627
+ verificationUrl.searchParams.set("userCode", deviceCodeData.user_code);
3628
+ prompts.note(`To authenticate, please visit:\n${verificationUrl.toString()}\n\n🔐 And enter the code: ${deviceCodeData.user_code}`, "Login Instructions");
3629
+ openInBrowser(verificationUrl.toString());
3630
+ async function pollForToken() {
3631
+ return new Promise((resolve, reject) => {
3632
+ let pollInterval = 5e3;
3633
+ let timeout;
3634
+ async function poll() {
3635
+ const { error: error$1, data } = await authClient.device.token({
3636
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
3637
+ device_code: deviceCodeData.device_code,
3638
+ client_id: clientId
3639
+ });
3640
+ if (error$1) match(error$1.error).returnType().with("authorization_pending", () => {
3641
+ clearTimeout(timeout);
3642
+ timeout = setTimeout(poll, pollInterval);
3643
+ }).with("slow_down", () => {
3644
+ clearTimeout(timeout);
3645
+ pollInterval += 5e3;
3646
+ timeout = setTimeout(poll, pollInterval);
3647
+ }).with("access_denied", () => {
3648
+ clearTimeout(timeout);
3649
+ reject(/* @__PURE__ */ new Error("access denied by user"));
3650
+ }).with("expired_token", () => {
3651
+ clearTimeout(timeout);
3652
+ reject(/* @__PURE__ */ new Error("device code has expired"));
3653
+ }).with("invalid_grant", () => {
3654
+ clearTimeout(timeout);
3655
+ reject(/* @__PURE__ */ new Error("invalid grant, please try again"));
3656
+ }).with("invalid_request", () => {
3657
+ clearTimeout(timeout);
3658
+ reject(/* @__PURE__ */ new Error("invalid request, please try again"));
3659
+ }).run();
3660
+ if (data) {
3661
+ clearTimeout(timeout);
3662
+ resolve(data.access_token);
3663
+ }
3664
+ }
3665
+ timeout = setTimeout(poll, pollInterval);
3666
+ });
3667
+ }
3668
+ const pollSpinner = prompts.spinner();
3669
+ pollSpinner.start("Waiting for authentication...");
3670
+ try {
3671
+ const token = await pollForToken();
3672
+ await Bun.secrets.set({
3673
+ service: TOOL_FORGE_CLI_DOMAIN,
3674
+ name: TOOL_FORGE_ACCESS_TOKEN_KEY,
3675
+ value: token
3676
+ });
3677
+ pollSpinner.stop("Authentication successful!");
3678
+ prompts.outro("Successfully authenticated and stored access token securely");
3679
+ } catch (error$1) {
3680
+ pollSpinner.stop("Authentication failed.");
3681
+ prompts.outro(getErrorMessage(error$1));
3682
+ }
3683
+ } catch (error) {
3684
+ spinner.stop("Failed to load configuration.");
3685
+ prompts.outro(getErrorMessage(error));
3686
+ }
3687
+ }
3688
+ function getDeviceLabel() {
3689
+ return `${os.userInfo().username}@${os.hostname()}`;
3690
+ }
3691
+
3263
3692
  //#endregion
3264
3693
  //#region src/internal/websocket-client.ts
3265
3694
  const optionsSchema$1 = z$1.object({
@@ -3313,7 +3742,10 @@ var WebSocketClient = class extends EventEmitter {
3313
3742
  this.#connectToServer();
3314
3743
  }
3315
3744
  #connectToServer() {
3316
- if (this.#connectionState !== "closing") this.#logger.warn("start called while closing, ignoring");
3745
+ if (this.#connectionState === "closing") {
3746
+ this.#logger.warn("start called while closing, ignoring");
3747
+ return;
3748
+ }
3317
3749
  if (this.#connectionState === "connected" || this.#connectionState === "connecting") {
3318
3750
  this.#logger.warn("already connecting, connected, or stopped, ignoring");
3319
3751
  return;
@@ -3530,8 +3962,8 @@ var WebSocketClient = class extends EventEmitter {
3530
3962
  #stop() {
3531
3963
  if (this.#heartbeatInterval) clearInterval(this.#heartbeatInterval);
3532
3964
  if (this.#heartbeatTimeout) clearTimeout(this.#heartbeatTimeout);
3533
- if (this.#socket) {
3534
- if (this.#socket.readyState === WebSocket.OPEN) this.#socket.close(WS_ERROR_CODES.GOING_AWAY, "runner shutting down");
3965
+ if (this.#socket && this.#socket.readyState !== WebSocket.CLOSED) {
3966
+ this.#socket.close(WS_ERROR_CODES.GOING_AWAY, "runner shutting down");
3535
3967
  this.#socket.removeEventListener("open", this.#handleWebSocketOpen.bind(this));
3536
3968
  this.#socket.removeEventListener("close", this.#handleWebSocketClose.bind(this));
3537
3969
  this.#socket.removeEventListener("message", this.#handleWebSocketMessage.bind(this));
@@ -3572,6 +4004,8 @@ var ForgeRunner = class extends EventEmitter {
3572
4004
  #logger;
3573
4005
  #config;
3574
4006
  #serverUrl;
4007
+ #apiKey;
4008
+ #refreshApiKeyInterval;
3575
4009
  #forgeItems = [];
3576
4010
  #tools = {};
3577
4011
  #agents = {};
@@ -3583,8 +4017,9 @@ var ForgeRunner = class extends EventEmitter {
3583
4017
  constructor(config, options, logger) {
3584
4018
  super({ captureRejections: true });
3585
4019
  this.#config = config;
4020
+ this.#apiKey = config.apiKey;
3586
4021
  const url = new URL(this.#config.sdkServer);
3587
- url.searchParams.set("apiKey", this.#config.apiKey);
4022
+ url.searchParams.set("apiKey", this.#apiKey);
3588
4023
  url.searchParams.set("runnerId", this.#id);
3589
4024
  url.pathname = "/socket";
3590
4025
  this.#serverUrl = url.toString();
@@ -3595,6 +4030,9 @@ var ForgeRunner = class extends EventEmitter {
3595
4030
  runnerId: this.#id
3596
4031
  }, this, this.#logger);
3597
4032
  this.#webSocketClient.on(WEB_SOCKET_CLIENT_EVENTS.COMMUNICATION_INITIALIZED, this.#handleWebSocketCommunicationInitialized.bind(this));
4033
+ if (this.#apiKey.startsWith(TOKEN_PREFIX)) this.#refreshApiKeyInterval = setInterval(() => {
4034
+ this.#refreshApiKey();
4035
+ }, 840 * 1e3);
3598
4036
  this.#logger.info({
3599
4037
  mode: this.#options.mode,
3600
4038
  id: this.#id
@@ -3612,6 +4050,8 @@ var ForgeRunner = class extends EventEmitter {
3612
4050
  this.#tools = tools;
3613
4051
  this.#agents = agents;
3614
4052
  this.#logger.debug(`gathered ${this.#forgeItems.length} tools and directories`);
4053
+ this.#logger.info("websocket communication initialized, sending forge list...");
4054
+ await this.#sendListForge();
3615
4055
  this.#watchToolChanges();
3616
4056
  this.#unsubscribeMessageHandler = this.#webSocketClient.subscribeToMessages(this.#handleWebSocketMessage.bind(this));
3617
4057
  } catch (error) {
@@ -3780,6 +4220,26 @@ var ForgeRunner = class extends EventEmitter {
3780
4220
  });
3781
4221
  }
3782
4222
  #handleStopAgentMessage(message) {}
4223
+ /**
4224
+ * Refreshes the API key by requesting a new token from the SDK server,
4225
+ * when the apiKey is a (short lived) token used when runner is deployed
4226
+ * using Tool Forge Cloud.
4227
+ */
4228
+ async #refreshApiKey() {
4229
+ const url = new URL(this.#serverUrl);
4230
+ url.pathname = "/token/refresh";
4231
+ const res = await fetch(url.toString(), {
4232
+ method: "POST",
4233
+ headers: { "Content-Type": "application/json" },
4234
+ body: JSON.stringify({ token: this.#apiKey })
4235
+ });
4236
+ if (!res.ok) throw new Error(`failed to refresh token: ${res.status} ${res.statusText}`);
4237
+ const data = await res.json();
4238
+ const result = z$1.object({ token: z$1.string().startsWith(TOKEN_PREFIX) }).safeParse(data);
4239
+ if (!result.success) throw new Error("invalid response from token refresh");
4240
+ this.#apiKey = result.data.token;
4241
+ this.#logger.info("successfully refreshed API token");
4242
+ }
3783
4243
  stop() {
3784
4244
  this.#logger.info("gracefully stopping tool runner...");
3785
4245
  if (this.#fsWatcher) {
@@ -3794,59 +4254,17 @@ var ForgeRunner = class extends EventEmitter {
3794
4254
  }
3795
4255
  this.#webSocketClient.off(WEB_SOCKET_CLIENT_EVENTS.COMMUNICATION_INITIALIZED, this.#handleWebSocketCommunicationInitialized.bind(this));
3796
4256
  this.#webSocketClient.stop();
4257
+ if (this.#refreshApiKeyInterval) {
4258
+ clearInterval(this.#refreshApiKeyInterval);
4259
+ this.#refreshApiKeyInterval = void 0;
4260
+ this.#logger.info("token refresh interval cleared");
4261
+ }
3797
4262
  this.#logger.info("tool runner stopped");
3798
4263
  }
3799
4264
  };
3800
4265
 
3801
4266
  //#endregion
3802
- //#region src/cli/index.ts
3803
- const version = JSON.parse(readFileSync(path.resolve(__dirname, "../../package.json"), "utf-8")).version;
3804
- program.name("toolforge").description("Tool Forge SDK cli").version(version);
3805
- program.command("dev").description("start the tool forge development server").option("-c, --config <path>", "path to the tool-forge config file", (val) => z$1.string().parse(val), "toolforge.config.ts").option("-d, --debug", "enable debug logging", false).action(async function startServer() {
3806
- const debug = z$1.boolean().optional().default(false).parse(this.opts().debug);
3807
- await startToolForge({
3808
- configRelPath: z$1.string().parse(this.opts().config),
3809
- debug,
3810
- mode: "development"
3811
- });
3812
- });
3813
- program.command("start").description("start the tool forge server in production mode").option("-c, --config <path>", "path to the tool-forge config file", (val) => z$1.string().parse(val), "toolforge.config.ts").option("-d, --debug", "enable debug logging", false).action(async function startServer() {
3814
- const debug = z$1.boolean().optional().default(false).parse(this.opts().debug);
3815
- await startToolForge({
3816
- configRelPath: z$1.string().parse(this.opts().config),
3817
- debug,
3818
- mode: "production"
3819
- });
3820
- });
3821
- program.command("build").description("build the tools for production").option("-c, --config <path>", "path to the tool-forge config file", (val) => z$1.string().parse(val), "toolforge.config.ts").option("-d, --debug", "enable debug logging", false).action(async function() {
3822
- const logger = pino({
3823
- level: z$1.boolean().optional().default(false).parse(this.opts().debug) ? "debug" : "info",
3824
- transport: { target: "pino-pretty" }
3825
- });
3826
- try {
3827
- const configRelPath = z$1.string().parse(this.opts().config);
3828
- const configPath = path.resolve(process.cwd(), configRelPath);
3829
- const config = toolForgeConfigSchema.parse(await import(configPath).then((mod) => mod.default));
3830
- logger.debug({ config }, "loaded config from %s", configPath);
3831
- const toolsDir = path.resolve(process.cwd(), config.toolsDir);
3832
- const { toolEntries, agentEntries, forgeItems } = await gatherToolAndAgentEntries(toolsDir, "production", logger);
3833
- if (forgeItems.length === 0) {
3834
- logger.warn("no tools or agents found to build");
3835
- process.exit(0);
3836
- }
3837
- await bundleForge({
3838
- toolEntries,
3839
- agentEntries,
3840
- forgeItems,
3841
- toolsDir,
3842
- logger
3843
- });
3844
- logger.info("tools built successfully");
3845
- } catch (error) {
3846
- logger.error(error, "failed to build tools");
3847
- process.exit(1);
3848
- }
3849
- });
4267
+ //#region src/cli/actions/start-tool-forge.ts
3850
4268
  async function startToolForge({ configRelPath, debug, mode }) {
3851
4269
  const logger = pino({
3852
4270
  level: debug ? "debug" : "info",
@@ -3854,8 +4272,7 @@ async function startToolForge({ configRelPath, debug, mode }) {
3854
4272
  });
3855
4273
  try {
3856
4274
  const spinner = ora("loading configuration...").start();
3857
- const configPath = path.resolve(process.cwd(), configRelPath);
3858
- const config = toolForgeConfigSchema.parse(await import(configPath).then((mod) => mod.default));
4275
+ const { config, configPath } = await getToolForgeConfig(configRelPath);
3859
4276
  spinner.stop();
3860
4277
  logger.debug({ config }, "loaded config from %s", configPath);
3861
4278
  const runner = new ForgeRunner(config, { mode }, logger);
@@ -3879,6 +4296,43 @@ async function startToolForge({ configRelPath, debug, mode }) {
3879
4296
  process.exit(1);
3880
4297
  }
3881
4298
  }
4299
+
4300
+ //#endregion
4301
+ //#region src/cli/index.ts
4302
+ const version = JSON.parse(readFileSync(path.resolve(__dirname, "../../package.json"), "utf-8")).version;
4303
+ program.name("toolforge").description("Tool Forge SDK cli").version(version);
4304
+ program.command("dev").description("start the tool forge development server").option("-c, --config <path>", "path to the tool-forge config file", (val) => z$1.string().parse(val), "toolforge.config.ts").option("-d, --debug", "enable debug logging", false).action(async function startServer() {
4305
+ const debug = z$1.boolean().optional().default(false).parse(this.opts().debug);
4306
+ await startToolForge({
4307
+ configRelPath: z$1.string().parse(this.opts().config),
4308
+ debug,
4309
+ mode: "development"
4310
+ }).catch((error) => {
4311
+ if (error instanceof Error) console.warn("Failed to start development server:", error.message);
4312
+ process.exit(1);
4313
+ });
4314
+ });
4315
+ program.command("start").description("start the tool forge server in production mode").option("-c, --config <path>", "path to the tool-forge config file", (val) => z$1.string().parse(val), "toolforge.config.ts").option("-d, --debug", "enable debug logging", false).action(async function startServer() {
4316
+ const debug = z$1.boolean().optional().default(false).parse(this.opts().debug);
4317
+ await startToolForge({
4318
+ configRelPath: z$1.string().parse(this.opts().config),
4319
+ debug,
4320
+ mode: "production"
4321
+ }).catch((error) => {
4322
+ if (error instanceof Error) console.warn("Failed to start server:", error.message);
4323
+ process.exit(1);
4324
+ });
4325
+ });
4326
+ program.command("build").description("build the tools for production").option("-c, --config <path>", "path to the tool-forge config file", (val) => z$1.string().parse(val), "toolforge.config.ts").option("-d, --debug", "enable debug logging", false).action(async function() {
4327
+ const debug = z$1.boolean().optional().default(false).parse(this.opts().debug);
4328
+ await build(z$1.string().parse(this.opts().config), debug);
4329
+ });
4330
+ program.command("login").description("login to Tool Forge").option("-c, --config <path>", "path to the tool-forge config file", (val) => z$1.string().parse(val), "toolforge.config.ts").action(async function() {
4331
+ await login(z$1.string().parse(this.opts().config));
4332
+ });
4333
+ program.command("deploy").description("deploy the tools to Tool Forge").option("-c, --config <path>", "path to the tool-forge config file", (val) => z$1.string().parse(val), "toolforge.config.ts").option("-d, --debug", "enable debug logging", false).action(async function() {
4334
+ await deploy(z$1.string().parse(this.opts().config), z$1.boolean().optional().default(false).parse(this.opts().debug));
4335
+ });
3882
4336
  program.parse(Bun.argv);
3883
4337
 
3884
4338
  //#endregion