@agentforge-ai/cli 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -316,8 +316,8 @@ var CloudClient = class {
316
316
  */
317
317
  getUrl(endpoint) {
318
318
  const base = this.baseUrl.replace(/\/$/, "");
319
- const path12 = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
320
- return `${base}${path12}`;
319
+ const path14 = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
320
+ return `${base}${path14}`;
321
321
  }
322
322
  /**
323
323
  * Get request headers with authentication
@@ -2770,6 +2770,268 @@ console.log('Hello from ${name}!');
2770
2770
  });
2771
2771
  }
2772
2772
 
2773
+ // src/commands/skill.ts
2774
+ import fs7 from "fs-extra";
2775
+ import path7 from "path";
2776
+ import os2 from "os";
2777
+ import { execFileSync } from "child_process";
2778
+ function getGlobalSkillsDir() {
2779
+ return path7.join(os2.homedir(), ".agentforge", "skills");
2780
+ }
2781
+ function parseSkillMd2(content) {
2782
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
2783
+ if (!fmMatch) {
2784
+ return { name: "", description: "", version: "1.0.0" };
2785
+ }
2786
+ const frontmatter = fmMatch[1];
2787
+ const data = {};
2788
+ for (const line of frontmatter.split("\n")) {
2789
+ const match = line.match(/^(\w+):\s*(.+)$/);
2790
+ if (match) {
2791
+ data[match[1]] = match[2].trim();
2792
+ }
2793
+ }
2794
+ return {
2795
+ name: data["name"] || "",
2796
+ description: data["description"] || "",
2797
+ version: data["version"] || "1.0.0"
2798
+ };
2799
+ }
2800
+ async function installSkillFromPath(sourcePath, skillsDir) {
2801
+ if (!await fs7.pathExists(sourcePath)) {
2802
+ throw new Error(`Source path not found: ${sourcePath}`);
2803
+ }
2804
+ const skillMdPath = path7.join(sourcePath, "SKILL.md");
2805
+ if (!await fs7.pathExists(skillMdPath)) {
2806
+ throw new Error(`No SKILL.md found in ${sourcePath}. Not a valid skill directory.`);
2807
+ }
2808
+ const skillName = path7.basename(sourcePath);
2809
+ const destPath = path7.join(skillsDir, skillName);
2810
+ await fs7.mkdirp(skillsDir);
2811
+ await fs7.copy(sourcePath, destPath, { overwrite: true });
2812
+ return destPath;
2813
+ }
2814
+ async function listInstalledSkills(skillsDir) {
2815
+ if (!await fs7.pathExists(skillsDir)) {
2816
+ return [];
2817
+ }
2818
+ const entries = await fs7.readdir(skillsDir);
2819
+ const skills = [];
2820
+ for (const entry of entries) {
2821
+ const entryPath = path7.join(skillsDir, entry);
2822
+ const stat = await fs7.stat(entryPath);
2823
+ if (!stat.isDirectory()) continue;
2824
+ const skillMdPath = path7.join(entryPath, "SKILL.md");
2825
+ if (!await fs7.pathExists(skillMdPath)) continue;
2826
+ const content = await fs7.readFile(skillMdPath, "utf-8");
2827
+ const meta = parseSkillMd2(content);
2828
+ skills.push({
2829
+ name: meta.name || entry,
2830
+ version: meta.version,
2831
+ description: meta.description
2832
+ });
2833
+ }
2834
+ return skills;
2835
+ }
2836
+ async function removeSkill(name, skillsDir) {
2837
+ const skillDir = path7.join(skillsDir, name);
2838
+ if (!await fs7.pathExists(skillDir)) {
2839
+ throw new Error(`Skill "${name}" not found in ${skillsDir}`);
2840
+ }
2841
+ await fs7.remove(skillDir);
2842
+ }
2843
+ function getGitHubUrl(nameOrUrl) {
2844
+ if (nameOrUrl.startsWith("https://") || nameOrUrl.startsWith("http://")) {
2845
+ return nameOrUrl;
2846
+ }
2847
+ return `https://github.com/${nameOrUrl}`;
2848
+ }
2849
+ function getConvexUrl2() {
2850
+ return process.env["CONVEX_URL"] || process.env["NEXT_PUBLIC_CONVEX_URL"];
2851
+ }
2852
+ function registerSkillCommand(program2) {
2853
+ const skillCmd = program2.command("skill").description("Manage global skills installed in ~/.agentforge/skills/");
2854
+ skillCmd.command("install").argument("<name>", "Local path or GitHub owner/repo to install from").description("Install a skill to ~/.agentforge/skills/").action(async (name) => {
2855
+ const skillsDir = getGlobalSkillsDir();
2856
+ const isLocalPath = await fs7.pathExists(name);
2857
+ if (isLocalPath) {
2858
+ const sourcePath = path7.resolve(name);
2859
+ try {
2860
+ const installedPath = await installSkillFromPath(sourcePath, skillsDir);
2861
+ const skillName = path7.basename(installedPath);
2862
+ success(`Skill "${skillName}" installed from local path.`);
2863
+ info(`Location: ${installedPath}`);
2864
+ } catch (err) {
2865
+ error(err.message);
2866
+ process.exit(1);
2867
+ }
2868
+ } else {
2869
+ const repoUrl = getGitHubUrl(name);
2870
+ const repoName = name.split("/").pop().replace(/\.git$/, "");
2871
+ const destPath = path7.join(skillsDir, repoName);
2872
+ await fs7.mkdirp(skillsDir);
2873
+ info(`Cloning skill from ${repoUrl}...`);
2874
+ try {
2875
+ execFileSync("git", ["clone", "--depth", "1", repoUrl, destPath], {
2876
+ encoding: "utf-8",
2877
+ stdio: "pipe"
2878
+ });
2879
+ await fs7.remove(path7.join(destPath, ".git"));
2880
+ const skillMdPath = path7.join(destPath, "SKILL.md");
2881
+ if (!await fs7.pathExists(skillMdPath)) {
2882
+ error("Cloned repo does not contain a SKILL.md. Not a valid skill.");
2883
+ await fs7.remove(destPath);
2884
+ process.exit(1);
2885
+ }
2886
+ success(`Skill "${repoName}" installed from GitHub.`);
2887
+ info(`Location: ${destPath}`);
2888
+ } catch (err) {
2889
+ error(`Failed to clone: ${err.message}`);
2890
+ process.exit(1);
2891
+ }
2892
+ }
2893
+ });
2894
+ skillCmd.command("list").description("List skills installed in ~/.agentforge/skills/").option("--json", "Output as JSON").action(async (opts) => {
2895
+ const skillsDir = getGlobalSkillsDir();
2896
+ header("Global Skills");
2897
+ let skills;
2898
+ try {
2899
+ skills = await listInstalledSkills(skillsDir);
2900
+ } catch {
2901
+ skills = [];
2902
+ }
2903
+ if (skills.length === 0) {
2904
+ info("No global skills installed.");
2905
+ dim(
2906
+ ` Install a skill with: ${colors.cyan}agentforge skill install <path-or-owner/repo>${colors.reset}`
2907
+ );
2908
+ return;
2909
+ }
2910
+ if (opts.json) {
2911
+ console.log(JSON.stringify(skills, null, 2));
2912
+ return;
2913
+ }
2914
+ table(
2915
+ skills.map((s) => ({
2916
+ Name: s.name,
2917
+ Version: s.version,
2918
+ Description: truncate(s.description, 60)
2919
+ }))
2920
+ );
2921
+ dim(` Skills directory: ${skillsDir}`);
2922
+ });
2923
+ skillCmd.command("remove").argument("<name>", "Skill name to remove").description("Remove a skill from ~/.agentforge/skills/").action(async (name) => {
2924
+ const skillsDir = getGlobalSkillsDir();
2925
+ try {
2926
+ await removeSkill(name, skillsDir);
2927
+ success(`Skill "${name}" removed.`);
2928
+ } catch (err) {
2929
+ error(err.message);
2930
+ info("List installed skills with: agentforge skill list");
2931
+ process.exit(1);
2932
+ }
2933
+ });
2934
+ skillCmd.command("search <query>").description("Search the skill marketplace").option("-c, --category <category>", "Filter by category").action(async (query, options) => {
2935
+ const { searchSkills: searchMarketplace } = await import("@agentforge-ai/core");
2936
+ const convexUrl = getConvexUrl2();
2937
+ if (!convexUrl) {
2938
+ error("No CONVEX_URL configured. Set CONVEX_URL environment variable.");
2939
+ return;
2940
+ }
2941
+ try {
2942
+ info(`Searching marketplace for "${query}"...`);
2943
+ const skills = await searchMarketplace(query, convexUrl, options.category);
2944
+ if (skills.length === 0) {
2945
+ info("No skills found matching your query.");
2946
+ return;
2947
+ }
2948
+ header(`Found ${skills.length} skill(s)`);
2949
+ table(
2950
+ skills.map((s) => ({
2951
+ Name: s.name,
2952
+ Version: s.version,
2953
+ Category: s.category,
2954
+ Downloads: s.downloads.toString(),
2955
+ Description: truncate(s.description, 50)
2956
+ }))
2957
+ );
2958
+ } catch (err) {
2959
+ error(`Search failed: ${err instanceof Error ? err.message : String(err)}`);
2960
+ }
2961
+ });
2962
+ skillCmd.command("publish").description("Publish a skill to the marketplace").option("-d, --dir <directory>", "Skill directory (default: current directory)", ".").action(async (options) => {
2963
+ const fsExtra = await import("fs-extra");
2964
+ const pathMod = await import("path");
2965
+ const { parseSkillManifest, publishSkill: publishToMarketplace } = await import("@agentforge-ai/core");
2966
+ const convexUrl = getConvexUrl2();
2967
+ if (!convexUrl) {
2968
+ error("No CONVEX_URL configured. Set CONVEX_URL environment variable.");
2969
+ return;
2970
+ }
2971
+ const skillDir = pathMod.resolve(options.dir);
2972
+ const skillMdPath = pathMod.join(skillDir, "SKILL.md");
2973
+ if (!await fsExtra.pathExists(skillMdPath)) {
2974
+ error("No SKILL.md found in the specified directory.");
2975
+ return;
2976
+ }
2977
+ try {
2978
+ const skillMdContent = await fsExtra.readFile(skillMdPath, "utf-8");
2979
+ const manifest = parseSkillManifest(skillMdContent);
2980
+ let readmeContent;
2981
+ const readmePath = pathMod.join(skillDir, "README.md");
2982
+ if (await fsExtra.pathExists(readmePath)) {
2983
+ readmeContent = await fsExtra.readFile(readmePath, "utf-8");
2984
+ }
2985
+ const meta = manifest.metadata ?? {};
2986
+ info(`Publishing "${manifest.name}" v${manifest.version}...`);
2987
+ await publishToMarketplace(
2988
+ {
2989
+ name: manifest.name,
2990
+ version: manifest.version,
2991
+ description: manifest.description,
2992
+ author: meta.author ?? "unknown",
2993
+ category: meta["category"] ?? "general",
2994
+ tags: meta.tags ?? [],
2995
+ skillMdContent,
2996
+ readmeContent,
2997
+ repositoryUrl: meta.repository
2998
+ },
2999
+ convexUrl
3000
+ );
3001
+ success(`Skill "${manifest.name}" published successfully!`);
3002
+ } catch (err) {
3003
+ error(`Publish failed: ${err instanceof Error ? err.message : String(err)}`);
3004
+ }
3005
+ });
3006
+ skillCmd.command("featured").description("Show featured skills from the marketplace").action(async () => {
3007
+ const { fetchFeaturedSkills } = await import("@agentforge-ai/core");
3008
+ const convexUrl = getConvexUrl2();
3009
+ if (!convexUrl) {
3010
+ error("No CONVEX_URL configured. Set CONVEX_URL environment variable.");
3011
+ return;
3012
+ }
3013
+ try {
3014
+ const skills = await fetchFeaturedSkills(convexUrl);
3015
+ if (skills.length === 0) {
3016
+ info("No featured skills available.");
3017
+ return;
3018
+ }
3019
+ header("Featured Skills");
3020
+ table(
3021
+ skills.map((s) => ({
3022
+ Name: s.name,
3023
+ Version: s.version,
3024
+ Category: s.category,
3025
+ Downloads: s.downloads.toString(),
3026
+ Description: truncate(s.description, 60)
3027
+ }))
3028
+ );
3029
+ } catch (err) {
3030
+ error(`Failed to fetch featured skills: ${err instanceof Error ? err.message : String(err)}`);
3031
+ }
3032
+ });
3033
+ }
3034
+
2773
3035
  // src/commands/cron.ts
2774
3036
  import readline4 from "readline";
2775
3037
  function prompt3(q) {
@@ -2933,8 +3195,8 @@ function registerMcpCommand(program2) {
2933
3195
  }
2934
3196
 
2935
3197
  // src/commands/files.ts
2936
- import fs7 from "fs-extra";
2937
- import path7 from "path";
3198
+ import fs8 from "fs-extra";
3199
+ import path8 from "path";
2938
3200
  function registerFilesCommand(program2) {
2939
3201
  const files = program2.command("files").description("Manage files");
2940
3202
  files.command("list").argument("[folder]", "Folder ID to list files from").option("--json", "Output as JSON").description("List files").action(async (folder, opts) => {
@@ -2961,14 +3223,14 @@ function registerFilesCommand(program2) {
2961
3223
  })));
2962
3224
  });
2963
3225
  files.command("upload").argument("<filepath>", "Path to file to upload").option("--folder <id>", "Folder ID to upload to").option("--project <id>", "Project ID to associate with").description("Upload a file").action(async (filepath, opts) => {
2964
- const absPath = path7.resolve(filepath);
2965
- if (!fs7.existsSync(absPath)) {
3226
+ const absPath = path8.resolve(filepath);
3227
+ if (!fs8.existsSync(absPath)) {
2966
3228
  error(`File not found: ${absPath}`);
2967
3229
  process.exit(1);
2968
3230
  }
2969
- const stat = fs7.statSync(absPath);
2970
- const name = path7.basename(absPath);
2971
- const ext = path7.extname(absPath).toLowerCase();
3231
+ const stat = fs8.statSync(absPath);
3232
+ const name = path8.basename(absPath);
3233
+ const ext = path8.extname(absPath).toLowerCase();
2972
3234
  const mimeTypes = {
2973
3235
  ".txt": "text/plain",
2974
3236
  ".md": "text/markdown",
@@ -3132,8 +3394,8 @@ function registerProjectsCommand(program2) {
3132
3394
  }
3133
3395
 
3134
3396
  // src/commands/config.ts
3135
- import fs8 from "fs-extra";
3136
- import path8 from "path";
3397
+ import fs9 from "fs-extra";
3398
+ import path9 from "path";
3137
3399
  import readline7 from "readline";
3138
3400
  function prompt6(q) {
3139
3401
  const rl = readline7.createInterface({ input: process.stdin, output: process.stdout });
@@ -3149,10 +3411,10 @@ function registerConfigCommand(program2) {
3149
3411
  const cwd = process.cwd();
3150
3412
  const envFiles = [".env", ".env.local", ".env.production"];
3151
3413
  for (const envFile of envFiles) {
3152
- const envPath = path8.join(cwd, envFile);
3153
- if (fs8.existsSync(envPath)) {
3414
+ const envPath = path9.join(cwd, envFile);
3415
+ if (fs9.existsSync(envPath)) {
3154
3416
  console.log(` ${colors.cyan}${envFile}${colors.reset}`);
3155
- const content = fs8.readFileSync(envPath, "utf-8");
3417
+ const content = fs9.readFileSync(envPath, "utf-8");
3156
3418
  content.split("\n").forEach((line) => {
3157
3419
  if (line.trim() && !line.startsWith("#")) {
3158
3420
  const [key, ...rest] = line.split("=");
@@ -3164,25 +3426,25 @@ function registerConfigCommand(program2) {
3164
3426
  console.log();
3165
3427
  }
3166
3428
  }
3167
- const convexDir = path8.join(cwd, ".convex");
3168
- if (fs8.existsSync(convexDir)) {
3429
+ const convexDir = path9.join(cwd, ".convex");
3430
+ if (fs9.existsSync(convexDir)) {
3169
3431
  info("Convex: Configured");
3170
3432
  } else {
3171
3433
  info("Convex: Not configured (run `npx convex dev`)");
3172
3434
  }
3173
- const skillsDir = path8.join(cwd, "skills");
3174
- if (fs8.existsSync(skillsDir)) {
3175
- const skills = fs8.readdirSync(skillsDir).filter((d) => fs8.statSync(path8.join(skillsDir, d)).isDirectory());
3435
+ const skillsDir = path9.join(cwd, "skills");
3436
+ if (fs9.existsSync(skillsDir)) {
3437
+ const skills = fs9.readdirSync(skillsDir).filter((d) => fs9.statSync(path9.join(skillsDir, d)).isDirectory());
3176
3438
  info(`Skills: ${skills.length} installed (${skills.join(", ")})`);
3177
3439
  } else {
3178
3440
  info("Skills: None installed");
3179
3441
  }
3180
3442
  });
3181
3443
  config.command("set").argument("<key>", "Configuration key").argument("<value>", "Configuration value").option("--env <file>", "Environment file to update", ".env.local").description("Set a configuration value").action(async (key, value, opts) => {
3182
- const envPath = path8.join(process.cwd(), opts.env);
3444
+ const envPath = path9.join(process.cwd(), opts.env);
3183
3445
  let content = "";
3184
- if (fs8.existsSync(envPath)) {
3185
- content = fs8.readFileSync(envPath, "utf-8");
3446
+ if (fs9.existsSync(envPath)) {
3447
+ content = fs9.readFileSync(envPath, "utf-8");
3186
3448
  }
3187
3449
  const lines = content.split("\n");
3188
3450
  const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
@@ -3191,16 +3453,16 @@ function registerConfigCommand(program2) {
3191
3453
  } else {
3192
3454
  lines.push(`${key}=${value}`);
3193
3455
  }
3194
- fs8.writeFileSync(envPath, lines.join("\n"));
3456
+ fs9.writeFileSync(envPath, lines.join("\n"));
3195
3457
  success(`Set ${key} in ${opts.env}`);
3196
3458
  });
3197
3459
  config.command("get").argument("<key>", "Configuration key").description("Get a configuration value").action(async (key) => {
3198
3460
  const cwd = process.cwd();
3199
3461
  const envFiles = [".env.local", ".env", ".env.production"];
3200
3462
  for (const envFile of envFiles) {
3201
- const envPath = path8.join(cwd, envFile);
3202
- if (fs8.existsSync(envPath)) {
3203
- const content = fs8.readFileSync(envPath, "utf-8");
3463
+ const envPath = path9.join(cwd, envFile);
3464
+ if (fs9.existsSync(envPath)) {
3465
+ const content = fs9.readFileSync(envPath, "utf-8");
3204
3466
  const match = content.match(new RegExp(`^${key}=(.+)$`, "m"));
3205
3467
  if (match) {
3206
3468
  console.log(match[1].trim());
@@ -3226,7 +3488,7 @@ function registerConfigCommand(program2) {
3226
3488
  else if (provider === "openrouter") envContent.push(`OPENROUTER_API_KEY=${apiKey}`);
3227
3489
  else if (provider === "anthropic") envContent.push(`ANTHROPIC_API_KEY=${apiKey}`);
3228
3490
  else if (provider === "google") envContent.push(`GOOGLE_API_KEY=${apiKey}`);
3229
- fs8.writeFileSync(path8.join(process.cwd(), ".env.local"), envContent.join("\n") + "\n");
3491
+ fs9.writeFileSync(path9.join(process.cwd(), ".env.local"), envContent.join("\n") + "\n");
3230
3492
  success("Configuration saved to .env.local");
3231
3493
  info("Run `npx convex dev` to start the Convex backend.");
3232
3494
  });
@@ -3248,9 +3510,9 @@ function registerConfigCommand(program2) {
3248
3510
  error("API key is required.");
3249
3511
  process.exit(1);
3250
3512
  }
3251
- const envPath = path8.join(process.cwd(), ".env.local");
3513
+ const envPath = path9.join(process.cwd(), ".env.local");
3252
3514
  let content = "";
3253
- if (fs8.existsSync(envPath)) content = fs8.readFileSync(envPath, "utf-8");
3515
+ if (fs9.existsSync(envPath)) content = fs9.readFileSync(envPath, "utf-8");
3254
3516
  const lines = content.split("\n");
3255
3517
  const idx = lines.findIndex((l) => l.startsWith(`${keyName}=`));
3256
3518
  if (idx >= 0) lines[idx] = `${keyName}=${apiKey}`;
@@ -3258,7 +3520,7 @@ function registerConfigCommand(program2) {
3258
3520
  const provIdx = lines.findIndex((l) => l.startsWith("LLM_PROVIDER="));
3259
3521
  if (provIdx >= 0) lines[provIdx] = `LLM_PROVIDER=${provider}`;
3260
3522
  else lines.push(`LLM_PROVIDER=${provider}`);
3261
- fs8.writeFileSync(envPath, lines.join("\n"));
3523
+ fs9.writeFileSync(envPath, lines.join("\n"));
3262
3524
  success(`Provider "${provider}" configured.`);
3263
3525
  });
3264
3526
  }
@@ -3425,7 +3687,7 @@ function maskKey(key) {
3425
3687
  }
3426
3688
  function promptSecret2(question) {
3427
3689
  return new Promise((resolve2) => {
3428
- const readline12 = __require("readline");
3690
+ const readline13 = __require("readline");
3429
3691
  if (process.stdin.isTTY) {
3430
3692
  process.stdout.write(question);
3431
3693
  process.stdin.setRawMode(true);
@@ -3453,7 +3715,7 @@ function promptSecret2(question) {
3453
3715
  };
3454
3716
  process.stdin.on("data", onData);
3455
3717
  } else {
3456
- const rl = readline12.createInterface({ input: process.stdin, output: process.stdout });
3718
+ const rl = readline13.createInterface({ input: process.stdin, output: process.stdout });
3457
3719
  rl.question(question, (ans) => {
3458
3720
  rl.close();
3459
3721
  resolve2(ans.trim());
@@ -3544,8 +3806,8 @@ function registerKeysCommand(program2) {
3544
3806
  }
3545
3807
  const target = items[0];
3546
3808
  if (!opts.force) {
3547
- const readline12 = __require("readline");
3548
- const rl = readline12.createInterface({ input: process.stdin, output: process.stdout });
3809
+ const readline13 = __require("readline");
3810
+ const rl = readline13.createInterface({ input: process.stdin, output: process.stdout });
3549
3811
  const answer = await new Promise((resolve2) => {
3550
3812
  rl.question(`Delete "${target.keyName}" for ${provider}? (y/N): `, (ans) => {
3551
3813
  rl.close();
@@ -3617,19 +3879,19 @@ function registerKeysCommand(program2) {
3617
3879
 
3618
3880
  // src/commands/status.ts
3619
3881
  import { spawn as spawn2 } from "child_process";
3620
- import path9 from "path";
3621
- import fs9 from "fs-extra";
3882
+ import path10 from "path";
3883
+ import fs10 from "fs-extra";
3622
3884
  import readline9 from "readline";
3623
3885
  function registerStatusCommand(program2) {
3624
3886
  program2.command("status").description("Show system health and connection status").action(async () => {
3625
3887
  header("AgentForge Status");
3626
3888
  const cwd = process.cwd();
3627
3889
  const checks = {};
3628
- checks["Project Root"] = fs9.existsSync(path9.join(cwd, "package.json")) ? "\u2714 Found" : "\u2716 Not found";
3629
- checks["Convex Dir"] = fs9.existsSync(path9.join(cwd, "convex")) ? "\u2714 Found" : "\u2716 Not found";
3630
- checks["Skills Dir"] = fs9.existsSync(path9.join(cwd, "skills")) ? "\u2714 Found" : "\u2716 Not configured";
3631
- checks["Dashboard Dir"] = fs9.existsSync(path9.join(cwd, "dashboard")) ? "\u2714 Found" : "\u2716 Not found";
3632
- checks["Env Config"] = fs9.existsSync(path9.join(cwd, ".env.local")) || fs9.existsSync(path9.join(cwd, ".env")) ? "\u2714 Found" : "\u2716 Not found";
3890
+ checks["Project Root"] = fs10.existsSync(path10.join(cwd, "package.json")) ? "\u2714 Found" : "\u2716 Not found";
3891
+ checks["Convex Dir"] = fs10.existsSync(path10.join(cwd, "convex")) ? "\u2714 Found" : "\u2716 Not found";
3892
+ checks["Skills Dir"] = fs10.existsSync(path10.join(cwd, "skills")) ? "\u2714 Found" : "\u2716 Not configured";
3893
+ checks["Dashboard Dir"] = fs10.existsSync(path10.join(cwd, "dashboard")) ? "\u2714 Found" : "\u2716 Not found";
3894
+ checks["Env Config"] = fs10.existsSync(path10.join(cwd, ".env.local")) || fs10.existsSync(path10.join(cwd, ".env")) ? "\u2714 Found" : "\u2716 Not found";
3633
3895
  try {
3634
3896
  const client = await createClient();
3635
3897
  const agents = await client.query("agents:list", {});
@@ -3640,9 +3902,9 @@ function registerStatusCommand(program2) {
3640
3902
  const envFiles = [".env.local", ".env"];
3641
3903
  let provider = "Not configured";
3642
3904
  for (const envFile of envFiles) {
3643
- const envPath = path9.join(cwd, envFile);
3644
- if (fs9.existsSync(envPath)) {
3645
- const content = fs9.readFileSync(envPath, "utf-8");
3905
+ const envPath = path10.join(cwd, envFile);
3906
+ if (fs10.existsSync(envPath)) {
3907
+ const content = fs10.readFileSync(envPath, "utf-8");
3646
3908
  const match = content.match(/LLM_PROVIDER=(.+)/);
3647
3909
  if (match) {
3648
3910
  provider = match[1].trim();
@@ -3664,16 +3926,16 @@ function registerStatusCommand(program2) {
3664
3926
  program2.command("dashboard").description("Launch the web dashboard").option("-p, --port <port>", "Port for the dashboard", "3000").option("--install", "Install dashboard dependencies before starting").action(async (opts) => {
3665
3927
  const cwd = process.cwd();
3666
3928
  const searchPaths = [
3667
- path9.join(cwd, "dashboard"),
3929
+ path10.join(cwd, "dashboard"),
3668
3930
  // 1. Bundled in project (agentforge create)
3669
- path9.join(cwd, "packages", "web"),
3931
+ path10.join(cwd, "packages", "web"),
3670
3932
  // 2. Monorepo structure
3671
- path9.join(cwd, "node_modules", "@agentforge-ai", "web")
3933
+ path10.join(cwd, "node_modules", "@agentforge-ai", "web")
3672
3934
  // 3. Installed as dependency
3673
3935
  ];
3674
3936
  let dashDir = "";
3675
3937
  for (const p of searchPaths) {
3676
- if (fs9.existsSync(path9.join(p, "package.json"))) {
3938
+ if (fs10.existsSync(path10.join(p, "package.json"))) {
3677
3939
  dashDir = p;
3678
3940
  break;
3679
3941
  }
@@ -3695,10 +3957,10 @@ function registerStatusCommand(program2) {
3695
3957
  console.log();
3696
3958
  return;
3697
3959
  }
3698
- const nodeModulesExists = fs9.existsSync(path9.join(dashDir, "node_modules"));
3960
+ const nodeModulesExists = fs10.existsSync(path10.join(dashDir, "node_modules"));
3699
3961
  if (!nodeModulesExists || opts.install) {
3700
3962
  header("AgentForge Dashboard \u2014 Installing Dependencies");
3701
- info(`Installing in ${path9.relative(cwd, dashDir) || "."}...`);
3963
+ info(`Installing in ${path10.relative(cwd, dashDir) || "."}...`);
3702
3964
  console.log();
3703
3965
  const installChild = spawn2("pnpm", ["install"], {
3704
3966
  cwd: dashDir,
@@ -3716,15 +3978,15 @@ function registerStatusCommand(program2) {
3716
3978
  success("Dependencies installed.");
3717
3979
  console.log();
3718
3980
  }
3719
- const envPath = path9.join(cwd, ".env.local");
3720
- if (fs9.existsSync(envPath)) {
3721
- const envContent = fs9.readFileSync(envPath, "utf-8");
3981
+ const envPath = path10.join(cwd, ".env.local");
3982
+ if (fs10.existsSync(envPath)) {
3983
+ const envContent = fs10.readFileSync(envPath, "utf-8");
3722
3984
  const convexUrlMatch = envContent.match(/CONVEX_URL=(.+)/);
3723
3985
  if (convexUrlMatch) {
3724
- const dashEnvPath = path9.join(dashDir, ".env.local");
3986
+ const dashEnvPath = path10.join(dashDir, ".env.local");
3725
3987
  const dashEnvContent = `VITE_CONVEX_URL=${convexUrlMatch[1].trim()}
3726
3988
  `;
3727
- fs9.writeFileSync(dashEnvPath, dashEnvContent);
3989
+ fs10.writeFileSync(dashEnvPath, dashEnvContent);
3728
3990
  }
3729
3991
  }
3730
3992
  header("AgentForge Dashboard");
@@ -3944,8 +4206,8 @@ function registerLoginCommand(program2) {
3944
4206
  }
3945
4207
 
3946
4208
  // src/commands/channel-telegram.ts
3947
- import fs10 from "fs-extra";
3948
- import path10 from "path";
4209
+ import fs11 from "fs-extra";
4210
+ import path11 from "path";
3949
4211
  import readline10 from "readline";
3950
4212
  function prompt8(q) {
3951
4213
  const rl = readline10.createInterface({ input: process.stdin, output: process.stdout });
@@ -3958,9 +4220,9 @@ function readEnvValue(key) {
3958
4220
  const cwd = process.cwd();
3959
4221
  const envFiles = [".env.local", ".env", ".env.production"];
3960
4222
  for (const envFile of envFiles) {
3961
- const envPath = path10.join(cwd, envFile);
3962
- if (fs10.existsSync(envPath)) {
3963
- const content = fs10.readFileSync(envPath, "utf-8");
4223
+ const envPath = path11.join(cwd, envFile);
4224
+ if (fs11.existsSync(envPath)) {
4225
+ const content = fs11.readFileSync(envPath, "utf-8");
3964
4226
  const match = content.match(new RegExp(`^${key}=(.+)$`, "m"));
3965
4227
  if (match) return match[1].trim().replace(/["']/g, "");
3966
4228
  }
@@ -3968,10 +4230,10 @@ function readEnvValue(key) {
3968
4230
  return void 0;
3969
4231
  }
3970
4232
  function writeEnvValue(key, value, envFile = ".env.local") {
3971
- const envPath = path10.join(process.cwd(), envFile);
4233
+ const envPath = path11.join(process.cwd(), envFile);
3972
4234
  let content = "";
3973
- if (fs10.existsSync(envPath)) {
3974
- content = fs10.readFileSync(envPath, "utf-8");
4235
+ if (fs11.existsSync(envPath)) {
4236
+ content = fs11.readFileSync(envPath, "utf-8");
3975
4237
  }
3976
4238
  const lines = content.split("\n");
3977
4239
  const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
@@ -3980,7 +4242,7 @@ function writeEnvValue(key, value, envFile = ".env.local") {
3980
4242
  } else {
3981
4243
  lines.push(`${key}=${value}`);
3982
4244
  }
3983
- fs10.writeFileSync(envPath, lines.join("\n"));
4245
+ fs11.writeFileSync(envPath, lines.join("\n"));
3984
4246
  }
3985
4247
  function registerChannelTelegramCommand(program2) {
3986
4248
  const channel = program2.command("channel:telegram").description("Manage the Telegram messaging channel");
@@ -4309,8 +4571,8 @@ Commands:
4309
4571
  }
4310
4572
 
4311
4573
  // src/commands/channel-whatsapp.ts
4312
- import fs11 from "fs-extra";
4313
- import path11 from "path";
4574
+ import fs12 from "fs-extra";
4575
+ import path12 from "path";
4314
4576
  import readline11 from "readline";
4315
4577
  function prompt9(q) {
4316
4578
  const rl = readline11.createInterface({ input: process.stdin, output: process.stdout });
@@ -4323,9 +4585,9 @@ function readEnvValue2(key) {
4323
4585
  const cwd = process.cwd();
4324
4586
  const envFiles = [".env.local", ".env", ".env.production"];
4325
4587
  for (const envFile of envFiles) {
4326
- const envPath = path11.join(cwd, envFile);
4327
- if (fs11.existsSync(envPath)) {
4328
- const content = fs11.readFileSync(envPath, "utf-8");
4588
+ const envPath = path12.join(cwd, envFile);
4589
+ if (fs12.existsSync(envPath)) {
4590
+ const content = fs12.readFileSync(envPath, "utf-8");
4329
4591
  const match = content.match(new RegExp(`^${key}=(.+)$`, "m"));
4330
4592
  if (match) return match[1].trim().replace(/["']/g, "");
4331
4593
  }
@@ -4333,10 +4595,10 @@ function readEnvValue2(key) {
4333
4595
  return void 0;
4334
4596
  }
4335
4597
  function writeEnvValue2(key, value, envFile = ".env.local") {
4336
- const envPath = path11.join(process.cwd(), envFile);
4598
+ const envPath = path12.join(process.cwd(), envFile);
4337
4599
  let content = "";
4338
- if (fs11.existsSync(envPath)) {
4339
- content = fs11.readFileSync(envPath, "utf-8");
4600
+ if (fs12.existsSync(envPath)) {
4601
+ content = fs12.readFileSync(envPath, "utf-8");
4340
4602
  }
4341
4603
  const lines = content.split("\n");
4342
4604
  const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
@@ -4345,7 +4607,7 @@ function writeEnvValue2(key, value, envFile = ".env.local") {
4345
4607
  } else {
4346
4608
  lines.push(`${key}=${value}`);
4347
4609
  }
4348
- fs11.writeFileSync(envPath, lines.join("\n"));
4610
+ fs12.writeFileSync(envPath, lines.join("\n"));
4349
4611
  }
4350
4612
  function registerChannelWhatsAppCommand(program2) {
4351
4613
  const channel = program2.command("channel:whatsapp").description("Manage the WhatsApp messaging channel");
@@ -4781,6 +5043,546 @@ async function runMinimalWhatsAppBot(config) {
4781
5043
  });
4782
5044
  }
4783
5045
 
5046
+ // src/commands/channel-slack.ts
5047
+ import fs13 from "fs-extra";
5048
+ import path13 from "path";
5049
+ import readline12 from "readline";
5050
+ function prompt10(q) {
5051
+ const rl = readline12.createInterface({ input: process.stdin, output: process.stdout });
5052
+ return new Promise((r) => rl.question(q, (a) => {
5053
+ rl.close();
5054
+ r(a.trim());
5055
+ }));
5056
+ }
5057
+ function readEnvValue3(key) {
5058
+ const cwd = process.cwd();
5059
+ const envFiles = [".env.local", ".env", ".env.production"];
5060
+ for (const envFile of envFiles) {
5061
+ const envPath = path13.join(cwd, envFile);
5062
+ if (fs13.existsSync(envPath)) {
5063
+ const content = fs13.readFileSync(envPath, "utf-8");
5064
+ const match = content.match(new RegExp(`^${key}=(.+)$`, "m"));
5065
+ if (match) return match[1].trim().replace(/["']/g, "");
5066
+ }
5067
+ }
5068
+ return void 0;
5069
+ }
5070
+ function writeEnvValue3(key, value, envFile = ".env.local") {
5071
+ const envPath = path13.join(process.cwd(), envFile);
5072
+ let content = "";
5073
+ if (fs13.existsSync(envPath)) {
5074
+ content = fs13.readFileSync(envPath, "utf-8");
5075
+ }
5076
+ const lines = content.split("\n");
5077
+ const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
5078
+ if (idx >= 0) {
5079
+ lines[idx] = `${key}=${value}`;
5080
+ } else {
5081
+ lines.push(`${key}=${value}`);
5082
+ }
5083
+ fs13.writeFileSync(envPath, lines.join("\n"));
5084
+ }
5085
+ function registerChannelSlackCommand(program2) {
5086
+ const channel = program2.command("channel:slack").description("Manage the Slack messaging channel");
5087
+ channel.command("start").description("Start the Slack bot and begin routing messages to an agent").option("-a, --agent <id>", "Agent ID to route messages to").option("--bot-token <token>", "Slack bot token (xoxb-...) (overrides .env)").option("--app-token <token>", "Slack app-level token (xapp-...) for socket mode (overrides .env)").option("--signing-secret <secret>", "Slack signing secret (overrides .env)").option("--socket-mode", "Enable socket mode (default: true)", true).option("--log-level <level>", "Log level: debug, info, warn, error", "info").action(async (opts) => {
5088
+ header("Slack Channel");
5089
+ const botToken = opts.botToken || readEnvValue3("SLACK_BOT_TOKEN") || process.env.SLACK_BOT_TOKEN;
5090
+ if (!botToken) {
5091
+ error("Slack Bot Token not found.");
5092
+ info("Set it with: agentforge channel:slack configure");
5093
+ info("Or pass it with: --bot-token <token>");
5094
+ info("Or set SLACK_BOT_TOKEN in your .env.local file");
5095
+ process.exit(1);
5096
+ }
5097
+ const appToken = opts.appToken || readEnvValue3("SLACK_APP_TOKEN") || process.env.SLACK_APP_TOKEN;
5098
+ if (opts.socketMode && !appToken) {
5099
+ error("Slack App Token not found (required for socket mode).");
5100
+ info("Set it with: agentforge channel:slack configure");
5101
+ info("Or pass it with: --app-token <token>");
5102
+ info("Or set SLACK_APP_TOKEN in your .env.local file");
5103
+ info("Or disable socket mode with: --no-socket-mode");
5104
+ process.exit(1);
5105
+ }
5106
+ const signingSecret = opts.signingSecret || readEnvValue3("SLACK_SIGNING_SECRET") || process.env.SLACK_SIGNING_SECRET;
5107
+ if (!signingSecret) {
5108
+ error("Slack Signing Secret not found.");
5109
+ info("Set it with: agentforge channel:slack configure");
5110
+ info("Or pass it with: --signing-secret <secret>");
5111
+ info("Or set SLACK_SIGNING_SECRET in your .env.local file");
5112
+ process.exit(1);
5113
+ }
5114
+ const convexUrl = readEnvValue3("CONVEX_URL") || process.env.CONVEX_URL;
5115
+ if (!convexUrl) {
5116
+ error("CONVEX_URL not found. Run `npx convex dev` first.");
5117
+ process.exit(1);
5118
+ }
5119
+ let agentId = opts.agent;
5120
+ if (!agentId) {
5121
+ agentId = readEnvValue3("AGENTFORGE_AGENT_ID") || process.env.AGENTFORGE_AGENT_ID;
5122
+ }
5123
+ if (!agentId) {
5124
+ info("No agent specified. Fetching available agents...");
5125
+ const client = await createClient();
5126
+ const agents = await safeCall(
5127
+ () => client.query("agents:list", {}),
5128
+ "Failed to list agents"
5129
+ );
5130
+ if (!agents || agents.length === 0) {
5131
+ error("No agents found. Create one first: agentforge agents create");
5132
+ process.exit(1);
5133
+ }
5134
+ console.log();
5135
+ agents.forEach((a, i) => {
5136
+ console.log(
5137
+ ` ${colors.cyan}${i + 1}.${colors.reset} ${a.name} ${colors.dim}(${a.id})${colors.reset} \u2014 ${a.model}`
5138
+ );
5139
+ });
5140
+ console.log();
5141
+ const choice = await prompt10("Select agent (number or ID): ");
5142
+ const idx = parseInt(choice) - 1;
5143
+ agentId = idx >= 0 && idx < agents.length ? agents[idx].id : choice;
5144
+ }
5145
+ info(`Agent: ${agentId}`);
5146
+ info(`Convex: ${convexUrl}`);
5147
+ info(`Mode: ${opts.socketMode ? "Socket Mode" : "Events API"}`);
5148
+ info(`Log: ${opts.logLevel}`);
5149
+ console.log();
5150
+ let startSlackChannel;
5151
+ try {
5152
+ const slackPkg = "@agentforge-ai/channels-slack";
5153
+ const mod = await import(
5154
+ /* @vite-ignore */
5155
+ slackPkg
5156
+ );
5157
+ startSlackChannel = mod.startSlackChannel;
5158
+ } catch (importError) {
5159
+ error("Could not import @agentforge-ai/channels-slack. Using built-in Slack runner.");
5160
+ dim(` Error: ${importError.message}`);
5161
+ console.log();
5162
+ await runMinimalSlackBot({
5163
+ botToken,
5164
+ appToken: appToken || "",
5165
+ signingSecret,
5166
+ agentId,
5167
+ convexUrl,
5168
+ socketMode: opts.socketMode,
5169
+ logLevel: opts.logLevel
5170
+ });
5171
+ return;
5172
+ }
5173
+ try {
5174
+ await startSlackChannel({
5175
+ botToken,
5176
+ appToken,
5177
+ signingSecret,
5178
+ agentId,
5179
+ convexUrl,
5180
+ socketMode: opts.socketMode,
5181
+ logLevel: opts.logLevel
5182
+ });
5183
+ success("Slack bot is running!");
5184
+ dim(" Press Ctrl+C to stop.");
5185
+ await new Promise(() => {
5186
+ });
5187
+ } catch (startError) {
5188
+ error(`Failed to start Slack bot: ${startError.message}`);
5189
+ process.exit(1);
5190
+ }
5191
+ });
5192
+ channel.command("configure").description("Configure the Slack bot credentials and settings").action(async () => {
5193
+ header("Configure Slack Channel");
5194
+ console.log();
5195
+ info("To set up a Slack app:");
5196
+ dim(" 1. Go to https://api.slack.com/apps and create a new app");
5197
+ dim(" 2. Enable Socket Mode under Settings > Socket Mode");
5198
+ dim(" 3. Add bot scopes: chat:write, im:history, im:read, channels:history");
5199
+ dim(" 4. Install the app to your workspace");
5200
+ dim(" 5. Copy the Bot Token, App-Level Token, and Signing Secret");
5201
+ console.log();
5202
+ const currentBotToken = readEnvValue3("SLACK_BOT_TOKEN");
5203
+ if (currentBotToken) {
5204
+ const masked = currentBotToken.slice(0, 8) + "****" + currentBotToken.slice(-4);
5205
+ info(`Current bot token: ${masked}`);
5206
+ }
5207
+ const botToken = await prompt10("Slack Bot Token (xoxb-...): ");
5208
+ if (!botToken) {
5209
+ error("Bot token is required.");
5210
+ process.exit(1);
5211
+ }
5212
+ if (!botToken.startsWith("xoxb-")) {
5213
+ warn('Bot token should start with "xoxb-". Please verify this is correct.');
5214
+ }
5215
+ info("Validating bot token...");
5216
+ try {
5217
+ const response = await fetch("https://slack.com/api/auth.test", {
5218
+ method: "POST",
5219
+ headers: {
5220
+ Authorization: `Bearer ${botToken}`,
5221
+ "Content-Type": "application/json"
5222
+ }
5223
+ });
5224
+ const data = await response.json();
5225
+ if (!data.ok) {
5226
+ warn(`Token validation warning: ${data.error}`);
5227
+ info("Saving token anyway. You can validate later with: agentforge channel:slack status");
5228
+ } else {
5229
+ success(`Bot verified: @${data.user} in workspace "${data.team}"`);
5230
+ }
5231
+ } catch (fetchError) {
5232
+ warn(`Could not validate token (network error): ${fetchError.message}`);
5233
+ info("Saving token anyway.");
5234
+ }
5235
+ writeEnvValue3("SLACK_BOT_TOKEN", botToken);
5236
+ success("Bot token saved to .env.local");
5237
+ console.log();
5238
+ const currentAppToken = readEnvValue3("SLACK_APP_TOKEN");
5239
+ if (currentAppToken) {
5240
+ const masked = currentAppToken.slice(0, 8) + "****" + currentAppToken.slice(-4);
5241
+ info(`Current app token: ${masked}`);
5242
+ }
5243
+ const appToken = await prompt10("Slack App-Level Token (xapp-..., for socket mode): ");
5244
+ if (!appToken) {
5245
+ warn("App-level token not provided. Socket mode will not be available.");
5246
+ } else {
5247
+ if (!appToken.startsWith("xapp-")) {
5248
+ warn('App token should start with "xapp-". Please verify this is correct.');
5249
+ }
5250
+ writeEnvValue3("SLACK_APP_TOKEN", appToken);
5251
+ success("App token saved to .env.local");
5252
+ }
5253
+ console.log();
5254
+ const currentSigningSecret = readEnvValue3("SLACK_SIGNING_SECRET");
5255
+ if (currentSigningSecret) {
5256
+ info(`Current signing secret: ${currentSigningSecret.slice(0, 6)}****`);
5257
+ }
5258
+ const signingSecret = await prompt10("Slack Signing Secret: ");
5259
+ if (!signingSecret) {
5260
+ error("Signing secret is required.");
5261
+ process.exit(1);
5262
+ }
5263
+ if (signingSecret.length < 20) {
5264
+ warn("Signing secret looks too short. Please verify this is correct.");
5265
+ }
5266
+ writeEnvValue3("SLACK_SIGNING_SECRET", signingSecret);
5267
+ success("Signing secret saved to .env.local");
5268
+ console.log();
5269
+ const defaultAgent = await prompt10("Default agent ID (optional, press Enter to skip): ");
5270
+ if (defaultAgent) {
5271
+ writeEnvValue3("AGENTFORGE_AGENT_ID", defaultAgent);
5272
+ success(`Default agent set to: ${defaultAgent}`);
5273
+ }
5274
+ console.log();
5275
+ success("Configuration complete!");
5276
+ info("Start the bot with: agentforge channel:slack start");
5277
+ });
5278
+ channel.command("status").description("Check the Slack bot configuration and connectivity").action(async () => {
5279
+ header("Slack Channel Status");
5280
+ const botToken = readEnvValue3("SLACK_BOT_TOKEN");
5281
+ const appToken = readEnvValue3("SLACK_APP_TOKEN");
5282
+ const signingSecret = readEnvValue3("SLACK_SIGNING_SECRET");
5283
+ const agentId = readEnvValue3("AGENTFORGE_AGENT_ID");
5284
+ const convexUrl = readEnvValue3("CONVEX_URL");
5285
+ const statusData = {
5286
+ "Bot Token": botToken ? `${botToken.slice(0, 8)}****${botToken.slice(-4)}` : `${colors.red}Not configured${colors.reset}`,
5287
+ "App Token": appToken ? `${appToken.slice(0, 8)}****${appToken.slice(-4)}` : `${colors.dim}Not set${colors.reset}`,
5288
+ "Signing Secret": signingSecret ? `${signingSecret.slice(0, 6)}****` : `${colors.red}Not configured${colors.reset}`,
5289
+ "Default Agent": agentId || `${colors.dim}Not set${colors.reset}`,
5290
+ "Convex URL": convexUrl || `${colors.red}Not configured${colors.reset}`
5291
+ };
5292
+ details(statusData);
5293
+ if (botToken) {
5294
+ info("Checking Slack API connectivity...");
5295
+ try {
5296
+ const response = await fetch("https://slack.com/api/auth.test", {
5297
+ method: "POST",
5298
+ headers: {
5299
+ Authorization: `Bearer ${botToken}`,
5300
+ "Content-Type": "application/json"
5301
+ }
5302
+ });
5303
+ const data = await response.json();
5304
+ if (data.ok) {
5305
+ success(`Slack API connected: @${data.user} in workspace "${data.team}" (${data.team_id})`);
5306
+ } else {
5307
+ error(`Slack API error: ${data.error}`);
5308
+ }
5309
+ } catch {
5310
+ warn("Could not reach Slack API (network error).");
5311
+ }
5312
+ }
5313
+ if (convexUrl) {
5314
+ info("Checking Convex connectivity...");
5315
+ try {
5316
+ const client = await createClient();
5317
+ const agents = await client.query("agents:list", {});
5318
+ success(`Convex connected. ${agents.length} agents available.`);
5319
+ } catch {
5320
+ warn("Could not reach Convex deployment.");
5321
+ }
5322
+ }
5323
+ });
5324
+ }
5325
+ async function runMinimalSlackBot(config) {
5326
+ const { botToken, appToken, signingSecret, agentId, convexUrl } = config;
5327
+ const convexBase = convexUrl.replace(/\/$/, "");
5328
+ const threadMap = /* @__PURE__ */ new Map();
5329
+ info("Verifying Slack bot token...");
5330
+ try {
5331
+ const res = await fetch("https://slack.com/api/auth.test", {
5332
+ method: "POST",
5333
+ headers: {
5334
+ Authorization: `Bearer ${botToken}`,
5335
+ "Content-Type": "application/json"
5336
+ }
5337
+ });
5338
+ const data = await res.json();
5339
+ if (!data.ok) {
5340
+ error(`Slack auth error: ${data.error}`);
5341
+ process.exit(1);
5342
+ }
5343
+ success(`Slack bot connected: @${data.user} in "${data.team}"`);
5344
+ } catch (fetchError) {
5345
+ warn(`Could not verify bot token: ${fetchError.message}`);
5346
+ info("Continuing anyway...");
5347
+ }
5348
+ async function convexMutation(fn, args) {
5349
+ const res = await fetch(`${convexBase}/api/mutation`, {
5350
+ method: "POST",
5351
+ headers: { "Content-Type": "application/json" },
5352
+ body: JSON.stringify({ path: fn, args })
5353
+ });
5354
+ const data = await res.json();
5355
+ if (data.status === "error") throw new Error(data.errorMessage);
5356
+ return data.value;
5357
+ }
5358
+ async function convexAction(fn, args) {
5359
+ const res = await fetch(`${convexBase}/api/action`, {
5360
+ method: "POST",
5361
+ headers: { "Content-Type": "application/json" },
5362
+ body: JSON.stringify({ path: fn, args })
5363
+ });
5364
+ const data = await res.json();
5365
+ if (data.status === "error") throw new Error(data.errorMessage);
5366
+ return data.value;
5367
+ }
5368
+ async function sendSlackMessage(channel, text, threadTs) {
5369
+ const body = { channel, text };
5370
+ if (threadTs) body.thread_ts = threadTs;
5371
+ await fetch("https://slack.com/api/chat.postMessage", {
5372
+ method: "POST",
5373
+ headers: {
5374
+ Authorization: `Bearer ${botToken}`,
5375
+ "Content-Type": "application/json"
5376
+ },
5377
+ body: JSON.stringify(body)
5378
+ });
5379
+ }
5380
+ async function getOrCreateThread(channelThreadKey, senderName) {
5381
+ const cached = threadMap.get(channelThreadKey);
5382
+ if (cached) return cached;
5383
+ const threadId = await convexMutation("chat:createThread", {
5384
+ agentId,
5385
+ name: senderName ? `Slack: ${senderName}` : `Slack ${channelThreadKey}`,
5386
+ userId: `slack:${channelThreadKey}`
5387
+ });
5388
+ threadMap.set(channelThreadKey, threadId);
5389
+ return threadId;
5390
+ }
5391
+ async function handleSlackMessage(event) {
5392
+ if (event.bot_id || event.subtype) return;
5393
+ const channelId = event.channel;
5394
+ const userId = event.user;
5395
+ const text = (event.text || "").trim();
5396
+ const threadTs = event.thread_ts || event.ts;
5397
+ if (!text) return;
5398
+ const threadKey = `${channelId}:${userId}`;
5399
+ console.log(`[Slack:${channelId}] ${text}`);
5400
+ try {
5401
+ const convexThreadId = await getOrCreateThread(threadKey, `slack:${userId}`);
5402
+ const result = await convexAction("chat:sendMessage", {
5403
+ agentId,
5404
+ threadId: convexThreadId,
5405
+ content: text,
5406
+ userId: `slack:${userId}`
5407
+ });
5408
+ if (result?.response) {
5409
+ const response = result.response;
5410
+ if (response.length <= 4e3) {
5411
+ await sendSlackMessage(channelId, response, threadTs);
5412
+ } else {
5413
+ const chunks = response.match(/.{1,4000}/gs) || [];
5414
+ for (const chunk of chunks) {
5415
+ await sendSlackMessage(channelId, chunk, threadTs);
5416
+ }
5417
+ }
5418
+ console.log(`[Agent] ${response.substring(0, 100)}${response.length > 100 ? "..." : ""}`);
5419
+ } else {
5420
+ await sendSlackMessage(channelId, "I couldn't generate a response. Please try again.", threadTs);
5421
+ }
5422
+ } catch (routeError) {
5423
+ console.error(`Error: ${routeError.message}`);
5424
+ await sendSlackMessage(channelId, "Sorry, I encountered an error. Please try again.", threadTs);
5425
+ }
5426
+ }
5427
+ if (appToken && config.socketMode !== false) {
5428
+ info("Starting in Socket Mode...");
5429
+ try {
5430
+ const wsUrlRes = await fetch("https://slack.com/api/apps.connections.open", {
5431
+ method: "POST",
5432
+ headers: {
5433
+ Authorization: `Bearer ${appToken}`,
5434
+ "Content-Type": "application/json"
5435
+ }
5436
+ });
5437
+ const wsData = await wsUrlRes.json();
5438
+ if (!wsData.ok || !wsData.url) {
5439
+ throw new Error(`Could not open WebSocket connection: ${wsData.error}`);
5440
+ }
5441
+ const { default: WebSocket } = await import("ws");
5442
+ const ws = new WebSocket(wsData.url);
5443
+ ws.on("open", () => {
5444
+ success("Socket Mode connected!");
5445
+ dim(" Listening for messages. Press Ctrl+C to stop.");
5446
+ console.log();
5447
+ });
5448
+ ws.on("message", async (data) => {
5449
+ try {
5450
+ const payload = JSON.parse(data.toString());
5451
+ if (payload.envelope_id) {
5452
+ ws.send(JSON.stringify({ envelope_id: payload.envelope_id }));
5453
+ }
5454
+ if (payload.type === "events_api" && payload.payload?.event) {
5455
+ const event = payload.payload.event;
5456
+ if (event.type === "message") {
5457
+ await handleSlackMessage(event);
5458
+ }
5459
+ }
5460
+ if (payload.type === "slash_commands" && payload.payload) {
5461
+ const slashPayload = payload.payload;
5462
+ const command = slashPayload.command;
5463
+ const channelId = slashPayload.channel_id;
5464
+ const userId = slashPayload.user_id;
5465
+ const text = slashPayload.text || "";
5466
+ if (command === "/start" || command === "/new") {
5467
+ const key = `${channelId}:${userId}`;
5468
+ threadMap.delete(key);
5469
+ await sendSlackMessage(channelId, "New conversation started! Send me a message.");
5470
+ } else if (command === "/help") {
5471
+ await sendSlackMessage(channelId, "AgentForge Slack Bot\n\nJust send me a message and I'll respond using AI.\n\nCommands:\n/start \u2014 Reset and start fresh\n/new \u2014 Start a fresh conversation\n/help \u2014 Show this help\n/ask <question> \u2014 Ask a question");
5472
+ } else if (command === "/ask") {
5473
+ await handleSlackMessage({ channel: channelId, user: userId, text, ts: Date.now().toString() });
5474
+ }
5475
+ }
5476
+ } catch (parseError) {
5477
+ console.error(`Error processing message: ${parseError.message}`);
5478
+ }
5479
+ });
5480
+ ws.on("close", (code) => {
5481
+ warn(`Socket Mode disconnected (code: ${code}). Reconnecting in 5s...`);
5482
+ setTimeout(() => runMinimalSlackBot(config), 5e3);
5483
+ });
5484
+ ws.on("error", (err) => {
5485
+ console.error(`WebSocket error: ${err.message}`);
5486
+ });
5487
+ process.on("SIGINT", () => {
5488
+ console.log("\nStopping...");
5489
+ ws.close();
5490
+ process.exit(0);
5491
+ });
5492
+ await new Promise(() => {
5493
+ });
5494
+ } catch (socketError) {
5495
+ warn(`Socket Mode failed: ${socketError.message}`);
5496
+ info("Falling back to HTTP Events API server...");
5497
+ }
5498
+ }
5499
+ info("Starting HTTP server for Events API...");
5500
+ const port = 3002;
5501
+ const path_ = "/slack/events";
5502
+ const http = await import("http");
5503
+ const { createHmac, timingSafeEqual } = await import("crypto");
5504
+ function verifySlackSignature(body, timestamp, signature) {
5505
+ const sigBaseString = `v0:${timestamp}:${body}`;
5506
+ const hmac = createHmac("sha256", signingSecret);
5507
+ hmac.update(sigBaseString);
5508
+ const computedSig = `v0=${hmac.digest("hex")}`;
5509
+ try {
5510
+ return timingSafeEqual(Buffer.from(computedSig), Buffer.from(signature));
5511
+ } catch {
5512
+ return false;
5513
+ }
5514
+ }
5515
+ const server = http.createServer(async (req, res) => {
5516
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
5517
+ if (url.pathname !== path_) {
5518
+ res.writeHead(404);
5519
+ res.end("Not Found");
5520
+ return;
5521
+ }
5522
+ if (req.method !== "POST") {
5523
+ res.writeHead(405);
5524
+ res.end("Method Not Allowed");
5525
+ return;
5526
+ }
5527
+ try {
5528
+ const chunks = [];
5529
+ for await (const chunk of req) {
5530
+ chunks.push(chunk);
5531
+ }
5532
+ const rawBody = Buffer.concat(chunks).toString();
5533
+ const timestamp = req.headers["x-slack-request-timestamp"];
5534
+ const signature = req.headers["x-slack-signature"];
5535
+ if (timestamp && signature && signingSecret) {
5536
+ const now = Math.floor(Date.now() / 1e3);
5537
+ if (Math.abs(now - parseInt(timestamp)) > 300) {
5538
+ res.writeHead(403);
5539
+ res.end("Request too old");
5540
+ return;
5541
+ }
5542
+ if (!verifySlackSignature(rawBody, timestamp, signature)) {
5543
+ res.writeHead(403);
5544
+ res.end("Invalid signature");
5545
+ return;
5546
+ }
5547
+ }
5548
+ const body = JSON.parse(rawBody);
5549
+ if (body.type === "url_verification") {
5550
+ res.writeHead(200, { "Content-Type": "application/json" });
5551
+ res.end(JSON.stringify({ challenge: body.challenge }));
5552
+ return;
5553
+ }
5554
+ res.writeHead(200);
5555
+ res.end("OK");
5556
+ if (body.type === "event_callback" && body.event?.type === "message") {
5557
+ await handleSlackMessage(body.event);
5558
+ }
5559
+ } catch (parseError) {
5560
+ console.error(`Parse error: ${parseError.message}`);
5561
+ if (!res.headersSent) {
5562
+ res.writeHead(400);
5563
+ res.end("Bad Request");
5564
+ }
5565
+ }
5566
+ });
5567
+ process.on("SIGINT", () => {
5568
+ console.log("\nStopping...");
5569
+ server.close();
5570
+ process.exit(0);
5571
+ });
5572
+ server.listen(port, () => {
5573
+ success(`Events API server listening on port ${port}`);
5574
+ info(`Events API URL: http://localhost:${port}${path_}`);
5575
+ console.log();
5576
+ info("Next steps:");
5577
+ dim(" 1. Expose this URL publicly (e.g., ngrok http " + port + ")");
5578
+ dim(" 2. Configure the Events API URL in your Slack app settings");
5579
+ dim(' 3. Subscribe to the "message.im" and "message.channels" events');
5580
+ dim(" Press Ctrl+C to stop.");
5581
+ });
5582
+ await new Promise(() => {
5583
+ });
5584
+ }
5585
+
4784
5586
  // src/index.ts
4785
5587
  import { readFileSync } from "fs";
4786
5588
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -4805,6 +5607,7 @@ registerChatCommand(program);
4805
5607
  registerSessionsCommand(program);
4806
5608
  registerThreadsCommand(program);
4807
5609
  registerSkillsCommand(program);
5610
+ registerSkillCommand(program);
4808
5611
  registerCronCommand(program);
4809
5612
  registerMcpCommand(program);
4810
5613
  registerFilesCommand(program);
@@ -4814,6 +5617,7 @@ registerVaultCommand(program);
4814
5617
  registerKeysCommand(program);
4815
5618
  registerChannelTelegramCommand(program);
4816
5619
  registerChannelWhatsAppCommand(program);
5620
+ registerChannelSlackCommand(program);
4817
5621
  registerStatusCommand(program);
4818
5622
  program.parse();
4819
5623
  //# sourceMappingURL=index.js.map