@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 +533 -76
- package/dist/components/index.d.ts +193 -87
- package/dist/components/index.js +1 -1
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +5 -2
- package/dist/config-schema-DcLVggqh.js +19 -0
- package/dist/{agent-DBDnKm26.js → tool-DDDEH8M3.js} +140 -87
- package/package.json +11 -5
- package/dist/config-schema-CcWOtgOv.js +0 -12
package/dist/cli/index.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
import { C as __toESM, S as __require, _ as
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
|
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)
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
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
|
|
3199
|
-
const bundleFilePath = path.resolve(
|
|
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(
|
|
3202
|
-
logger.debug("ensured output directory exists at %s",
|
|
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
|
|
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
|
|
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
|
-
|
|
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.#
|
|
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/
|
|
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
|
|
3856
|
-
const config =
|
|
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
|