@economic/agents 0.0.1 → 1.0.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/README.md +323 -429
- package/bin/cli.mjs +2 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +561 -0
- package/dist/hono.d.mts +21 -0
- package/dist/hono.mjs +71 -0
- package/dist/index.d.mts +124 -46
- package/dist/index.mjs +280 -117
- package/package.json +20 -8
- package/schema/agent.sql +12 -0
- package/schema/{schema.sql → chat.sql} +1 -5
- package/dist/react.d.mts +0 -25
- package/dist/react.mjs +0 -38
package/bin/cli.mjs
ADDED
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
//#region src/cli/utils.ts
|
|
6
|
+
function toKebabCase(str) {
|
|
7
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
function toCamelCase(str) {
|
|
10
|
+
return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toLowerCase());
|
|
11
|
+
}
|
|
12
|
+
function toSnakeCase(str) {
|
|
13
|
+
return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
function ensureDir(dir) {
|
|
16
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
function writeFile(filePath, content) {
|
|
19
|
+
ensureDir(path.dirname(filePath));
|
|
20
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
21
|
+
}
|
|
22
|
+
function fileExists(filePath) {
|
|
23
|
+
return fs.existsSync(filePath);
|
|
24
|
+
}
|
|
25
|
+
function findSkills(cwd) {
|
|
26
|
+
const skillsDir = path.join(cwd, "src", "skills");
|
|
27
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
28
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
29
|
+
}
|
|
30
|
+
function getSrcDir(cwd) {
|
|
31
|
+
return path.join(cwd, "src");
|
|
32
|
+
}
|
|
33
|
+
function readFile(filePath) {
|
|
34
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
35
|
+
}
|
|
36
|
+
function globSync(dir, pattern, results = []) {
|
|
37
|
+
if (!fs.existsSync(dir)) return results;
|
|
38
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const fullPath = path.join(dir, entry.name);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
if (entry.name !== "node_modules") globSync(fullPath, pattern, results);
|
|
43
|
+
} else if (pattern.test(entry.name)) results.push(fullPath);
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
const IMPORT_PATTERN = /import\s+\{[^}]*\b(ChatAgentHarness|ChatAgent|Agent)\b[^}]*\}\s+from\s+["']@economic\/agents["']/;
|
|
48
|
+
const CLASS_PATTERN = /class\s+(\w+)\s+extends\s+(ChatAgentHarness|ChatAgent|Agent)(?:<|[\s{])/;
|
|
49
|
+
function parseAgentFromContent(content, filePath) {
|
|
50
|
+
const importMatch = content.match(IMPORT_PATTERN);
|
|
51
|
+
if (!importMatch) return null;
|
|
52
|
+
const importedClasses = importMatch[0];
|
|
53
|
+
const classMatch = content.match(CLASS_PATTERN);
|
|
54
|
+
if (!classMatch) return null;
|
|
55
|
+
const [, className, baseClass] = classMatch;
|
|
56
|
+
if (!importedClasses.includes(baseClass)) return null;
|
|
57
|
+
return {
|
|
58
|
+
path: filePath,
|
|
59
|
+
className,
|
|
60
|
+
baseClass
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function findAgentFiles(cwd) {
|
|
64
|
+
const tsFiles = globSync(getSrcDir(cwd), /\.tsx?$/);
|
|
65
|
+
const agents = [];
|
|
66
|
+
for (const filePath of tsFiles) {
|
|
67
|
+
const agent = parseAgentFromContent(readFile(filePath), filePath);
|
|
68
|
+
if (agent) agents.push(agent);
|
|
69
|
+
}
|
|
70
|
+
return agents;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/cli/templates/skill.ts
|
|
74
|
+
function generateSkillTemplate(options) {
|
|
75
|
+
const { name, camelName, description, toolImports, toolNames } = options;
|
|
76
|
+
return `import type { Skill } from "@economic/agents";
|
|
77
|
+
${toolImports.length > 0 ? toolImports.map((t) => `import { ${t} } from "./tools/${t}";`).join("\n") + "\n" : ""}
|
|
78
|
+
export const ${camelName}Skill = {
|
|
79
|
+
name: "${name}",
|
|
80
|
+
description: "${description}",
|
|
81
|
+
guidance:
|
|
82
|
+
"TODO: Add guidance for the LLM on how to use this skill's tools. " +
|
|
83
|
+
"Describe the workflow, when to call each tool, and how to present results.",
|
|
84
|
+
tools: {
|
|
85
|
+
${toolNames.length > 0 ? toolNames.map((t) => ` ${t},`).join("\n") : " // Add your tools here"}
|
|
86
|
+
},
|
|
87
|
+
} satisfies Skill;
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/cli/templates/tool.ts
|
|
92
|
+
function generateToolTemplate(options) {
|
|
93
|
+
const { snakeName, description, useContext } = options;
|
|
94
|
+
if (useContext) return `import { tool } from "ai";
|
|
95
|
+
import { z } from "zod";
|
|
96
|
+
import type { AgentToolContext } from "@economic/agents";
|
|
97
|
+
|
|
98
|
+
export const ${snakeName} = tool({
|
|
99
|
+
description: "${description}",
|
|
100
|
+
inputSchema: z.object({
|
|
101
|
+
// Add your input parameters here
|
|
102
|
+
// example: z.string().describe("Description of the parameter"),
|
|
103
|
+
}),
|
|
104
|
+
execute: async (input, { experimental_context }) => {
|
|
105
|
+
const ctx = experimental_context as AgentToolContext;
|
|
106
|
+
|
|
107
|
+
// Implement your tool logic here
|
|
108
|
+
return {
|
|
109
|
+
result: "TODO: implement ${snakeName}",
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
`;
|
|
114
|
+
return `import { tool } from "ai";
|
|
115
|
+
import { z } from "zod";
|
|
116
|
+
|
|
117
|
+
export const ${snakeName} = tool({
|
|
118
|
+
description: "${description}",
|
|
119
|
+
inputSchema: z.object({
|
|
120
|
+
// Add your input parameters here
|
|
121
|
+
// example: z.string().describe("Description of the parameter"),
|
|
122
|
+
}),
|
|
123
|
+
execute: async (input) => {
|
|
124
|
+
// Implement your tool logic here
|
|
125
|
+
return {
|
|
126
|
+
result: "TODO: implement ${snakeName}",
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/cli/codegen.ts
|
|
134
|
+
function addImport(content, importName, importPath) {
|
|
135
|
+
const importStatement = `import { ${importName} } from "${importPath}";`;
|
|
136
|
+
const lines = content.split("\n");
|
|
137
|
+
let lastRelativeImportIndex = -1;
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
const line = lines[i];
|
|
140
|
+
if (/^import\s+.*from\s+["']\.\//m.test(line)) lastRelativeImportIndex = i;
|
|
141
|
+
}
|
|
142
|
+
if (lastRelativeImportIndex === -1) {
|
|
143
|
+
for (let i = 0; i < lines.length; i++) if (/^import\s+/m.test(lines[i])) lastRelativeImportIndex = i;
|
|
144
|
+
}
|
|
145
|
+
if (lastRelativeImportIndex === -1) return importStatement + "\n" + content;
|
|
146
|
+
lines.splice(lastRelativeImportIndex + 1, 0, importStatement);
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
}
|
|
149
|
+
function hasComplexPattern(code) {
|
|
150
|
+
return /\.\.\./.test(code) || /\w+\s*\(/.test(code);
|
|
151
|
+
}
|
|
152
|
+
function addToArrayReturn(methodContent, itemName) {
|
|
153
|
+
const returnMatch = methodContent.match(/return\s*\[([^\]]*)\]/s);
|
|
154
|
+
if (!returnMatch) return null;
|
|
155
|
+
const [fullMatch, arrayContent] = returnMatch;
|
|
156
|
+
if (hasComplexPattern(arrayContent)) return null;
|
|
157
|
+
const trimmed = arrayContent.trim();
|
|
158
|
+
const newReturn = `return [${trimmed ? `${trimmed}, ${itemName}` : itemName}]`;
|
|
159
|
+
return methodContent.replace(fullMatch, newReturn);
|
|
160
|
+
}
|
|
161
|
+
function addToObjectReturn(methodContent, itemName) {
|
|
162
|
+
const returnMatch = methodContent.match(/return\s*\{([^}]*)\}/s);
|
|
163
|
+
if (!returnMatch) return null;
|
|
164
|
+
const [fullMatch, objectContent] = returnMatch;
|
|
165
|
+
if (hasComplexPattern(objectContent)) return null;
|
|
166
|
+
const trimmed = objectContent.trim();
|
|
167
|
+
const newReturn = `return {${trimmed ? `${trimmed}, ${itemName}` : ` ${itemName} `}}`;
|
|
168
|
+
return methodContent.replace(fullMatch, newReturn);
|
|
169
|
+
}
|
|
170
|
+
function findMethodBounds(content, methodName) {
|
|
171
|
+
const match = new RegExp(`(${methodName})\\s*\\([^)]*\\)\\s*(?::\\s*[^{]+)?\\s*\\{`, "g").exec(content);
|
|
172
|
+
if (!match) return null;
|
|
173
|
+
const start = match.index;
|
|
174
|
+
let braceCount = 0;
|
|
175
|
+
let inMethod = false;
|
|
176
|
+
let end = start;
|
|
177
|
+
for (let i = start; i < content.length; i++) if (content[i] === "{") {
|
|
178
|
+
braceCount++;
|
|
179
|
+
inMethod = true;
|
|
180
|
+
} else if (content[i] === "}") {
|
|
181
|
+
braceCount--;
|
|
182
|
+
if (inMethod && braceCount === 0) {
|
|
183
|
+
end = i + 1;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
start,
|
|
189
|
+
end
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function updateHarnessMethod(content, methodName, itemName) {
|
|
193
|
+
const bounds = findMethodBounds(content, methodName);
|
|
194
|
+
if (!bounds) return null;
|
|
195
|
+
const methodContent = content.slice(bounds.start, bounds.end);
|
|
196
|
+
let newMethodContent;
|
|
197
|
+
if (methodName === "getSkills") newMethodContent = addToArrayReturn(methodContent, itemName);
|
|
198
|
+
else newMethodContent = addToObjectReturn(methodContent, itemName);
|
|
199
|
+
if (!newMethodContent) return null;
|
|
200
|
+
return content.slice(0, bounds.start) + newMethodContent + content.slice(bounds.end);
|
|
201
|
+
}
|
|
202
|
+
function findClassEnd(content, className) {
|
|
203
|
+
const match = new RegExp(`class\\s+${className}\\s+extends`).exec(content);
|
|
204
|
+
if (!match) return null;
|
|
205
|
+
let braceCount = 0;
|
|
206
|
+
let inClass = false;
|
|
207
|
+
for (let i = match.index; i < content.length; i++) if (content[i] === "{") {
|
|
208
|
+
braceCount++;
|
|
209
|
+
inClass = true;
|
|
210
|
+
} else if (content[i] === "}") {
|
|
211
|
+
braceCount--;
|
|
212
|
+
if (inClass && braceCount === 0) return i;
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
function addMethodToClass(content, className, methodName, itemName) {
|
|
217
|
+
const classEnd = findClassEnd(content, className);
|
|
218
|
+
if (classEnd === null) return null;
|
|
219
|
+
let methodCode;
|
|
220
|
+
if (methodName === "getSkills") methodCode = `\n ${methodName}() {\n return [${itemName}];\n }\n`;
|
|
221
|
+
else methodCode = `\n ${methodName}() {\n return { ${itemName} };\n }\n`;
|
|
222
|
+
return content.slice(0, classEnd) + methodCode + content.slice(classEnd);
|
|
223
|
+
}
|
|
224
|
+
function updateBuildLLMParams(content, propertyName, itemName) {
|
|
225
|
+
const paramsMatch = content.match(/buildLLMParams\s*\(\s*\{/);
|
|
226
|
+
if (!paramsMatch) return null;
|
|
227
|
+
const paramsStart = paramsMatch.index;
|
|
228
|
+
let braceCount = 0;
|
|
229
|
+
let paramsEnd = paramsStart;
|
|
230
|
+
let inParams = false;
|
|
231
|
+
for (let i = paramsStart; i < content.length; i++) if (content[i] === "{") {
|
|
232
|
+
braceCount++;
|
|
233
|
+
inParams = true;
|
|
234
|
+
} else if (content[i] === "}") {
|
|
235
|
+
braceCount--;
|
|
236
|
+
if (inParams && braceCount === 0) {
|
|
237
|
+
paramsEnd = i + 1;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const paramsContent = content.slice(paramsStart, paramsEnd);
|
|
242
|
+
if (new RegExp(`${propertyName}\\s*:`).test(paramsContent)) {
|
|
243
|
+
const propertyLiteralPattern = new RegExp(`${propertyName}\\s*:\\s*(\\[[^\\]]*\\]|\\{[^}]*\\})`);
|
|
244
|
+
const propertyMatch = paramsContent.match(propertyLiteralPattern);
|
|
245
|
+
if (!propertyMatch) return null;
|
|
246
|
+
const [fullPropertyMatch, propertyValue] = propertyMatch;
|
|
247
|
+
if (hasComplexPattern(propertyValue)) return null;
|
|
248
|
+
let newPropertyValue;
|
|
249
|
+
if (propertyName === "skills") {
|
|
250
|
+
const inner = propertyValue.slice(1, -1).trim();
|
|
251
|
+
newPropertyValue = inner ? `[${inner}, ${itemName}]` : `[${itemName}]`;
|
|
252
|
+
} else {
|
|
253
|
+
const inner = propertyValue.slice(1, -1).trim();
|
|
254
|
+
newPropertyValue = inner ? `{ ${inner}, ${itemName} }` : `{ ${itemName} }`;
|
|
255
|
+
}
|
|
256
|
+
const newPropertyDecl = `${propertyName}: ${newPropertyValue}`;
|
|
257
|
+
const newParamsContent = paramsContent.replace(fullPropertyMatch, newPropertyDecl);
|
|
258
|
+
return content.slice(0, paramsStart) + newParamsContent + content.slice(paramsEnd);
|
|
259
|
+
}
|
|
260
|
+
const insertPoint = paramsStart + paramsMatch[0].length;
|
|
261
|
+
let newProperty;
|
|
262
|
+
if (propertyName === "skills") newProperty = `\n ${propertyName}: [${itemName}],`;
|
|
263
|
+
else newProperty = `\n ${propertyName}: { ${itemName} },`;
|
|
264
|
+
return content.slice(0, insertPoint) + newProperty + content.slice(insertPoint);
|
|
265
|
+
}
|
|
266
|
+
function registerSkill(agent, skillName, skillImportName, skillImportPath, agentFilePath) {
|
|
267
|
+
let content = readFile(agentFilePath);
|
|
268
|
+
const relativePath = path.relative(path.dirname(agentFilePath), skillImportPath).replace(/\.ts$/, "").replace(/^(?!\.)/, "./");
|
|
269
|
+
content = addImport(content, skillImportName, relativePath);
|
|
270
|
+
let updated = null;
|
|
271
|
+
if (agent.baseClass === "ChatAgentHarness") {
|
|
272
|
+
updated = updateHarnessMethod(content, "getSkills", skillImportName);
|
|
273
|
+
if (!updated) updated = addMethodToClass(content, agent.className, "getSkills", skillImportName);
|
|
274
|
+
} else updated = updateBuildLLMParams(content, "skills", skillImportName);
|
|
275
|
+
if (!updated) return {
|
|
276
|
+
success: false,
|
|
277
|
+
message: `Could not auto-register skill. Add manually:\n 1. Import: import { ${skillImportName} } from "${relativePath}";\n 2. Add ${skillImportName} to your skills array`
|
|
278
|
+
};
|
|
279
|
+
writeFile(agentFilePath, updated);
|
|
280
|
+
return {
|
|
281
|
+
success: true,
|
|
282
|
+
message: `Registered ${skillImportName} in ${path.basename(agentFilePath)}`
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function registerTool(agent, toolName, toolImportName, toolImportPath, agentFilePath) {
|
|
286
|
+
let content = readFile(agentFilePath);
|
|
287
|
+
const relativePath = path.relative(path.dirname(agentFilePath), toolImportPath).replace(/\.ts$/, "").replace(/^(?!\.)/, "./");
|
|
288
|
+
content = addImport(content, toolImportName, relativePath);
|
|
289
|
+
let updated = null;
|
|
290
|
+
if (agent.baseClass === "ChatAgentHarness") {
|
|
291
|
+
updated = updateHarnessMethod(content, "getTools", toolImportName);
|
|
292
|
+
if (!updated) updated = addMethodToClass(content, agent.className, "getTools", toolImportName);
|
|
293
|
+
} else updated = updateBuildLLMParams(content, "tools", toolImportName);
|
|
294
|
+
if (!updated) return {
|
|
295
|
+
success: false,
|
|
296
|
+
message: `Could not auto-register tool. Add manually:\n 1. Import: import { ${toolImportName} } from "${relativePath}";\n 2. Add ${toolImportName} to your tools object`
|
|
297
|
+
};
|
|
298
|
+
writeFile(agentFilePath, updated);
|
|
299
|
+
return {
|
|
300
|
+
success: true,
|
|
301
|
+
message: `Registered ${toolImportName} in ${path.basename(agentFilePath)}`
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/cli/commands/generate-skill.ts
|
|
306
|
+
async function generateSkill(name) {
|
|
307
|
+
const cwd = process.cwd();
|
|
308
|
+
const kebabName = toKebabCase(name);
|
|
309
|
+
const camelName = toCamelCase(name);
|
|
310
|
+
const skillDir = path.join(getSrcDir(cwd), "skills", kebabName);
|
|
311
|
+
const skillFile = path.join(skillDir, `${kebabName}.ts`);
|
|
312
|
+
p.intro(`Creating skill: ${kebabName}`);
|
|
313
|
+
if (fileExists(skillFile)) {
|
|
314
|
+
p.cancel(`Skill already exists at ${skillFile}`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
const answers = await p.group({
|
|
318
|
+
description: () => p.text({
|
|
319
|
+
message: "Skill description",
|
|
320
|
+
placeholder: "What does this skill do?",
|
|
321
|
+
validate: (value) => {
|
|
322
|
+
if (!value) return "Description is required";
|
|
323
|
+
}
|
|
324
|
+
}),
|
|
325
|
+
tools: () => p.text({
|
|
326
|
+
message: "Initial tools (comma-separated, leave empty to skip)",
|
|
327
|
+
placeholder: "e.g., calculate_tax, generate_invoice"
|
|
328
|
+
})
|
|
329
|
+
}, { onCancel: () => {
|
|
330
|
+
p.cancel("Operation cancelled");
|
|
331
|
+
process.exit(0);
|
|
332
|
+
} });
|
|
333
|
+
const toolNames = answers.tools ? answers.tools.split(",").map((t) => t.trim()).filter(Boolean).map(toSnakeCase) : [];
|
|
334
|
+
const toolDescriptions = {};
|
|
335
|
+
const toolUseContext = {};
|
|
336
|
+
if (toolNames.length > 0) {
|
|
337
|
+
p.note(`Configuring ${toolNames.length} tool(s)...`);
|
|
338
|
+
for (const toolName of toolNames) {
|
|
339
|
+
const desc = await p.text({
|
|
340
|
+
message: `Description for ${toolName}`,
|
|
341
|
+
placeholder: "What does this tool do?",
|
|
342
|
+
validate: (value) => {
|
|
343
|
+
if (!value) return "Description is required";
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
if (p.isCancel(desc)) {
|
|
347
|
+
p.cancel("Operation cancelled");
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
toolDescriptions[toolName] = desc;
|
|
351
|
+
const useContext = await p.confirm({
|
|
352
|
+
message: `Does ${toolName} need access to AgentToolContext?`,
|
|
353
|
+
initialValue: false
|
|
354
|
+
});
|
|
355
|
+
if (p.isCancel(useContext)) {
|
|
356
|
+
p.cancel("Operation cancelled");
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
toolUseContext[toolName] = useContext;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const spinner = p.spinner();
|
|
363
|
+
spinner.start("Creating files...");
|
|
364
|
+
writeFile(skillFile, generateSkillTemplate({
|
|
365
|
+
name: kebabName,
|
|
366
|
+
camelName,
|
|
367
|
+
description: answers.description,
|
|
368
|
+
toolImports: toolNames,
|
|
369
|
+
toolNames
|
|
370
|
+
}));
|
|
371
|
+
const createdFiles = [skillFile];
|
|
372
|
+
for (const toolName of toolNames) {
|
|
373
|
+
const toolFile = path.join(skillDir, "tools", `${toolName}.ts`);
|
|
374
|
+
writeFile(toolFile, generateToolTemplate({
|
|
375
|
+
snakeName: toolName,
|
|
376
|
+
description: toolDescriptions[toolName],
|
|
377
|
+
useContext: toolUseContext[toolName]
|
|
378
|
+
}));
|
|
379
|
+
createdFiles.push(toolFile);
|
|
380
|
+
}
|
|
381
|
+
spinner.stop("Files created");
|
|
382
|
+
p.note(createdFiles.map((f) => ` ${path.relative(cwd, f)}`).join("\n"), "Created files");
|
|
383
|
+
const skillImportName = `${camelName}Skill`;
|
|
384
|
+
const agents = findAgentFiles(cwd);
|
|
385
|
+
let selectedAgent = null;
|
|
386
|
+
if (agents.length === 1) selectedAgent = agents[0];
|
|
387
|
+
else if (agents.length > 1) {
|
|
388
|
+
const choice = await p.select({
|
|
389
|
+
message: "Which agent should this skill be registered with?",
|
|
390
|
+
options: agents.map((a) => ({
|
|
391
|
+
value: a.path,
|
|
392
|
+
label: `${a.className} (${path.relative(cwd, a.path)})`
|
|
393
|
+
}))
|
|
394
|
+
});
|
|
395
|
+
if (p.isCancel(choice)) {
|
|
396
|
+
p.outro(`Skill created. Add ${skillImportName} to your agent manually.`);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
selectedAgent = agents.find((a) => a.path === choice) ?? null;
|
|
400
|
+
}
|
|
401
|
+
if (selectedAgent) {
|
|
402
|
+
const result = registerSkill(selectedAgent, kebabName, skillImportName, skillFile, selectedAgent.path);
|
|
403
|
+
if (result.success) p.note(result.message, "Registered skill");
|
|
404
|
+
else p.note(result.message, "Manual registration required");
|
|
405
|
+
} else p.note(`Add ${skillImportName} to your agent's getSkills() method`, "No agent found");
|
|
406
|
+
p.outro(`Next steps:
|
|
407
|
+
1. Implement tool logic in tools/*.ts
|
|
408
|
+
2. Update the skill guidance in ${kebabName}.ts`);
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/cli/commands/generate-tool.ts
|
|
412
|
+
async function generateTool(name) {
|
|
413
|
+
const cwd = process.cwd();
|
|
414
|
+
const snakeName = toSnakeCase(name);
|
|
415
|
+
const srcDir = getSrcDir(cwd);
|
|
416
|
+
p.intro(`Creating tool: ${snakeName}`);
|
|
417
|
+
const locationOptions = [{
|
|
418
|
+
value: "global",
|
|
419
|
+
label: "Global tool (src/tools/)"
|
|
420
|
+
}, ...findSkills(cwd).map((skill) => ({
|
|
421
|
+
value: `skill:${skill}`,
|
|
422
|
+
label: `In skill: ${skill}`
|
|
423
|
+
}))];
|
|
424
|
+
const answers = await p.group({
|
|
425
|
+
description: () => p.text({
|
|
426
|
+
message: "Tool description",
|
|
427
|
+
placeholder: "What does this tool do?",
|
|
428
|
+
validate: (value) => {
|
|
429
|
+
if (!value) return "Description is required";
|
|
430
|
+
}
|
|
431
|
+
}),
|
|
432
|
+
location: () => p.select({
|
|
433
|
+
message: "Where should this tool be created?",
|
|
434
|
+
options: locationOptions
|
|
435
|
+
}),
|
|
436
|
+
useContext: () => p.confirm({
|
|
437
|
+
message: "Does this tool need access to AgentToolContext?",
|
|
438
|
+
initialValue: false
|
|
439
|
+
})
|
|
440
|
+
}, { onCancel: () => {
|
|
441
|
+
p.cancel("Operation cancelled");
|
|
442
|
+
process.exit(0);
|
|
443
|
+
} });
|
|
444
|
+
let toolFile;
|
|
445
|
+
if (answers.location === "global") toolFile = path.join(srcDir, "tools", `${snakeName}.ts`);
|
|
446
|
+
else {
|
|
447
|
+
const skillName = answers.location.replace("skill:", "");
|
|
448
|
+
toolFile = path.join(srcDir, "skills", skillName, "tools", `${snakeName}.ts`);
|
|
449
|
+
}
|
|
450
|
+
if (fileExists(toolFile)) {
|
|
451
|
+
p.cancel(`Tool already exists at ${toolFile}`);
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
const spinner = p.spinner();
|
|
455
|
+
spinner.start("Creating file...");
|
|
456
|
+
const toolContent = generateToolTemplate({
|
|
457
|
+
snakeName,
|
|
458
|
+
description: answers.description,
|
|
459
|
+
useContext: answers.useContext
|
|
460
|
+
});
|
|
461
|
+
writeFile(toolFile, toolContent);
|
|
462
|
+
spinner.stop("File created");
|
|
463
|
+
const relativePath = path.relative(cwd, toolFile);
|
|
464
|
+
p.note(` ${relativePath}`, "Created file");
|
|
465
|
+
if (answers.location === "global") {
|
|
466
|
+
const agents = findAgentFiles(cwd);
|
|
467
|
+
let selectedAgent = null;
|
|
468
|
+
if (agents.length === 1) selectedAgent = agents[0];
|
|
469
|
+
else if (agents.length > 1) {
|
|
470
|
+
const choice = await p.select({
|
|
471
|
+
message: "Which agent should this tool be registered with?",
|
|
472
|
+
options: agents.map((a) => ({
|
|
473
|
+
value: a.path,
|
|
474
|
+
label: `${a.className} (${path.relative(cwd, a.path)})`
|
|
475
|
+
}))
|
|
476
|
+
});
|
|
477
|
+
if (p.isCancel(choice)) {
|
|
478
|
+
p.outro(`Tool created. Add ${snakeName} to your agent manually.`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
selectedAgent = agents.find((a) => a.path === choice) ?? null;
|
|
482
|
+
}
|
|
483
|
+
if (selectedAgent) {
|
|
484
|
+
const result = registerTool(selectedAgent, snakeName, snakeName, toolFile, selectedAgent.path);
|
|
485
|
+
if (result.success) p.note(result.message, "Registered tool");
|
|
486
|
+
else p.note(result.message, "Manual registration required");
|
|
487
|
+
} else p.note(`Add ${snakeName} to your agent's getTools() method`, "No agent found");
|
|
488
|
+
p.outro(`Next step: Implement tool logic in ${relativePath}`);
|
|
489
|
+
} else {
|
|
490
|
+
const skillName = answers.location.replace("skill:", "");
|
|
491
|
+
const skillFile = path.join(srcDir, "skills", skillName, `${skillName}.ts`);
|
|
492
|
+
if (fileExists(skillFile)) {
|
|
493
|
+
let skillContent = readFile(skillFile);
|
|
494
|
+
const toolRelativePath = `./tools/${snakeName}`;
|
|
495
|
+
skillContent = addImport(skillContent, snakeName, toolRelativePath);
|
|
496
|
+
const toolsMatch = skillContent.match(/tools:\s*\{([^}]*)\}/s);
|
|
497
|
+
if (toolsMatch) {
|
|
498
|
+
const [fullMatch, toolsContent] = toolsMatch;
|
|
499
|
+
const trimmed = toolsContent.trim();
|
|
500
|
+
const newTools = `tools: {${trimmed ? `${trimmed},\n ${snakeName},` : `\n ${snakeName},\n `}}`;
|
|
501
|
+
skillContent = skillContent.replace(fullMatch, newTools);
|
|
502
|
+
writeFile(skillFile, skillContent);
|
|
503
|
+
p.note(`Added ${snakeName} to ${skillName}.ts`, "Registered tool");
|
|
504
|
+
} else p.note(`Add ${snakeName} to the tools object in src/skills/${skillName}/${skillName}.ts`, "Manual registration required");
|
|
505
|
+
}
|
|
506
|
+
p.outro(`Next step: Implement tool logic in ${relativePath}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/cli/index.ts
|
|
511
|
+
const args = process.argv.slice(2);
|
|
512
|
+
const [command, type, name] = args;
|
|
513
|
+
function printHelp() {
|
|
514
|
+
console.log(`
|
|
515
|
+
Usage: economic-agents <command> <type> <name>
|
|
516
|
+
|
|
517
|
+
Commands:
|
|
518
|
+
generate skill <name> Create a new skill with tools
|
|
519
|
+
generate tool <name> Create a new tool
|
|
520
|
+
|
|
521
|
+
Options:
|
|
522
|
+
--help, -h Show this help message
|
|
523
|
+
--version, -v Show version
|
|
524
|
+
`);
|
|
525
|
+
}
|
|
526
|
+
function printVersion() {
|
|
527
|
+
console.log("0.0.1");
|
|
528
|
+
}
|
|
529
|
+
async function main() {
|
|
530
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
531
|
+
printHelp();
|
|
532
|
+
process.exit(0);
|
|
533
|
+
}
|
|
534
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
535
|
+
printVersion();
|
|
536
|
+
process.exit(0);
|
|
537
|
+
}
|
|
538
|
+
if (command !== "generate") {
|
|
539
|
+
printHelp();
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
if (type === "skill") {
|
|
543
|
+
if (!name) {
|
|
544
|
+
console.error("Error: skill name is required");
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
await generateSkill(name);
|
|
548
|
+
} else if (type === "tool") {
|
|
549
|
+
if (!name) {
|
|
550
|
+
console.error("Error: tool name is required");
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
await generateTool(name);
|
|
554
|
+
} else {
|
|
555
|
+
console.error(`Error: unknown type "${type}". Use "skill" or "tool".`);
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
main();
|
|
560
|
+
//#endregion
|
|
561
|
+
export {};
|
package/dist/hono.d.mts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { MiddlewareHandler } from "hono";
|
|
2
|
+
|
|
3
|
+
//#region src/hono/jwt-auth.d.ts
|
|
4
|
+
interface JwtAuthConfig {
|
|
5
|
+
/** Issuers whose tokens are accepted (exact string or RegExp). */
|
|
6
|
+
allowedIssuers: readonly (string | RegExp)[];
|
|
7
|
+
/** Expected `aud` claim. */
|
|
8
|
+
audience: string;
|
|
9
|
+
/** Required OAuth scopes; token `scope` must include all (empty = no scope check). */
|
|
10
|
+
requiredScopes?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Hono middleware: verify JWT via JWKS derived from the token `iss` claim,
|
|
14
|
+
* after `iss` passes `allowedIssuers`.
|
|
15
|
+
*
|
|
16
|
+
* Reads the token from `Authorization: Bearer` only. Browser WebSocket clients
|
|
17
|
+
* cannot set that header; use `routeAgentRequest` `onBeforeConnect` for those.
|
|
18
|
+
*/
|
|
19
|
+
declare function jwtAuth(config: JwtAuthConfig): MiddlewareHandler;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { type JwtAuthConfig, jwtAuth };
|
package/dist/hono.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createRemoteJWKSet, decodeJwt, errors, jwtVerify } from "jose";
|
|
2
|
+
//#region src/hono/jwt-auth.ts
|
|
3
|
+
const jwksByIssuer = /* @__PURE__ */ new Map();
|
|
4
|
+
function getJwksForIssuer(issuer) {
|
|
5
|
+
const normalized = issuer.replace(/\/$/, "");
|
|
6
|
+
let jwks = jwksByIssuer.get(normalized);
|
|
7
|
+
if (!jwks) {
|
|
8
|
+
jwks = createRemoteJWKSet(new URL(`${normalized}/.well-known/openid-configuration/jwks`));
|
|
9
|
+
jwksByIssuer.set(normalized, jwks);
|
|
10
|
+
}
|
|
11
|
+
return jwks;
|
|
12
|
+
}
|
|
13
|
+
function isIssuerAllowed(iss, allowed) {
|
|
14
|
+
return allowed.some((rule) => typeof rule === "string" ? rule === iss : rule.test(iss));
|
|
15
|
+
}
|
|
16
|
+
function bearerTokenFromAuthorizationHeader(authorization) {
|
|
17
|
+
if (!authorization) return null;
|
|
18
|
+
return authorization.match(/^Bearer\s+(.+)$/i)?.[1]?.trim() ?? null;
|
|
19
|
+
}
|
|
20
|
+
function hasRequiredScopes(tokenScope, required) {
|
|
21
|
+
if (required.length === 0) return true;
|
|
22
|
+
if (tokenScope === void 0) return false;
|
|
23
|
+
const tokens = Array.isArray(tokenScope) ? tokenScope : tokenScope.split(" ").map((s) => s.trim()).filter(Boolean);
|
|
24
|
+
const granted = new Set(tokens);
|
|
25
|
+
return required.every((scope) => granted.has(scope));
|
|
26
|
+
}
|
|
27
|
+
function authErrorResponse(status, message) {
|
|
28
|
+
return new Response(message, { status });
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Hono middleware: verify JWT via JWKS derived from the token `iss` claim,
|
|
32
|
+
* after `iss` passes `allowedIssuers`.
|
|
33
|
+
*
|
|
34
|
+
* Reads the token from `Authorization: Bearer` only. Browser WebSocket clients
|
|
35
|
+
* cannot set that header; use `routeAgentRequest` `onBeforeConnect` for those.
|
|
36
|
+
*/
|
|
37
|
+
function jwtAuth(config) {
|
|
38
|
+
const requiredScopes = config.requiredScopes ?? [];
|
|
39
|
+
return async (c, next) => {
|
|
40
|
+
const token = bearerTokenFromAuthorizationHeader(c.req.header("Authorization"));
|
|
41
|
+
if (!token) return authErrorResponse(401, "Unauthorized: Missing authentication token");
|
|
42
|
+
let unverifiedPayload;
|
|
43
|
+
try {
|
|
44
|
+
unverifiedPayload = decodeJwt(token);
|
|
45
|
+
} catch {
|
|
46
|
+
return authErrorResponse(401, "Unauthorized: Invalid token");
|
|
47
|
+
}
|
|
48
|
+
const iss = typeof unverifiedPayload.iss === "string" ? unverifiedPayload.iss : void 0;
|
|
49
|
+
if (!iss) return authErrorResponse(401, "Unauthorized: Missing issuer claim");
|
|
50
|
+
if (!isIssuerAllowed(iss, config.allowedIssuers)) return authErrorResponse(401, "Unauthorized: Untrusted issuer");
|
|
51
|
+
const jwks = getJwksForIssuer(iss);
|
|
52
|
+
let payload;
|
|
53
|
+
try {
|
|
54
|
+
payload = (await jwtVerify(token, jwks, {
|
|
55
|
+
issuer: iss,
|
|
56
|
+
audience: config.audience
|
|
57
|
+
})).payload;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Authentication failed", error);
|
|
60
|
+
if (error instanceof errors.JWTExpired) return authErrorResponse(401, "Unauthorized: Token expired");
|
|
61
|
+
if (error instanceof errors.JWTClaimValidationFailed) return authErrorResponse(401, "Unauthorized: Token claim validation failed");
|
|
62
|
+
if (error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JWKSNoMatchingKey) return authErrorResponse(401, "Unauthorized: Invalid token signature");
|
|
63
|
+
return authErrorResponse(401, "Authentication failed");
|
|
64
|
+
}
|
|
65
|
+
const scopeClaim = payload.scope;
|
|
66
|
+
if (!hasRequiredScopes(scopeClaim, requiredScopes)) return authErrorResponse(403, "Forbidden: Insufficient scope");
|
|
67
|
+
await next();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { jwtAuth };
|