@toolforge-js/sdk 0.7.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,20 +1,26 @@
1
- import { C as __toESM, S as __require, _ as startAgentMessageSchema, a as TOOL_TAG_NAME, b as stopToolMessageSchema, c as getErrorMessage, d as runWithRetries, f as ackMessageSchema, g as runnerId, h as initCommunicationMessageSchema, i as TOOL_HANDLER_TAG_NAME, l as invariant, m as heartbeatAckMessageSchema, n as AGENT_TAG_NAME, o as Tool, p as baseMessageSchema, r as Agent, s as convertToWords, t as AGENT_STEP_TAG_NAME, u as exponentialBackoff, v as startToolMessageSchema, x as __commonJS, y as stopAgentMessageSchema } from "../agent-DBDnKm26.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 { P, match } from "ts-pattern";
6
- import { setTimeout as setTimeout$1 } from "node:timers/promises";
7
- import { nanoid } from "nanoid";
8
- import { readFileSync } from "node:fs";
5
+ import { createReadStream, readFileSync } from "node:fs";
9
6
  import * as path from "node:path";
10
7
  import { EventEmitter } from "node:events";
8
+ import ora from "ora";
11
9
  import { pino } from "pino";
12
- import chokidar from "chokidar";
13
- import picocolors from "picocolors";
10
+ import * as crypto from "node:crypto";
14
11
  import * as fs from "node:fs/promises";
12
+ import { create } from "tar";
15
13
  import * as os from "node:os";
16
14
  import { camelCase, capitalize, kebabCase } from "es-toolkit/string";
17
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";
18
24
 
19
25
  //#region ../../node_modules/.bun/commander@14.0.2/node_modules/commander/lib/error.js
20
26
  var require_error = /* @__PURE__ */ __commonJS({ "../../node_modules/.bun/commander@14.0.2/node_modules/commander/lib/error.js": ((exports) => {
@@ -3065,13 +3071,17 @@ async function gatherForgeItems(toolsDir, mode, logger) {
3065
3071
  return await import(bundleFilePath);
3066
3072
  }
3067
3073
  const { forgeItems, toolEntries, agentEntries } = await gatherToolAndAgentEntries(toolsDir, mode, logger);
3068
- if (forgeItems.length || toolEntries.length || agentEntries.length) return await import(`${await bundleForge({
3069
- toolEntries,
3070
- agentEntries,
3071
- forgeItems,
3072
- toolsDir,
3073
- logger
3074
- })}?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
+ }
3075
3085
  return {
3076
3086
  forgeItems: [],
3077
3087
  tools: {},
@@ -3195,11 +3205,11 @@ ${agentEntries.map(({ variableName, id }) => ` '${id}': agent_${variableName},`
3195
3205
  }
3196
3206
  const entryFileContent = entryFileContentGroups.join("\n\n");
3197
3207
  logger.debug(`generate file content\n${"-".repeat(70)}\n%s\n${"-".repeat(70)}`, entryFileContent);
3198
- const outDir = path.resolve(process.cwd(), TOOL_FORGE_BUILD_DIR);
3199
- 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);
3200
3210
  logger.debug("bundling tools to %s", bundleFilePath);
3201
- await setupOutputDir(outDir);
3202
- logger.debug("ensured output directory exists at %s", outDir);
3211
+ await setupOutputDir(bundleDirPath);
3212
+ logger.debug("ensured output directory exists at %s", bundleDirPath);
3203
3213
  logger.debug("starting esbuild bundling...");
3204
3214
  await esbuild.build({
3205
3215
  stdin: {
@@ -3211,11 +3221,20 @@ ${agentEntries.map(({ variableName, id }) => ` '${id}': agent_${variableName},`
3211
3221
  platform: "node",
3212
3222
  outfile: bundleFilePath,
3213
3223
  logLevel: "error",
3214
- 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") }
3215
3231
  });
3216
3232
  logger.debug("esbuild bundling completed");
3217
- return bundleFilePath;
3218
- }
3233
+ return {
3234
+ bundleFilePath,
3235
+ bundleDirPath
3236
+ };
3237
+ } else throw new Error("no tools or agents found to bundle");
3219
3238
  }
3220
3239
  function isToolDefinition(obj) {
3221
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;
@@ -3259,6 +3278,417 @@ function getMemoryUsage() {
3259
3278
  };
3260
3279
  }
3261
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
+
3262
3692
  //#endregion
3263
3693
  //#region src/internal/websocket-client.ts
3264
3694
  const optionsSchema$1 = z$1.object({
@@ -3312,7 +3742,10 @@ var WebSocketClient = class extends EventEmitter {
3312
3742
  this.#connectToServer();
3313
3743
  }
3314
3744
  #connectToServer() {
3315
- 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
+ }
3316
3749
  if (this.#connectionState === "connected" || this.#connectionState === "connecting") {
3317
3750
  this.#logger.warn("already connecting, connected, or stopped, ignoring");
3318
3751
  return;
@@ -3529,8 +3962,8 @@ var WebSocketClient = class extends EventEmitter {
3529
3962
  #stop() {
3530
3963
  if (this.#heartbeatInterval) clearInterval(this.#heartbeatInterval);
3531
3964
  if (this.#heartbeatTimeout) clearTimeout(this.#heartbeatTimeout);
3532
- if (this.#socket) {
3533
- 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");
3534
3967
  this.#socket.removeEventListener("open", this.#handleWebSocketOpen.bind(this));
3535
3968
  this.#socket.removeEventListener("close", this.#handleWebSocketClose.bind(this));
3536
3969
  this.#socket.removeEventListener("message", this.#handleWebSocketMessage.bind(this));
@@ -3571,6 +4004,8 @@ var ForgeRunner = class extends EventEmitter {
3571
4004
  #logger;
3572
4005
  #config;
3573
4006
  #serverUrl;
4007
+ #apiKey;
4008
+ #refreshApiKeyInterval;
3574
4009
  #forgeItems = [];
3575
4010
  #tools = {};
3576
4011
  #agents = {};
@@ -3582,8 +4017,9 @@ var ForgeRunner = class extends EventEmitter {
3582
4017
  constructor(config, options, logger) {
3583
4018
  super({ captureRejections: true });
3584
4019
  this.#config = config;
4020
+ this.#apiKey = config.apiKey;
3585
4021
  const url = new URL(this.#config.sdkServer);
3586
- url.searchParams.set("apiKey", this.#config.apiKey);
4022
+ url.searchParams.set("apiKey", this.#apiKey);
3587
4023
  url.searchParams.set("runnerId", this.#id);
3588
4024
  url.pathname = "/socket";
3589
4025
  this.#serverUrl = url.toString();
@@ -3594,6 +4030,9 @@ var ForgeRunner = class extends EventEmitter {
3594
4030
  runnerId: this.#id
3595
4031
  }, this, this.#logger);
3596
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);
3597
4036
  this.#logger.info({
3598
4037
  mode: this.#options.mode,
3599
4038
  id: this.#id
@@ -3611,6 +4050,8 @@ var ForgeRunner = class extends EventEmitter {
3611
4050
  this.#tools = tools;
3612
4051
  this.#agents = agents;
3613
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();
3614
4055
  this.#watchToolChanges();
3615
4056
  this.#unsubscribeMessageHandler = this.#webSocketClient.subscribeToMessages(this.#handleWebSocketMessage.bind(this));
3616
4057
  } catch (error) {
@@ -3779,6 +4220,26 @@ var ForgeRunner = class extends EventEmitter {
3779
4220
  });
3780
4221
  }
3781
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
+ }
3782
4243
  stop() {
3783
4244
  this.#logger.info("gracefully stopping tool runner...");
3784
4245
  if (this.#fsWatcher) {
@@ -3793,67 +4254,26 @@ var ForgeRunner = class extends EventEmitter {
3793
4254
  }
3794
4255
  this.#webSocketClient.off(WEB_SOCKET_CLIENT_EVENTS.COMMUNICATION_INITIALIZED, this.#handleWebSocketCommunicationInitialized.bind(this));
3795
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
+ }
3796
4262
  this.#logger.info("tool runner stopped");
3797
4263
  }
3798
4264
  };
3799
4265
 
3800
4266
  //#endregion
3801
- //#region src/cli/index.ts
3802
- const version = JSON.parse(readFileSync(path.resolve(__dirname, "../../package.json"), "utf-8")).version;
3803
- program.name("toolforge-js").description("Tool Forge SDK").version(version);
3804
- program.command("dev").description("start the tool forge development server").option("-c, --config <path>", "path to the tool-forge config file", "toolforge.config.ts").option("-d, --debug", "enable debug logging", false).action(async function startServer() {
3805
- const debug = z$1.boolean().optional().default(false).parse(this.opts().debug);
3806
- await startToolForge({
3807
- configRelPath: z$1.string().parse(this.opts().config),
3808
- debug,
3809
- mode: "development"
3810
- });
3811
- });
3812
- program.command("build").description("build the tools for production").option("-c, --config <path>", "path to the tool-forge config file", "toolforge.config.ts").option("-d, --debug", "enable debug logging", false).action(async function() {
3813
- const logger = pino({
3814
- level: z$1.boolean().optional().default(false).parse(this.opts().debug) ? "debug" : "info",
3815
- transport: { target: "pino-pretty" }
3816
- });
3817
- try {
3818
- const configRelPath = z$1.string().parse(this.opts().config);
3819
- const configPath = path.resolve(process.cwd(), configRelPath);
3820
- const config = toolForgeConfigSchema.parse(await import(configPath).then((mod) => mod.default));
3821
- logger.debug({ config }, "loaded config from %s", configPath);
3822
- const toolsDir = path.resolve(process.cwd(), config.toolsDir);
3823
- const { toolEntries, agentEntries, forgeItems } = await gatherToolAndAgentEntries(toolsDir, "production", logger);
3824
- if (forgeItems.length === 0) {
3825
- logger.warn("no tools or agents found to build");
3826
- process.exit(0);
3827
- }
3828
- await bundleForge({
3829
- toolEntries,
3830
- agentEntries,
3831
- forgeItems,
3832
- toolsDir,
3833
- logger
3834
- });
3835
- logger.info("tools built successfully");
3836
- } catch (error) {
3837
- logger.error(error, "failed to build tools");
3838
- process.exit(1);
3839
- }
3840
- });
3841
- program.command("start").description("start the tool forge server in production mode").option("-c, --config <path>", "path to the tool-forge config file", "toolforge.config.ts").option("-d, --debug", "enable debug logging", false).action(async function startServer() {
3842
- const debug = z$1.boolean().optional().default(false).parse(this.opts().debug);
3843
- await startToolForge({
3844
- configRelPath: z$1.string().parse(this.opts().config),
3845
- debug,
3846
- mode: "production"
3847
- });
3848
- });
4267
+ //#region src/cli/actions/start-tool-forge.ts
3849
4268
  async function startToolForge({ configRelPath, debug, mode }) {
3850
4269
  const logger = pino({
3851
4270
  level: debug ? "debug" : "info",
3852
4271
  transport: { target: "pino-pretty" }
3853
4272
  });
3854
4273
  try {
3855
- const configPath = path.resolve(process.cwd(), configRelPath);
3856
- const config = toolForgeConfigSchema.parse(await import(configPath).then((mod) => mod.default));
4274
+ const spinner = ora("loading configuration...").start();
4275
+ const { config, configPath } = await getToolForgeConfig(configRelPath);
4276
+ spinner.stop();
3857
4277
  logger.debug({ config }, "loaded config from %s", configPath);
3858
4278
  const runner = new ForgeRunner(config, { mode }, logger);
3859
4279
  runner.on("error", async (error) => {
@@ -3876,6 +4296,43 @@ async function startToolForge({ configRelPath, debug, mode }) {
3876
4296
  process.exit(1);
3877
4297
  }
3878
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
+ });
3879
4336
  program.parse(Bun.argv);
3880
4337
 
3881
4338
  //#endregion