@agentforge-ai/cli 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ async function createProject(projectName, options) {
35
35
  ];
36
36
  let templateDir = "";
37
37
  for (const dir of searchDirs) {
38
- if (await fs.pathExists(path.join(dir, "package.json"))) {
38
+ if (await fs.pathExists(dir)) {
39
39
  templateDir = dir;
40
40
  break;
41
41
  }
@@ -126,6 +126,10 @@ Next steps:
126
126
  # Or chat with your agent from the CLI
127
127
  agentforge chat
128
128
 
129
+ # Install skills to extend agent capabilities
130
+ agentforge skills list --registry
131
+ agentforge skills install web-search
132
+
129
133
  # Check system status
130
134
  agentforge status
131
135
 
@@ -157,10 +161,26 @@ async function runProject(options) {
157
161
  \u{1F680} Starting AgentForge development server...
158
162
  `);
159
163
  console.log(` Convex dev server starting on port ${options.port}...`);
164
+ if (options.sandbox === "docker") {
165
+ console.log(` \u{1F433} Docker sandbox enabled \u2014 agent tools will execute in isolated containers`);
166
+ console.log(` Image: ${process.env["DOCKER_IMAGE"] ?? "node:22-slim (default)"}`);
167
+ console.log(` Host: ${process.env["DOCKER_HOST"] ?? "/var/run/docker.sock (default)"}`);
168
+ } else if (options.sandbox === "e2b") {
169
+ console.log(` \u2601\uFE0F E2B sandbox enabled \u2014 agent tools will execute in cloud sandboxes`);
170
+ } else if (options.sandbox === "none") {
171
+ console.log(` \u26A0\uFE0F No sandbox \u2014 agent tools will execute directly on the host (unsafe)`);
172
+ } else {
173
+ console.log(` \u{1F4E6} Local sandbox enabled (default)`);
174
+ }
175
+ const sandboxEnv = {
176
+ ...process.env,
177
+ AGENTFORGE_SANDBOX_PROVIDER: options.sandbox
178
+ };
160
179
  const convexProcess = spawn("npx", ["convex", "dev"], {
161
180
  cwd: projectDir,
162
181
  stdio: "inherit",
163
- shell: true
182
+ shell: true,
183
+ env: sandboxEnv
164
184
  });
165
185
  convexProcess.on("error", (err) => {
166
186
  console.error(`Failed to start Convex dev server: ${err.message}`);
@@ -296,8 +316,8 @@ var CloudClient = class {
296
316
  */
297
317
  getUrl(endpoint) {
298
318
  const base = this.baseUrl.replace(/\/$/, "");
299
- const path10 = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
300
- return `${base}${path10}`;
319
+ const path12 = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
320
+ return `${base}${path12}`;
301
321
  }
302
322
  /**
303
323
  * Get request headers with authentication
@@ -338,7 +358,7 @@ var CloudClient = class {
338
358
  const isJson = contentType?.includes("application/json");
339
359
  if (!response.ok) {
340
360
  const errorBody = isJson ? await response.json() : null;
341
- const message = errorBody?.message || errorBody?.error || `HTTP ${response.status}: ${response.statusText}`;
361
+ const message = errorBody?.message || `HTTP ${response.status}: ${response.statusText}`;
342
362
  throw new CloudClientError(
343
363
  message,
344
364
  errorBody?.code || `HTTP_${response.status}`,
@@ -857,6 +877,9 @@ function success(msg) {
857
877
  function error(msg) {
858
878
  console.error(`${colors.red}\u2716${colors.reset} ${msg}`);
859
879
  }
880
+ function warn(msg) {
881
+ console.log(`${colors.yellow}\u26A0${colors.reset} ${msg}`);
882
+ }
860
883
  function info(msg) {
861
884
  console.log(`${colors.blue}\u2139${colors.reset} ${msg}`);
862
885
  }
@@ -901,6 +924,10 @@ function details(data) {
901
924
  function formatDate(ts) {
902
925
  return new Date(ts).toLocaleString();
903
926
  }
927
+ function truncate(str, max) {
928
+ if (str.length <= max) return str;
929
+ return str.slice(0, max - 1) + "\u2026";
930
+ }
904
931
 
905
932
  // src/commands/agents.ts
906
933
  import readline from "readline";
@@ -957,8 +984,7 @@ function registerAgentsCommand(program2) {
957
984
  name,
958
985
  instructions,
959
986
  model,
960
- provider,
961
- isActive: true
987
+ provider
962
988
  }),
963
989
  "Failed to create agent"
964
990
  );
@@ -967,7 +993,7 @@ function registerAgentsCommand(program2) {
967
993
  agents.command("inspect").argument("<id>", "Agent ID").description("Show detailed agent information").action(async (id) => {
968
994
  const client = await createClient();
969
995
  const agent = await safeCall(
970
- () => client.query("agents:getByAgentId", { id }),
996
+ () => client.query("agents:get", { id }),
971
997
  "Failed to fetch agent"
972
998
  );
973
999
  if (!agent) {
@@ -995,7 +1021,7 @@ function registerAgentsCommand(program2) {
995
1021
  agents.command("edit").argument("<id>", "Agent ID").option("--name <name>", "New name").option("--model <model>", "New model").option("--instructions <text>", "New instructions").description("Edit an agent").action(async (id, opts) => {
996
1022
  const client = await createClient();
997
1023
  const agent = await safeCall(
998
- () => client.query("agents:getByAgentId", { id }),
1024
+ () => client.query("agents:get", { id }),
999
1025
  "Failed to fetch agent"
1000
1026
  );
1001
1027
  if (!agent) {
@@ -1026,7 +1052,7 @@ function registerAgentsCommand(program2) {
1026
1052
  return;
1027
1053
  }
1028
1054
  await safeCall(
1029
- () => client.mutation("agents:update", { _id: agent._id, ...updates }),
1055
+ () => client.mutation("agents:update", { id, ...updates }),
1030
1056
  "Failed to update agent"
1031
1057
  );
1032
1058
  success(`Agent "${id}" updated.`);
@@ -1041,7 +1067,7 @@ function registerAgentsCommand(program2) {
1041
1067
  }
1042
1068
  const client = await createClient();
1043
1069
  const agent = await safeCall(
1044
- () => client.query("agents:getByAgentId", { id }),
1070
+ () => client.query("agents:get", { id }),
1045
1071
  "Failed to fetch agent"
1046
1072
  );
1047
1073
  if (!agent) {
@@ -1049,29 +1075,29 @@ function registerAgentsCommand(program2) {
1049
1075
  process.exit(1);
1050
1076
  }
1051
1077
  await safeCall(
1052
- () => client.mutation("agents:remove", { _id: agent._id }),
1078
+ () => client.mutation("agents:remove", { id }),
1053
1079
  "Failed to delete agent"
1054
1080
  );
1055
1081
  success(`Agent "${id}" deleted.`);
1056
1082
  });
1057
1083
  agents.command("enable").argument("<id>", "Agent ID").description("Enable an agent").action(async (id) => {
1058
1084
  const client = await createClient();
1059
- const agent = await safeCall(() => client.query("agents:getByAgentId", { id }), "Failed to fetch agent");
1085
+ const agent = await safeCall(() => client.query("agents:get", { id }), "Failed to fetch agent");
1060
1086
  if (!agent) {
1061
1087
  error(`Agent "${id}" not found.`);
1062
1088
  process.exit(1);
1063
1089
  }
1064
- await safeCall(() => client.mutation("agents:update", { _id: agent._id, isActive: true }), "Failed");
1090
+ await safeCall(() => client.mutation("agents:update", { id, isActive: true }), "Failed");
1065
1091
  success(`Agent "${id}" enabled.`);
1066
1092
  });
1067
1093
  agents.command("disable").argument("<id>", "Agent ID").description("Disable an agent").action(async (id) => {
1068
1094
  const client = await createClient();
1069
- const agent = await safeCall(() => client.query("agents:getByAgentId", { id }), "Failed to fetch agent");
1095
+ const agent = await safeCall(() => client.query("agents:get", { id }), "Failed to fetch agent");
1070
1096
  if (!agent) {
1071
1097
  error(`Agent "${id}" not found.`);
1072
1098
  process.exit(1);
1073
1099
  }
1074
- await safeCall(() => client.mutation("agents:update", { _id: agent._id, isActive: false }), "Failed");
1100
+ await safeCall(() => client.mutation("agents:update", { id, isActive: false }), "Failed");
1075
1101
  success(`Agent "${id}" disabled.`);
1076
1102
  });
1077
1103
  }
@@ -1100,7 +1126,7 @@ function registerChatCommand(program2) {
1100
1126
  const idx = parseInt(choice) - 1;
1101
1127
  agentId = idx >= 0 && idx < agents.length ? agents[idx].id : choice;
1102
1128
  }
1103
- const agent = await safeCall(() => client.query("agents:getByAgentId", { id: agentId }), "Failed to fetch agent");
1129
+ const agent = await safeCall(() => client.query("agents:get", { id: agentId }), "Failed to fetch agent");
1104
1130
  if (!agent) {
1105
1131
  error(`Agent "${agentId}" not found.`);
1106
1132
  process.exit(1);
@@ -1111,7 +1137,7 @@ function registerChatCommand(program2) {
1111
1137
  dim(` Type "exit" or "quit" to end. "/new" for new thread. "/history" for messages.`);
1112
1138
  console.log();
1113
1139
  let threadId = await safeCall(
1114
- () => client.mutation("threads:create", { agentId: a.id, status: "active" }),
1140
+ () => client.mutation("threads:create", { agentId: a.id }),
1115
1141
  "Failed to create thread"
1116
1142
  );
1117
1143
  const history = [];
@@ -1128,7 +1154,7 @@ function registerChatCommand(program2) {
1128
1154
  process.exit(0);
1129
1155
  }
1130
1156
  if (input === "/new") {
1131
- threadId = await safeCall(() => client.mutation("threads:create", { agentId: a.id, status: "active" }), "Failed");
1157
+ threadId = await safeCall(() => client.mutation("threads:create", { agentId: a.id }), "Failed");
1132
1158
  history.length = 0;
1133
1159
  info("New thread started.");
1134
1160
  rl.prompt();
@@ -1145,14 +1171,14 @@ function registerChatCommand(program2) {
1145
1171
  return;
1146
1172
  }
1147
1173
  history.push({ role: "user", content: input });
1148
- await safeCall(() => client.mutation("messages:send", { threadId, role: "user", content: input }), "Failed to send");
1174
+ await safeCall(() => client.mutation("messages:add", { threadId, role: "user", content: input }), "Failed to send");
1149
1175
  process.stdout.write(`${colors.cyan}${a.name}${colors.reset} > `);
1150
1176
  try {
1151
1177
  const response = await safeCall(
1152
- () => client.action("mastraIntegration:generateResponse", { agentId: a.id, threadId, message: input }),
1178
+ () => client.action("mastraIntegration:executeAgent", { agentId: a.id, prompt: input, threadId }),
1153
1179
  "Failed to get response"
1154
1180
  );
1155
- const text = response?.text || response?.content || String(response);
1181
+ const text = response?.response || response?.text || response?.content || String(response);
1156
1182
  console.log(text);
1157
1183
  history.push({ role: "assistant", content: text });
1158
1184
  } catch {
@@ -1188,7 +1214,7 @@ function registerSessionsCommand(program2) {
1188
1214
  const filtered = opts.status ? items.filter((s) => s.status === opts.status) : items;
1189
1215
  table(filtered.map((s) => ({
1190
1216
  ID: s._id?.slice(-8) || "N/A",
1191
- Name: s.name || "Unnamed",
1217
+ Session: s.sessionId,
1192
1218
  Agent: s.agentId,
1193
1219
  Status: s.status,
1194
1220
  Started: formatDate(s.startedAt),
@@ -1197,18 +1223,18 @@ function registerSessionsCommand(program2) {
1197
1223
  });
1198
1224
  sessions.command("inspect").argument("<id>", "Session ID").description("Show session details").action(async (id) => {
1199
1225
  const client = await createClient();
1200
- const session = await safeCall(() => client.query("sessions:getById", { id }), "Failed to fetch session");
1226
+ const session = await safeCall(() => client.query("sessions:get", { sessionId: id }), "Failed to fetch session");
1201
1227
  if (!session) {
1202
1228
  error(`Session "${id}" not found.`);
1203
1229
  process.exit(1);
1204
1230
  }
1205
1231
  const s = session;
1206
- header(`Session: ${s.name || "Unnamed"}`);
1207
- details({ ID: s._id, Name: s.name, Agent: s.agentId, Status: s.status, Started: formatDate(s.startedAt), "Last Activity": formatDate(s.lastActivityAt) });
1232
+ header(`Session: ${s.sessionId}`);
1233
+ details({ ID: s._id, "Session ID": s.sessionId, Agent: s.agentId, Status: s.status, Started: formatDate(s.startedAt), "Last Activity": formatDate(s.lastActivityAt) });
1208
1234
  });
1209
1235
  sessions.command("end").argument("<id>", "Session ID").description("End an active session").action(async (id) => {
1210
1236
  const client = await createClient();
1211
- await safeCall(() => client.mutation("sessions:update", { _id: id, status: "ended" }), "Failed to end session");
1237
+ await safeCall(() => client.mutation("sessions:updateStatus", { sessionId: id, status: "completed" }), "Failed to end session");
1212
1238
  success(`Session "${id}" ended.`);
1213
1239
  });
1214
1240
  }
@@ -1232,13 +1258,13 @@ function registerThreadsCommand(program2) {
1232
1258
  ID: t._id?.slice(-8) || "N/A",
1233
1259
  Name: t.name || "Unnamed",
1234
1260
  Agent: t.agentId,
1235
- Status: t.status,
1261
+ Messages: t.metadata?.messageCount || "-",
1236
1262
  Created: formatDate(t.createdAt)
1237
1263
  })));
1238
1264
  });
1239
1265
  threads.command("inspect").argument("<id>", "Thread ID").description("Show thread messages").action(async (id) => {
1240
1266
  const client = await createClient();
1241
- const messages = await safeCall(() => client.query("messages:listByThread", { threadId: id }), "Failed to fetch messages");
1267
+ const messages = await safeCall(() => client.query("messages:list", { threadId: id }), "Failed to fetch messages");
1242
1268
  header(`Thread: ${id}`);
1243
1269
  const items = messages || [];
1244
1270
  if (items.length === 0) {
@@ -1253,7 +1279,7 @@ function registerThreadsCommand(program2) {
1253
1279
  });
1254
1280
  threads.command("delete").argument("<id>", "Thread ID").description("Delete a thread and its messages").action(async (id) => {
1255
1281
  const client = await createClient();
1256
- await safeCall(() => client.mutation("threads:remove", { _id: id }), "Failed to delete thread");
1282
+ await safeCall(() => client.mutation("threads:remove", { id }), "Failed to delete thread");
1257
1283
  success(`Thread "${id}" deleted.`);
1258
1284
  });
1259
1285
  }
@@ -1262,6 +1288,68 @@ function registerThreadsCommand(program2) {
1262
1288
  import fs6 from "fs-extra";
1263
1289
  import path6 from "path";
1264
1290
  import readline3 from "readline";
1291
+ import { execSync as execSync3 } from "child_process";
1292
+ var SKILLS_DIR_NAME = "skills";
1293
+ var SKILLS_LOCK_FILE = "skills.lock.json";
1294
+ var WORKSPACE_DIR_NAME = "workspace";
1295
+ var BUILTIN_REGISTRY = [
1296
+ {
1297
+ name: "web-search",
1298
+ description: "Search the web for information using DuckDuckGo. Provides structured search results with titles, URLs, and snippets.",
1299
+ version: "1.0.0",
1300
+ tags: ["web", "search", "research"],
1301
+ author: "AgentForge",
1302
+ source: "builtin"
1303
+ },
1304
+ {
1305
+ name: "file-manager",
1306
+ description: "Advanced file management operations including batch rename, find-and-replace across files, directory comparison, and file organization.",
1307
+ version: "1.0.0",
1308
+ tags: ["files", "utility", "management"],
1309
+ author: "AgentForge",
1310
+ source: "builtin"
1311
+ },
1312
+ {
1313
+ name: "code-review",
1314
+ description: "Systematic code review following best practices. Checks for bugs, security vulnerabilities, style issues, and suggests improvements.",
1315
+ version: "1.0.0",
1316
+ tags: ["development", "review", "quality"],
1317
+ author: "AgentForge",
1318
+ source: "builtin"
1319
+ },
1320
+ {
1321
+ name: "data-analyst",
1322
+ description: "Analyze CSV, JSON, and tabular data. Generate summaries, statistics, and insights from structured datasets.",
1323
+ version: "1.0.0",
1324
+ tags: ["data", "analysis", "csv", "json"],
1325
+ author: "AgentForge",
1326
+ source: "builtin"
1327
+ },
1328
+ {
1329
+ name: "api-tester",
1330
+ description: "Test REST APIs with structured request/response validation. Supports GET, POST, PUT, DELETE with headers and body.",
1331
+ version: "1.0.0",
1332
+ tags: ["api", "testing", "http", "rest"],
1333
+ author: "AgentForge",
1334
+ source: "builtin"
1335
+ },
1336
+ {
1337
+ name: "git-workflow",
1338
+ description: "Git workflow automation including conventional commits, branch management, PR descriptions, and changelog generation.",
1339
+ version: "1.0.0",
1340
+ tags: ["git", "workflow", "development"],
1341
+ author: "AgentForge",
1342
+ source: "builtin"
1343
+ },
1344
+ {
1345
+ name: "browser-automation",
1346
+ description: "Browser automation using Playwright. Navigate web pages, click elements, type text, extract content, take screenshots, and run JavaScript. Supports Docker sandbox mode for secure execution.",
1347
+ version: "1.0.0",
1348
+ tags: ["web", "browser", "automation", "scraping"],
1349
+ author: "AgentForge",
1350
+ source: "builtin"
1351
+ }
1352
+ ];
1265
1353
  function prompt2(q) {
1266
1354
  const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
1267
1355
  return new Promise((r) => rl.question(q, (a) => {
@@ -1269,247 +1357,1490 @@ function prompt2(q) {
1269
1357
  r(a.trim());
1270
1358
  }));
1271
1359
  }
1272
- function registerSkillsCommand(program2) {
1273
- const skills = program2.command("skills").description("Manage agent skills");
1274
- skills.command("list").option("--installed", "Show only installed skills").option("--json", "Output as JSON").description("List all skills").action(async (opts) => {
1275
- const client = await createClient();
1276
- const result = await safeCall(() => client.query("skills:list", {}), "Failed to list skills");
1277
- if (opts.json) {
1278
- console.log(JSON.stringify(result, null, 2));
1279
- return;
1360
+ function resolveSkillsDir() {
1361
+ const cwd = process.cwd();
1362
+ const workspaceSkillsDir = path6.join(cwd, WORKSPACE_DIR_NAME, SKILLS_DIR_NAME);
1363
+ if (fs6.existsSync(path6.join(cwd, WORKSPACE_DIR_NAME))) {
1364
+ return workspaceSkillsDir;
1365
+ }
1366
+ return path6.join(cwd, SKILLS_DIR_NAME);
1367
+ }
1368
+ function readSkillsLock(skillsDir) {
1369
+ const lockPath = path6.join(path6.dirname(skillsDir), SKILLS_LOCK_FILE);
1370
+ if (fs6.existsSync(lockPath)) {
1371
+ try {
1372
+ return JSON.parse(fs6.readFileSync(lockPath, "utf-8"));
1373
+ } catch {
1280
1374
  }
1281
- header("Skills");
1282
- const localSkills = [];
1283
- const skillsDir = path6.join(process.cwd(), "skills");
1284
- if (fs6.existsSync(skillsDir)) {
1285
- const dirs = fs6.readdirSync(skillsDir).filter((d) => fs6.statSync(path6.join(skillsDir, d)).isDirectory());
1286
- localSkills.push(...dirs);
1375
+ }
1376
+ return { version: 1, skills: {} };
1377
+ }
1378
+ function writeSkillsLock(skillsDir, lock) {
1379
+ const lockPath = path6.join(path6.dirname(skillsDir), SKILLS_LOCK_FILE);
1380
+ fs6.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + "\n");
1381
+ }
1382
+ function parseSkillMd(content) {
1383
+ try {
1384
+ const matter = __require("gray-matter");
1385
+ const parsed = matter(content);
1386
+ return {
1387
+ data: parsed.data,
1388
+ content: parsed.content
1389
+ };
1390
+ } catch {
1391
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
1392
+ if (!fmMatch) {
1393
+ return { data: { name: "", description: "", version: "1.0.0" }, content };
1287
1394
  }
1288
- const items = result || [];
1289
- if (items.length === 0 && localSkills.length === 0) {
1290
- info("No skills found. Install one with: agentforge skills install <name>");
1291
- info("Or create one with: agentforge skills create");
1292
- return;
1395
+ const frontmatter = fmMatch[1];
1396
+ const body = fmMatch[2];
1397
+ const data = {};
1398
+ for (const line of frontmatter.split("\n")) {
1399
+ const match = line.match(/^(\w+):\s*(.+)$/);
1400
+ if (match) {
1401
+ const value = match[2].trim();
1402
+ data[match[1]] = value;
1403
+ }
1293
1404
  }
1294
- if (localSkills.length > 0) {
1295
- dim(" Local Skills:");
1296
- localSkills.forEach((s) => {
1297
- const configPath = path6.join(skillsDir, s, "config.json");
1298
- let desc = "";
1299
- if (fs6.existsSync(configPath)) {
1300
- try {
1301
- desc = JSON.parse(fs6.readFileSync(configPath, "utf-8")).description || "";
1302
- } catch {
1405
+ return {
1406
+ data,
1407
+ content: body
1408
+ };
1409
+ }
1410
+ }
1411
+ function readSkillMetadata(skillDir) {
1412
+ const skillMdPath = path6.join(skillDir, "SKILL.md");
1413
+ if (!fs6.existsSync(skillMdPath)) return null;
1414
+ const content = fs6.readFileSync(skillMdPath, "utf-8");
1415
+ const { data } = parseSkillMd(content);
1416
+ return {
1417
+ name: data.name || path6.basename(skillDir),
1418
+ description: data.description || "",
1419
+ version: data.version || "1.0.0",
1420
+ tags: data.tags || [],
1421
+ author: data.author || "Unknown"
1422
+ };
1423
+ }
1424
+ function findInRegistry(name) {
1425
+ return BUILTIN_REGISTRY.find((s) => s.name === name);
1426
+ }
1427
+ function generateBuiltinSkill(name) {
1428
+ const generators = {
1429
+ "web-search": generateWebSearchSkill,
1430
+ "file-manager": generateFileManagerSkill,
1431
+ "code-review": generateCodeReviewSkill,
1432
+ "data-analyst": generateDataAnalystSkill,
1433
+ "api-tester": generateApiTesterSkill,
1434
+ "git-workflow": generateGitWorkflowSkill,
1435
+ "browser-automation": generateBrowserAutomationSkill
1436
+ };
1437
+ const generator = generators[name];
1438
+ return generator ? generator() : null;
1439
+ }
1440
+ function generateWebSearchSkill() {
1441
+ const files = /* @__PURE__ */ new Map();
1442
+ files.set("SKILL.md", `---
1443
+ name: web-search
1444
+ description: Search the web for information using DuckDuckGo and return structured results
1445
+ version: 1.0.0
1446
+ tags:
1447
+ - web
1448
+ - search
1449
+ - research
1450
+ ---
1451
+
1452
+ # Web Search
1453
+
1454
+ You are a web research assistant. When the user asks you to search for information:
1455
+
1456
+ 1. Use the workspace sandbox to execute the search script at \`scripts/search.ts\`
1457
+ 2. Parse the results and present them in a clear, organized format
1458
+ 3. Include source URLs for all information
1459
+ 4. Summarize key findings at the top
1460
+
1461
+ ## How to Search
1462
+
1463
+ Run the search script with the user's query:
1464
+
1465
+ \`\`\`bash
1466
+ npx tsx scripts/search.ts "user query here"
1467
+ \`\`\`
1468
+
1469
+ The script returns JSON with structured results including title, URL, and snippet.
1470
+
1471
+ ## Result Format
1472
+
1473
+ Present results as:
1474
+ - **Title** \u2014 Brief description ([Source](url))
1475
+ - Group related results together
1476
+ - Highlight the most relevant findings first
1477
+
1478
+ ## Guidelines
1479
+
1480
+ - Always cite sources with URLs
1481
+ - If results are insufficient, suggest refined queries
1482
+ - Cross-reference multiple results for accuracy
1483
+ - Note when information may be outdated
1484
+ `);
1485
+ files.set("scripts/search.ts", `#!/usr/bin/env npx tsx
1486
+ /**
1487
+ * Web Search Script \u2014 Uses DuckDuckGo Instant Answer API
1488
+ *
1489
+ * Usage: npx tsx scripts/search.ts "your query"
1490
+ */
1491
+
1492
+ const query = process.argv[2];
1493
+ if (!query) {
1494
+ console.error('Usage: npx tsx scripts/search.ts "query"');
1495
+ process.exit(1);
1496
+ }
1497
+
1498
+ interface SearchResult {
1499
+ title: string;
1500
+ url: string;
1501
+ snippet: string;
1502
+ }
1503
+
1504
+ async function search(q: string): Promise<SearchResult[]> {
1505
+ const url = \`https://api.duckduckgo.com/?q=\${encodeURIComponent(q)}&format=json&no_redirect=1&no_html=1\`;
1506
+ const res = await fetch(url);
1507
+ const data = await res.json();
1508
+
1509
+ const results: SearchResult[] = [];
1510
+
1511
+ // Abstract (main answer)
1512
+ if (data.Abstract) {
1513
+ results.push({
1514
+ title: data.Heading || q,
1515
+ url: data.AbstractURL || '',
1516
+ snippet: data.Abstract,
1517
+ });
1518
+ }
1519
+
1520
+ // Related topics
1521
+ if (data.RelatedTopics) {
1522
+ for (const topic of data.RelatedTopics) {
1523
+ if (topic.Text && topic.FirstURL) {
1524
+ results.push({
1525
+ title: topic.Text.split(' - ')[0] || topic.Text.slice(0, 80),
1526
+ url: topic.FirstURL,
1527
+ snippet: topic.Text,
1528
+ });
1529
+ }
1530
+ // Subtopics
1531
+ if (topic.Topics) {
1532
+ for (const sub of topic.Topics) {
1533
+ if (sub.Text && sub.FirstURL) {
1534
+ results.push({
1535
+ title: sub.Text.split(' - ')[0] || sub.Text.slice(0, 80),
1536
+ url: sub.FirstURL,
1537
+ snippet: sub.Text,
1538
+ });
1303
1539
  }
1304
1540
  }
1305
- console.log(` ${colors.cyan}\u25CF${colors.reset} ${s} ${colors.dim}${desc}${colors.reset}`);
1306
- });
1307
- console.log();
1308
- }
1309
- if (items.length > 0) {
1310
- const filtered = opts.installed ? items.filter((s) => s.isInstalled) : items;
1311
- table(filtered.map((s) => ({
1312
- Name: s.name,
1313
- Category: s.category,
1314
- Version: s.version,
1315
- Installed: s.isInstalled ? "\u2714" : "\u2716",
1316
- Agent: s.agentId || "all"
1317
- })));
1541
+ }
1318
1542
  }
1543
+ }
1544
+
1545
+ return results.slice(0, 10);
1546
+ }
1547
+
1548
+ search(query)
1549
+ .then((results) => console.log(JSON.stringify({ query, results, count: results.length }, null, 2)))
1550
+ .catch((err) => {
1551
+ console.error(JSON.stringify({ error: err.message }));
1552
+ process.exit(1);
1319
1553
  });
1320
- skills.command("create").description("Create a new skill (interactive)").option("--name <name>", "Skill name").option("--description <desc>", "Skill description").option("--category <cat>", "Category (utility, web, file, data, integration, ai, custom)").action(async (opts) => {
1321
- const name = opts.name || await prompt2("Skill name (kebab-case): ");
1322
- const description = opts.description || await prompt2("Description: ");
1323
- const category = opts.category || await prompt2("Category (utility/web/file/data/integration/ai/custom): ") || "custom";
1324
- if (!name) {
1325
- error("Skill name is required.");
1326
- process.exit(1);
1327
- }
1328
- const skillDir = path6.join(process.cwd(), "skills", name);
1329
- if (fs6.existsSync(skillDir)) {
1330
- error(`Skill "${name}" already exists at ${skillDir}`);
1331
- process.exit(1);
1332
- }
1333
- fs6.mkdirSync(skillDir, { recursive: true });
1334
- fs6.writeFileSync(path6.join(skillDir, "config.json"), JSON.stringify({
1335
- name,
1336
- version: "1.0.0",
1337
- description,
1338
- category,
1339
- author: "User",
1340
- tools: [name],
1341
- dependencies: [],
1342
- agentInstructions: `You have access to the ${name} skill. ${description}`
1343
- }, null, 2));
1344
- fs6.writeFileSync(path6.join(skillDir, "index.ts"), `import { z } from 'zod';
1554
+ `);
1555
+ files.set("references/search-tips.md", `# Search Tips
1556
+
1557
+ ## Effective Queries
1558
+ - Use specific keywords rather than full sentences
1559
+ - Include domain-specific terms for technical searches
1560
+ - Use quotes for exact phrase matching (in the query string)
1561
+ - Add "site:example.com" to limit to specific domains
1562
+
1563
+ ## Result Evaluation
1564
+ - Check the date of sources when available
1565
+ - Cross-reference claims across multiple results
1566
+ - Prefer authoritative sources (.edu, .gov, established publications)
1567
+ - Note when results are from forums vs. official documentation
1568
+ `);
1569
+ return files;
1570
+ }
1571
+ function generateFileManagerSkill() {
1572
+ const files = /* @__PURE__ */ new Map();
1573
+ files.set("SKILL.md", `---
1574
+ name: file-manager
1575
+ description: Advanced file management operations including batch rename, find-and-replace, and directory organization
1576
+ version: 1.0.0
1577
+ tags:
1578
+ - files
1579
+ - utility
1580
+ - management
1581
+ ---
1582
+
1583
+ # File Manager
1584
+
1585
+ You are a file management assistant. Help users organize, search, and manipulate files in the workspace.
1586
+
1587
+ ## Capabilities
1588
+
1589
+ 1. **List & Search** \u2014 Find files by name, extension, or content
1590
+ 2. **Batch Rename** \u2014 Rename multiple files using patterns
1591
+ 3. **Find & Replace** \u2014 Search and replace text across files
1592
+ 4. **Organize** \u2014 Sort files into directories by type, date, or custom rules
1593
+ 5. **Compare** \u2014 Show differences between files or directories
1594
+
1595
+ ## How to Use
1596
+
1597
+ Use the workspace filesystem tools to perform operations:
1598
+
1599
+ - \`mastra_workspace_list_files\` \u2014 List directory contents as a tree
1600
+ - \`mastra_workspace_read_file\` \u2014 Read file contents
1601
+ - \`mastra_workspace_write_file\` \u2014 Create or overwrite files
1602
+ - \`mastra_workspace_edit_file\` \u2014 Find and replace in files
1603
+ - \`mastra_workspace_delete\` \u2014 Remove files or directories
1604
+ - \`mastra_workspace_file_stat\` \u2014 Get file metadata (size, dates)
1605
+ - \`mastra_workspace_mkdir\` \u2014 Create directories
1345
1606
 
1607
+ For complex operations, use \`mastra_workspace_execute_command\` with the scripts in this skill.
1608
+
1609
+ ## Scripts
1610
+
1611
+ - \`scripts/batch-rename.ts\` \u2014 Batch rename files with pattern support
1612
+ - \`scripts/find-replace.ts\` \u2014 Find and replace across multiple files
1613
+ - \`scripts/organize.ts\` \u2014 Organize files by extension into directories
1614
+
1615
+ ## Guidelines
1616
+
1617
+ - Always confirm destructive operations (delete, overwrite) with the user
1618
+ - Show a preview of changes before executing batch operations
1619
+ - Create backups when performing bulk modifications
1620
+ - Report the number of files affected after each operation
1621
+ `);
1622
+ files.set("scripts/batch-rename.ts", `#!/usr/bin/env npx tsx
1346
1623
  /**
1347
- * ${name} \u2014 AgentForge Skill
1348
- * ${description}
1624
+ * Batch Rename Script
1625
+ *
1626
+ * Usage: npx tsx scripts/batch-rename.ts <directory> <find-pattern> <replace-pattern>
1627
+ * Example: npx tsx scripts/batch-rename.ts ./docs "report-" "2026-report-"
1349
1628
  */
1350
- export const tools = [
1351
- {
1352
- name: '${name}',
1353
- description: '${description}',
1354
- inputSchema: z.object({
1355
- input: z.string().describe('Input for ${name}'),
1356
- }),
1357
- outputSchema: z.object({
1358
- result: z.string(),
1359
- success: z.boolean(),
1360
- }),
1361
- handler: async (params: { input: string }) => {
1362
- // TODO: Implement your skill logic here
1363
- return { result: \`Processed: \${params.input}\`, success: true };
1364
- },
1365
- },
1366
- ];
1367
1629
 
1368
- export default { tools };
1369
- `);
1370
- fs6.writeFileSync(path6.join(skillDir, "SKILL.md"), `# ${name}
1630
+ import { readdirSync, renameSync } from 'fs';
1631
+ import { join, basename } from 'path';
1371
1632
 
1372
- ${description}
1633
+ const [dir, findPattern, replacePattern] = process.argv.slice(2);
1634
+ if (!dir || !findPattern || !replacePattern) {
1635
+ console.error('Usage: npx tsx scripts/batch-rename.ts <dir> <find> <replace>');
1636
+ process.exit(1);
1637
+ }
1638
+
1639
+ const files = readdirSync(dir);
1640
+ const renames: Array<{ from: string; to: string }> = [];
1373
1641
 
1374
- ## Usage
1642
+ for (const file of files) {
1643
+ if (file.includes(findPattern)) {
1644
+ const newName = file.replace(findPattern, replacePattern);
1645
+ renames.push({ from: file, to: newName });
1646
+ }
1647
+ }
1375
1648
 
1376
- Ask your agent: "Use the ${name} tool to [your request]"
1649
+ if (renames.length === 0) {
1650
+ console.log(JSON.stringify({ message: 'No files matched the pattern', count: 0 }));
1651
+ process.exit(0);
1652
+ }
1377
1653
 
1378
- ## Configuration
1654
+ for (const { from, to } of renames) {
1655
+ renameSync(join(dir, from), join(dir, to));
1656
+ }
1379
1657
 
1380
- Edit \`skills/${name}/config.json\` to customize.
1658
+ console.log(JSON.stringify({ renames, count: renames.length }));
1381
1659
  `);
1382
- success(`Skill "${name}" created at skills/${name}/`);
1383
- info("Files created: index.ts, config.json, SKILL.md");
1384
- info(`Edit skills/${name}/index.ts to implement your tool logic.`);
1385
- });
1386
- skills.command("install").argument("<name>", "Skill name to install").description("Install a skill").action(async (name) => {
1387
- const client = await createClient();
1388
- await safeCall(
1389
- () => client.mutation("skills:create", {
1390
- name,
1391
- category: "custom",
1392
- version: "1.0.0",
1393
- isInstalled: true
1394
- }),
1395
- "Failed to install skill"
1396
- );
1397
- success(`Skill "${name}" installed.`);
1398
- });
1399
- skills.command("remove").argument("<name>", "Skill name to remove").description("Remove a skill").action(async (name) => {
1400
- const skillDir = path6.join(process.cwd(), "skills", name);
1401
- if (fs6.existsSync(skillDir)) {
1402
- const confirm = await prompt2(`Remove skill "${name}" and delete files? (y/N): `);
1403
- if (confirm.toLowerCase() === "y") {
1404
- fs6.removeSync(skillDir);
1405
- success(`Skill "${name}" removed from disk.`);
1406
- }
1407
- }
1408
- const client = await createClient();
1409
- try {
1410
- const skills2 = await client.query("skills:list", {});
1411
- const skill = skills2.find((s) => s.name === name);
1412
- if (skill) {
1413
- await client.mutation("skills:remove", { _id: skill._id });
1414
- success(`Skill "${name}" removed from database.`);
1660
+ files.set("scripts/find-replace.ts", `#!/usr/bin/env npx tsx
1661
+ /**
1662
+ * Find and Replace Script
1663
+ *
1664
+ * Usage: npx tsx scripts/find-replace.ts <directory> <find-text> <replace-text> [--ext .ts,.js]
1665
+ */
1666
+
1667
+ import { readdirSync, readFileSync, writeFileSync, statSync } from 'fs';
1668
+ import { join, extname } from 'path';
1669
+
1670
+ const args = process.argv.slice(2);
1671
+ const dir = args[0];
1672
+ const findText = args[1];
1673
+ const replaceText = args[2];
1674
+ const extFilter = args.includes('--ext') ? args[args.indexOf('--ext') + 1]?.split(',') : null;
1675
+
1676
+ if (!dir || !findText || replaceText === undefined) {
1677
+ console.error('Usage: npx tsx scripts/find-replace.ts <dir> <find> <replace> [--ext .ts,.js]');
1678
+ process.exit(1);
1679
+ }
1680
+
1681
+ interface Change { file: string; count: number; }
1682
+ const changes: Change[] = [];
1683
+
1684
+ function processDir(dirPath: string) {
1685
+ for (const entry of readdirSync(dirPath)) {
1686
+ const fullPath = join(dirPath, entry);
1687
+ const stat = statSync(fullPath);
1688
+ if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
1689
+ processDir(fullPath);
1690
+ } else if (stat.isFile()) {
1691
+ if (extFilter && !extFilter.includes(extname(entry))) continue;
1692
+ const content = readFileSync(fullPath, 'utf-8');
1693
+ const count = (content.match(new RegExp(findText.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&'), 'g')) || []).length;
1694
+ if (count > 0) {
1695
+ const newContent = content.replaceAll(findText, replaceText);
1696
+ writeFileSync(fullPath, newContent);
1697
+ changes.push({ file: fullPath, count });
1415
1698
  }
1416
- } catch {
1417
- }
1418
- });
1419
- skills.command("search").argument("<query>", "Search query").description("Search available skills").action(async (query) => {
1420
- header("Skill Search Results");
1421
- const examples = [
1422
- { name: "web-search", desc: "Search the web for information", cat: "web" },
1423
- { name: "calculator", desc: "Evaluate mathematical expressions", cat: "utility" },
1424
- { name: "file-reader", desc: "Read file contents", cat: "file" },
1425
- { name: "http-request", desc: "Make HTTP requests", cat: "web" },
1426
- { name: "json-transformer", desc: "Transform JSON data", cat: "data" },
1427
- { name: "text-summarizer", desc: "Summarize text into key points", cat: "ai" },
1428
- { name: "csv-parser", desc: "Parse CSV into structured JSON", cat: "data" }
1429
- ];
1430
- const q = query.toLowerCase();
1431
- const matches = examples.filter((e) => e.name.includes(q) || e.desc.toLowerCase().includes(q) || e.cat.includes(q));
1432
- if (matches.length === 0) {
1433
- info(`No skills matching "${query}". Try: agentforge skills create`);
1434
- return;
1435
1699
  }
1436
- table(matches.map((e) => ({ Name: e.name, Description: e.desc, Category: e.cat })));
1437
- info("Install with: agentforge skills install <name>");
1438
- info("Or see examples: check skills/skill-creator/SKILL.md");
1439
- });
1700
+ }
1440
1701
  }
1441
1702
 
1442
- // src/commands/cron.ts
1443
- import readline4 from "readline";
1444
- function prompt3(q) {
1445
- const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
1446
- return new Promise((r) => rl.question(q, (a) => {
1447
- rl.close();
1448
- r(a.trim());
1449
- }));
1703
+ processDir(dir);
1704
+ console.log(JSON.stringify({ changes, totalFiles: changes.length, totalReplacements: changes.reduce((s, c) => s + c.count, 0) }));
1705
+ `);
1706
+ return files;
1450
1707
  }
1451
- function registerCronCommand(program2) {
1452
- const cron = program2.command("cron").description("Manage cron jobs");
1453
- cron.command("list").option("--json", "Output as JSON").description("List all cron jobs").action(async (opts) => {
1454
- const client = await createClient();
1455
- const result = await safeCall(() => client.query("cronJobs:list", {}), "Failed to list cron jobs");
1456
- if (opts.json) {
1457
- console.log(JSON.stringify(result, null, 2));
1458
- return;
1708
+ function generateCodeReviewSkill() {
1709
+ const files = /* @__PURE__ */ new Map();
1710
+ files.set("SKILL.md", `---
1711
+ name: code-review
1712
+ description: Systematic code review following best practices for quality, security, and style
1713
+ version: 1.0.0
1714
+ tags:
1715
+ - development
1716
+ - review
1717
+ - quality
1718
+ ---
1719
+
1720
+ # Code Review
1721
+
1722
+ You are a code reviewer. When reviewing code, follow this systematic process:
1723
+
1724
+ ## Review Process
1725
+
1726
+ 1. **Critical Issues** \u2014 Security vulnerabilities, memory leaks, logic bugs, missing error handling
1727
+ 2. **Code Quality** \u2014 Functions over 50 lines, code duplication, confusing names, missing types
1728
+ 3. **Style Guide** \u2014 Check references/style-guide.md for naming and organization conventions
1729
+ 4. **Performance** \u2014 Unnecessary re-renders, N+1 queries, missing memoization, large bundle imports
1730
+ 5. **Testing** \u2014 Missing test coverage, edge cases not handled, brittle assertions
1731
+
1732
+ ## Feedback Format
1733
+
1734
+ Provide feedback in this structure:
1735
+
1736
+ **Summary**: One sentence overview of the code quality
1737
+
1738
+ **Critical Issues**: List with file paths and line numbers
1739
+ - \`file.ts:42\` \u2014 Description of the issue
1740
+
1741
+ **Suggestions**: Improvements that would help
1742
+ - Description of suggestion with code example
1743
+
1744
+ **Positive Notes**: What the code does well
1745
+
1746
+ ## What to Look Out For
1747
+
1748
+ - Unused variables and imports
1749
+ - Missing error handling (try/catch, null checks)
1750
+ - Security vulnerabilities (SQL injection, XSS, secrets in code)
1751
+ - Performance issues (unnecessary loops, missing indexes)
1752
+ - TypeScript: any types, missing return types, loose generics
1753
+ - React: missing keys, stale closures, missing deps in useEffect
1754
+
1755
+ ## Scripts
1756
+
1757
+ - \`scripts/lint.ts\` \u2014 Run linting checks on a file or directory
1758
+ `);
1759
+ files.set("references/style-guide.md", `# Code Style Guide
1760
+
1761
+ ## TypeScript Conventions
1762
+ - Use \`const\` by default, \`let\` only when reassignment is needed
1763
+ - Prefer \`interface\` over \`type\` for object shapes
1764
+ - Always specify return types for exported functions
1765
+ - Use \`unknown\` instead of \`any\` where possible
1766
+ - Prefer \`readonly\` for properties that shouldn't change
1767
+
1768
+ ## Naming Conventions
1769
+ - **Files**: kebab-case (\`my-component.tsx\`)
1770
+ - **Components**: PascalCase (\`MyComponent\`)
1771
+ - **Functions**: camelCase (\`getUserById\`)
1772
+ - **Constants**: UPPER_SNAKE_CASE (\`MAX_RETRIES\`)
1773
+ - **Types/Interfaces**: PascalCase (\`UserProfile\`)
1774
+
1775
+ ## File Organization
1776
+ - One component per file
1777
+ - Co-locate tests with source files (\`*.test.ts\`)
1778
+ - Group by feature, not by type
1779
+ - Keep files under 300 lines
1780
+
1781
+ ## Error Handling
1782
+ - Always handle promise rejections
1783
+ - Use typed errors with error codes
1784
+ - Log errors with context (user ID, request ID)
1785
+ - Never swallow errors silently
1786
+ `);
1787
+ files.set("scripts/lint.ts", `#!/usr/bin/env npx tsx
1788
+ /**
1789
+ * Simple Lint Script \u2014 Checks for common issues
1790
+ *
1791
+ * Usage: npx tsx scripts/lint.ts <file-or-directory>
1792
+ */
1793
+
1794
+ import { readFileSync, readdirSync, statSync } from 'fs';
1795
+ import { join, extname } from 'path';
1796
+
1797
+ const target = process.argv[2];
1798
+ if (!target) {
1799
+ console.error('Usage: npx tsx scripts/lint.ts <file-or-directory>');
1800
+ process.exit(1);
1801
+ }
1802
+
1803
+ interface LintIssue {
1804
+ file: string;
1805
+ line: number;
1806
+ severity: 'error' | 'warning';
1807
+ message: string;
1808
+ }
1809
+
1810
+ const issues: LintIssue[] = [];
1811
+
1812
+ function lintFile(filePath: string) {
1813
+ const content = readFileSync(filePath, 'utf-8');
1814
+ const lines = content.split('\\n');
1815
+
1816
+ lines.forEach((line, i) => {
1817
+ const lineNum = i + 1;
1818
+ // Check for console.log
1819
+ if (line.includes('console.log') && !filePath.includes('test')) {
1820
+ issues.push({ file: filePath, line: lineNum, severity: 'warning', message: 'console.log found \u2014 remove before production' });
1459
1821
  }
1460
- header("Cron Jobs");
1461
- const items = result || [];
1462
- if (items.length === 0) {
1463
- info("No cron jobs. Create one with: agentforge cron create");
1464
- return;
1822
+ // Check for debugger
1823
+ if (line.trim() === 'debugger' || line.trim() === 'debugger;') {
1824
+ issues.push({ file: filePath, line: lineNum, severity: 'error', message: 'debugger statement found' });
1465
1825
  }
1466
- table(items.map((c) => ({
1467
- ID: c._id?.slice(-8) || "N/A",
1468
- Name: c.name,
1469
- Schedule: c.schedule,
1470
- Agent: c.agentId,
1471
- Enabled: c.isEnabled ? "\u2714" : "\u2716",
1472
- "Last Run": c.lastRunAt ? formatDate(c.lastRunAt) : "Never",
1473
- "Next Run": c.nextRunAt ? formatDate(c.nextRunAt) : "N/A"
1474
- })));
1475
- });
1476
- cron.command("create").description("Create a new cron job (interactive)").option("--name <name>", "Job name").option("--schedule <cron>", "Cron expression").option("--agent <id>", "Agent ID").option("--action <action>", "Action to execute").action(async (opts) => {
1477
- const name = opts.name || await prompt3("Job name: ");
1478
- const schedule = opts.schedule || await prompt3('Cron schedule (e.g., "0 */5 * * * *" for every 5 min): ');
1479
- const agentId = opts.agent || await prompt3("Agent ID: ");
1480
- const action = opts.action || await prompt3("Action (message to send to agent): ");
1481
- if (!name || !schedule || !agentId || !action) {
1482
- error("All fields are required.");
1483
- process.exit(1);
1826
+ // Check for any type
1827
+ if (line.includes(': any') || line.includes('<any>')) {
1828
+ issues.push({ file: filePath, line: lineNum, severity: 'warning', message: 'Use of "any" type \u2014 prefer "unknown" or specific type' });
1829
+ }
1830
+ // Check for var usage
1831
+ if (/\\bvar\\s+/.test(line)) {
1832
+ issues.push({ file: filePath, line: lineNum, severity: 'error', message: 'Use "const" or "let" instead of "var"' });
1833
+ }
1834
+ // Check for TODO/FIXME
1835
+ if (/\\/\\/\\s*(TODO|FIXME|HACK|XXX)/.test(line)) {
1836
+ issues.push({ file: filePath, line: lineNum, severity: 'warning', message: 'Unresolved TODO/FIXME comment' });
1484
1837
  }
1485
- const client = await createClient();
1486
- await safeCall(
1487
- () => client.mutation("cronJobs:create", { name, schedule, agentId, action, isEnabled: true }),
1488
- "Failed to create cron job"
1489
- );
1490
- success(`Cron job "${name}" created.`);
1491
- });
1492
- cron.command("delete").argument("<id>", "Cron job ID").description("Delete a cron job").action(async (id) => {
1493
- const client = await createClient();
1494
- await safeCall(() => client.mutation("cronJobs:remove", { _id: id }), "Failed to delete");
1495
- success(`Cron job "${id}" deleted.`);
1496
- });
1497
- cron.command("enable").argument("<id>", "Cron job ID").description("Enable a cron job").action(async (id) => {
1498
- const client = await createClient();
1499
- await safeCall(() => client.mutation("cronJobs:update", { _id: id, isEnabled: true }), "Failed");
1500
- success(`Cron job "${id}" enabled.`);
1501
- });
1502
- cron.command("disable").argument("<id>", "Cron job ID").description("Disable a cron job").action(async (id) => {
1503
- const client = await createClient();
1504
- await safeCall(() => client.mutation("cronJobs:update", { _id: id, isEnabled: false }), "Failed");
1505
- success(`Cron job "${id}" disabled.`);
1506
1838
  });
1507
1839
  }
1508
1840
 
1509
- // src/commands/mcp.ts
1510
- import readline5 from "readline";
1511
- function prompt4(q) {
1512
- const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
1841
+ function processPath(p: string) {
1842
+ const stat = statSync(p);
1843
+ if (stat.isFile() && ['.ts', '.tsx', '.js', '.jsx'].includes(extname(p))) {
1844
+ lintFile(p);
1845
+ } else if (stat.isDirectory()) {
1846
+ for (const entry of readdirSync(p)) {
1847
+ if (!entry.startsWith('.') && entry !== 'node_modules' && entry !== 'dist') {
1848
+ processPath(join(p, entry));
1849
+ }
1850
+ }
1851
+ }
1852
+ }
1853
+
1854
+ processPath(target);
1855
+ console.log(JSON.stringify({ issues, total: issues.length, errors: issues.filter(i => i.severity === 'error').length, warnings: issues.filter(i => i.severity === 'warning').length }));
1856
+ `);
1857
+ return files;
1858
+ }
1859
+ function generateDataAnalystSkill() {
1860
+ const files = /* @__PURE__ */ new Map();
1861
+ files.set("SKILL.md", `---
1862
+ name: data-analyst
1863
+ description: Analyze CSV, JSON, and tabular data to generate summaries, statistics, and insights
1864
+ version: 1.0.0
1865
+ tags:
1866
+ - data
1867
+ - analysis
1868
+ - csv
1869
+ - json
1870
+ ---
1871
+
1872
+ # Data Analyst
1873
+
1874
+ You are a data analysis assistant. Help users understand and extract insights from structured data.
1875
+
1876
+ ## Capabilities
1877
+
1878
+ 1. **Load Data** \u2014 Read CSV, JSON, and TSV files from the workspace
1879
+ 2. **Summarize** \u2014 Generate column statistics (min, max, mean, median, mode)
1880
+ 3. **Filter & Query** \u2014 Filter rows by conditions, select columns
1881
+ 4. **Aggregate** \u2014 Group by columns and compute aggregates
1882
+ 5. **Detect Anomalies** \u2014 Find outliers and missing values
1883
+
1884
+ ## How to Analyze
1885
+
1886
+ 1. First, read the data file using workspace filesystem tools
1887
+ 2. Use \`scripts/analyze.ts\` for statistical analysis
1888
+ 3. Present findings in a clear table format
1889
+ 4. Suggest follow-up analyses based on initial findings
1890
+
1891
+ ## Scripts
1892
+
1893
+ - \`scripts/analyze.ts\` \u2014 Compute statistics on CSV/JSON data
1894
+
1895
+ ## Output Format
1896
+
1897
+ Present analysis results as:
1898
+ - **Dataset Overview**: Row count, column count, column types
1899
+ - **Key Statistics**: Per-column min, max, mean, median
1900
+ - **Missing Data**: Columns with null/empty values and their percentages
1901
+ - **Insights**: Notable patterns, correlations, or anomalies
1902
+
1903
+ ## Guidelines
1904
+
1905
+ - Always show a sample of the data (first 5 rows) before analysis
1906
+ - Handle missing values gracefully \u2014 report them, don't crash
1907
+ - Use appropriate precision for numbers (2 decimal places for percentages)
1908
+ - Suggest visualizations when patterns would be clearer in chart form
1909
+ `);
1910
+ files.set("scripts/analyze.ts", `#!/usr/bin/env npx tsx
1911
+ /**
1912
+ * Data Analysis Script \u2014 Basic statistics for CSV/JSON data
1913
+ *
1914
+ * Usage: npx tsx scripts/analyze.ts <file.csv|file.json>
1915
+ */
1916
+
1917
+ import { readFileSync } from 'fs';
1918
+ import { extname } from 'path';
1919
+
1920
+ const filePath = process.argv[2];
1921
+ if (!filePath) {
1922
+ console.error('Usage: npx tsx scripts/analyze.ts <file.csv|file.json>');
1923
+ process.exit(1);
1924
+ }
1925
+
1926
+ function parseCSV(content: string): Record<string, string>[] {
1927
+ const lines = content.trim().split('\\n');
1928
+ const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
1929
+ return lines.slice(1).map(line => {
1930
+ const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
1931
+ return Object.fromEntries(headers.map((h, i) => [h, values[i] ?? '']));
1932
+ });
1933
+ }
1934
+
1935
+ const content = readFileSync(filePath, 'utf-8');
1936
+ const ext = extname(filePath).toLowerCase();
1937
+ let data: Record<string, string>[];
1938
+
1939
+ if (ext === '.json') {
1940
+ const parsed = JSON.parse(content);
1941
+ data = Array.isArray(parsed) ? parsed : [parsed];
1942
+ } else {
1943
+ data = parseCSV(content);
1944
+ }
1945
+
1946
+ const columns = Object.keys(data[0] || {});
1947
+ const stats: Record<string, any> = {};
1948
+
1949
+ for (const col of columns) {
1950
+ const values = data.map(row => row[col]).filter(v => v !== '' && v !== null && v !== undefined);
1951
+ const numValues = values.map(Number).filter(n => !isNaN(n));
1952
+
1953
+ stats[col] = {
1954
+ total: data.length,
1955
+ nonNull: values.length,
1956
+ missing: data.length - values.length,
1957
+ missingPct: ((data.length - values.length) / data.length * 100).toFixed(1) + '%',
1958
+ unique: new Set(values).size,
1959
+ };
1960
+
1961
+ if (numValues.length > 0) {
1962
+ numValues.sort((a, b) => a - b);
1963
+ stats[col].type = 'numeric';
1964
+ stats[col].min = Math.min(...numValues);
1965
+ stats[col].max = Math.max(...numValues);
1966
+ stats[col].mean = +(numValues.reduce((s, n) => s + n, 0) / numValues.length).toFixed(2);
1967
+ stats[col].median = numValues.length % 2 === 0
1968
+ ? +((numValues[numValues.length / 2 - 1] + numValues[numValues.length / 2]) / 2).toFixed(2)
1969
+ : numValues[Math.floor(numValues.length / 2)];
1970
+ } else {
1971
+ stats[col].type = 'string';
1972
+ stats[col].sample = values.slice(0, 3);
1973
+ }
1974
+ }
1975
+
1976
+ console.log(JSON.stringify({
1977
+ rows: data.length,
1978
+ columns: columns.length,
1979
+ columnNames: columns,
1980
+ stats,
1981
+ sample: data.slice(0, 5),
1982
+ }, null, 2));
1983
+ `);
1984
+ return files;
1985
+ }
1986
+ function generateApiTesterSkill() {
1987
+ const files = /* @__PURE__ */ new Map();
1988
+ files.set("SKILL.md", `---
1989
+ name: api-tester
1990
+ description: Test REST APIs with structured request/response validation
1991
+ version: 1.0.0
1992
+ tags:
1993
+ - api
1994
+ - testing
1995
+ - http
1996
+ - rest
1997
+ ---
1998
+
1999
+ # API Tester
2000
+
2001
+ You are an API testing assistant. Help users test and validate REST API endpoints.
2002
+
2003
+ ## Capabilities
2004
+
2005
+ 1. **Send Requests** \u2014 GET, POST, PUT, PATCH, DELETE with headers and body
2006
+ 2. **Validate Responses** \u2014 Check status codes, response structure, and timing
2007
+ 3. **Chain Requests** \u2014 Use output from one request as input to another
2008
+ 4. **Generate Reports** \u2014 Summarize test results with pass/fail status
2009
+
2010
+ ## How to Test
2011
+
2012
+ Use \`scripts/request.ts\` to make HTTP requests:
2013
+
2014
+ \`\`\`bash
2015
+ npx tsx scripts/request.ts GET https://api.example.com/users
2016
+ npx tsx scripts/request.ts POST https://api.example.com/users --body '{"name":"test"}'
2017
+ \`\`\`
2018
+
2019
+ ## Report Format
2020
+
2021
+ For each API test, report:
2022
+ - **Endpoint**: METHOD URL
2023
+ - **Status**: HTTP status code (with pass/fail indicator)
2024
+ - **Response Time**: Duration in milliseconds
2025
+ - **Response Body**: Formatted JSON (truncated if large)
2026
+ - **Headers**: Key response headers
2027
+
2028
+ ## Guidelines
2029
+
2030
+ - Always show the full request details (method, URL, headers, body)
2031
+ - Time every request and flag slow responses (>2s)
2032
+ - Validate JSON response structure when a schema is provided
2033
+ - Never send real credentials \u2014 use placeholders and warn the user
2034
+ - Group related tests together (e.g., CRUD operations on one resource)
2035
+ `);
2036
+ files.set("scripts/request.ts", `#!/usr/bin/env npx tsx
2037
+ /**
2038
+ * HTTP Request Script \u2014 Make API requests from the command line
2039
+ *
2040
+ * Usage: npx tsx scripts/request.ts <METHOD> <URL> [--header "Key: Value"] [--body '{"key":"value"}']
2041
+ */
2042
+
2043
+ const args = process.argv.slice(2);
2044
+ const method = args[0]?.toUpperCase() || 'GET';
2045
+ const url = args[1];
2046
+
2047
+ if (!url) {
2048
+ console.error('Usage: npx tsx scripts/request.ts <METHOD> <URL> [--header "K: V"] [--body JSON]');
2049
+ process.exit(1);
2050
+ }
2051
+
2052
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
2053
+ let body: string | undefined;
2054
+
2055
+ for (let i = 2; i < args.length; i++) {
2056
+ if (args[i] === '--header' && args[i + 1]) {
2057
+ const [key, ...valueParts] = args[++i].split(':');
2058
+ headers[key.trim()] = valueParts.join(':').trim();
2059
+ }
2060
+ if (args[i] === '--body' && args[i + 1]) {
2061
+ body = args[++i];
2062
+ }
2063
+ }
2064
+
2065
+ async function makeRequest() {
2066
+ const start = Date.now();
2067
+ const res = await fetch(url, {
2068
+ method,
2069
+ headers,
2070
+ ...(body && method !== 'GET' ? { body } : {}),
2071
+ });
2072
+ const elapsed = Date.now() - start;
2073
+ const responseHeaders: Record<string, string> = {};
2074
+ res.headers.forEach((v, k) => { responseHeaders[k] = v; });
2075
+
2076
+ let responseBody: any;
2077
+ const contentType = res.headers.get('content-type') || '';
2078
+ if (contentType.includes('json')) {
2079
+ responseBody = await res.json();
2080
+ } else {
2081
+ responseBody = await res.text();
2082
+ }
2083
+
2084
+ console.log(JSON.stringify({
2085
+ request: { method, url, headers, body: body ? JSON.parse(body) : undefined },
2086
+ response: {
2087
+ status: res.status,
2088
+ statusText: res.statusText,
2089
+ headers: responseHeaders,
2090
+ body: responseBody,
2091
+ timeMs: elapsed,
2092
+ },
2093
+ }, null, 2));
2094
+ }
2095
+
2096
+ makeRequest().catch((err) => {
2097
+ console.error(JSON.stringify({ error: err.message }));
2098
+ process.exit(1);
2099
+ });
2100
+ `);
2101
+ return files;
2102
+ }
2103
+ function generateGitWorkflowSkill() {
2104
+ const files = /* @__PURE__ */ new Map();
2105
+ files.set("SKILL.md", `---
2106
+ name: git-workflow
2107
+ description: Git workflow automation including conventional commits, branch management, and changelog generation
2108
+ version: 1.0.0
2109
+ tags:
2110
+ - git
2111
+ - workflow
2112
+ - development
2113
+ ---
2114
+
2115
+ # Git Workflow
2116
+
2117
+ You are a Git workflow assistant. Help users follow best practices for version control.
2118
+
2119
+ ## Capabilities
2120
+
2121
+ 1. **Conventional Commits** \u2014 Generate commit messages following the Conventional Commits spec
2122
+ 2. **Branch Management** \u2014 Create, switch, and clean up branches following naming conventions
2123
+ 3. **PR Descriptions** \u2014 Generate pull request descriptions from commit history
2124
+ 4. **Changelog** \u2014 Generate changelogs from commit history
2125
+
2126
+ ## Conventional Commit Format
2127
+
2128
+ \`\`\`
2129
+ <type>(<scope>): <description>
2130
+
2131
+ [optional body]
2132
+
2133
+ [optional footer(s)]
2134
+ \`\`\`
2135
+
2136
+ ### Types
2137
+ - \`feat\`: New feature (MINOR version bump)
2138
+ - \`fix\`: Bug fix (PATCH version bump)
2139
+ - \`docs\`: Documentation changes
2140
+ - \`style\`: Code style changes (formatting, semicolons)
2141
+ - \`refactor\`: Code refactoring (no feature/fix)
2142
+ - \`perf\`: Performance improvements
2143
+ - \`test\`: Adding or updating tests
2144
+ - \`chore\`: Build process, tooling, dependencies
2145
+
2146
+ ## Branch Naming
2147
+
2148
+ - \`feat/<ticket>-<description>\` \u2014 New features
2149
+ - \`fix/<ticket>-<description>\` \u2014 Bug fixes
2150
+ - \`chore/<description>\` \u2014 Maintenance tasks
2151
+ - \`release/<version>\` \u2014 Release branches
2152
+
2153
+ ## Scripts
2154
+
2155
+ - \`scripts/changelog.ts\` \u2014 Generate changelog from git log
2156
+
2157
+ ## Guidelines
2158
+
2159
+ - One logical change per commit
2160
+ - Write commit messages in imperative mood ("Add feature" not "Added feature")
2161
+ - Reference issue/ticket numbers in commits
2162
+ - Keep PR descriptions focused on the "what" and "why"
2163
+ - Squash fix-up commits before merging
2164
+ `);
2165
+ files.set("references/commit-examples.md", `# Commit Message Examples
2166
+
2167
+ ## Good Examples
2168
+ \`\`\`
2169
+ feat(auth): add OAuth2 login with Google provider
2170
+ fix(api): handle null response from payment gateway
2171
+ docs(readme): add deployment instructions for Cloudflare
2172
+ refactor(db): extract query builder into separate module
2173
+ perf(search): add index on user_email column
2174
+ test(auth): add integration tests for JWT refresh flow
2175
+ chore(deps): upgrade @mastra/core to 1.4.0
2176
+ \`\`\`
2177
+
2178
+ ## Bad Examples
2179
+ \`\`\`
2180
+ fixed stuff
2181
+ update
2182
+ WIP
2183
+ asdf
2184
+ changes
2185
+ \`\`\`
2186
+ `);
2187
+ files.set("scripts/changelog.ts", `#!/usr/bin/env npx tsx
2188
+ /**
2189
+ * Changelog Generator \u2014 Generate changelog from git log
2190
+ *
2191
+ * Usage: npx tsx scripts/changelog.ts [--since v1.0.0] [--until HEAD]
2192
+ */
2193
+
2194
+ import { execSync } from 'child_process';
2195
+
2196
+ const args = process.argv.slice(2);
2197
+ let since = '';
2198
+ let until = 'HEAD';
2199
+
2200
+ for (let i = 0; i < args.length; i++) {
2201
+ if (args[i] === '--since' && args[i + 1]) since = args[++i];
2202
+ if (args[i] === '--until' && args[i + 1]) until = args[++i];
2203
+ }
2204
+
2205
+ const range = since ? \`\${since}..\${until}\` : until;
2206
+ const log = execSync(\`git log \${range} --pretty=format:"%H|%s|%an|%ai" 2>/dev/null || echo ""\`, { encoding: 'utf-8' });
2207
+
2208
+ interface Commit {
2209
+ hash: string;
2210
+ message: string;
2211
+ author: string;
2212
+ date: string;
2213
+ type: string;
2214
+ scope: string;
2215
+ description: string;
2216
+ }
2217
+
2218
+ const commits: Commit[] = log.trim().split('\\n').filter(Boolean).map(line => {
2219
+ const [hash, message, author, date] = line.split('|');
2220
+ const match = message.match(/^(\\w+)(?:\\(([^)]+)\\))?:\\s*(.+)$/);
2221
+ return {
2222
+ hash: hash.slice(0, 7),
2223
+ message,
2224
+ author,
2225
+ date: date.split(' ')[0],
2226
+ type: match?.[1] || 'other',
2227
+ scope: match?.[2] || '',
2228
+ description: match?.[3] || message,
2229
+ };
2230
+ });
2231
+
2232
+ const grouped: Record<string, Commit[]> = {};
2233
+ for (const c of commits) {
2234
+ if (!grouped[c.type]) grouped[c.type] = [];
2235
+ grouped[c.type].push(c);
2236
+ }
2237
+
2238
+ const typeLabels: Record<string, string> = {
2239
+ feat: 'Features',
2240
+ fix: 'Bug Fixes',
2241
+ docs: 'Documentation',
2242
+ refactor: 'Refactoring',
2243
+ perf: 'Performance',
2244
+ test: 'Tests',
2245
+ chore: 'Chores',
2246
+ };
2247
+
2248
+ let changelog = '# Changelog\\n\\n';
2249
+ for (const [type, label] of Object.entries(typeLabels)) {
2250
+ if (grouped[type]?.length) {
2251
+ changelog += \`## \${label}\\n\\n\`;
2252
+ for (const c of grouped[type]) {
2253
+ const scope = c.scope ? \`**\${c.scope}**: \` : '';
2254
+ changelog += \`- \${scope}\${c.description} (\${c.hash})\\n\`;
2255
+ }
2256
+ changelog += '\\n';
2257
+ }
2258
+ }
2259
+
2260
+ console.log(JSON.stringify({ changelog, totalCommits: commits.length, types: Object.keys(grouped) }));
2261
+ `);
2262
+ return files;
2263
+ }
2264
+ function generateBrowserAutomationSkill() {
2265
+ const files = /* @__PURE__ */ new Map();
2266
+ files.set("SKILL.md", `---
2267
+ name: browser-automation
2268
+ description: Browser automation using Playwright. Navigate web pages, interact with elements, extract content, take screenshots, and run JavaScript.
2269
+ version: 1.0.0
2270
+ tags:
2271
+ - web
2272
+ - browser
2273
+ - automation
2274
+ - scraping
2275
+ ---
2276
+
2277
+ # Browser Automation
2278
+
2279
+ You are a browser automation assistant. Help users interact with web pages programmatically.
2280
+
2281
+ ## Capabilities
2282
+
2283
+ 1. **Navigate** \u2014 Go to any URL and wait for the page to load
2284
+ 2. **Click** \u2014 Click elements by CSS selector
2285
+ 3. **Type** \u2014 Fill text into input fields
2286
+ 4. **Screenshot** \u2014 Capture the current page as an image
2287
+ 5. **Extract Text** \u2014 Get readable text content from pages or specific elements
2288
+ 6. **Snapshot** \u2014 Get the accessibility tree for understanding page structure
2289
+ 7. **Evaluate** \u2014 Run arbitrary JavaScript on the page
2290
+ 8. **Wait** \u2014 Wait for elements to appear or for a specific duration
2291
+ 9. **Scroll** \u2014 Scroll the page up or down
2292
+ 10. **Select** \u2014 Choose options from dropdown menus
2293
+ 11. **Hover** \u2014 Hover over elements to trigger menus or tooltips
2294
+ 12. **Navigation** \u2014 Go back, forward, or reload the page
2295
+
2296
+ ## How to Use
2297
+
2298
+ ### Setup
2299
+
2300
+ \`\`\`typescript
2301
+ import { createBrowserTool, MCPServer } from '@agentforge-ai/core';
2302
+
2303
+ const server = new MCPServer({ name: 'my-tools' });
2304
+ const { tool, shutdown } = createBrowserTool({ headless: true });
2305
+ server.registerTool(tool);
2306
+ \`\`\`
2307
+
2308
+ ### Docker Sandbox Mode
2309
+
2310
+ For secure, isolated execution:
2311
+
2312
+ \`\`\`typescript
2313
+ const { tool, shutdown } = createBrowserTool({
2314
+ sandboxMode: true,
2315
+ headless: true,
2316
+ });
2317
+ \`\`\`
2318
+
2319
+ ## Agent Instructions
2320
+
2321
+ 1. Navigate to the target URL first
2322
+ 2. Wait for key elements before interacting
2323
+ 3. Use snapshot to understand page structure
2324
+ 4. Use extractText to get readable content
2325
+ 5. Use click and type for form interactions
2326
+ 6. Take screenshots for visual verification
2327
+ 7. Always close sessions when done
2328
+
2329
+ ## Guidelines
2330
+
2331
+ - Prefer \`#id\` selectors over class-based selectors
2332
+ - Use \`wait\` before clicking or typing on dynamic pages
2333
+ - Use \`extractText\` with a selector for specific content
2334
+ - Take screenshots before and after critical actions
2335
+ - Close sessions to free resources
2336
+ `);
2337
+ files.set("references/selectors.md", `# CSS Selector Guide for Browser Automation
2338
+
2339
+ ## Recommended Selectors (most to least reliable)
2340
+
2341
+ 1. \`#id\` \u2014 Element with a specific ID
2342
+ 2. \`[data-testid="value"]\` \u2014 Test ID attributes
2343
+ 3. \`[aria-label="value"]\` \u2014 Accessibility labels
2344
+ 4. \`button:has-text("Click me")\` \u2014 Playwright text selectors
2345
+ 5. \`.class-name\` \u2014 CSS class selectors
2346
+ 6. \`tag.class\` \u2014 Tag + class combination
2347
+
2348
+ ## Examples
2349
+
2350
+ \`\`\`
2351
+ #login-button \u2192 Click the login button
2352
+ input[name="email"] \u2192 Type into email field
2353
+ .nav-menu a:first-child \u2192 Click first nav link
2354
+ form button[type=submit] \u2192 Submit a form
2355
+ \`\`\`
2356
+
2357
+ ## Tips
2358
+
2359
+ - Avoid fragile selectors like \`div > div > span:nth-child(3)\`
2360
+ - Use Playwright's text selectors: \`text=Submit\`
2361
+ - For dynamic content, wait for the element first
2362
+ - Use \`snapshot\` action to discover available selectors
2363
+ `);
2364
+ files.set("scripts/scrape.ts", `#!/usr/bin/env npx tsx
2365
+ /**
2366
+ * Example: Scrape a web page and extract its text content
2367
+ *
2368
+ * Usage: npx tsx scripts/scrape.ts <url>
2369
+ */
2370
+
2371
+ const url = process.argv[2];
2372
+ if (!url) {
2373
+ console.error('Usage: npx tsx scripts/scrape.ts <url>');
2374
+ process.exit(1);
2375
+ }
2376
+
2377
+ console.log(JSON.stringify({
2378
+ instruction: 'Use the browser tool to scrape this URL',
2379
+ url,
2380
+ steps: [
2381
+ { action: 'navigate', url },
2382
+ { action: 'wait', timeMs: 2000 },
2383
+ { action: 'extractText' },
2384
+ { action: 'screenshot', fullPage: true },
2385
+ { action: 'close' },
2386
+ ],
2387
+ }));
2388
+ `);
2389
+ return files;
2390
+ }
2391
+ function registerSkillsCommand(program2) {
2392
+ const skills = program2.command("skills").description("Manage agent skills (Mastra Workspace Skills)");
2393
+ skills.command("list").option("--json", "Output as JSON").option("--registry", "Show available skills from the registry").description("List installed skills or browse the registry").action(async (opts) => {
2394
+ if (opts.registry) {
2395
+ header("AgentForge Skills Registry");
2396
+ if (opts.json) {
2397
+ console.log(JSON.stringify(BUILTIN_REGISTRY, null, 2));
2398
+ return;
2399
+ }
2400
+ table(BUILTIN_REGISTRY.map((s) => ({
2401
+ Name: s.name,
2402
+ Description: truncate(s.description, 60),
2403
+ Version: s.version,
2404
+ Tags: s.tags.join(", ")
2405
+ })));
2406
+ info(`Install with: ${colors.cyan}agentforge skills install <name>${colors.reset}`);
2407
+ return;
2408
+ }
2409
+ const skillsDir = resolveSkillsDir();
2410
+ header("Installed Skills");
2411
+ if (!fs6.existsSync(skillsDir)) {
2412
+ info("No skills directory found. Install a skill with:");
2413
+ dim(` agentforge skills install <name>`);
2414
+ dim(` agentforge skills list --registry # browse available skills`);
2415
+ return;
2416
+ }
2417
+ const dirs = fs6.readdirSync(skillsDir).filter((d) => {
2418
+ const fullPath = path6.join(skillsDir, d);
2419
+ return fs6.statSync(fullPath).isDirectory() && fs6.existsSync(path6.join(fullPath, "SKILL.md"));
2420
+ });
2421
+ if (dirs.length === 0) {
2422
+ info("No skills installed. Browse available skills with:");
2423
+ dim(` agentforge skills list --registry`);
2424
+ return;
2425
+ }
2426
+ const lock = readSkillsLock(skillsDir);
2427
+ const skillData = dirs.map((d) => {
2428
+ const meta = readSkillMetadata(path6.join(skillsDir, d));
2429
+ const lockEntry = lock.skills[d];
2430
+ return {
2431
+ Name: meta?.name || d,
2432
+ Description: truncate(meta?.description || "", 50),
2433
+ Version: meta?.version || "?",
2434
+ Tags: (meta?.tags || []).join(", "),
2435
+ Source: lockEntry?.source || "local",
2436
+ Installed: lockEntry?.installedAt ? new Date(lockEntry.installedAt).toLocaleDateString() : "\u2014"
2437
+ };
2438
+ });
2439
+ if (opts.json) {
2440
+ console.log(JSON.stringify(skillData, null, 2));
2441
+ return;
2442
+ }
2443
+ table(skillData);
2444
+ dim(` Skills directory: ${skillsDir}`);
2445
+ info("Skills are auto-discovered by the Mastra Workspace.");
2446
+ });
2447
+ skills.command("install").argument("<name>", "Skill name from registry, GitHub URL, or local path").option("--from <source>", "Source: registry (default), github, local", "registry").description("Install a skill into the workspace").action(async (name, opts) => {
2448
+ const skillsDir = resolveSkillsDir();
2449
+ const targetDir = path6.join(skillsDir, name.split("/").pop().replace(/\.git$/, ""));
2450
+ if (fs6.existsSync(targetDir) && fs6.existsSync(path6.join(targetDir, "SKILL.md"))) {
2451
+ warn(`Skill "${name}" is already installed at ${targetDir}`);
2452
+ const overwrite = await prompt2("Overwrite? (y/N): ");
2453
+ if (overwrite.toLowerCase() !== "y") {
2454
+ info("Installation cancelled.");
2455
+ return;
2456
+ }
2457
+ fs6.removeSync(targetDir);
2458
+ }
2459
+ fs6.mkdirSync(skillsDir, { recursive: true });
2460
+ let source = opts.from;
2461
+ let installedName = name;
2462
+ if (opts.from === "local" || fs6.existsSync(name)) {
2463
+ source = "local";
2464
+ const sourcePath = path6.resolve(name);
2465
+ if (!fs6.existsSync(sourcePath)) {
2466
+ error(`Local path not found: ${sourcePath}`);
2467
+ process.exit(1);
2468
+ }
2469
+ if (!fs6.existsSync(path6.join(sourcePath, "SKILL.md"))) {
2470
+ error(`No SKILL.md found in ${sourcePath}. Not a valid skill directory.`);
2471
+ process.exit(1);
2472
+ }
2473
+ installedName = path6.basename(sourcePath);
2474
+ const dest = path6.join(skillsDir, installedName);
2475
+ fs6.copySync(sourcePath, dest);
2476
+ success(`Skill "${installedName}" installed from local path.`);
2477
+ } else if (opts.from === "github" || name.includes("github.com") || name.includes("/")) {
2478
+ source = "github";
2479
+ const repoUrl = name.includes("github.com") ? name : `https://github.com/${name}`;
2480
+ installedName = name.split("/").pop().replace(/\.git$/, "");
2481
+ const dest = path6.join(skillsDir, installedName);
2482
+ info(`Cloning skill from ${repoUrl}...`);
2483
+ try {
2484
+ execSync3(`git clone --depth 1 ${repoUrl} ${dest} 2>&1`, { encoding: "utf-8" });
2485
+ fs6.removeSync(path6.join(dest, ".git"));
2486
+ if (!fs6.existsSync(path6.join(dest, "SKILL.md"))) {
2487
+ error(`Cloned repo does not contain a SKILL.md. Not a valid skill.`);
2488
+ fs6.removeSync(dest);
2489
+ process.exit(1);
2490
+ }
2491
+ success(`Skill "${installedName}" installed from GitHub.`);
2492
+ } catch (err) {
2493
+ error(`Failed to clone: ${err.message}`);
2494
+ process.exit(1);
2495
+ }
2496
+ } else {
2497
+ source = "builtin";
2498
+ const entry = findInRegistry(name);
2499
+ if (!entry) {
2500
+ error(`Skill "${name}" not found in the registry.`);
2501
+ info("Available skills:");
2502
+ BUILTIN_REGISTRY.forEach((s) => {
2503
+ dim(` ${colors.cyan}${s.name}${colors.reset} \u2014 ${s.description}`);
2504
+ });
2505
+ info(`
2506
+ Or install from GitHub: ${colors.cyan}agentforge skills install owner/repo --from github${colors.reset}`);
2507
+ process.exit(1);
2508
+ }
2509
+ installedName = entry.name;
2510
+ const files = generateBuiltinSkill(entry.name);
2511
+ if (!files) {
2512
+ error(`No content generator for skill "${entry.name}".`);
2513
+ process.exit(1);
2514
+ }
2515
+ const dest = path6.join(skillsDir, installedName);
2516
+ fs6.mkdirSync(dest, { recursive: true });
2517
+ for (const [filePath, content] of files) {
2518
+ const fullPath = path6.join(dest, filePath);
2519
+ fs6.mkdirSync(path6.dirname(fullPath), { recursive: true });
2520
+ fs6.writeFileSync(fullPath, content);
2521
+ }
2522
+ success(`Skill "${installedName}" installed from AgentForge registry.`);
2523
+ }
2524
+ const lock = readSkillsLock(skillsDir);
2525
+ const meta = readSkillMetadata(path6.join(skillsDir, installedName));
2526
+ lock.skills[installedName] = {
2527
+ name: installedName,
2528
+ version: meta?.version || "1.0.0",
2529
+ source,
2530
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
2531
+ };
2532
+ writeSkillsLock(skillsDir, lock);
2533
+ if (meta) {
2534
+ console.log();
2535
+ details({
2536
+ Name: meta.name,
2537
+ Description: meta.description,
2538
+ Version: meta.version,
2539
+ Tags: (meta.tags || []).join(", ") || "\u2014",
2540
+ Path: path6.join(skillsDir, installedName)
2541
+ });
2542
+ }
2543
+ info("The skill is now available to agents via the Mastra Workspace.");
2544
+ dim(" Skills in the workspace/skills/ directory are auto-discovered.");
2545
+ try {
2546
+ const client = await createClient();
2547
+ await safeCall(
2548
+ () => client.mutation("skills:create", {
2549
+ name: installedName,
2550
+ displayName: meta?.name || installedName,
2551
+ description: meta?.description || "",
2552
+ category: (meta?.tags || [])[0] || "custom",
2553
+ version: meta?.version || "1.0.0",
2554
+ author: meta?.author || "Unknown",
2555
+ code: `// Skill: ${installedName}
2556
+ // This skill uses the Agent Skills Specification (SKILL.md format)
2557
+ // See: workspace/skills/${installedName}/SKILL.md`
2558
+ }),
2559
+ "Failed to sync skill to Convex"
2560
+ );
2561
+ dim(" Skill synced to Convex database.");
2562
+ } catch {
2563
+ dim(" Convex not connected \u2014 skill installed locally only.");
2564
+ }
2565
+ });
2566
+ skills.command("remove").argument("<name>", "Skill name to remove").option("--force", "Skip confirmation prompt", false).description("Remove an installed skill").action(async (name, opts) => {
2567
+ const skillsDir = resolveSkillsDir();
2568
+ const skillDir = path6.join(skillsDir, name);
2569
+ if (!fs6.existsSync(skillDir)) {
2570
+ error(`Skill "${name}" not found in ${skillsDir}`);
2571
+ info("List installed skills with: agentforge skills list");
2572
+ process.exit(1);
2573
+ }
2574
+ if (!opts.force) {
2575
+ const confirm = await prompt2(`Remove skill "${name}" and delete all files? (y/N): `);
2576
+ if (confirm.toLowerCase() !== "y") {
2577
+ info("Removal cancelled.");
2578
+ return;
2579
+ }
2580
+ }
2581
+ fs6.removeSync(skillDir);
2582
+ success(`Skill "${name}" removed from disk.`);
2583
+ const lock = readSkillsLock(skillsDir);
2584
+ delete lock.skills[name];
2585
+ writeSkillsLock(skillsDir, lock);
2586
+ try {
2587
+ const client = await createClient();
2588
+ const skills2 = await client.query("skills:list", {});
2589
+ const skill = skills2.find((s) => s.name === name);
2590
+ if (skill) {
2591
+ await client.mutation("skills:remove", { id: skill._id });
2592
+ dim(" Skill removed from Convex database.");
2593
+ }
2594
+ } catch {
2595
+ }
2596
+ info("Skill removed. Agents will no longer discover it.");
2597
+ });
2598
+ skills.command("search").argument("<query>", "Search query").description("Search for skills in the registry").action(async (query) => {
2599
+ header("Skill Search Results");
2600
+ const q = query.toLowerCase();
2601
+ const matches = BUILTIN_REGISTRY.filter(
2602
+ (e) => e.name.includes(q) || e.description.toLowerCase().includes(q) || e.tags.some((t) => t.includes(q))
2603
+ );
2604
+ if (matches.length === 0) {
2605
+ info(`No skills matching "${query}".`);
2606
+ info("Browse all skills: agentforge skills list --registry");
2607
+ return;
2608
+ }
2609
+ table(matches.map((e) => ({
2610
+ Name: e.name,
2611
+ Description: truncate(e.description, 60),
2612
+ Tags: e.tags.join(", "),
2613
+ Version: e.version
2614
+ })));
2615
+ info(`Install with: ${colors.cyan}agentforge skills install <name>${colors.reset}`);
2616
+ });
2617
+ skills.command("create").description("Create a new skill (interactive)").option("--name <name>", "Skill name (kebab-case)").option("--description <desc>", "Skill description").option("--tags <tags>", "Comma-separated tags").action(async (opts) => {
2618
+ const name = opts.name || await prompt2("Skill name (kebab-case): ");
2619
+ const description = opts.description || await prompt2("Description: ");
2620
+ const tagsInput = opts.tags || await prompt2("Tags (comma-separated, e.g. web,search): ");
2621
+ const tags = tagsInput ? tagsInput.split(",").map((t) => t.trim()).filter(Boolean) : [];
2622
+ if (!name) {
2623
+ error("Skill name is required.");
2624
+ process.exit(1);
2625
+ }
2626
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
2627
+ error("Skill name must be kebab-case (lowercase letters, numbers, hyphens).");
2628
+ process.exit(1);
2629
+ }
2630
+ const skillsDir = resolveSkillsDir();
2631
+ const skillDir = path6.join(skillsDir, name);
2632
+ if (fs6.existsSync(skillDir)) {
2633
+ error(`Skill "${name}" already exists at ${skillDir}`);
2634
+ process.exit(1);
2635
+ }
2636
+ fs6.mkdirSync(path6.join(skillDir, "references"), { recursive: true });
2637
+ fs6.mkdirSync(path6.join(skillDir, "scripts"), { recursive: true });
2638
+ const tagsYaml = tags.length > 0 ? `tags:
2639
+ ${tags.map((t) => ` - ${t}`).join("\n")}` : "tags: []";
2640
+ fs6.writeFileSync(path6.join(skillDir, "SKILL.md"), `---
2641
+ name: ${name}
2642
+ description: ${description}
2643
+ version: 1.0.0
2644
+ ${tagsYaml}
2645
+ ---
2646
+
2647
+ # ${name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}
2648
+
2649
+ ${description}
2650
+
2651
+ ## Instructions
2652
+
2653
+ <!-- Add instructions for how the agent should use this skill -->
2654
+
2655
+ 1. Step one
2656
+ 2. Step two
2657
+ 3. Step three
2658
+
2659
+ ## References
2660
+
2661
+ See \`references/\` for supporting documentation.
2662
+
2663
+ ## Scripts
2664
+
2665
+ See \`scripts/\` for executable scripts the agent can run.
2666
+
2667
+ ## Guidelines
2668
+
2669
+ - Guideline one
2670
+ - Guideline two
2671
+ `);
2672
+ fs6.writeFileSync(
2673
+ path6.join(skillDir, "references", "README.md"),
2674
+ `# References for ${name}
2675
+
2676
+ Add supporting documentation here.
2677
+ `
2678
+ );
2679
+ fs6.writeFileSync(
2680
+ path6.join(skillDir, "scripts", "example.ts"),
2681
+ `#!/usr/bin/env npx tsx
2682
+ /**
2683
+ * Example script for ${name}
2684
+ */
2685
+ console.log('Hello from ${name}!');
2686
+ `
2687
+ );
2688
+ const lock = readSkillsLock(skillsDir);
2689
+ lock.skills[name] = {
2690
+ name,
2691
+ version: "1.0.0",
2692
+ source: "local",
2693
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
2694
+ };
2695
+ writeSkillsLock(skillsDir, lock);
2696
+ success(`Skill "${name}" created at ${skillDir}/`);
2697
+ info("Files created:");
2698
+ dim(` ${skillDir}/SKILL.md`);
2699
+ dim(` ${skillDir}/references/README.md`);
2700
+ dim(` ${skillDir}/scripts/example.ts`);
2701
+ console.log();
2702
+ info(`Edit ${colors.cyan}SKILL.md${colors.reset} to add instructions for your agent.`);
2703
+ info("The skill will be auto-discovered by the Mastra Workspace.");
2704
+ });
2705
+ skills.command("info").argument("<name>", "Skill name").description("Show detailed information about a skill").action(async (name) => {
2706
+ const skillsDir = resolveSkillsDir();
2707
+ const skillDir = path6.join(skillsDir, name);
2708
+ if (fs6.existsSync(skillDir) && fs6.existsSync(path6.join(skillDir, "SKILL.md"))) {
2709
+ const meta = readSkillMetadata(skillDir);
2710
+ const lock = readSkillsLock(skillsDir);
2711
+ const lockEntry = lock.skills[name];
2712
+ header(`Skill: ${meta?.name || name}`);
2713
+ details({
2714
+ Name: meta?.name || name,
2715
+ Description: meta?.description || "\u2014",
2716
+ Version: meta?.version || "\u2014",
2717
+ Tags: (meta?.tags || []).join(", ") || "\u2014",
2718
+ Author: meta?.author || "\u2014",
2719
+ Source: lockEntry?.source || "local",
2720
+ "Installed At": lockEntry?.installedAt || "\u2014",
2721
+ Path: skillDir
2722
+ });
2723
+ dim(" Files:");
2724
+ const listFiles = (dir, prefix = "") => {
2725
+ const entries = fs6.readdirSync(dir);
2726
+ for (const entry2 of entries) {
2727
+ const fullPath = path6.join(dir, entry2);
2728
+ const stat = fs6.statSync(fullPath);
2729
+ if (stat.isDirectory()) {
2730
+ dim(` ${prefix}${entry2}/`);
2731
+ listFiles(fullPath, prefix + " ");
2732
+ } else {
2733
+ dim(` ${prefix}${entry2}`);
2734
+ }
2735
+ }
2736
+ };
2737
+ listFiles(skillDir, " ");
2738
+ console.log();
2739
+ const content = fs6.readFileSync(path6.join(skillDir, "SKILL.md"), "utf-8");
2740
+ const { content: body } = parseSkillMd(content);
2741
+ info("Instructions preview:");
2742
+ dim(body.trim().split("\n").slice(0, 10).map((l) => ` ${l}`).join("\n"));
2743
+ if (body.trim().split("\n").length > 10) {
2744
+ dim(" ...");
2745
+ }
2746
+ return;
2747
+ }
2748
+ const entry = findInRegistry(name);
2749
+ if (entry) {
2750
+ header(`Registry Skill: ${entry.name}`);
2751
+ details({
2752
+ Name: entry.name,
2753
+ Description: entry.description,
2754
+ Version: entry.version,
2755
+ Tags: entry.tags.join(", "),
2756
+ Author: entry.author,
2757
+ Source: entry.source,
2758
+ Status: "Not installed"
2759
+ });
2760
+ info(`Install with: ${colors.cyan}agentforge skills install ${entry.name}${colors.reset}`);
2761
+ return;
2762
+ }
2763
+ error(`Skill "${name}" not found (installed or in registry).`);
2764
+ });
2765
+ program2.command("install").argument("<name>", "Skill name to install").option("--from <source>", "Source: registry (default), github, local", "registry").description("Install a skill (alias for: agentforge skills install)").action(async (name, opts) => {
2766
+ const skillsCmd = skills.commands.find((c) => c.name() === "install");
2767
+ if (skillsCmd) {
2768
+ await skillsCmd.parseAsync([name, ...opts.from !== "registry" ? ["--from", opts.from] : []], { from: "user" });
2769
+ }
2770
+ });
2771
+ }
2772
+
2773
+ // src/commands/cron.ts
2774
+ import readline4 from "readline";
2775
+ function prompt3(q) {
2776
+ const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
2777
+ return new Promise((r) => rl.question(q, (a) => {
2778
+ rl.close();
2779
+ r(a.trim());
2780
+ }));
2781
+ }
2782
+ function registerCronCommand(program2) {
2783
+ const cron = program2.command("cron").description("Manage cron jobs");
2784
+ cron.command("list").option("--json", "Output as JSON").description("List all cron jobs").action(async (opts) => {
2785
+ const client = await createClient();
2786
+ const result = await safeCall(() => client.query("cronJobs:list", {}), "Failed to list cron jobs");
2787
+ if (opts.json) {
2788
+ console.log(JSON.stringify(result, null, 2));
2789
+ return;
2790
+ }
2791
+ header("Cron Jobs");
2792
+ const items = result || [];
2793
+ if (items.length === 0) {
2794
+ info("No cron jobs. Create one with: agentforge cron create");
2795
+ return;
2796
+ }
2797
+ table(items.map((c) => ({
2798
+ ID: c._id?.slice(-8) || "N/A",
2799
+ Name: c.name,
2800
+ Schedule: c.schedule,
2801
+ Agent: c.agentId,
2802
+ Enabled: c.isEnabled ? "\u2714" : "\u2716",
2803
+ "Last Run": c.lastRun ? formatDate(c.lastRun) : "Never",
2804
+ "Next Run": c.nextRun ? formatDate(c.nextRun) : "N/A"
2805
+ })));
2806
+ });
2807
+ cron.command("create").description("Create a new cron job (interactive)").option("--name <name>", "Job name").option("--schedule <cron>", "Cron expression").option("--agent <id>", "Agent ID").option("--action <action>", "Action to execute").action(async (opts) => {
2808
+ const name = opts.name || await prompt3("Job name: ");
2809
+ const schedule = opts.schedule || await prompt3('Cron schedule (e.g., "0 */5 * * * *" for every 5 min): ');
2810
+ const agentId = opts.agent || await prompt3("Agent ID: ");
2811
+ const action = opts.action || await prompt3("Action (message to send to agent): ");
2812
+ if (!name || !schedule || !agentId || !action) {
2813
+ error("All fields are required.");
2814
+ process.exit(1);
2815
+ }
2816
+ const client = await createClient();
2817
+ await safeCall(
2818
+ () => client.mutation("cronJobs:create", { name, schedule, agentId, prompt: action }),
2819
+ "Failed to create cron job"
2820
+ );
2821
+ success(`Cron job "${name}" created.`);
2822
+ });
2823
+ cron.command("delete").argument("<id>", "Cron job ID").description("Delete a cron job").action(async (id) => {
2824
+ const client = await createClient();
2825
+ await safeCall(() => client.mutation("cronJobs:remove", { id }), "Failed to delete");
2826
+ success(`Cron job "${id}" deleted.`);
2827
+ });
2828
+ cron.command("enable").argument("<id>", "Cron job ID").description("Enable a cron job").action(async (id) => {
2829
+ const client = await createClient();
2830
+ await safeCall(() => client.mutation("cronJobs:update", { id, isEnabled: true }), "Failed");
2831
+ success(`Cron job "${id}" enabled.`);
2832
+ });
2833
+ cron.command("disable").argument("<id>", "Cron job ID").description("Disable a cron job").action(async (id) => {
2834
+ const client = await createClient();
2835
+ await safeCall(() => client.mutation("cronJobs:update", { id, isEnabled: false }), "Failed");
2836
+ success(`Cron job "${id}" disabled.`);
2837
+ });
2838
+ }
2839
+
2840
+ // src/commands/mcp.ts
2841
+ import readline5 from "readline";
2842
+ function prompt4(q) {
2843
+ const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
1513
2844
  return new Promise((r) => rl.question(q, (a) => {
1514
2845
  rl.close();
1515
2846
  r(a.trim());
@@ -1533,8 +2864,8 @@ function registerMcpCommand(program2) {
1533
2864
  table(items.map((c) => ({
1534
2865
  ID: c._id?.slice(-8) || "N/A",
1535
2866
  Name: c.name,
1536
- Type: c.type,
1537
- Endpoint: c.endpoint,
2867
+ Type: c.protocol,
2868
+ Endpoint: c.serverUrl,
1538
2869
  Connected: c.isConnected ? "\u2714" : "\u2716",
1539
2870
  Enabled: c.isEnabled ? "\u2714" : "\u2716"
1540
2871
  })));
@@ -1551,10 +2882,8 @@ function registerMcpCommand(program2) {
1551
2882
  await safeCall(
1552
2883
  () => client.mutation("mcpConnections:create", {
1553
2884
  name,
1554
- type,
1555
- endpoint,
1556
- isConnected: false,
1557
- isEnabled: true
2885
+ serverUrl: endpoint,
2886
+ protocol: type
1558
2887
  }),
1559
2888
  "Failed to add connection"
1560
2889
  );
@@ -1562,7 +2891,7 @@ function registerMcpCommand(program2) {
1562
2891
  });
1563
2892
  mcp.command("remove").argument("<id>", "Connection ID").description("Remove an MCP connection").action(async (id) => {
1564
2893
  const client = await createClient();
1565
- await safeCall(() => client.mutation("mcpConnections:remove", { _id: id }), "Failed");
2894
+ await safeCall(() => client.mutation("mcpConnections:remove", { id }), "Failed");
1566
2895
  success(`Connection "${id}" removed.`);
1567
2896
  });
1568
2897
  mcp.command("test").argument("<id>", "Connection ID").description("Test an MCP connection").action(async (id) => {
@@ -1574,12 +2903,12 @@ function registerMcpCommand(program2) {
1574
2903
  error(`Connection "${id}" not found.`);
1575
2904
  process.exit(1);
1576
2905
  }
1577
- if (conn.type === "http" || conn.type === "sse") {
2906
+ if (conn.protocol === "http" || conn.protocol === "sse") {
1578
2907
  try {
1579
- const res = await fetch(conn.endpoint, { method: "HEAD", signal: AbortSignal.timeout(5e3) });
2908
+ const res = await fetch(conn.serverUrl, { method: "HEAD", signal: AbortSignal.timeout(5e3) });
1580
2909
  if (res.ok) {
1581
2910
  success(`Connection "${conn.name}" is reachable (HTTP ${res.status}).`);
1582
- await client.mutation("mcpConnections:update", { _id: conn._id, isConnected: true });
2911
+ await client.mutation("mcpConnections:updateStatus", { id: conn._id, isConnected: true });
1583
2912
  } else {
1584
2913
  error(`Connection "${conn.name}" returned HTTP ${res.status}.`);
1585
2914
  }
@@ -1587,18 +2916,18 @@ function registerMcpCommand(program2) {
1587
2916
  error(`Connection "${conn.name}" failed: ${e.message}`);
1588
2917
  }
1589
2918
  } else {
1590
- info(`Connection type "${conn.type}" \u2014 manual verification required.`);
1591
- info(`Endpoint: ${conn.endpoint}`);
2919
+ info(`Connection type "${conn.protocol}" \u2014 manual verification required.`);
2920
+ info(`Endpoint: ${conn.serverUrl}`);
1592
2921
  }
1593
2922
  });
1594
2923
  mcp.command("enable").argument("<id>", "Connection ID").description("Enable a connection").action(async (id) => {
1595
2924
  const client = await createClient();
1596
- await safeCall(() => client.mutation("mcpConnections:update", { _id: id, isEnabled: true }), "Failed");
2925
+ await safeCall(() => client.mutation("mcpConnections:update", { id, isEnabled: true }), "Failed");
1597
2926
  success(`Connection "${id}" enabled.`);
1598
2927
  });
1599
2928
  mcp.command("disable").argument("<id>", "Connection ID").description("Disable a connection").action(async (id) => {
1600
2929
  const client = await createClient();
1601
- await safeCall(() => client.mutation("mcpConnections:update", { _id: id, isEnabled: false }), "Failed");
2930
+ await safeCall(() => client.mutation("mcpConnections:update", { id, isEnabled: false }), "Failed");
1602
2931
  success(`Connection "${id}" disabled.`);
1603
2932
  });
1604
2933
  }
@@ -1628,7 +2957,7 @@ function registerFilesCommand(program2) {
1628
2957
  Type: f.mimeType,
1629
2958
  Size: formatSize(f.size),
1630
2959
  Folder: f.folderId || "root",
1631
- Created: formatDate(f.createdAt)
2960
+ Uploaded: formatDate(f.uploadedAt)
1632
2961
  })));
1633
2962
  });
1634
2963
  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) => {
@@ -1659,8 +2988,10 @@ function registerFilesCommand(program2) {
1659
2988
  await safeCall(
1660
2989
  () => client.mutation("files:create", {
1661
2990
  name,
2991
+ originalName: name,
1662
2992
  mimeType,
1663
2993
  size: stat.size,
2994
+ url: "pending-upload",
1664
2995
  folderId: opts.folder,
1665
2996
  projectId: opts.project
1666
2997
  }),
@@ -1671,7 +3002,7 @@ function registerFilesCommand(program2) {
1671
3002
  });
1672
3003
  files.command("delete").argument("<id>", "File ID").description("Delete a file").action(async (id) => {
1673
3004
  const client = await createClient();
1674
- await safeCall(() => client.mutation("files:remove", { _id: id }), "Failed to delete file");
3005
+ await safeCall(() => client.mutation("files:remove", { id }), "Failed to delete file");
1675
3006
  success(`File "${id}" deleted.`);
1676
3007
  });
1677
3008
  const folders = program2.command("folders").description("Manage folders");
@@ -1705,7 +3036,7 @@ function registerFilesCommand(program2) {
1705
3036
  });
1706
3037
  folders.command("delete").argument("<id>", "Folder ID").description("Delete a folder").action(async (id) => {
1707
3038
  const client = await createClient();
1708
- await safeCall(() => client.mutation("folders:remove", { _id: id }), "Failed to delete folder");
3039
+ await safeCall(() => client.mutation("folders:remove", { id }), "Failed to delete folder");
1709
3040
  success(`Folder "${id}" deleted.`);
1710
3041
  });
1711
3042
  }
@@ -1742,7 +3073,6 @@ function registerProjectsCommand(program2) {
1742
3073
  table(items.map((p) => ({
1743
3074
  ID: p._id?.slice(-8) || "N/A",
1744
3075
  Name: p.name,
1745
- Status: p.status,
1746
3076
  Description: (p.description || "").slice(0, 40),
1747
3077
  Created: formatDate(p.createdAt)
1748
3078
  })));
@@ -1751,7 +3081,7 @@ function registerProjectsCommand(program2) {
1751
3081
  const description = opts.description || await prompt5("Description (optional): ");
1752
3082
  const client = await createClient();
1753
3083
  await safeCall(
1754
- () => client.mutation("projects:create", { name, description, status: "active" }),
3084
+ () => client.mutation("projects:create", { name, description: description || void 0 }),
1755
3085
  "Failed to create project"
1756
3086
  );
1757
3087
  success(`Project "${name}" created.`);
@@ -1768,7 +3098,6 @@ function registerProjectsCommand(program2) {
1768
3098
  details({
1769
3099
  ID: project._id,
1770
3100
  Name: project.name,
1771
- Status: project.status,
1772
3101
  Description: project.description || "N/A",
1773
3102
  Created: formatDate(project.createdAt),
1774
3103
  Updated: formatDate(project.updatedAt)
@@ -1783,7 +3112,7 @@ function registerProjectsCommand(program2) {
1783
3112
  }
1784
3113
  }
1785
3114
  const client = await createClient();
1786
- await safeCall(() => client.mutation("projects:remove", { _id: id }), "Failed");
3115
+ await safeCall(() => client.mutation("projects:remove", { id }), "Failed");
1787
3116
  success(`Project "${id}" deleted.`);
1788
3117
  });
1789
3118
  projects.command("switch").argument("<id>", "Project ID to switch to").description("Set the active project").action(async (id) => {
@@ -2001,7 +3330,7 @@ function registerVaultCommand(program2) {
2001
3330
  Name: s.name,
2002
3331
  Category: s.category || "general",
2003
3332
  Provider: s.provider || "N/A",
2004
- "Last Rotated": s.lastRotatedAt ? formatDate(s.lastRotatedAt) : "Never",
3333
+ "Last Updated": s.updatedAt ? formatDate(s.updatedAt) : "Never",
2005
3334
  Created: formatDate(s.createdAt)
2006
3335
  })));
2007
3336
  });
@@ -2017,7 +3346,7 @@ function registerVaultCommand(program2) {
2017
3346
  await safeCall(
2018
3347
  () => client.mutation("vault:store", {
2019
3348
  name,
2020
- encryptedValue: value,
3349
+ value,
2021
3350
  category: opts.category,
2022
3351
  provider: opts.provider
2023
3352
  }),
@@ -2034,15 +3363,11 @@ function registerVaultCommand(program2) {
2034
3363
  process.exit(1);
2035
3364
  }
2036
3365
  if (opts.reveal) {
2037
- const value = await safeCall(
2038
- () => client.query("vault:getDecrypted", { _id: secret._id }),
2039
- "Failed to retrieve secret"
2040
- );
2041
- console.log(value);
3366
+ info(`${name} = ${secret.maskedValue || "****"}`);
3367
+ dim(" Note: Full decryption is only available server-side for security.");
2042
3368
  } else {
2043
- const masked = secret.encryptedValue ? secret.encryptedValue.slice(0, 4) + "****" + secret.encryptedValue.slice(-4) : "****";
2044
- info(`${name} = ${masked}`);
2045
- dim(" Use --reveal to show the full value.");
3369
+ info(`${name} = ${secret.maskedValue || "****"}`);
3370
+ dim(" Use --reveal to attempt to show more details.");
2046
3371
  }
2047
3372
  });
2048
3373
  vault.command("delete").argument("<name>", "Secret name").option("-f, --force", "Skip confirmation").description("Delete a secret").action(async (name, opts) => {
@@ -2060,7 +3385,7 @@ function registerVaultCommand(program2) {
2060
3385
  error(`Secret "${name}" not found.`);
2061
3386
  process.exit(1);
2062
3387
  }
2063
- await safeCall(() => client.mutation("vault:remove", { _id: secret._id }), "Failed");
3388
+ await safeCall(() => client.mutation("vault:remove", { id: secret._id }), "Failed");
2064
3389
  success(`Secret "${name}" deleted.`);
2065
3390
  });
2066
3391
  vault.command("rotate").argument("<name>", "Secret name").description("Rotate a secret (set a new value)").action(async (name) => {
@@ -2077,7 +3402,7 @@ function registerVaultCommand(program2) {
2077
3402
  process.exit(1);
2078
3403
  }
2079
3404
  await safeCall(
2080
- () => client.mutation("vault:rotate", { _id: secret._id, newValue }),
3405
+ () => client.mutation("vault:update", { id: secret._id, value: newValue }),
2081
3406
  "Failed to rotate secret"
2082
3407
  );
2083
3408
  success(`Secret "${name}" rotated.`);
@@ -2100,7 +3425,7 @@ function maskKey(key) {
2100
3425
  }
2101
3426
  function promptSecret2(question) {
2102
3427
  return new Promise((resolve2) => {
2103
- const readline10 = __require("readline");
3428
+ const readline12 = __require("readline");
2104
3429
  if (process.stdin.isTTY) {
2105
3430
  process.stdout.write(question);
2106
3431
  process.stdin.setRawMode(true);
@@ -2128,7 +3453,7 @@ function promptSecret2(question) {
2128
3453
  };
2129
3454
  process.stdin.on("data", onData);
2130
3455
  } else {
2131
- const rl = readline10.createInterface({ input: process.stdin, output: process.stdout });
3456
+ const rl = readline12.createInterface({ input: process.stdin, output: process.stdout });
2132
3457
  rl.question(question, (ans) => {
2133
3458
  rl.close();
2134
3459
  resolve2(ans.trim());
@@ -2219,8 +3544,8 @@ function registerKeysCommand(program2) {
2219
3544
  }
2220
3545
  const target = items[0];
2221
3546
  if (!opts.force) {
2222
- const readline10 = __require("readline");
2223
- const rl = readline10.createInterface({ input: process.stdin, output: process.stdout });
3547
+ const readline12 = __require("readline");
3548
+ const rl = readline12.createInterface({ input: process.stdin, output: process.stdout });
2224
3549
  const answer = await new Promise((resolve2) => {
2225
3550
  rl.question(`Delete "${target.keyName}" for ${provider}? (y/N): `, (ans) => {
2226
3551
  rl.close();
@@ -2448,7 +3773,7 @@ function registerStatusCommand(program2) {
2448
3773
  const args = {};
2449
3774
  if (opts.agent) args.agentId = opts.agent;
2450
3775
  const result = await safeCall(
2451
- () => client.query("heartbeat:listPending", args),
3776
+ () => client.query("heartbeat:listActive", args),
2452
3777
  "Failed to check heartbeat"
2453
3778
  );
2454
3779
  const items = result || [];
@@ -2456,26 +3781,26 @@ function registerStatusCommand(program2) {
2456
3781
  success("All tasks complete. No pending work.");
2457
3782
  return;
2458
3783
  }
2459
- info(`Found ${items.length} pending task(s):`);
3784
+ info(`Found ${items.length} active heartbeat(s):`);
2460
3785
  items.forEach((task, i) => {
2461
- console.log(` ${colors.yellow}${i + 1}.${colors.reset} [${task.agentId}] ${task.taskDescription || "Unnamed task"}`);
2462
- console.log(` ${colors.dim}Status: ${task.status} | Thread: ${task.threadId || "N/A"}${colors.reset}`);
3786
+ console.log(` ${colors.yellow}${i + 1}.${colors.reset} [${task.agentId}] ${task.currentTask || "No current task"}`);
3787
+ console.log(` ${colors.dim}Status: ${task.status} | Pending: ${(task.pendingTasks || []).length} task(s)${colors.reset}`);
2463
3788
  });
2464
3789
  console.log();
2465
3790
  const rl = readline9.createInterface({ input: process.stdin, output: process.stdout });
2466
- const answer = await new Promise((r) => rl.question("Resume pending tasks? (y/N): ", (a) => {
3791
+ const answer = await new Promise((r) => rl.question("Reset stalled heartbeats? (y/N): ", (a) => {
2467
3792
  rl.close();
2468
3793
  r(a.trim());
2469
3794
  }));
2470
3795
  if (answer.toLowerCase() === "y") {
2471
3796
  for (const task of items) {
2472
- info(`Resuming task for agent "${task.agentId}"...`);
3797
+ info(`Resetting heartbeat for agent "${task.agentId}"...`);
2473
3798
  await safeCall(
2474
- () => client.mutation("heartbeat:resume", { _id: task._id }),
2475
- "Failed to resume task"
3799
+ () => client.mutation("heartbeat:updateStatus", { agentId: task.agentId, status: "active", currentTask: void 0 }),
3800
+ "Failed to reset heartbeat"
2476
3801
  );
2477
3802
  }
2478
- success("All pending tasks resumed.");
3803
+ success("All heartbeats reset.");
2479
3804
  }
2480
3805
  });
2481
3806
  }
@@ -2618,6 +3943,844 @@ function registerLoginCommand(program2) {
2618
3943
  });
2619
3944
  }
2620
3945
 
3946
+ // src/commands/channel-telegram.ts
3947
+ import fs10 from "fs-extra";
3948
+ import path10 from "path";
3949
+ import readline10 from "readline";
3950
+ function prompt8(q) {
3951
+ const rl = readline10.createInterface({ input: process.stdin, output: process.stdout });
3952
+ return new Promise((r) => rl.question(q, (a) => {
3953
+ rl.close();
3954
+ r(a.trim());
3955
+ }));
3956
+ }
3957
+ function readEnvValue(key) {
3958
+ const cwd = process.cwd();
3959
+ const envFiles = [".env.local", ".env", ".env.production"];
3960
+ 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");
3964
+ const match = content.match(new RegExp(`^${key}=(.+)$`, "m"));
3965
+ if (match) return match[1].trim().replace(/["']/g, "");
3966
+ }
3967
+ }
3968
+ return void 0;
3969
+ }
3970
+ function writeEnvValue(key, value, envFile = ".env.local") {
3971
+ const envPath = path10.join(process.cwd(), envFile);
3972
+ let content = "";
3973
+ if (fs10.existsSync(envPath)) {
3974
+ content = fs10.readFileSync(envPath, "utf-8");
3975
+ }
3976
+ const lines = content.split("\n");
3977
+ const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
3978
+ if (idx >= 0) {
3979
+ lines[idx] = `${key}=${value}`;
3980
+ } else {
3981
+ lines.push(`${key}=${value}`);
3982
+ }
3983
+ fs10.writeFileSync(envPath, lines.join("\n"));
3984
+ }
3985
+ function registerChannelTelegramCommand(program2) {
3986
+ const channel = program2.command("channel:telegram").description("Manage the Telegram messaging channel");
3987
+ channel.command("start").description("Start the Telegram bot and begin routing messages to an agent").option("-a, --agent <id>", "Agent ID to route messages to").option("-t, --token <token>", "Telegram Bot Token (overrides .env)").option("--webhook-url <url>", "Use webhook mode with this URL").option("--webhook-secret <secret>", "Webhook verification secret").option("--bot-username <username>", "Bot username for @mention detection").option("--polling-interval <ms>", "Polling interval in milliseconds", "1000").option("--log-level <level>", "Log level: debug, info, warn, error", "info").option("--group-mention-only", "Only respond to @mentions in groups", true).action(async (opts) => {
3988
+ header("Telegram Channel");
3989
+ const botToken = opts.token || readEnvValue("TELEGRAM_BOT_TOKEN") || process.env.TELEGRAM_BOT_TOKEN;
3990
+ if (!botToken) {
3991
+ error("Telegram Bot Token not found.");
3992
+ info("Set it with: agentforge channel:telegram configure");
3993
+ info("Or pass it with: --token <bot-token>");
3994
+ info("Or set TELEGRAM_BOT_TOKEN in your .env.local file");
3995
+ process.exit(1);
3996
+ }
3997
+ const convexUrl = readEnvValue("CONVEX_URL") || process.env.CONVEX_URL;
3998
+ if (!convexUrl) {
3999
+ error("CONVEX_URL not found. Run `npx convex dev` first.");
4000
+ process.exit(1);
4001
+ }
4002
+ let agentId = opts.agent;
4003
+ if (!agentId) {
4004
+ agentId = readEnvValue("AGENTFORGE_AGENT_ID") || process.env.AGENTFORGE_AGENT_ID;
4005
+ }
4006
+ if (!agentId) {
4007
+ info("No agent specified. Fetching available agents...");
4008
+ const client = await createClient();
4009
+ const agents = await safeCall(
4010
+ () => client.query("agents:list", {}),
4011
+ "Failed to list agents"
4012
+ );
4013
+ if (!agents || agents.length === 0) {
4014
+ error("No agents found. Create one first: agentforge agents create");
4015
+ process.exit(1);
4016
+ }
4017
+ console.log();
4018
+ agents.forEach((a, i) => {
4019
+ console.log(
4020
+ ` ${colors.cyan}${i + 1}.${colors.reset} ${a.name} ${colors.dim}(${a.id})${colors.reset} \u2014 ${a.model}`
4021
+ );
4022
+ });
4023
+ console.log();
4024
+ const choice = await prompt8("Select agent (number or ID): ");
4025
+ const idx = parseInt(choice) - 1;
4026
+ agentId = idx >= 0 && idx < agents.length ? agents[idx].id : choice;
4027
+ }
4028
+ info(`Agent: ${agentId}`);
4029
+ info(`Convex: ${convexUrl}`);
4030
+ info(`Mode: ${opts.webhookUrl ? "Webhook" : "Long-polling"}`);
4031
+ info(`Log: ${opts.logLevel}`);
4032
+ console.log();
4033
+ let TelegramChannel;
4034
+ try {
4035
+ const corePkg = "@agentforge-ai/core/channels/telegram";
4036
+ const mod = await import(
4037
+ /* @vite-ignore */
4038
+ corePkg
4039
+ );
4040
+ TelegramChannel = mod.TelegramChannel;
4041
+ } catch (importError) {
4042
+ error("Could not import @agentforge-ai/core. Using built-in Telegram runner.");
4043
+ dim(` Error: ${importError.message}`);
4044
+ console.log();
4045
+ await runMinimalTelegramBot({
4046
+ botToken,
4047
+ agentId,
4048
+ convexUrl,
4049
+ logLevel: opts.logLevel,
4050
+ pollingIntervalMs: parseInt(opts.pollingInterval)
4051
+ });
4052
+ return;
4053
+ }
4054
+ try {
4055
+ const channel2 = new TelegramChannel({
4056
+ botToken,
4057
+ agentId,
4058
+ convexUrl,
4059
+ useWebhook: !!opts.webhookUrl,
4060
+ webhookUrl: opts.webhookUrl,
4061
+ webhookSecret: opts.webhookSecret,
4062
+ botUsername: opts.botUsername,
4063
+ groupMentionOnly: opts.groupMentionOnly,
4064
+ pollingIntervalMs: parseInt(opts.pollingInterval),
4065
+ logLevel: opts.logLevel
4066
+ });
4067
+ await channel2.start();
4068
+ success("Telegram bot is running!");
4069
+ dim(" Press Ctrl+C to stop.");
4070
+ await new Promise(() => {
4071
+ });
4072
+ } catch (startError) {
4073
+ error(`Failed to start Telegram bot: ${startError.message}`);
4074
+ process.exit(1);
4075
+ }
4076
+ });
4077
+ channel.command("configure").description("Configure the Telegram bot token and settings").action(async () => {
4078
+ header("Configure Telegram Channel");
4079
+ const currentToken = readEnvValue("TELEGRAM_BOT_TOKEN");
4080
+ if (currentToken) {
4081
+ const masked = currentToken.slice(0, 6) + "****" + currentToken.slice(-4);
4082
+ info(`Current token: ${masked}`);
4083
+ }
4084
+ console.log();
4085
+ info("To get a bot token:");
4086
+ dim(" 1. Open Telegram and search for @BotFather");
4087
+ dim(" 2. Send /newbot and follow the instructions");
4088
+ dim(" 3. Copy the token provided");
4089
+ console.log();
4090
+ const token = await prompt8("Telegram Bot Token: ");
4091
+ if (!token) {
4092
+ error("Bot token is required.");
4093
+ process.exit(1);
4094
+ }
4095
+ info("Validating token...");
4096
+ try {
4097
+ const response = await fetch(`https://api.telegram.org/bot${token}/getMe`);
4098
+ const data = await response.json();
4099
+ if (!data.ok) {
4100
+ error("Invalid bot token. Please check and try again.");
4101
+ process.exit(1);
4102
+ }
4103
+ success(`Bot verified: @${data.result?.username} (${data.result?.first_name})`);
4104
+ if (data.result?.username) {
4105
+ writeEnvValue("TELEGRAM_BOT_USERNAME", data.result.username);
4106
+ }
4107
+ } catch (fetchError) {
4108
+ warn(`Could not validate token (network error): ${fetchError.message}`);
4109
+ info("Saving token anyway. You can validate later with: agentforge channel:telegram status");
4110
+ }
4111
+ writeEnvValue("TELEGRAM_BOT_TOKEN", token);
4112
+ success("Token saved to .env.local");
4113
+ console.log();
4114
+ const defaultAgent = await prompt8("Default agent ID (optional, press Enter to skip): ");
4115
+ if (defaultAgent) {
4116
+ writeEnvValue("AGENTFORGE_AGENT_ID", defaultAgent);
4117
+ success(`Default agent set to: ${defaultAgent}`);
4118
+ }
4119
+ console.log();
4120
+ success("Configuration complete!");
4121
+ info("Start the bot with: agentforge channel:telegram start");
4122
+ });
4123
+ channel.command("status").description("Check the Telegram bot configuration and connectivity").action(async () => {
4124
+ header("Telegram Channel Status");
4125
+ const token = readEnvValue("TELEGRAM_BOT_TOKEN");
4126
+ const agentId = readEnvValue("AGENTFORGE_AGENT_ID");
4127
+ const convexUrl = readEnvValue("CONVEX_URL");
4128
+ const botUsername = readEnvValue("TELEGRAM_BOT_USERNAME");
4129
+ const statusData = {
4130
+ "Bot Token": token ? `${token.slice(0, 6)}****${token.slice(-4)}` : `${colors.red}Not configured${colors.reset}`,
4131
+ "Bot Username": botUsername ? `@${botUsername}` : `${colors.dim}Unknown${colors.reset}`,
4132
+ "Default Agent": agentId || `${colors.dim}Not set${colors.reset}`,
4133
+ "Convex URL": convexUrl || `${colors.red}Not configured${colors.reset}`
4134
+ };
4135
+ details(statusData);
4136
+ if (token) {
4137
+ info("Checking bot connectivity...");
4138
+ try {
4139
+ const response = await fetch(`https://api.telegram.org/bot${token}/getMe`);
4140
+ const data = await response.json();
4141
+ if (data.ok) {
4142
+ success(`Bot online: @${data.result?.username} (ID: ${data.result?.id})`);
4143
+ } else {
4144
+ error("Bot token is invalid or expired.");
4145
+ }
4146
+ } catch {
4147
+ warn("Could not reach Telegram API (network error).");
4148
+ }
4149
+ }
4150
+ if (convexUrl) {
4151
+ info("Checking Convex connectivity...");
4152
+ try {
4153
+ const client = await createClient();
4154
+ const agents = await client.query("agents:list", {});
4155
+ success(`Convex connected. ${agents.length} agents available.`);
4156
+ } catch {
4157
+ warn("Could not reach Convex deployment.");
4158
+ }
4159
+ }
4160
+ });
4161
+ }
4162
+ async function runMinimalTelegramBot(config) {
4163
+ const { botToken, agentId, convexUrl } = config;
4164
+ const apiBase = `https://api.telegram.org/bot${botToken}`;
4165
+ const convexBase = convexUrl.replace(/\/$/, "");
4166
+ const threadMap = /* @__PURE__ */ new Map();
4167
+ let lastUpdateId = 0;
4168
+ info("Verifying bot token...");
4169
+ const meRes = await fetch(`${apiBase}/getMe`);
4170
+ const meData = await meRes.json();
4171
+ if (!meData.ok) {
4172
+ error("Invalid bot token.");
4173
+ process.exit(1);
4174
+ }
4175
+ success(`Bot connected: @${meData.result?.username}`);
4176
+ await fetch(`${apiBase}/deleteWebhook`, { method: "POST" });
4177
+ info("Polling for messages...");
4178
+ dim(" Press Ctrl+C to stop.");
4179
+ console.log();
4180
+ process.on("SIGINT", () => {
4181
+ console.log("\nStopping...");
4182
+ process.exit(0);
4183
+ });
4184
+ async function convexMutation(fn, args) {
4185
+ const res = await fetch(`${convexBase}/api/mutation`, {
4186
+ method: "POST",
4187
+ headers: { "Content-Type": "application/json" },
4188
+ body: JSON.stringify({ path: fn, args })
4189
+ });
4190
+ const data = await res.json();
4191
+ if (data.status === "error") throw new Error(data.errorMessage);
4192
+ return data.value;
4193
+ }
4194
+ async function convexAction(fn, args) {
4195
+ const res = await fetch(`${convexBase}/api/action`, {
4196
+ method: "POST",
4197
+ headers: { "Content-Type": "application/json" },
4198
+ body: JSON.stringify({ path: fn, args })
4199
+ });
4200
+ const data = await res.json();
4201
+ if (data.status === "error") throw new Error(data.errorMessage);
4202
+ return data.value;
4203
+ }
4204
+ async function sendTelegramMessage(chatId, text) {
4205
+ await fetch(`${apiBase}/sendMessage`, {
4206
+ method: "POST",
4207
+ headers: { "Content-Type": "application/json" },
4208
+ body: JSON.stringify({ chat_id: chatId, text })
4209
+ });
4210
+ }
4211
+ async function sendTyping(chatId) {
4212
+ await fetch(`${apiBase}/sendChatAction`, {
4213
+ method: "POST",
4214
+ headers: { "Content-Type": "application/json" },
4215
+ body: JSON.stringify({ chat_id: chatId, action: "typing" })
4216
+ }).catch(() => {
4217
+ });
4218
+ }
4219
+ async function getOrCreateThread(chatId, senderName) {
4220
+ const cached = threadMap.get(chatId);
4221
+ if (cached) return cached;
4222
+ const threadId = await convexMutation("chat:createThread", {
4223
+ agentId,
4224
+ name: senderName ? `Telegram: ${senderName}` : `Telegram Chat ${chatId}`,
4225
+ userId: `telegram:${chatId}`
4226
+ });
4227
+ threadMap.set(chatId, threadId);
4228
+ return threadId;
4229
+ }
4230
+ while (true) {
4231
+ try {
4232
+ const res = await fetch(`${apiBase}/getUpdates`, {
4233
+ method: "POST",
4234
+ headers: { "Content-Type": "application/json" },
4235
+ body: JSON.stringify({
4236
+ offset: lastUpdateId + 1,
4237
+ timeout: 30,
4238
+ allowed_updates: ["message"]
4239
+ })
4240
+ });
4241
+ const data = await res.json();
4242
+ if (!data.ok || !data.result) continue;
4243
+ for (const update of data.result) {
4244
+ lastUpdateId = update.update_id;
4245
+ const msg = update.message;
4246
+ if (!msg?.text) continue;
4247
+ const chatId = String(msg.chat.id);
4248
+ const senderName = msg.from?.first_name || "User";
4249
+ const text = msg.text.trim();
4250
+ if (text === "/start") {
4251
+ threadMap.delete(chatId);
4252
+ await sendTelegramMessage(chatId, `\u{1F44B} Welcome! I'm powered by AgentForge.
4253
+
4254
+ Send me a message and I'll respond using AI.
4255
+
4256
+ Commands:
4257
+ /new \u2014 Start a new conversation
4258
+ /help \u2014 Show help`);
4259
+ continue;
4260
+ }
4261
+ if (text === "/new") {
4262
+ threadMap.delete(chatId);
4263
+ await sendTelegramMessage(chatId, "\u{1F504} New conversation started. Send me a message!");
4264
+ continue;
4265
+ }
4266
+ if (text === "/help") {
4267
+ await sendTelegramMessage(chatId, "\u{1F916} AgentForge Telegram Bot\n\nJust send me a message and I'll respond using AI.\n\nCommands:\n/start \u2014 Reset and show welcome\n/new \u2014 Start a fresh conversation\n/help \u2014 Show this help");
4268
+ continue;
4269
+ }
4270
+ console.log(`[${senderName}] ${text}`);
4271
+ await sendTyping(chatId);
4272
+ try {
4273
+ const threadId = await getOrCreateThread(chatId, senderName);
4274
+ const result = await convexAction("chat:sendMessage", {
4275
+ agentId,
4276
+ threadId,
4277
+ content: text,
4278
+ userId: `telegram:${msg.from?.id || chatId}`
4279
+ });
4280
+ if (result?.response) {
4281
+ const response = result.response;
4282
+ if (response.length <= 4096) {
4283
+ await sendTelegramMessage(chatId, response);
4284
+ } else {
4285
+ const chunks = response.match(/.{1,4096}/gs) || [];
4286
+ for (const chunk of chunks) {
4287
+ await sendTelegramMessage(chatId, chunk);
4288
+ }
4289
+ }
4290
+ console.log(`[Agent] ${response.substring(0, 100)}${response.length > 100 ? "..." : ""}`);
4291
+ } else {
4292
+ await sendTelegramMessage(chatId, "\u{1F914} I couldn't generate a response. Please try again.");
4293
+ }
4294
+ } catch (routeError) {
4295
+ console.error(`Error: ${routeError.message}`);
4296
+ await sendTelegramMessage(chatId, "\u26A0\uFE0F Sorry, I encountered an error. Please try again.");
4297
+ }
4298
+ }
4299
+ } catch (pollError) {
4300
+ if (pollError.message?.includes("ECONNREFUSED") || pollError.message?.includes("fetch failed")) {
4301
+ warn("Network error. Retrying in 5s...");
4302
+ await new Promise((r) => setTimeout(r, 5e3));
4303
+ } else {
4304
+ console.error(`Poll error: ${pollError.message}`);
4305
+ await new Promise((r) => setTimeout(r, 1e3));
4306
+ }
4307
+ }
4308
+ }
4309
+ }
4310
+
4311
+ // src/commands/channel-whatsapp.ts
4312
+ import fs11 from "fs-extra";
4313
+ import path11 from "path";
4314
+ import readline11 from "readline";
4315
+ function prompt9(q) {
4316
+ const rl = readline11.createInterface({ input: process.stdin, output: process.stdout });
4317
+ return new Promise((r) => rl.question(q, (a) => {
4318
+ rl.close();
4319
+ r(a.trim());
4320
+ }));
4321
+ }
4322
+ function readEnvValue2(key) {
4323
+ const cwd = process.cwd();
4324
+ const envFiles = [".env.local", ".env", ".env.production"];
4325
+ 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");
4329
+ const match = content.match(new RegExp(`^${key}=(.+)$`, "m"));
4330
+ if (match) return match[1].trim().replace(/["']/g, "");
4331
+ }
4332
+ }
4333
+ return void 0;
4334
+ }
4335
+ function writeEnvValue2(key, value, envFile = ".env.local") {
4336
+ const envPath = path11.join(process.cwd(), envFile);
4337
+ let content = "";
4338
+ if (fs11.existsSync(envPath)) {
4339
+ content = fs11.readFileSync(envPath, "utf-8");
4340
+ }
4341
+ const lines = content.split("\n");
4342
+ const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
4343
+ if (idx >= 0) {
4344
+ lines[idx] = `${key}=${value}`;
4345
+ } else {
4346
+ lines.push(`${key}=${value}`);
4347
+ }
4348
+ fs11.writeFileSync(envPath, lines.join("\n"));
4349
+ }
4350
+ function registerChannelWhatsAppCommand(program2) {
4351
+ const channel = program2.command("channel:whatsapp").description("Manage the WhatsApp messaging channel");
4352
+ channel.command("start").description("Start the WhatsApp webhook server and begin routing messages to an agent").option("-a, --agent <id>", "Agent ID to route messages to").option("--access-token <token>", "WhatsApp Cloud API access token (overrides .env)").option("--phone-number-id <id>", "WhatsApp Business Phone Number ID (overrides .env)").option("--verify-token <token>", "Webhook verify token (overrides .env)").option("--webhook-port <port>", "Port for the webhook server", "3001").option("--webhook-path <path>", "Path for the webhook endpoint", "/webhook/whatsapp").option("--api-version <version>", "WhatsApp Cloud API version", "v21.0").option("--log-level <level>", "Log level: debug, info, warn, error", "info").action(async (opts) => {
4353
+ header("WhatsApp Channel");
4354
+ const accessToken = opts.accessToken || readEnvValue2("WHATSAPP_ACCESS_TOKEN") || process.env.WHATSAPP_ACCESS_TOKEN;
4355
+ if (!accessToken) {
4356
+ error("WhatsApp Access Token not found.");
4357
+ info("Set it with: agentforge channel:whatsapp configure");
4358
+ info("Or pass it with: --access-token <token>");
4359
+ info("Or set WHATSAPP_ACCESS_TOKEN in your .env.local file");
4360
+ process.exit(1);
4361
+ }
4362
+ const phoneNumberId = opts.phoneNumberId || readEnvValue2("WHATSAPP_PHONE_NUMBER_ID") || process.env.WHATSAPP_PHONE_NUMBER_ID;
4363
+ if (!phoneNumberId) {
4364
+ error("WhatsApp Phone Number ID not found.");
4365
+ info("Set it with: agentforge channel:whatsapp configure");
4366
+ info("Or pass it with: --phone-number-id <id>");
4367
+ info("Or set WHATSAPP_PHONE_NUMBER_ID in your .env.local file");
4368
+ process.exit(1);
4369
+ }
4370
+ const verifyToken = opts.verifyToken || readEnvValue2("WHATSAPP_VERIFY_TOKEN") || process.env.WHATSAPP_VERIFY_TOKEN;
4371
+ if (!verifyToken) {
4372
+ error("WhatsApp Verify Token not found.");
4373
+ info("Set it with: agentforge channel:whatsapp configure");
4374
+ info("Or pass it with: --verify-token <token>");
4375
+ info("Or set WHATSAPP_VERIFY_TOKEN in your .env.local file");
4376
+ process.exit(1);
4377
+ }
4378
+ const convexUrl = readEnvValue2("CONVEX_URL") || process.env.CONVEX_URL;
4379
+ if (!convexUrl) {
4380
+ error("CONVEX_URL not found. Run `npx convex dev` first.");
4381
+ process.exit(1);
4382
+ }
4383
+ let agentId = opts.agent;
4384
+ if (!agentId) {
4385
+ agentId = readEnvValue2("AGENTFORGE_AGENT_ID") || process.env.AGENTFORGE_AGENT_ID;
4386
+ }
4387
+ if (!agentId) {
4388
+ info("No agent specified. Fetching available agents...");
4389
+ const client = await createClient();
4390
+ const agents = await safeCall(
4391
+ () => client.query("agents:list", {}),
4392
+ "Failed to list agents"
4393
+ );
4394
+ if (!agents || agents.length === 0) {
4395
+ error("No agents found. Create one first: agentforge agents create");
4396
+ process.exit(1);
4397
+ }
4398
+ console.log();
4399
+ agents.forEach((a, i) => {
4400
+ console.log(
4401
+ ` ${colors.cyan}${i + 1}.${colors.reset} ${a.name} ${colors.dim}(${a.id})${colors.reset} \u2014 ${a.model}`
4402
+ );
4403
+ });
4404
+ console.log();
4405
+ const choice = await prompt9("Select agent (number or ID): ");
4406
+ const idx = parseInt(choice) - 1;
4407
+ agentId = idx >= 0 && idx < agents.length ? agents[idx].id : choice;
4408
+ }
4409
+ const webhookPort = parseInt(opts.webhookPort);
4410
+ const webhookPath = opts.webhookPath;
4411
+ info(`Agent: ${agentId}`);
4412
+ info(`Convex: ${convexUrl}`);
4413
+ info(`Webhook: http://localhost:${webhookPort}${webhookPath}`);
4414
+ info(`API Version: ${opts.apiVersion}`);
4415
+ info(`Log: ${opts.logLevel}`);
4416
+ console.log();
4417
+ let WhatsAppChannel;
4418
+ try {
4419
+ const corePkg = "@agentforge-ai/core/channels/whatsapp";
4420
+ const mod = await import(
4421
+ /* @vite-ignore */
4422
+ corePkg
4423
+ );
4424
+ WhatsAppChannel = mod.WhatsAppChannel;
4425
+ } catch (importError) {
4426
+ error("Could not import @agentforge-ai/core. Using built-in WhatsApp runner.");
4427
+ dim(` Error: ${importError.message}`);
4428
+ console.log();
4429
+ await runMinimalWhatsAppBot({
4430
+ accessToken,
4431
+ phoneNumberId,
4432
+ verifyToken,
4433
+ agentId,
4434
+ convexUrl,
4435
+ webhookPort,
4436
+ webhookPath,
4437
+ logLevel: opts.logLevel
4438
+ });
4439
+ return;
4440
+ }
4441
+ try {
4442
+ const channel2 = new WhatsAppChannel({
4443
+ accessToken,
4444
+ phoneNumberId,
4445
+ verifyToken,
4446
+ agentId,
4447
+ convexUrl,
4448
+ webhookPort,
4449
+ webhookPath,
4450
+ apiVersion: opts.apiVersion,
4451
+ logLevel: opts.logLevel
4452
+ });
4453
+ await channel2.start();
4454
+ success("WhatsApp webhook server is running!");
4455
+ dim(` Webhook URL: http://localhost:${webhookPort}${webhookPath}`);
4456
+ dim(" Configure this URL in your Meta App Dashboard.");
4457
+ dim(" Press Ctrl+C to stop.");
4458
+ await new Promise(() => {
4459
+ });
4460
+ } catch (startError) {
4461
+ error(`Failed to start WhatsApp channel: ${startError.message}`);
4462
+ process.exit(1);
4463
+ }
4464
+ });
4465
+ channel.command("configure").description("Configure the WhatsApp Cloud API credentials").action(async () => {
4466
+ header("Configure WhatsApp Channel");
4467
+ console.log();
4468
+ info("To set up WhatsApp Cloud API:");
4469
+ dim(" 1. Go to https://developers.facebook.com/apps/");
4470
+ dim(" 2. Create or select a Meta App with WhatsApp product");
4471
+ dim(" 3. Go to WhatsApp > API Setup");
4472
+ dim(" 4. Copy the Access Token, Phone Number ID, and set a Verify Token");
4473
+ console.log();
4474
+ const currentToken = readEnvValue2("WHATSAPP_ACCESS_TOKEN");
4475
+ if (currentToken) {
4476
+ const masked = currentToken.slice(0, 10) + "****" + currentToken.slice(-4);
4477
+ info(`Current access token: ${masked}`);
4478
+ }
4479
+ const accessToken = await prompt9("WhatsApp Access Token: ");
4480
+ if (!accessToken) {
4481
+ error("Access token is required.");
4482
+ process.exit(1);
4483
+ }
4484
+ info("Validating access token...");
4485
+ try {
4486
+ const response = await fetch("https://graph.facebook.com/v21.0/me", {
4487
+ headers: { Authorization: `Bearer ${accessToken}` }
4488
+ });
4489
+ const data = await response.json();
4490
+ if (data.error) {
4491
+ warn(`Token validation warning: ${data.error.message}`);
4492
+ info("Saving token anyway. You can validate later with: agentforge channel:whatsapp status");
4493
+ } else {
4494
+ success(`Token verified: ${data.name || data.id}`);
4495
+ }
4496
+ } catch (fetchError) {
4497
+ warn(`Could not validate token (network error): ${fetchError.message}`);
4498
+ info("Saving token anyway.");
4499
+ }
4500
+ writeEnvValue2("WHATSAPP_ACCESS_TOKEN", accessToken);
4501
+ success("Access token saved to .env.local");
4502
+ console.log();
4503
+ const currentPhoneId = readEnvValue2("WHATSAPP_PHONE_NUMBER_ID");
4504
+ if (currentPhoneId) {
4505
+ info(`Current Phone Number ID: ${currentPhoneId}`);
4506
+ }
4507
+ const phoneNumberId = await prompt9("WhatsApp Phone Number ID: ");
4508
+ if (!phoneNumberId) {
4509
+ error("Phone Number ID is required.");
4510
+ process.exit(1);
4511
+ }
4512
+ info("Validating phone number...");
4513
+ try {
4514
+ const response = await fetch(`https://graph.facebook.com/v21.0/${phoneNumberId}`, {
4515
+ headers: { Authorization: `Bearer ${accessToken}` }
4516
+ });
4517
+ const data = await response.json();
4518
+ if (data.error) {
4519
+ warn(`Phone number validation warning: ${data.error.message}`);
4520
+ } else {
4521
+ success(`Phone number verified: ${data.display_phone_number} (${data.verified_name})`);
4522
+ }
4523
+ } catch {
4524
+ warn("Could not validate phone number (network error).");
4525
+ }
4526
+ writeEnvValue2("WHATSAPP_PHONE_NUMBER_ID", phoneNumberId);
4527
+ success("Phone Number ID saved to .env.local");
4528
+ console.log();
4529
+ const currentVerifyToken = readEnvValue2("WHATSAPP_VERIFY_TOKEN");
4530
+ if (currentVerifyToken) {
4531
+ info(`Current verify token: ${currentVerifyToken.slice(0, 6)}****`);
4532
+ }
4533
+ let verifyToken = await prompt9("Webhook Verify Token (press Enter to auto-generate): ");
4534
+ if (!verifyToken) {
4535
+ verifyToken = `agentforge_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
4536
+ info(`Generated verify token: ${verifyToken}`);
4537
+ }
4538
+ writeEnvValue2("WHATSAPP_VERIFY_TOKEN", verifyToken);
4539
+ success("Verify token saved to .env.local");
4540
+ console.log();
4541
+ const defaultAgent = await prompt9("Default agent ID (optional, press Enter to skip): ");
4542
+ if (defaultAgent) {
4543
+ writeEnvValue2("AGENTFORGE_AGENT_ID", defaultAgent);
4544
+ success(`Default agent set to: ${defaultAgent}`);
4545
+ }
4546
+ console.log();
4547
+ success("Configuration complete!");
4548
+ info("Start the webhook server with: agentforge channel:whatsapp start");
4549
+ console.log();
4550
+ info("Next steps:");
4551
+ dim(" 1. Start the webhook server: agentforge channel:whatsapp start");
4552
+ dim(" 2. Expose the webhook URL (e.g., with ngrok or cloudflared)");
4553
+ dim(" 3. Configure the webhook URL in your Meta App Dashboard");
4554
+ dim(' 4. Subscribe to "messages" webhook field');
4555
+ });
4556
+ channel.command("status").description("Check the WhatsApp channel configuration and connectivity").action(async () => {
4557
+ header("WhatsApp Channel Status");
4558
+ const accessToken = readEnvValue2("WHATSAPP_ACCESS_TOKEN");
4559
+ const phoneNumberId = readEnvValue2("WHATSAPP_PHONE_NUMBER_ID");
4560
+ const verifyToken = readEnvValue2("WHATSAPP_VERIFY_TOKEN");
4561
+ const agentId = readEnvValue2("AGENTFORGE_AGENT_ID");
4562
+ const convexUrl = readEnvValue2("CONVEX_URL");
4563
+ const statusData = {
4564
+ "Access Token": accessToken ? `${accessToken.slice(0, 10)}****${accessToken.slice(-4)}` : `${colors.red}Not configured${colors.reset}`,
4565
+ "Phone Number ID": phoneNumberId || `${colors.red}Not configured${colors.reset}`,
4566
+ "Verify Token": verifyToken ? `${verifyToken.slice(0, 6)}****` : `${colors.red}Not configured${colors.reset}`,
4567
+ "Default Agent": agentId || `${colors.dim}Not set${colors.reset}`,
4568
+ "Convex URL": convexUrl || `${colors.red}Not configured${colors.reset}`
4569
+ };
4570
+ details(statusData);
4571
+ if (accessToken && phoneNumberId) {
4572
+ info("Checking WhatsApp Cloud API connectivity...");
4573
+ try {
4574
+ const response = await fetch(`https://graph.facebook.com/v21.0/${phoneNumberId}`, {
4575
+ headers: { Authorization: `Bearer ${accessToken}` }
4576
+ });
4577
+ const data = await response.json();
4578
+ if (data.error) {
4579
+ error(`API error: ${data.error.message}`);
4580
+ } else {
4581
+ success(`WhatsApp Business: ${data.verified_name || data.display_phone_number} (ID: ${data.id})`);
4582
+ }
4583
+ } catch {
4584
+ warn("Could not reach WhatsApp Cloud API (network error).");
4585
+ }
4586
+ }
4587
+ if (convexUrl) {
4588
+ info("Checking Convex connectivity...");
4589
+ try {
4590
+ const client = await createClient();
4591
+ const agents = await client.query("agents:list", {});
4592
+ success(`Convex connected. ${agents.length} agents available.`);
4593
+ } catch {
4594
+ warn("Could not reach Convex deployment.");
4595
+ }
4596
+ }
4597
+ });
4598
+ }
4599
+ async function runMinimalWhatsAppBot(config) {
4600
+ const { accessToken, phoneNumberId, verifyToken, agentId, convexUrl, webhookPort, webhookPath } = config;
4601
+ const apiBase = `https://graph.facebook.com/v21.0`;
4602
+ const convexBase = convexUrl.replace(/\/$/, "");
4603
+ const threadMap = /* @__PURE__ */ new Map();
4604
+ info("Verifying WhatsApp access token...");
4605
+ try {
4606
+ const res = await fetch(`${apiBase}/${phoneNumberId}`, {
4607
+ headers: { Authorization: `Bearer ${accessToken}` }
4608
+ });
4609
+ const data = await res.json();
4610
+ if (data.error) {
4611
+ error(`API error: ${data.error.message}`);
4612
+ process.exit(1);
4613
+ }
4614
+ success(`WhatsApp Business: ${data.verified_name || data.display_phone_number}`);
4615
+ } catch (fetchError) {
4616
+ warn(`Could not verify token: ${fetchError.message}`);
4617
+ info("Continuing anyway...");
4618
+ }
4619
+ async function convexMutation(fn, args) {
4620
+ const res = await fetch(`${convexBase}/api/mutation`, {
4621
+ method: "POST",
4622
+ headers: { "Content-Type": "application/json" },
4623
+ body: JSON.stringify({ path: fn, args })
4624
+ });
4625
+ const data = await res.json();
4626
+ if (data.status === "error") throw new Error(data.errorMessage);
4627
+ return data.value;
4628
+ }
4629
+ async function convexAction(fn, args) {
4630
+ const res = await fetch(`${convexBase}/api/action`, {
4631
+ method: "POST",
4632
+ headers: { "Content-Type": "application/json" },
4633
+ body: JSON.stringify({ path: fn, args })
4634
+ });
4635
+ const data = await res.json();
4636
+ if (data.status === "error") throw new Error(data.errorMessage);
4637
+ return data.value;
4638
+ }
4639
+ async function sendWhatsAppMessage(to, text) {
4640
+ await fetch(`${apiBase}/${phoneNumberId}/messages`, {
4641
+ method: "POST",
4642
+ headers: {
4643
+ Authorization: `Bearer ${accessToken}`,
4644
+ "Content-Type": "application/json"
4645
+ },
4646
+ body: JSON.stringify({
4647
+ messaging_product: "whatsapp",
4648
+ to,
4649
+ type: "text",
4650
+ text: { body: text }
4651
+ })
4652
+ });
4653
+ }
4654
+ async function markAsRead(messageId) {
4655
+ await fetch(`${apiBase}/${phoneNumberId}/messages`, {
4656
+ method: "POST",
4657
+ headers: {
4658
+ Authorization: `Bearer ${accessToken}`,
4659
+ "Content-Type": "application/json"
4660
+ },
4661
+ body: JSON.stringify({
4662
+ messaging_product: "whatsapp",
4663
+ status: "read",
4664
+ message_id: messageId
4665
+ })
4666
+ }).catch(() => {
4667
+ });
4668
+ }
4669
+ async function getOrCreateThread(phoneNumber, senderName) {
4670
+ const cached = threadMap.get(phoneNumber);
4671
+ if (cached) return cached;
4672
+ const threadId = await convexMutation("chat:createThread", {
4673
+ agentId,
4674
+ name: senderName ? `WhatsApp: ${senderName}` : `WhatsApp +${phoneNumber}`,
4675
+ userId: `whatsapp:${phoneNumber}`
4676
+ });
4677
+ threadMap.set(phoneNumber, threadId);
4678
+ return threadId;
4679
+ }
4680
+ const http = await import("http");
4681
+ const server = http.createServer(async (req, res) => {
4682
+ const url = new URL(req.url || "/", `http://localhost:${webhookPort}`);
4683
+ if (url.pathname !== webhookPath) {
4684
+ res.writeHead(404);
4685
+ res.end("Not Found");
4686
+ return;
4687
+ }
4688
+ if (req.method === "GET") {
4689
+ const mode = url.searchParams.get("hub.mode");
4690
+ const token = url.searchParams.get("hub.verify_token");
4691
+ const challenge = url.searchParams.get("hub.challenge");
4692
+ if (mode === "subscribe" && token === verifyToken) {
4693
+ res.writeHead(200, { "Content-Type": "text/plain" });
4694
+ res.end(challenge);
4695
+ } else {
4696
+ res.writeHead(403);
4697
+ res.end("Forbidden");
4698
+ }
4699
+ return;
4700
+ }
4701
+ if (req.method === "POST") {
4702
+ try {
4703
+ const chunks = [];
4704
+ for await (const chunk of req) {
4705
+ chunks.push(chunk);
4706
+ }
4707
+ const body = JSON.parse(Buffer.concat(chunks).toString());
4708
+ res.writeHead(200);
4709
+ res.end("OK");
4710
+ if (body.object !== "whatsapp_business_account") return;
4711
+ for (const entry of body.entry || []) {
4712
+ for (const change of entry.changes || []) {
4713
+ if (change.field !== "messages") continue;
4714
+ const contacts = change.value.contacts || [];
4715
+ const messages = change.value.messages || [];
4716
+ for (const msg of messages) {
4717
+ if (msg.type !== "text" || !msg.text?.body) continue;
4718
+ const from = msg.from;
4719
+ const text = msg.text.body.trim();
4720
+ const contact = contacts.find((c) => c.wa_id === from);
4721
+ const senderName = contact?.profile?.name || from;
4722
+ console.log(`[${senderName}] ${text}`);
4723
+ await markAsRead(msg.id);
4724
+ try {
4725
+ const threadId = await getOrCreateThread(from, senderName);
4726
+ const result = await convexAction("chat:sendMessage", {
4727
+ agentId,
4728
+ threadId,
4729
+ content: text,
4730
+ userId: `whatsapp:${from}`
4731
+ });
4732
+ if (result?.response) {
4733
+ const response = result.response;
4734
+ if (response.length <= 4096) {
4735
+ await sendWhatsAppMessage(from, response);
4736
+ } else {
4737
+ const chunks2 = response.match(/.{1,4096}/gs) || [];
4738
+ for (const chunk of chunks2) {
4739
+ await sendWhatsAppMessage(from, chunk);
4740
+ }
4741
+ }
4742
+ console.log(`[Agent] ${response.substring(0, 100)}${response.length > 100 ? "..." : ""}`);
4743
+ } else {
4744
+ await sendWhatsAppMessage(from, "\u{1F914} I couldn't generate a response. Please try again.");
4745
+ }
4746
+ } catch (routeError) {
4747
+ console.error(`Error: ${routeError.message}`);
4748
+ await sendWhatsAppMessage(from, "\u26A0\uFE0F Sorry, I encountered an error. Please try again.");
4749
+ }
4750
+ }
4751
+ }
4752
+ }
4753
+ } catch (parseError) {
4754
+ console.error(`Parse error: ${parseError.message}`);
4755
+ if (!res.headersSent) {
4756
+ res.writeHead(400);
4757
+ res.end("Bad Request");
4758
+ }
4759
+ }
4760
+ return;
4761
+ }
4762
+ res.writeHead(405);
4763
+ res.end("Method Not Allowed");
4764
+ });
4765
+ process.on("SIGINT", () => {
4766
+ console.log("\nStopping...");
4767
+ server.close();
4768
+ process.exit(0);
4769
+ });
4770
+ server.listen(webhookPort, () => {
4771
+ success(`Webhook server listening on port ${webhookPort}`);
4772
+ info(`Webhook URL: http://localhost:${webhookPort}${webhookPath}`);
4773
+ console.log();
4774
+ info("Next steps:");
4775
+ dim(" 1. Expose this URL publicly (e.g., ngrok http " + webhookPort + ")");
4776
+ dim(" 2. Configure the webhook URL in your Meta App Dashboard");
4777
+ dim(' 3. Subscribe to "messages" webhook field');
4778
+ dim(" Press Ctrl+C to stop.");
4779
+ });
4780
+ await new Promise(() => {
4781
+ });
4782
+ }
4783
+
2621
4784
  // src/index.ts
2622
4785
  import { readFileSync } from "fs";
2623
4786
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -2630,7 +4793,7 @@ program.name("agentforge").description("AgentForge \u2014 NanoClaw: A minimalist
2630
4793
  program.command("create").argument("<project-name>", "Name of the project to create").description("Create a new AgentForge project").option("-t, --template <template>", "Project template to use", "default").action(async (projectName, options) => {
2631
4794
  await createProject(projectName, options);
2632
4795
  });
2633
- program.command("run").description("Start the local development environment").option("-p, --port <port>", "Port for the dev server", "3000").action(async (options) => {
4796
+ program.command("run").description("Start the local development environment").option("-p, --port <port>", "Port for the dev server", "3000").option("-s, --sandbox <type>", "Sandbox provider for agent execution (local, docker, e2b, none)", "local").action(async (options) => {
2634
4797
  await runProject(options);
2635
4798
  });
2636
4799
  program.command("deploy").description("Deploy the project to production").option("--env <path>", "Path to environment file", ".env.production").option("--dry-run", "Preview deployment without executing", false).option("--rollback", "Rollback to previous deployment", false).option("--force", "Skip confirmation prompts", false).option("--provider <provider>", "Deployment provider (convex or cloud)", "convex").option("--project <projectId>", "Project ID for cloud deployments").option("--version <tag>", "Version tag for the deployment").action(async (options) => {
@@ -2649,6 +4812,8 @@ registerProjectsCommand(program);
2649
4812
  registerConfigCommand(program);
2650
4813
  registerVaultCommand(program);
2651
4814
  registerKeysCommand(program);
4815
+ registerChannelTelegramCommand(program);
4816
+ registerChannelWhatsAppCommand(program);
2652
4817
  registerStatusCommand(program);
2653
4818
  program.parse();
2654
4819
  //# sourceMappingURL=index.js.map