@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/bin/cli.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/cli.mjs";
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 {};
@@ -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 };