agent-resource-management 2.1.1 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -577,7 +577,7 @@ function setOutputMode(mode) {
577
577
  }
578
578
 
579
579
  // src/lib/validate.ts
580
- import { readFileSync as readFileSync2, existsSync as existsSync2, statSync } from "fs";
580
+ import { readFileSync as readFileSync2, existsSync as existsSync2, statSync as statSync2 } from "fs";
581
581
  import { execSync } from "child_process";
582
582
  import { join as join2, extname } from "path";
583
583
  import { mkdtempSync, rmSync } from "fs";
@@ -597,7 +597,7 @@ function validateZip(filePath) {
597
597
  result.errors.push("文件必须是 ZIP 格式");
598
598
  return result;
599
599
  }
600
- const stats = statSync(filePath);
600
+ const stats = statSync2(filePath);
601
601
  if (stats.size === 0) {
602
602
  result.valid = false;
603
603
  result.errors.push("ZIP 文件为空");
@@ -747,9 +747,95 @@ function validateSkillDir(dirPath) {
747
747
  }
748
748
  return result;
749
749
  }
750
+ function validateAgentDir(dirPath) {
751
+ const result = {
752
+ valid: true,
753
+ errors: [],
754
+ warnings: []
755
+ };
756
+ if (!existsSync2(dirPath)) {
757
+ result.valid = false;
758
+ result.errors.push(`目录不存在: ${dirPath}`);
759
+ return result;
760
+ }
761
+ const agentMdPath = join2(dirPath, "AGENT.md");
762
+ if (!existsSync2(agentMdPath)) {
763
+ result.valid = false;
764
+ result.errors.push("目录内缺少 AGENT.md 文件");
765
+ return result;
766
+ }
767
+ try {
768
+ const agentMdContent = readFileSync2(agentMdPath, "utf-8");
769
+ const frontmatterMatch = agentMdContent.match(/^---\n([\s\S]*?)\n---/);
770
+ if (frontmatterMatch) {
771
+ const frontmatter = frontmatterMatch[1];
772
+ result.metadata = {};
773
+ const nameMatch = frontmatter.match(/name:\s*(.+)/);
774
+ const versionMatch = frontmatter.match(/version:\s*(.+)/);
775
+ const descMatch = frontmatter.match(/description:\s*(.+)/);
776
+ const skillsMatch = frontmatter.match(/skills:\s*([\s\S]*?)(?=\n\w|---)/);
777
+ const knowledgesMatch = frontmatter.match(/knowledges:\s*([\s\S]*?)(?=\n\w|---)/);
778
+ if (nameMatch) {
779
+ result.metadata.name = nameMatch[1].trim();
780
+ } else {
781
+ result.valid = false;
782
+ result.errors.push("缺少 name 字段");
783
+ }
784
+ if (versionMatch) {
785
+ result.metadata.version = versionMatch[1].trim();
786
+ }
787
+ if (descMatch) {
788
+ result.metadata.description = descMatch[1].trim();
789
+ }
790
+ if (skillsMatch) {
791
+ const skillsLines = skillsMatch[1].split(`
792
+ `).filter((l) => l.trim().startsWith("-"));
793
+ result.metadata.skills = skillsLines.map((l) => l.replace(/^\s*-\s*/, "").trim());
794
+ }
795
+ if (knowledgesMatch) {
796
+ const knowledgesLines = knowledgesMatch[1].split(`
797
+ `).filter((l) => l.trim().startsWith("-"));
798
+ result.metadata.knowledges = knowledgesLines.map((l) => l.replace(/^\s*-\s*/, "").trim());
799
+ }
800
+ const contentAfterFrontmatter = agentMdContent.replace(/^---[\s\S]*?---\n/, "");
801
+ const promptMatch = contentAfterFrontmatter.match(/#\s*System\s*Prompt\n([\s\S]*?)$/);
802
+ if (promptMatch) {
803
+ result.metadata.prompt = promptMatch[1].trim();
804
+ } else if (contentAfterFrontmatter.trim()) {
805
+ result.metadata.prompt = contentAfterFrontmatter.trim();
806
+ }
807
+ } else {
808
+ result.valid = false;
809
+ result.errors.push("AGENT.md 缺少 frontmatter (--- 包裹的 YAML)");
810
+ }
811
+ const skillsDir = join2(dirPath, "skills");
812
+ if (existsSync2(skillsDir) && statSync2(skillsDir).isDirectory()) {
813
+ const skillDirs = execSync(`ls -1 "${skillsDir}"`, { encoding: "utf-8" }).split(`
814
+ `).filter((l) => l.trim() && existsSync2(join2(skillsDir, l)) && statSync2(join2(skillsDir, l)).isDirectory());
815
+ for (const skillDir of skillDirs) {
816
+ const skillMdPath = join2(skillsDir, skillDir, "SKILL.md");
817
+ if (!existsSync2(skillMdPath)) {
818
+ result.warnings.push(`skills/${skillDir} 目录内缺少 SKILL.md 文件`);
819
+ }
820
+ }
821
+ }
822
+ const knowledgesDir = join2(dirPath, "knowledges");
823
+ if (existsSync2(knowledgesDir) && statSync2(knowledgesDir).isDirectory()) {
824
+ const mdFiles = execSync(`ls -1 "${knowledgesDir}"`, { encoding: "utf-8" }).split(`
825
+ `).filter((l) => l.trim().endsWith(".md"));
826
+ if (mdFiles.length === 0) {
827
+ result.warnings.push("knowledges/ 目录内没有 .md 文件");
828
+ }
829
+ }
830
+ } catch (err) {
831
+ result.valid = false;
832
+ result.errors.push(`AGENT.md 读取失败: ${err instanceof Error ? err.message : "未知错误"}`);
833
+ }
834
+ return result;
835
+ }
750
836
 
751
837
  // src/cmd/skill.ts
752
- import { writeFileSync as writeFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
838
+ import { writeFileSync as writeFileSync2, existsSync as existsSync3, statSync as statSync3 } from "fs";
753
839
  import { join as join3, basename, dirname } from "path";
754
840
  import { execSync as execSync2 } from "child_process";
755
841
  import { mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "fs";
@@ -870,13 +956,33 @@ async function downloadSkill(name, outputDir) {
870
956
  info(`正在下载 ${name}...`);
871
957
  }
872
958
  const buffer = await client.downloadSkill(name);
873
- const outputPath = join3(outputDir || ".", `${name}.zip`);
874
- writeFileSync2(outputPath, Buffer.from(buffer));
959
+ const tempDir = mkdtempSync2("/tmp/skill-download-");
960
+ const zipPath = join3(tempDir, `${name}.zip`);
961
+ writeFileSync2(zipPath, Buffer.from(buffer));
962
+ execSync2(`unzip -q "${zipPath}" -d "${tempDir}"`, { stdio: "pipe" });
963
+ const targetDir = join3(outputDir || ".", name);
964
+ execSync2(`mkdir -p "${targetDir}"`, { stdio: "pipe" });
965
+ const entries = execSync2(`ls -1 "${tempDir}"`, { encoding: "utf-8" }).split(`
966
+ `).filter((e) => e.trim());
967
+ const nonZipEntries = entries.filter((e) => e !== `${name}.zip`);
968
+ if (nonZipEntries.length === 1) {
969
+ const onlyEntry = join3(tempDir, nonZipEntries[0]);
970
+ if (statSync3(onlyEntry).isDirectory()) {
971
+ execSync2(`cp -r "${onlyEntry}"/* "${targetDir}/"`, { stdio: "pipe" });
972
+ } else {
973
+ execSync2(`cp -r "${onlyEntry}" "${targetDir}/"`, { stdio: "pipe" });
974
+ }
975
+ } else {
976
+ for (const entry of nonZipEntries) {
977
+ execSync2(`cp -r "${join3(tempDir, entry)}" "${targetDir}/"`, { stdio: "pipe" });
978
+ }
979
+ }
980
+ rmSync2(tempDir, { recursive: true, force: true });
875
981
  if (shouldOutputJson()) {
876
- outputJson({ success: true, data: { path: outputPath } });
982
+ outputJson({ success: true, data: { path: targetDir } });
877
983
  return;
878
984
  }
879
- success(`已下载到 ${outputPath}`);
985
+ success(`已下载到 ${targetDir}`);
880
986
  } catch (err) {
881
987
  if (shouldOutputJson()) {
882
988
  outputJson({ success: false, error: { code: "DOWNLOAD_FAILED", message: err instanceof Error ? err.message : "未知错误" } });
@@ -1010,7 +1116,7 @@ async function deleteSkill(name) {
1010
1116
  }
1011
1117
  }
1012
1118
  async function validateSkill(filePath) {
1013
- const isDir = existsSync3(filePath) && statSync2(filePath).isDirectory();
1119
+ const isDir = existsSync3(filePath) && statSync3(filePath).isDirectory();
1014
1120
  const result = isDir ? validateSkillDir(filePath) : validateZip(filePath);
1015
1121
  if (shouldOutputJson()) {
1016
1122
  outputJson({
@@ -1462,10 +1568,153 @@ async function downloadAgent(name, outputDir) {
1462
1568
  process.exit(1);
1463
1569
  }
1464
1570
  }
1571
+ async function createAgentFromFolder(folderPath) {
1572
+ const config = loadConfig();
1573
+ if (!config?.token) {
1574
+ if (shouldOutputJson()) {
1575
+ outputJson({ success: false, error: { code: "NOT_LOGGED_IN", message: "未登录,请先运行 arm login" } });
1576
+ process.exit(1);
1577
+ }
1578
+ error("未登录,请先运行 arm login");
1579
+ process.exit(1);
1580
+ }
1581
+ if (!existsSync4(folderPath)) {
1582
+ if (shouldOutputJson()) {
1583
+ outputJson({ success: false, error: { code: "FILE_NOT_FOUND", message: `目录不存在: ${folderPath}` } });
1584
+ process.exit(1);
1585
+ }
1586
+ error(`目录不存在: ${folderPath}`);
1587
+ process.exit(1);
1588
+ }
1589
+ const validation = validateAgentDir(folderPath);
1590
+ if (!validation.valid) {
1591
+ if (shouldOutputJson()) {
1592
+ outputJson({ success: false, error: { code: "VALIDATION_FAILED", message: validation.errors.join(", ") } });
1593
+ process.exit(1);
1594
+ }
1595
+ error(`验证失败: ${validation.errors.join(", ")}`);
1596
+ process.exit(1);
1597
+ }
1598
+ if (shouldOutputJson()) {
1599
+ info("正在解析 Agent 文件夹...");
1600
+ }
1601
+ const metadata = validation.metadata;
1602
+ const client = new ApiClient(config.serverUrl, config.token);
1603
+ const uploadedSkills = [];
1604
+ const uploadedKnowledges = [];
1605
+ const skillsDir = join4(folderPath, "skills");
1606
+ if (existsSync4(skillsDir) && statSync(skillsDir).isDirectory()) {
1607
+ const skillDirs = execSync3(`ls -1 "${skillsDir}"`, { encoding: "utf-8" }).split(`
1608
+ `).filter((l) => l.trim() && existsSync4(join4(skillsDir, l)) && statSync(join4(skillsDir, l)).isDirectory());
1609
+ for (const skillDir of skillDirs) {
1610
+ const skillPath = join4(skillsDir, skillDir);
1611
+ try {
1612
+ const existingSkill = await client.getSkill(skillDir).catch(() => null);
1613
+ if (existingSkill) {
1614
+ if (shouldOutputJson()) {
1615
+ info(`技能 ${skillDir} 已存在,将仅绑定`);
1616
+ }
1617
+ uploadedSkills.push({ name: skillDir, id: existingSkill.id });
1618
+ } else {
1619
+ if (shouldOutputJson()) {
1620
+ info(`上传新技能: ${skillDir}`);
1621
+ }
1622
+ const skillTempDir = mkdtempSync3("/tmp/skill-upload-");
1623
+ const zipPath = join4(skillTempDir, `${skillDir}.zip`);
1624
+ execSync3(`cd "${skillPath}" && zip -r "${zipPath}" . -x ".*"`, { stdio: "pipe" });
1625
+ const uploadedSkill = await client.uploadSkill(zipPath);
1626
+ uploadedSkills.push({ name: skillDir, id: uploadedSkill.id });
1627
+ rmSync3(skillTempDir, { recursive: true, force: true });
1628
+ }
1629
+ } catch (err) {
1630
+ if (shouldOutputJson()) {
1631
+ outputJson({ success: false, error: { code: "SKILL_UPLOAD_FAILED", message: `处理技能 ${skillDir} 失败: ${err instanceof Error ? err.message : "未知错误"}` } });
1632
+ process.exit(1);
1633
+ }
1634
+ error(`处理技能 ${skillDir} 失败: ${err instanceof Error ? err.message : "未知错误"}`);
1635
+ process.exit(1);
1636
+ }
1637
+ }
1638
+ }
1639
+ const knowledgesDir = join4(folderPath, "knowledges");
1640
+ if (existsSync4(knowledgesDir) && statSync(knowledgesDir).isDirectory()) {
1641
+ const mdFiles = execSync3(`ls -1 "${knowledgesDir}"`, { encoding: "utf-8" }).split(`
1642
+ `).filter((l) => l.trim().endsWith(".md"));
1643
+ for (const mdFile of mdFiles) {
1644
+ const mdPath = join4(knowledgesDir, mdFile);
1645
+ const knowledgeName = mdFile.replace(".md", "");
1646
+ try {
1647
+ const existingKnowledge = await client.getKnowledge(knowledgeName).catch(() => null);
1648
+ if (existingKnowledge) {
1649
+ if (shouldOutputJson()) {
1650
+ info(`知识 ${knowledgeName} 已存在,将仅绑定`);
1651
+ }
1652
+ uploadedKnowledges.push({ title: knowledgeName, id: existingKnowledge.id });
1653
+ } else {
1654
+ if (shouldOutputJson()) {
1655
+ info(`上传新知识: ${knowledgeName}`);
1656
+ }
1657
+ const uploadedKnowledge = await client.uploadKnowledge(mdPath);
1658
+ uploadedKnowledges.push({ title: knowledgeName, id: uploadedKnowledge.id });
1659
+ }
1660
+ } catch (err) {
1661
+ if (shouldOutputJson()) {
1662
+ outputJson({ success: false, error: { code: "KNOWLEDGE_UPLOAD_FAILED", message: `处理知识 ${knowledgeName} 失败: ${err instanceof Error ? err.message : "未知错误"}` } });
1663
+ process.exit(1);
1664
+ }
1665
+ error(`处理知识 ${knowledgeName} 失败: ${err instanceof Error ? err.message : "未知错误"}`);
1666
+ process.exit(1);
1667
+ }
1668
+ }
1669
+ }
1670
+ if (shouldOutputJson()) {
1671
+ info(`创建 Agent: ${metadata.name}`);
1672
+ }
1673
+ try {
1674
+ const agent = await client.createAgent({
1675
+ name: metadata.name,
1676
+ description: metadata.description,
1677
+ prompt: metadata.prompt
1678
+ });
1679
+ for (const skill of uploadedSkills) {
1680
+ if (shouldOutputJson()) {
1681
+ info(`绑定技能: ${skill.name}`);
1682
+ }
1683
+ await client.bindSkillToAgent(agent.id, skill.id);
1684
+ }
1685
+ for (const knowledge of uploadedKnowledges) {
1686
+ if (shouldOutputJson()) {
1687
+ info(`绑定知识: ${knowledge.title}`);
1688
+ }
1689
+ await client.bindKnowledgeToAgent(agent.id, knowledge.id);
1690
+ }
1691
+ if (shouldOutputJson()) {
1692
+ outputJson({
1693
+ success: true,
1694
+ data: {
1695
+ id: agent.id,
1696
+ name: agent.name,
1697
+ skillsCount: uploadedSkills.length,
1698
+ knowledgesCount: uploadedKnowledges.length
1699
+ }
1700
+ });
1701
+ return;
1702
+ }
1703
+ success(`Agent "${metadata.name}" 创建成功 (ID: ${agent.id})`);
1704
+ success(`已绑定 ${uploadedSkills.length} 个技能和 ${uploadedKnowledges.length} 个知识`);
1705
+ } catch (err) {
1706
+ if (shouldOutputJson()) {
1707
+ outputJson({ success: false, error: { code: "CREATE_FAILED", message: err instanceof Error ? err.message : "未知错误" } });
1708
+ process.exit(1);
1709
+ }
1710
+ error(`创建失败: ${err instanceof Error ? err.message : "未知错误"}`);
1711
+ process.exit(1);
1712
+ }
1713
+ }
1465
1714
 
1466
1715
  // src/cmd/knowledge.ts
1467
- import { writeFileSync as writeFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
1468
- import { join as join5, basename as basename2, dirname as dirname2 } from "path";
1716
+ import { writeFileSync as writeFileSync4, existsSync as existsSync5, statSync as statSync4 } from "fs";
1717
+ import { join as join5, basename as basename3, dirname as dirname2 } from "path";
1469
1718
  import { execSync as execSync4 } from "child_process";
1470
1719
  import { mkdtempSync as mkdtempSync4, rmSync as rmSync4 } from "fs";
1471
1720
  async function listKnowledge() {
@@ -1619,12 +1868,12 @@ async function uploadKnowledge(filePath) {
1619
1868
  error(`上传失败: 目录不存在: ${filePath}`);
1620
1869
  process.exit(1);
1621
1870
  }
1622
- const knowledgeName = basename2(filePath);
1871
+ const knowledgeName = basename3(filePath);
1623
1872
  const tempDir = mkdtempSync4("/tmp/knowledge-upload-");
1624
1873
  const zipPath = join5(tempDir, `${knowledgeName}.zip`);
1625
1874
  try {
1626
- if (statSync3(filePath).isDirectory()) {
1627
- execSync4(`cd "${dirname2(filePath)}" && zip -r "${zipPath}" "${basename2(filePath)}" -x ".*"`, { stdio: "pipe" });
1875
+ if (statSync4(filePath).isDirectory()) {
1876
+ execSync4(`cd "${dirname2(filePath)}" && zip -r "${zipPath}" "${basename3(filePath)}" -x ".*"`, { stdio: "pipe" });
1628
1877
  } else {
1629
1878
  execSync4(`cp "${filePath}" "${zipPath}"`, { stdio: "pipe" });
1630
1879
  }
@@ -1928,7 +2177,7 @@ async function main() {
1928
2177
  break;
1929
2178
  case "create":
1930
2179
  if (!args[2]) {
1931
- console.error(`用法: arm agent create <name> [--description="..."] [--prompt="..."] [--avatar="..."] [--skill=id] [--knowledge=id] [--skill-config='{...}'] [--knowledge-config='{...}'] [--json]`);
2180
+ console.error(`用法: arm agent create <name> [--description="..."] [--prompt="..."] [--avatar="..."] [--skill=id] [--knowledge=id] [--skill-config='{...}'] [--knowledge-config='{...}'] [--from=<folder-path>] [--json]`);
1932
2181
  process.exit(1);
1933
2182
  }
1934
2183
  {
@@ -1938,6 +2187,7 @@ async function main() {
1938
2187
  const knowledges = [];
1939
2188
  const skillConfigs = [];
1940
2189
  const knowledgeConfigs = [];
2190
+ let fromFolder;
1941
2191
  for (let i = 3;i < args.length; i++) {
1942
2192
  const arg = args[i];
1943
2193
  if (arg.startsWith("--description=")) {
@@ -1954,17 +2204,23 @@ async function main() {
1954
2204
  skillConfigs.push(arg.split("=").slice(1).join("="));
1955
2205
  } else if (arg.startsWith("--knowledge-config=")) {
1956
2206
  knowledgeConfigs.push(arg.split("=").slice(1).join("="));
2207
+ } else if (arg.startsWith("--from=")) {
2208
+ fromFolder = arg.split("=").slice(1).join("=");
1957
2209
  }
1958
2210
  }
1959
- await createAgent(name, {
1960
- description: options.description,
1961
- prompt: options.prompt,
1962
- avatar: options.avatar,
1963
- skills,
1964
- knowledges,
1965
- skillConfigs,
1966
- knowledgeConfigs
1967
- });
2211
+ if (fromFolder) {
2212
+ await createAgentFromFolder(fromFolder);
2213
+ } else {
2214
+ await createAgent(name, {
2215
+ description: options.description,
2216
+ prompt: options.prompt,
2217
+ avatar: options.avatar,
2218
+ skills,
2219
+ knowledges,
2220
+ skillConfigs,
2221
+ knowledgeConfigs
2222
+ });
2223
+ }
1968
2224
  }
1969
2225
  break;
1970
2226
  case "update":
@@ -2073,6 +2329,7 @@ async function main() {
2073
2329
  arm agent info <name> 查看 Agent 详情
2074
2330
  arm agent download <name> [dir] 下载 Agent
2075
2331
  arm agent create <name> 创建 Agent (--description, --prompt, --avatar, --skill, --knowledge)
2332
+ arm agent create --from=<folder> 从本地文件夹创建 Agent
2076
2333
  arm agent update <id> 更新 Agent (--name, --description, --prompt, --avatar, --status)
2077
2334
  arm agent delete <id> 删除 Agent
2078
2335
  arm agent bind <id> --skill=<id> 绑定 Skill 到 Agent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-resource-management",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "arm": "./dist/main.js"
package/src/cmd/agent.ts CHANGED
@@ -2,8 +2,9 @@ import { ApiClient } from '../lib/client';
2
2
  import { loadConfig } from '../lib/storage';
3
3
  import { formatAgent, formatAgentDetail, success, error, info } from '../lib/formatter';
4
4
  import { shouldOutputJson, outputJson } from '../lib/output';
5
- import { writeFileSync, existsSync } from 'fs';
6
- import { join } from 'path';
5
+ import { validateAgentDir } from '../lib/validate';
6
+ import { writeFileSync, existsSync, readFileSync } from 'fs';
7
+ import { join, basename } from 'path';
7
8
  import { execSync } from 'child_process';
8
9
  import { mkdtempSync, rmSync } from 'fs';
9
10
 
@@ -463,4 +464,164 @@ export async function downloadAgent(name: string, outputDir?: string): Promise<v
463
464
  error(`下载失败: ${err instanceof Error ? err.message : '未知错误'}`);
464
465
  process.exit(1);
465
466
  }
467
+ }
468
+
469
+ export async function createAgentFromFolder(folderPath: string): Promise<void> {
470
+ const config = loadConfig();
471
+ if (!config?.token) {
472
+ if (shouldOutputJson()) {
473
+ outputJson({ success: false, error: { code: 'NOT_LOGGED_IN', message: '未登录,请先运行 arm login' } });
474
+ process.exit(1);
475
+ }
476
+ error('未登录,请先运行 arm login');
477
+ process.exit(1);
478
+ }
479
+
480
+ if (!existsSync(folderPath)) {
481
+ if (shouldOutputJson()) {
482
+ outputJson({ success: false, error: { code: 'FILE_NOT_FOUND', message: `目录不存在: ${folderPath}` } });
483
+ process.exit(1);
484
+ }
485
+ error(`目录不存在: ${folderPath}`);
486
+ process.exit(1);
487
+ }
488
+
489
+ const validation = validateAgentDir(folderPath);
490
+ if (!validation.valid) {
491
+ if (shouldOutputJson()) {
492
+ outputJson({ success: false, error: { code: 'VALIDATION_FAILED', message: validation.errors.join(', ') } });
493
+ process.exit(1);
494
+ }
495
+ error(`验证失败: ${validation.errors.join(', ')}`);
496
+ process.exit(1);
497
+ }
498
+
499
+ if (shouldOutputJson()) {
500
+ info('正在解析 Agent 文件夹...');
501
+ }
502
+
503
+ const metadata = validation.metadata!;
504
+ const client = new ApiClient(config.serverUrl, config.token);
505
+
506
+ const uploadedSkills: { name: string; id: string }[] = [];
507
+ const uploadedKnowledges: { title: string; id: string }[] = [];
508
+
509
+ const skillsDir = join(folderPath, 'skills');
510
+ if (existsSync(skillsDir) && statSync(skillsDir).isDirectory()) {
511
+ const skillDirs = execSync(`ls -1 "${skillsDir}"`, { encoding: 'utf-8' })
512
+ .split('\n')
513
+ .filter(l => l.trim() && existsSync(join(skillsDir, l)) && statSync(join(skillsDir, l)).isDirectory());
514
+
515
+ for (const skillDir of skillDirs) {
516
+ const skillPath = join(skillsDir, skillDir);
517
+ try {
518
+ const existingSkill = await client.getSkill(skillDir).catch(() => null);
519
+ if (existingSkill) {
520
+ if (shouldOutputJson()) {
521
+ info(`技能 ${skillDir} 已存在,将仅绑定`);
522
+ }
523
+ uploadedSkills.push({ name: skillDir, id: existingSkill.id });
524
+ } else {
525
+ if (shouldOutputJson()) {
526
+ info(`上传新技能: ${skillDir}`);
527
+ }
528
+ const skillTempDir = mkdtempSync('/tmp/skill-upload-');
529
+ const zipPath = join(skillTempDir, `${skillDir}.zip`);
530
+ execSync(`cd "${skillPath}" && zip -r "${zipPath}" . -x ".*"`, { stdio: 'pipe' });
531
+ const uploadedSkill = await client.uploadSkill(zipPath);
532
+ uploadedSkills.push({ name: skillDir, id: uploadedSkill.id });
533
+ rmSync(skillTempDir, { recursive: true, force: true });
534
+ }
535
+ } catch (err) {
536
+ if (shouldOutputJson()) {
537
+ outputJson({ success: false, error: { code: 'SKILL_UPLOAD_FAILED', message: `处理技能 ${skillDir} 失败: ${err instanceof Error ? err.message : '未知错误'}` } });
538
+ process.exit(1);
539
+ }
540
+ error(`处理技能 ${skillDir} 失败: ${err instanceof Error ? err.message : '未知错误'}`);
541
+ process.exit(1);
542
+ }
543
+ }
544
+ }
545
+
546
+ const knowledgesDir = join(folderPath, 'knowledges');
547
+ if (existsSync(knowledgesDir) && statSync(knowledgesDir).isDirectory()) {
548
+ const mdFiles = execSync(`ls -1 "${knowledgesDir}"`, { encoding: 'utf-8' })
549
+ .split('\n')
550
+ .filter(l => l.trim().endsWith('.md'));
551
+
552
+ for (const mdFile of mdFiles) {
553
+ const mdPath = join(knowledgesDir, mdFile);
554
+ const knowledgeName = mdFile.replace('.md', '');
555
+ try {
556
+ const existingKnowledge = await client.getKnowledge(knowledgeName).catch(() => null);
557
+ if (existingKnowledge) {
558
+ if (shouldOutputJson()) {
559
+ info(`知识 ${knowledgeName} 已存在,将仅绑定`);
560
+ }
561
+ uploadedKnowledges.push({ title: knowledgeName, id: existingKnowledge.id });
562
+ } else {
563
+ if (shouldOutputJson()) {
564
+ info(`上传新知识: ${knowledgeName}`);
565
+ }
566
+ const uploadedKnowledge = await client.uploadKnowledge(mdPath);
567
+ uploadedKnowledges.push({ title: knowledgeName, id: uploadedKnowledge.id });
568
+ }
569
+ } catch (err) {
570
+ if (shouldOutputJson()) {
571
+ outputJson({ success: false, error: { code: 'KNOWLEDGE_UPLOAD_FAILED', message: `处理知识 ${knowledgeName} 失败: ${err instanceof Error ? err.message : '未知错误'}` } });
572
+ process.exit(1);
573
+ }
574
+ error(`处理知识 ${knowledgeName} 失败: ${err instanceof Error ? err.message : '未知错误'}`);
575
+ process.exit(1);
576
+ }
577
+ }
578
+ }
579
+
580
+ if (shouldOutputJson()) {
581
+ info(`创建 Agent: ${metadata.name}`);
582
+ }
583
+
584
+ try {
585
+ const agent = await client.createAgent({
586
+ name: metadata.name!,
587
+ description: metadata.description,
588
+ prompt: metadata.prompt,
589
+ });
590
+
591
+ for (const skill of uploadedSkills) {
592
+ if (shouldOutputJson()) {
593
+ info(`绑定技能: ${skill.name}`);
594
+ }
595
+ await client.bindSkillToAgent(agent.id, skill.id);
596
+ }
597
+
598
+ for (const knowledge of uploadedKnowledges) {
599
+ if (shouldOutputJson()) {
600
+ info(`绑定知识: ${knowledge.title}`);
601
+ }
602
+ await client.bindKnowledgeToAgent(agent.id, knowledge.id);
603
+ }
604
+
605
+ if (shouldOutputJson()) {
606
+ outputJson({
607
+ success: true,
608
+ data: {
609
+ id: agent.id,
610
+ name: agent.name,
611
+ skillsCount: uploadedSkills.length,
612
+ knowledgesCount: uploadedKnowledges.length,
613
+ },
614
+ });
615
+ return;
616
+ }
617
+ success(`Agent "${metadata.name}" 创建成功 (ID: ${agent.id})`);
618
+ success(`已绑定 ${uploadedSkills.length} 个技能和 ${uploadedKnowledges.length} 个知识`);
619
+ } catch (err) {
620
+ if (shouldOutputJson()) {
621
+ outputJson({ success: false, error: { code: 'CREATE_FAILED', message: err instanceof Error ? err.message : '未知错误' } });
622
+ process.exit(1);
623
+ }
624
+ error(`创建失败: ${err instanceof Error ? err.message : '未知错误'}`);
625
+ process.exit(1);
626
+ }
466
627
  }
package/src/cmd/skill.ts CHANGED
@@ -130,13 +130,40 @@ export async function downloadSkill(name: string, outputDir?: string): Promise<v
130
130
  info(`正在下载 ${name}...`);
131
131
  }
132
132
  const buffer = await client.downloadSkill(name);
133
- const outputPath = join(outputDir || '.', `${name}.zip`);
134
- writeFileSync(outputPath, Buffer.from(buffer));
133
+
134
+ const tempDir = mkdtempSync('/tmp/skill-download-');
135
+ const zipPath = join(tempDir, `${name}.zip`);
136
+ writeFileSync(zipPath, Buffer.from(buffer));
137
+
138
+ execSync(`unzip -q "${zipPath}" -d "${tempDir}"`, { stdio: 'pipe' });
139
+
140
+ const targetDir = join(outputDir || '.', name);
141
+ execSync(`mkdir -p "${targetDir}"`, { stdio: 'pipe' });
142
+
143
+ const entries = execSync(`ls -1 "${tempDir}"`, { encoding: 'utf-8' }).split('\n').filter(e => e.trim());
144
+
145
+ const nonZipEntries = entries.filter(e => e !== `${name}.zip`);
146
+
147
+ if (nonZipEntries.length === 1) {
148
+ const onlyEntry = join(tempDir, nonZipEntries[0]);
149
+ if (statSync(onlyEntry).isDirectory()) {
150
+ execSync(`cp -r "${onlyEntry}"/* "${targetDir}/"`, { stdio: 'pipe' });
151
+ } else {
152
+ execSync(`cp -r "${onlyEntry}" "${targetDir}/"`, { stdio: 'pipe' });
153
+ }
154
+ } else {
155
+ for (const entry of nonZipEntries) {
156
+ execSync(`cp -r "${join(tempDir, entry)}" "${targetDir}/"`, { stdio: 'pipe' });
157
+ }
158
+ }
159
+
160
+ rmSync(tempDir, { recursive: true, force: true });
161
+
135
162
  if (shouldOutputJson()) {
136
- outputJson({ success: true, data: { path: outputPath } });
163
+ outputJson({ success: true, data: { path: targetDir } });
137
164
  return;
138
165
  }
139
- success(`已下载到 ${outputPath}`);
166
+ success(`已下载到 ${targetDir}`);
140
167
  } catch (err) {
141
168
  if (shouldOutputJson()) {
142
169
  outputJson({ success: false, error: { code: 'DOWNLOAD_FAILED', message: err instanceof Error ? err.message : '未知错误' } });
@@ -203,7 +203,124 @@ export function validateSkillDir(dirPath: string): ValidationResult {
203
203
  result.errors.push('SKILL.md 缺少 frontmatter (--- 包裹的 YAML)');
204
204
  }
205
205
  } catch (err) {
206
- result.warnings.push(`SKILL.md 读取失败: ${err instanceof Error ? err.message : '未知错误'}`);
206
+ result.warnings.push(`SKILL.md 读取失败: ${err instanceof Error ? err.message : '未知错误'}`);
207
+ }
208
+
209
+ return result;
210
+ }
211
+
212
+ export interface AgentValidationResult {
213
+ valid: boolean;
214
+ errors: string[];
215
+ warnings: string[];
216
+ metadata?: {
217
+ name?: string;
218
+ version?: string;
219
+ description?: string;
220
+ prompt?: string;
221
+ skills?: string[];
222
+ knowledges?: string[];
223
+ };
224
+ }
225
+
226
+ export function validateAgentDir(dirPath: string): AgentValidationResult {
227
+ const result: AgentValidationResult = {
228
+ valid: true,
229
+ errors: [],
230
+ warnings: [],
231
+ };
232
+
233
+ if (!existsSync(dirPath)) {
234
+ result.valid = false;
235
+ result.errors.push(`目录不存在: ${dirPath}`);
236
+ return result;
237
+ }
238
+
239
+ const agentMdPath = join(dirPath, 'AGENT.md');
240
+ if (!existsSync(agentMdPath)) {
241
+ result.valid = false;
242
+ result.errors.push('目录内缺少 AGENT.md 文件');
243
+ return result;
244
+ }
245
+
246
+ try {
247
+ const agentMdContent = readFileSync(agentMdPath, 'utf-8');
248
+ const frontmatterMatch = agentMdContent.match(/^---\n([\s\S]*?)\n---/);
249
+
250
+ if (frontmatterMatch) {
251
+ const frontmatter = frontmatterMatch[1];
252
+ result.metadata = {};
253
+
254
+ const nameMatch = frontmatter.match(/name:\s*(.+)/);
255
+ const versionMatch = frontmatter.match(/version:\s*(.+)/);
256
+ const descMatch = frontmatter.match(/description:\s*(.+)/);
257
+ const skillsMatch = frontmatter.match(/skills:\s*([\s\S]*?)(?=\n\w|---)/);
258
+ const knowledgesMatch = frontmatter.match(/knowledges:\s*([\s\S]*?)(?=\n\w|---)/);
259
+
260
+ if (nameMatch) {
261
+ result.metadata.name = nameMatch[1].trim();
262
+ } else {
263
+ result.valid = false;
264
+ result.errors.push('缺少 name 字段');
265
+ }
266
+
267
+ if (versionMatch) {
268
+ result.metadata.version = versionMatch[1].trim();
269
+ }
270
+
271
+ if (descMatch) {
272
+ result.metadata.description = descMatch[1].trim();
273
+ }
274
+
275
+ if (skillsMatch) {
276
+ const skillsLines = skillsMatch[1].split('\n').filter(l => l.trim().startsWith('-'));
277
+ result.metadata.skills = skillsLines.map(l => l.replace(/^\s*-\s*/, '').trim());
278
+ }
279
+
280
+ if (knowledgesMatch) {
281
+ const knowledgesLines = knowledgesMatch[1].split('\n').filter(l => l.trim().startsWith('-'));
282
+ result.metadata.knowledges = knowledgesLines.map(l => l.replace(/^\s*-\s*/, '').trim());
283
+ }
284
+
285
+ const contentAfterFrontmatter = agentMdContent.replace(/^---[\s\S]*?---\n/, '');
286
+ const promptMatch = contentAfterFrontmatter.match(/#\s*System\s*Prompt\n([\s\S]*?)$/);
287
+ if (promptMatch) {
288
+ result.metadata.prompt = promptMatch[1].trim();
289
+ } else if (contentAfterFrontmatter.trim()) {
290
+ result.metadata.prompt = contentAfterFrontmatter.trim();
291
+ }
292
+ } else {
293
+ result.valid = false;
294
+ result.errors.push('AGENT.md 缺少 frontmatter (--- 包裹的 YAML)');
295
+ }
296
+
297
+ const skillsDir = join(dirPath, 'skills');
298
+ if (existsSync(skillsDir) && statSync(skillsDir).isDirectory()) {
299
+ const skillDirs = execSync(`ls -1 "${skillsDir}"`, { encoding: 'utf-8' })
300
+ .split('\n')
301
+ .filter(l => l.trim() && existsSync(join(skillsDir, l)) && statSync(join(skillsDir, l)).isDirectory());
302
+
303
+ for (const skillDir of skillDirs) {
304
+ const skillMdPath = join(skillsDir, skillDir, 'SKILL.md');
305
+ if (!existsSync(skillMdPath)) {
306
+ result.warnings.push(`skills/${skillDir} 目录内缺少 SKILL.md 文件`);
307
+ }
308
+ }
309
+ }
310
+
311
+ const knowledgesDir = join(dirPath, 'knowledges');
312
+ if (existsSync(knowledgesDir) && statSync(knowledgesDir).isDirectory()) {
313
+ const mdFiles = execSync(`ls -1 "${knowledgesDir}"`, { encoding: 'utf-8' })
314
+ .split('\n')
315
+ .filter(l => l.trim().endsWith('.md'));
316
+
317
+ if (mdFiles.length === 0) {
318
+ result.warnings.push('knowledges/ 目录内没有 .md 文件');
319
+ }
320
+ }
321
+ } catch (err) {
322
+ result.valid = false;
323
+ result.errors.push(`AGENT.md 读取失败: ${err instanceof Error ? err.message : '未知错误'}`);
207
324
  }
208
325
 
209
326
  return result;
package/src/main.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { login, logout, getCurrentUser, register } from './cmd/auth';
2
2
  import { listSkills, searchSkills, infoSkill, downloadSkill, uploadSkill, mySkills, deleteSkill, validateSkill } from './cmd/skill';
3
- import { listAgents, searchAgents, infoAgent, downloadAgent, createAgent, updateAgent, deleteAgent, bindSkill, unbindSkill, bindKnowledge, unbindKnowledge } from './cmd/agent';
3
+ import { listAgents, searchAgents, infoAgent, downloadAgent, createAgent, updateAgent, deleteAgent, bindSkill, unbindSkill, bindKnowledge, unbindKnowledge, createAgentFromFolder } from './cmd/agent';
4
4
  import { listKnowledge, searchKnowledge, infoKnowledge, downloadKnowledge, uploadKnowledge, myKnowledge, deleteKnowledge } from './cmd/knowledge';
5
5
  import { showServer, setServer } from './cmd/server';
6
6
  import { getOutputMode, setOutputMode } from './lib/output';
@@ -215,7 +215,7 @@ async function main() {
215
215
  break;
216
216
  case 'create':
217
217
  if (!args[2]) {
218
- console.error('用法: arm agent create <name> [--description="..."] [--prompt="..."] [--avatar="..."] [--skill=id] [--knowledge=id] [--skill-config=\'{...}\'] [--knowledge-config=\'{...}\'] [--json]');
218
+ console.error('用法: arm agent create <name> [--description="..."] [--prompt="..."] [--avatar="..."] [--skill=id] [--knowledge=id] [--skill-config=\'{...}\'] [--knowledge-config=\'{...}\'] [--from=<folder-path>] [--json]');
219
219
  process.exit(1);
220
220
  }
221
221
  {
@@ -225,6 +225,7 @@ async function main() {
225
225
  const knowledges: string[] = [];
226
226
  const skillConfigs: string[] = [];
227
227
  const knowledgeConfigs: string[] = [];
228
+ let fromFolder: string | undefined;
228
229
 
229
230
  for (let i = 3; i < args.length; i++) {
230
231
  const arg = args[i];
@@ -242,18 +243,24 @@ async function main() {
242
243
  skillConfigs.push(arg.split('=').slice(1).join('='));
243
244
  } else if (arg.startsWith('--knowledge-config=')) {
244
245
  knowledgeConfigs.push(arg.split('=').slice(1).join('='));
246
+ } else if (arg.startsWith('--from=')) {
247
+ fromFolder = arg.split('=').slice(1).join('=');
245
248
  }
246
249
  }
247
250
 
248
- await createAgent(name, {
249
- description: options.description,
250
- prompt: options.prompt,
251
- avatar: options.avatar,
252
- skills,
253
- knowledges,
254
- skillConfigs,
255
- knowledgeConfigs,
256
- });
251
+ if (fromFolder) {
252
+ await createAgentFromFolder(fromFolder);
253
+ } else {
254
+ await createAgent(name, {
255
+ description: options.description,
256
+ prompt: options.prompt,
257
+ avatar: options.avatar,
258
+ skills,
259
+ knowledges,
260
+ skillConfigs,
261
+ knowledgeConfigs,
262
+ });
263
+ }
257
264
  }
258
265
  break;
259
266
  case 'update':
@@ -368,6 +375,7 @@ async function main() {
368
375
  arm agent info <name> 查看 Agent 详情
369
376
  arm agent download <name> [dir] 下载 Agent
370
377
  arm agent create <name> 创建 Agent (--description, --prompt, --avatar, --skill, --knowledge)
378
+ arm agent create --from=<folder> 从本地文件夹创建 Agent
371
379
  arm agent update <id> 更新 Agent (--name, --description, --prompt, --avatar, --status)
372
380
  arm agent delete <id> 删除 Agent
373
381
  arm agent bind <id> --skill=<id> 绑定 Skill 到 Agent