@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/LICENSE +197 -0
- package/dist/default/agentforge.config.ts +126 -6
- package/dist/default/convex/agents.ts +15 -21
- package/dist/default/convex/chat.ts +302 -0
- package/dist/default/convex/mastraIntegration.ts +101 -69
- package/dist/default/dashboard/app/routes/chat.tsx +462 -167
- package/dist/default/skills/browser-automation/SKILL.md +137 -0
- package/dist/default/skills/browser-automation/config.json +11 -0
- package/dist/default/skills/browser-automation/index.ts +93 -0
- package/dist/default/skills/skill-creator/SKILL.md +69 -230
- package/dist/index.js +2455 -290
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
- package/templates/default/agentforge.config.ts +126 -6
- package/templates/default/convex/agents.ts +15 -21
- package/templates/default/convex/chat.ts +302 -0
- package/templates/default/convex/mastraIntegration.ts +101 -69
- package/templates/default/dashboard/app/routes/chat.tsx +462 -167
- package/templates/default/skills/browser-automation/SKILL.md +137 -0
- package/templates/default/skills/browser-automation/config.json +11 -0
- package/templates/default/skills/browser-automation/index.ts +93 -0
- package/templates/default/skills/skill-creator/SKILL.md +69 -230
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(
|
|
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
|
|
300
|
-
return `${base}${
|
|
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 ||
|
|
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:
|
|
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:
|
|
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", {
|
|
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:
|
|
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", {
|
|
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:
|
|
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", {
|
|
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:
|
|
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", {
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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.
|
|
1207
|
-
details({ ID: s._id,
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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", {
|
|
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
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
-
*
|
|
1348
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1654
|
+
for (const { from, to } of renames) {
|
|
1655
|
+
renameSync(join(dir, from), join(dir, to));
|
|
1656
|
+
}
|
|
1379
1657
|
|
|
1380
|
-
|
|
1658
|
+
console.log(JSON.stringify({ renames, count: renames.length }));
|
|
1381
1659
|
`);
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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.
|
|
1537
|
-
Endpoint: c.
|
|
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
|
-
|
|
1555
|
-
|
|
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", {
|
|
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.
|
|
2906
|
+
if (conn.protocol === "http" || conn.protocol === "sse") {
|
|
1578
2907
|
try {
|
|
1579
|
-
const res = await fetch(conn.
|
|
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:
|
|
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.
|
|
1591
|
-
info(`Endpoint: ${conn.
|
|
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", {
|
|
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", {
|
|
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
|
-
|
|
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", {
|
|
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", {
|
|
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
|
|
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", {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2038
|
-
|
|
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
|
-
|
|
2044
|
-
|
|
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", {
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
2223
|
-
const rl =
|
|
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:
|
|
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}
|
|
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.
|
|
2462
|
-
console.log(` ${colors.dim}Status: ${task.status} |
|
|
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("
|
|
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(`
|
|
3797
|
+
info(`Resetting heartbeat for agent "${task.agentId}"...`);
|
|
2473
3798
|
await safeCall(
|
|
2474
|
-
() => client.mutation("heartbeat:
|
|
2475
|
-
"Failed to
|
|
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
|
|
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
|