@annals/agent-mesh 0.15.1 → 0.16.1

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.
Files changed (2) hide show
  1. package/dist/index.js +474 -114
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2562,7 +2562,7 @@ var ERROR_HINTS = {
2562
2562
  agent_offline: "Agent must be online for first publish. Run `agent-mesh connect` first.",
2563
2563
  email_required: "Email required. Visit https://agents.hot/settings to add one.",
2564
2564
  github_required: "GitHub account required. Visit https://agents.hot/settings to link one.",
2565
- validation_error: "Invalid input. Check your skill.json or command flags.",
2565
+ validation_error: "Invalid input. Check your SKILL.md frontmatter or command flags.",
2566
2566
  permission_denied: "You don't have permission to modify this skill.",
2567
2567
  file_too_large: "Package file exceeds the 50MB limit.",
2568
2568
  subscription_required: "This is a private agent. Subscribe first: agent-mesh subscribe <author-login>"
@@ -2593,6 +2593,31 @@ var PlatformClient = class {
2593
2593
  async del(path, body) {
2594
2594
  return this.request("DELETE", path, body);
2595
2595
  }
2596
+ async getRaw(path) {
2597
+ const url = `${this.baseUrl}${path}`;
2598
+ let res;
2599
+ try {
2600
+ res = await fetch(url, {
2601
+ method: "GET",
2602
+ headers: { Authorization: `Bearer ${this.token}` }
2603
+ });
2604
+ } catch (err) {
2605
+ throw new PlatformApiError(0, "network_error", `Network error: ${err.message}`);
2606
+ }
2607
+ if (!res.ok) {
2608
+ let errorCode = "unknown";
2609
+ let message = `HTTP ${res.status}`;
2610
+ try {
2611
+ const data = await res.json();
2612
+ errorCode = data.error ?? errorCode;
2613
+ message = data.error_description ?? data.message ?? message;
2614
+ } catch {
2615
+ }
2616
+ const hint = ERROR_HINTS[errorCode];
2617
+ throw new PlatformApiError(res.status, errorCode, hint ?? message);
2618
+ }
2619
+ return res;
2620
+ }
2596
2621
  async postFormData(path, formData) {
2597
2622
  const url = `${this.baseUrl}${path}`;
2598
2623
  let res;
@@ -2919,6 +2944,12 @@ async function asyncChat(opts) {
2919
2944
  process.stderr.write(` done
2920
2945
  `);
2921
2946
  process.stdout.write((task.result || "") + "\n");
2947
+ if (task.attachments?.length) {
2948
+ for (const att of task.attachments) {
2949
+ process.stdout.write(`${GRAY}[file: ${att.name} -> ${att.url}]${RESET}
2950
+ `);
2951
+ }
2952
+ }
2922
2953
  return;
2923
2954
  }
2924
2955
  if (task.status === "failed") {
@@ -3121,11 +3152,11 @@ function registerChatCommand(program2) {
3121
3152
  }
3122
3153
 
3123
3154
  // src/commands/skills.ts
3124
- import { readFile as readFile4, writeFile as writeFile2, readdir as readdir2, stat as stat3, mkdir as mkdir2 } from "fs/promises";
3155
+ import { readFile as readFile4, writeFile as writeFile3, readdir as readdir2, mkdir as mkdir2, rm } from "fs/promises";
3125
3156
  import { join as join8, resolve, relative as relative4 } from "path";
3126
3157
 
3127
3158
  // src/utils/skill-parser.ts
3128
- import { readFile as readFile3, stat as stat2 } from "fs/promises";
3159
+ import { readFile as readFile3, writeFile as writeFile2, stat as stat2 } from "fs/promises";
3129
3160
  import { join as join7 } from "path";
3130
3161
  function parseSkillMd(raw) {
3131
3162
  const trimmed = raw.trimStart();
@@ -3186,36 +3217,13 @@ function parseSkillMd(raw) {
3186
3217
  return { frontmatter, content };
3187
3218
  }
3188
3219
  async function loadSkillManifest(dir) {
3189
- const skillJsonPath = join7(dir, "skill.json");
3190
- try {
3191
- const raw = await readFile3(skillJsonPath, "utf-8");
3192
- const data = JSON.parse(raw);
3193
- if (!data.name) throw new Error("skill.json missing required field: name");
3194
- if (!data.version) throw new Error("skill.json missing required field: version");
3195
- return {
3196
- name: data.name,
3197
- version: data.version,
3198
- description: data.description,
3199
- main: data.main || "SKILL.md",
3200
- category: data.category,
3201
- tags: data.tags,
3202
- author: data.author,
3203
- source_url: data.source_url,
3204
- private: data.private,
3205
- files: data.files
3206
- };
3207
- } catch (err) {
3208
- if (err.code !== "ENOENT") {
3209
- throw err;
3210
- }
3211
- }
3212
3220
  const skillMdPath = join7(dir, "SKILL.md");
3213
3221
  try {
3214
3222
  const raw = await readFile3(skillMdPath, "utf-8");
3215
3223
  const { frontmatter } = parseSkillMd(raw);
3216
3224
  const name = frontmatter.name;
3217
3225
  if (!name) {
3218
- throw new Error('No skill.json found and SKILL.md has no "name" in frontmatter');
3226
+ throw new Error('SKILL.md has no "name" in frontmatter');
3219
3227
  }
3220
3228
  return {
3221
3229
  name,
@@ -3230,7 +3238,7 @@ async function loadSkillManifest(dir) {
3230
3238
  };
3231
3239
  } catch (err) {
3232
3240
  if (err.code === "ENOENT") {
3233
- throw new Error(`No skill.json or SKILL.md found in ${dir}`);
3241
+ throw new Error(`No SKILL.md found in ${dir}`);
3234
3242
  }
3235
3243
  throw err;
3236
3244
  }
@@ -3243,9 +3251,33 @@ async function pathExists(p) {
3243
3251
  return false;
3244
3252
  }
3245
3253
  }
3254
+ async function updateFrontmatterField(filePath, field, value) {
3255
+ const raw = await readFile3(filePath, "utf-8");
3256
+ const trimmed = raw.trimStart();
3257
+ if (!trimmed.startsWith("---")) {
3258
+ throw new Error("SKILL.md has no frontmatter block");
3259
+ }
3260
+ const endIdx = trimmed.indexOf("\n---", 3);
3261
+ if (endIdx === -1) {
3262
+ throw new Error("SKILL.md has no frontmatter block");
3263
+ }
3264
+ const yamlBlock = trimmed.slice(4, endIdx);
3265
+ const after = trimmed.slice(endIdx);
3266
+ const fieldRegex = new RegExp(`^(${field}\\s*:\\s*)(.*)$`, "m");
3267
+ if (fieldRegex.test(yamlBlock)) {
3268
+ const updated = yamlBlock.replace(fieldRegex, `$1${value}`);
3269
+ await writeFile2(filePath, `---
3270
+ ${updated}${after}`);
3271
+ } else {
3272
+ const updated = `${yamlBlock}
3273
+ ${field}: ${value}`;
3274
+ await writeFile2(filePath, `---
3275
+ ${updated}${after}`);
3276
+ }
3277
+ }
3246
3278
 
3247
3279
  // src/utils/zip.ts
3248
- import { deflateRawSync } from "zlib";
3280
+ import { deflateRawSync, inflateRawSync } from "zlib";
3249
3281
  function dosTime(date) {
3250
3282
  return {
3251
3283
  time: date.getHours() << 11 | date.getMinutes() << 5 | date.getSeconds() >> 1,
@@ -3341,6 +3373,55 @@ function createZipBuffer(entries) {
3341
3373
  chunks.push(eocd);
3342
3374
  return Buffer.concat(chunks);
3343
3375
  }
3376
+ function extractZipBuffer(buf) {
3377
+ const entries = [];
3378
+ let eocdOffset = -1;
3379
+ for (let i = buf.length - 22; i >= 0; i--) {
3380
+ if (buf.readUInt32LE(i) === 101010256) {
3381
+ eocdOffset = i;
3382
+ break;
3383
+ }
3384
+ }
3385
+ if (eocdOffset === -1) {
3386
+ throw new Error("Invalid ZIP: EOCD not found");
3387
+ }
3388
+ const entryCount = buf.readUInt16LE(eocdOffset + 10);
3389
+ const centralDirOffset = buf.readUInt32LE(eocdOffset + 16);
3390
+ let offset = centralDirOffset;
3391
+ for (let i = 0; i < entryCount; i++) {
3392
+ if (buf.readUInt32LE(offset) !== 33639248) {
3393
+ throw new Error(`Invalid ZIP: bad central directory signature at ${offset}`);
3394
+ }
3395
+ const compressionMethod = buf.readUInt16LE(offset + 10);
3396
+ const compressedSize = buf.readUInt32LE(offset + 20);
3397
+ const uncompressedSize = buf.readUInt32LE(offset + 24);
3398
+ const nameLen = buf.readUInt16LE(offset + 28);
3399
+ const extraLen = buf.readUInt16LE(offset + 30);
3400
+ const commentLen = buf.readUInt16LE(offset + 32);
3401
+ const localHeaderOffset = buf.readUInt32LE(offset + 42);
3402
+ const name = buf.subarray(offset + 46, offset + 46 + nameLen).toString("utf-8");
3403
+ if (!name.endsWith("/")) {
3404
+ const localNameLen = buf.readUInt16LE(localHeaderOffset + 26);
3405
+ const localExtraLen = buf.readUInt16LE(localHeaderOffset + 28);
3406
+ const dataOffset = localHeaderOffset + 30 + localNameLen + localExtraLen;
3407
+ const compressedData = buf.subarray(dataOffset, dataOffset + compressedSize);
3408
+ let data;
3409
+ if (compressionMethod === 0) {
3410
+ data = Buffer.from(compressedData);
3411
+ } else if (compressionMethod === 8) {
3412
+ data = inflateRawSync(compressedData);
3413
+ } else {
3414
+ throw new Error(`Unsupported compression method: ${compressionMethod}`);
3415
+ }
3416
+ if (data.length !== uncompressedSize) {
3417
+ throw new Error(`Size mismatch for ${name}: expected ${uncompressedSize}, got ${data.length}`);
3418
+ }
3419
+ entries.push({ path: name, data });
3420
+ }
3421
+ offset += 46 + nameLen + extraLen + commentLen;
3422
+ }
3423
+ return entries;
3424
+ }
3344
3425
 
3345
3426
  // src/commands/skills.ts
3346
3427
  var slog = {
@@ -3373,31 +3454,34 @@ function outputError(error, message, hint) {
3373
3454
  function resolveSkillDir(pathArg) {
3374
3455
  return pathArg ? resolve(pathArg) : process.cwd();
3375
3456
  }
3457
+ function parseSkillRef(ref) {
3458
+ if (!ref.includes("/")) {
3459
+ outputError("validation_error", `Invalid skill reference: "${ref}". Use author/slug format (e.g. kcsx/code-review)`);
3460
+ }
3461
+ const [authorLogin, slug] = ref.split("/", 2);
3462
+ if (!authorLogin || !slug) {
3463
+ outputError("validation_error", `Invalid skill reference: "${ref}". Use author/slug format (e.g. kcsx/code-review)`);
3464
+ }
3465
+ return { authorLogin, slug };
3466
+ }
3467
+ function skillApiPath(authorLogin, slug) {
3468
+ return `/api/skills/${encodeURIComponent(authorLogin)}/${encodeURIComponent(slug)}`;
3469
+ }
3470
+ async function resolveSkillsRootAsync(pathArg) {
3471
+ const projectRoot = pathArg ? resolve(pathArg) : process.cwd();
3472
+ const agentsDir = join8(projectRoot, ".agents", "skills");
3473
+ if (await pathExists(agentsDir)) {
3474
+ return { projectRoot, skillsDir: agentsDir, convention: "agents" };
3475
+ }
3476
+ const claudeDir = join8(projectRoot, ".claude", "skills");
3477
+ return { projectRoot, skillsDir: claudeDir, convention: "claude" };
3478
+ }
3376
3479
  async function collectPackFiles(dir, manifest) {
3377
3480
  const results = [];
3378
- if (manifest.files && manifest.files.length > 0) {
3379
- for (const pattern of manifest.files) {
3380
- const fullPath = join8(dir, pattern);
3381
- try {
3382
- const s = await stat3(fullPath);
3383
- if (s.isDirectory()) {
3384
- const sub = await walkDir(fullPath);
3385
- for (const f of sub) {
3386
- results.push(relative4(dir, f));
3387
- }
3388
- } else {
3389
- results.push(pattern);
3390
- }
3391
- } catch {
3392
- }
3393
- }
3394
- } else {
3395
- const all = await walkDir(dir);
3396
- for (const f of all) {
3397
- const rel = relative4(dir, f);
3398
- if (rel === "skill.json") continue;
3399
- results.push(rel);
3400
- }
3481
+ const all = await walkDir(dir);
3482
+ for (const f of all) {
3483
+ const rel = relative4(dir, f);
3484
+ results.push(rel);
3401
3485
  }
3402
3486
  const mainFile = manifest.main || "SKILL.md";
3403
3487
  if (!results.includes(mainFile)) {
@@ -3468,8 +3552,42 @@ function bumpVersion(current, bump) {
3468
3552
  throw new Error(`Invalid bump type: ${bump}. Use major, minor, patch, or a version string.`);
3469
3553
  }
3470
3554
  }
3555
+ async function downloadAndInstallSkill(client, authorLogin, slug, skillsDir) {
3556
+ const meta = await client.get(skillApiPath(authorLogin, slug));
3557
+ const targetDir = join8(skillsDir, slug);
3558
+ await mkdir2(targetDir, { recursive: true });
3559
+ if (meta.has_files) {
3560
+ const res = await client.getRaw(`${skillApiPath(authorLogin, slug)}/download`);
3561
+ const arrayBuf = await res.arrayBuffer();
3562
+ const buf = Buffer.from(arrayBuf);
3563
+ const entries = extractZipBuffer(buf);
3564
+ for (const entry of entries) {
3565
+ const filePath = join8(targetDir, entry.path);
3566
+ const dir = join8(filePath, "..");
3567
+ await mkdir2(dir, { recursive: true });
3568
+ await writeFile3(filePath, entry.data);
3569
+ }
3570
+ return {
3571
+ slug,
3572
+ name: meta.name,
3573
+ version: meta.version || "1.0.0",
3574
+ files_count: entries.length
3575
+ };
3576
+ } else {
3577
+ const res = await client.getRaw(`${skillApiPath(authorLogin, slug)}/raw`);
3578
+ const content = await res.text();
3579
+ await writeFile3(join8(targetDir, "SKILL.md"), content);
3580
+ return {
3581
+ slug,
3582
+ name: meta.name,
3583
+ version: meta.version || "1.0.0",
3584
+ files_count: 1
3585
+ };
3586
+ }
3587
+ }
3471
3588
  var SKILL_MD_TEMPLATE = `---
3472
3589
  name: {{name}}
3590
+ description: "{{description}}"
3473
3591
  version: 1.0.0
3474
3592
  ---
3475
3593
 
@@ -3481,62 +3599,31 @@ version: 1.0.0
3481
3599
 
3482
3600
  Describe how to use this skill.
3483
3601
  `;
3484
- var SKILL_JSON_TEMPLATE = (name, description) => ({
3485
- name,
3486
- version: "1.0.0",
3487
- description,
3488
- main: "SKILL.md",
3489
- category: "general",
3490
- tags: [],
3491
- files: ["SKILL.md"]
3492
- });
3493
3602
  function registerSkillsCommand(program2) {
3494
- const skills = program2.command("skills").description("Manage skill packages (publish, pack, version)");
3603
+ const skills = program2.command("skills").description("Manage skill packages (publish, install, pack, version)");
3495
3604
  skills.command("init [path]").description("Initialize a new skill project").option("--name <name>", "Skill name").option("--description <desc>", "Skill description").action(async (pathArg, opts) => {
3496
3605
  try {
3497
3606
  const dir = resolveSkillDir(pathArg);
3498
3607
  await mkdir2(dir, { recursive: true });
3499
- let name = opts.name;
3500
- let description = opts.description || "";
3501
3608
  const skillMdPath = join8(dir, "SKILL.md");
3502
- const skillJsonPath = join8(dir, "skill.json");
3503
- if (await pathExists(skillJsonPath)) {
3504
- outputError("already_exists", "skill.json already exists in this directory");
3505
- }
3506
3609
  if (await pathExists(skillMdPath)) {
3507
3610
  const raw = await readFile4(skillMdPath, "utf-8");
3508
3611
  const { frontmatter } = parseSkillMd(raw);
3509
3612
  if (frontmatter.name) {
3510
- name = name || frontmatter.name;
3511
- description = description || frontmatter.description || "";
3512
- const manifest2 = {
3513
- name,
3514
- version: frontmatter.version || "1.0.0",
3515
- description,
3516
- main: "SKILL.md",
3517
- category: frontmatter.category || "general",
3518
- tags: frontmatter.tags || [],
3519
- author: frontmatter.author,
3520
- source_url: frontmatter.source_url,
3521
- files: ["SKILL.md"]
3522
- };
3523
- await writeFile2(skillJsonPath, JSON.stringify(manifest2, null, 2) + "\n");
3524
- slog.info(`Migrated frontmatter from SKILL.md to skill.json`);
3525
- outputJson({ success: true, path: skillJsonPath, migrated: true });
3613
+ slog.info(`SKILL.md already exists with name: ${frontmatter.name}`);
3614
+ outputJson({ success: true, exists: true, path: skillMdPath });
3526
3615
  return;
3527
3616
  }
3528
3617
  }
3618
+ let name = opts.name;
3619
+ const description = opts.description || "";
3529
3620
  if (!name) {
3530
3621
  name = dir.split("/").pop()?.replace(/[^a-z0-9-]/gi, "-").toLowerCase() || "my-skill";
3531
3622
  }
3532
- const manifest = SKILL_JSON_TEMPLATE(name, description);
3533
- await writeFile2(skillJsonPath, JSON.stringify(manifest, null, 2) + "\n");
3534
- if (!await pathExists(skillMdPath)) {
3535
- const content = SKILL_MD_TEMPLATE.replace(/\{\{name\}\}/g, name).replace(/\{\{description\}\}/g, description || "A new skill.");
3536
- await writeFile2(skillMdPath, content);
3537
- }
3623
+ const content = SKILL_MD_TEMPLATE.replace(/\{\{name\}\}/g, name).replace(/\{\{description\}\}/g, description || "A new skill.");
3624
+ await writeFile3(skillMdPath, content);
3538
3625
  slog.info(`Initialized skill: ${name}`);
3539
- outputJson({ success: true, path: skillJsonPath });
3626
+ outputJson({ success: true, path: skillMdPath });
3540
3627
  } catch (err) {
3541
3628
  if (err instanceof Error && err.message.includes("already_exists")) throw err;
3542
3629
  outputError("init_failed", err.message);
@@ -3548,7 +3635,7 @@ function registerSkillsCommand(program2) {
3548
3635
  const manifest = await loadSkillManifest(dir);
3549
3636
  const result = await packSkill(dir, manifest);
3550
3637
  const outPath = join8(dir, result.filename);
3551
- await writeFile2(outPath, result.buffer);
3638
+ await writeFile3(outPath, result.buffer);
3552
3639
  slog.info(`Packed ${result.files.length} files \u2192 ${result.filename} (${result.size} bytes)`);
3553
3640
  outputJson({
3554
3641
  success: true,
@@ -3618,11 +3705,13 @@ function registerSkillsCommand(program2) {
3618
3705
  const client = createClient();
3619
3706
  const result = await client.postFormData("/api/skills/publish", formData);
3620
3707
  slog.success(`Skill ${result.action}: ${manifest.name}`);
3708
+ const authorLogin = result.skill.author_login;
3709
+ const skillUrl = authorLogin ? `https://agents.hot/skills/${authorLogin}/${result.skill.slug}` : `https://agents.hot/skills/${result.skill.slug}`;
3621
3710
  outputJson({
3622
3711
  success: true,
3623
3712
  action: result.action,
3624
3713
  skill: result.skill,
3625
- url: `https://agents.hot/skills/${result.skill.slug}`
3714
+ url: skillUrl
3626
3715
  });
3627
3716
  } catch (err) {
3628
3717
  if (err instanceof PlatformApiError) {
@@ -3631,16 +3720,17 @@ function registerSkillsCommand(program2) {
3631
3720
  outputError("publish_failed", err.message);
3632
3721
  }
3633
3722
  });
3634
- skills.command("info <slug>").description("View skill details").option("--human", "Human-readable output").action(async (slug, opts) => {
3723
+ skills.command("info <ref>").description("View skill details (use author/slug format)").option("--human", "Human-readable output").action(async (ref, opts) => {
3635
3724
  try {
3725
+ const { authorLogin, slug } = parseSkillRef(ref);
3636
3726
  const client = createClient();
3637
- const data = await client.get(`/api/skills/${encodeURIComponent(slug)}`);
3727
+ const data = await client.get(skillApiPath(authorLogin, slug));
3638
3728
  if (opts.human) {
3639
3729
  console.log("");
3640
3730
  console.log(` ${BOLD}${data.name}${RESET} v${data.version || "?"}`);
3641
3731
  if (data.description) console.log(` ${data.description}`);
3642
- console.log(` ${GRAY}slug${RESET} ${data.slug}`);
3643
- console.log(` ${GRAY}author${RESET} ${data.author || "\u2014"}`);
3732
+ console.log(` ${GRAY}ref${RESET} ${authorLogin}/${data.slug}`);
3733
+ console.log(` ${GRAY}author${RESET} ${data.author_login || data.author || "\u2014"}`);
3644
3734
  console.log(` ${GRAY}category${RESET} ${data.category || "\u2014"}`);
3645
3735
  console.log(` ${GRAY}installs${RESET} ${data.installs ?? 0}`);
3646
3736
  console.log(` ${GRAY}private${RESET} ${data.is_private ? "yes" : "no"}`);
@@ -3669,12 +3759,14 @@ function registerSkillsCommand(program2) {
3669
3759
  const table = renderTable(
3670
3760
  [
3671
3761
  { key: "name", label: "NAME", width: 24 },
3762
+ { key: "author", label: "AUTHOR", width: 16 },
3672
3763
  { key: "version", label: "VERSION", width: 12 },
3673
3764
  { key: "installs", label: "INSTALLS", width: 12, align: "right" },
3674
3765
  { key: "private", label: "PRIVATE", width: 10 }
3675
3766
  ],
3676
3767
  data.owned.map((s) => ({
3677
3768
  name: s.name,
3769
+ author: s.author_login || s.author || "\u2014",
3678
3770
  version: s.version || "\u2014",
3679
3771
  installs: String(s.installs ?? 0),
3680
3772
  private: s.is_private ? "yes" : `${GREEN}no${RESET}`
@@ -3692,7 +3784,7 @@ function registerSkillsCommand(program2) {
3692
3784
  ],
3693
3785
  data.authorized.map((s) => ({
3694
3786
  name: s.name,
3695
- author: s.author || "\u2014",
3787
+ author: s.author_login || s.author || "\u2014",
3696
3788
  version: s.version || "\u2014"
3697
3789
  }))
3698
3790
  );
@@ -3708,11 +3800,12 @@ function registerSkillsCommand(program2) {
3708
3800
  outputError("list_failed", err.message);
3709
3801
  }
3710
3802
  });
3711
- skills.command("unpublish <slug>").description("Unpublish a skill").action(async (slug) => {
3803
+ skills.command("unpublish <ref>").description("Unpublish a skill (use author/slug format)").action(async (ref) => {
3712
3804
  try {
3805
+ const { authorLogin, slug } = parseSkillRef(ref);
3713
3806
  const client = createClient();
3714
- const result = await client.del(`/api/skills/${encodeURIComponent(slug)}`);
3715
- slog.success(`Skill unpublished: ${slug}`);
3807
+ const result = await client.del(skillApiPath(authorLogin, slug));
3808
+ slog.success(`Skill unpublished: ${authorLogin}/${slug}`);
3716
3809
  outputJson(result);
3717
3810
  } catch (err) {
3718
3811
  if (err instanceof PlatformApiError) {
@@ -3724,16 +3817,15 @@ function registerSkillsCommand(program2) {
3724
3817
  skills.command("version <bump> [path]").description("Bump skill version (patch | minor | major | x.y.z)").action(async (bump, pathArg) => {
3725
3818
  try {
3726
3819
  const dir = resolveSkillDir(pathArg);
3727
- const skillJsonPath = join8(dir, "skill.json");
3728
- if (!await pathExists(skillJsonPath)) {
3729
- outputError("not_found", "No skill.json found. Run `agent-mesh skills init` first.");
3820
+ const skillMdPath = join8(dir, "SKILL.md");
3821
+ if (!await pathExists(skillMdPath)) {
3822
+ outputError("not_found", "No SKILL.md found. Run `agent-mesh skills init` first.");
3730
3823
  }
3731
- const raw = await readFile4(skillJsonPath, "utf-8");
3732
- const data = JSON.parse(raw);
3733
- const oldVersion = data.version || "0.0.0";
3824
+ const raw = await readFile4(skillMdPath, "utf-8");
3825
+ const { frontmatter } = parseSkillMd(raw);
3826
+ const oldVersion = frontmatter.version || "0.0.0";
3734
3827
  const newVersion = bumpVersion(oldVersion, bump);
3735
- data.version = newVersion;
3736
- await writeFile2(skillJsonPath, JSON.stringify(data, null, 2) + "\n");
3828
+ await updateFrontmatterField(skillMdPath, "version", newVersion);
3737
3829
  slog.success(`${oldVersion} \u2192 ${newVersion}`);
3738
3830
  outputJson({ success: true, old: oldVersion, new: newVersion });
3739
3831
  } catch (err) {
@@ -3741,6 +3833,196 @@ function registerSkillsCommand(program2) {
3741
3833
  outputError("version_failed", err.message);
3742
3834
  }
3743
3835
  });
3836
+ skills.command("install <ref> [path]").description("Install a skill from agents.hot (use author/slug format)").option("--force", "Overwrite if already installed").action(async (ref, pathArg, opts) => {
3837
+ try {
3838
+ const { authorLogin, slug } = parseSkillRef(ref);
3839
+ const { skillsDir } = await resolveSkillsRootAsync(pathArg);
3840
+ const targetDir = join8(skillsDir, slug);
3841
+ if (await pathExists(targetDir)) {
3842
+ if (!opts.force) {
3843
+ outputError("already_installed", `Skill "${slug}" is already installed at ${targetDir}. Use --force to overwrite.`);
3844
+ }
3845
+ await rm(targetDir, { recursive: true, force: true });
3846
+ }
3847
+ slog.info(`Installing ${authorLogin}/${slug}...`);
3848
+ const client = createClient();
3849
+ const result = await downloadAndInstallSkill(client, authorLogin, slug, skillsDir);
3850
+ slog.success(`Installed ${result.name} (${result.files_count} files)`);
3851
+ outputJson({
3852
+ success: true,
3853
+ skill: {
3854
+ author: authorLogin,
3855
+ slug: result.slug,
3856
+ name: result.name,
3857
+ version: result.version
3858
+ },
3859
+ installed_to: targetDir,
3860
+ files_count: result.files_count
3861
+ });
3862
+ } catch (err) {
3863
+ if (err instanceof PlatformApiError) {
3864
+ outputError(err.errorCode, err.message);
3865
+ }
3866
+ outputError("install_failed", err.message);
3867
+ }
3868
+ });
3869
+ skills.command("update [ref] [path]").description("Update installed skill(s) from agents.hot").action(async (ref, pathArg) => {
3870
+ try {
3871
+ const { skillsDir } = await resolveSkillsRootAsync(pathArg);
3872
+ const client = createClient();
3873
+ const updated = [];
3874
+ const skipped = [];
3875
+ const failed = [];
3876
+ if (ref) {
3877
+ const { authorLogin, slug } = parseSkillRef(ref);
3878
+ const targetDir = join8(skillsDir, slug);
3879
+ if (!await pathExists(targetDir)) {
3880
+ outputError("not_installed", `Skill "${slug}" is not installed. Use "skills install ${ref}" first.`);
3881
+ }
3882
+ const skillMdPath = join8(targetDir, "SKILL.md");
3883
+ let localVersion = "0.0.0";
3884
+ if (await pathExists(skillMdPath)) {
3885
+ const raw = await readFile4(skillMdPath, "utf-8");
3886
+ const { frontmatter } = parseSkillMd(raw);
3887
+ localVersion = frontmatter.version || "0.0.0";
3888
+ }
3889
+ const remote = await client.get(skillApiPath(authorLogin, slug));
3890
+ const remoteVersion = remote.version || "0.0.0";
3891
+ if (remoteVersion === localVersion) {
3892
+ slog.info(`${slug} is already up to date (v${localVersion})`);
3893
+ skipped.push({ slug, reason: "up_to_date" });
3894
+ } else {
3895
+ slog.info(`Updating ${slug}: v${localVersion} \u2192 v${remoteVersion}...`);
3896
+ await rm(targetDir, { recursive: true, force: true });
3897
+ await downloadAndInstallSkill(client, authorLogin, slug, skillsDir);
3898
+ updated.push({ slug, name: remote.name, old_version: localVersion, new_version: remoteVersion });
3899
+ slog.success(`Updated ${slug} to v${remoteVersion}`);
3900
+ }
3901
+ } else {
3902
+ if (!await pathExists(skillsDir)) {
3903
+ outputError("no_skills_dir", `Skills directory not found: ${skillsDir}`);
3904
+ }
3905
+ const entries = await readdir2(skillsDir, { withFileTypes: true });
3906
+ for (const entry of entries) {
3907
+ if (!entry.isDirectory()) continue;
3908
+ const slug = entry.name;
3909
+ const skillMdPath = join8(skillsDir, slug, "SKILL.md");
3910
+ if (!await pathExists(skillMdPath)) {
3911
+ skipped.push({ slug, reason: "no_skill_md" });
3912
+ continue;
3913
+ }
3914
+ const raw = await readFile4(skillMdPath, "utf-8");
3915
+ const { frontmatter } = parseSkillMd(raw);
3916
+ const localVersion = frontmatter.version || "0.0.0";
3917
+ const authorLogin = frontmatter.author;
3918
+ if (!authorLogin) {
3919
+ skipped.push({ slug, reason: "no_author_in_frontmatter" });
3920
+ continue;
3921
+ }
3922
+ try {
3923
+ const remote = await client.get(skillApiPath(authorLogin, slug));
3924
+ const remoteVersion = remote.version || "0.0.0";
3925
+ if (remoteVersion === localVersion) {
3926
+ skipped.push({ slug, reason: "up_to_date" });
3927
+ } else {
3928
+ slog.info(`Updating ${slug}: v${localVersion} \u2192 v${remoteVersion}...`);
3929
+ await rm(join8(skillsDir, slug), { recursive: true, force: true });
3930
+ await downloadAndInstallSkill(client, authorLogin, slug, skillsDir);
3931
+ updated.push({ slug, name: remote.name, old_version: localVersion, new_version: remoteVersion });
3932
+ }
3933
+ } catch (err) {
3934
+ failed.push({ slug, error: err.message });
3935
+ }
3936
+ }
3937
+ }
3938
+ slog.success(`Update complete: ${updated.length} updated, ${skipped.length} skipped, ${failed.length} failed`);
3939
+ outputJson({ success: true, updated, skipped, failed });
3940
+ } catch (err) {
3941
+ if (err instanceof PlatformApiError) {
3942
+ outputError(err.errorCode, err.message);
3943
+ }
3944
+ outputError("update_failed", err.message);
3945
+ }
3946
+ });
3947
+ skills.command("remove <slug> [path]").description("Remove a locally installed skill").action(async (slug, pathArg) => {
3948
+ try {
3949
+ const { skillsDir } = await resolveSkillsRootAsync(pathArg);
3950
+ const targetDir = join8(skillsDir, slug);
3951
+ if (!await pathExists(targetDir)) {
3952
+ outputError("not_installed", `Skill "${slug}" is not installed at ${targetDir}`);
3953
+ }
3954
+ await rm(targetDir, { recursive: true, force: true });
3955
+ slog.success(`Removed skill: ${slug}`);
3956
+ outputJson({ success: true, removed: slug, path: targetDir });
3957
+ } catch (err) {
3958
+ outputError("remove_failed", err.message);
3959
+ }
3960
+ });
3961
+ skills.command("installed [path]").description("List locally installed skills").option("--check-updates", "Check for available updates").option("--human", "Human-readable table output").action(async (pathArg, opts) => {
3962
+ try {
3963
+ const { skillsDir } = await resolveSkillsRootAsync(pathArg);
3964
+ if (!await pathExists(skillsDir)) {
3965
+ if (opts.human) {
3966
+ slog.info(`No skills directory found at ${skillsDir}`);
3967
+ return;
3968
+ }
3969
+ outputJson({ skills_dir: skillsDir, skills: [] });
3970
+ return;
3971
+ }
3972
+ const entries = await readdir2(skillsDir, { withFileTypes: true });
3973
+ const skills2 = [];
3974
+ for (const entry of entries) {
3975
+ if (!entry.isDirectory()) continue;
3976
+ const slug = entry.name;
3977
+ const skillMdPath = join8(skillsDir, slug, "SKILL.md");
3978
+ if (!await pathExists(skillMdPath)) continue;
3979
+ const raw = await readFile4(skillMdPath, "utf-8");
3980
+ const { frontmatter } = parseSkillMd(raw);
3981
+ const skillInfo = {
3982
+ slug,
3983
+ name: frontmatter.name || slug,
3984
+ version: frontmatter.version || "0.0.0",
3985
+ author: frontmatter.author
3986
+ };
3987
+ if (opts.checkUpdates && skillInfo.author) {
3988
+ try {
3989
+ const client = createClient();
3990
+ const remote = await client.get(skillApiPath(skillInfo.author, slug));
3991
+ skillInfo.remote_version = remote.version || "0.0.0";
3992
+ skillInfo.has_update = skillInfo.remote_version !== skillInfo.version;
3993
+ } catch {
3994
+ }
3995
+ }
3996
+ skills2.push(skillInfo);
3997
+ }
3998
+ if (opts.human) {
3999
+ if (skills2.length === 0) {
4000
+ slog.info("No skills installed.");
4001
+ return;
4002
+ }
4003
+ const columns = [
4004
+ { key: "name", label: "NAME", width: 24 },
4005
+ { key: "version", label: "VERSION", width: 12 },
4006
+ { key: "author", label: "AUTHOR", width: 16 }
4007
+ ];
4008
+ if (opts.checkUpdates) {
4009
+ columns.push({ key: "update", label: "UPDATE", width: 14 });
4010
+ }
4011
+ const rows = skills2.map((s) => ({
4012
+ name: s.name,
4013
+ version: s.version,
4014
+ author: s.author || "\u2014",
4015
+ update: s.has_update ? `${GREEN}${s.remote_version}${RESET}` : "\u2014"
4016
+ }));
4017
+ slog.banner("Installed Skills");
4018
+ console.log(renderTable(columns, rows));
4019
+ return;
4020
+ }
4021
+ outputJson({ skills_dir: skillsDir, skills: skills2 });
4022
+ } catch (err) {
4023
+ outputError("installed_failed", err.message);
4024
+ }
4025
+ });
3744
4026
  }
3745
4027
 
3746
4028
  // src/commands/discover.ts
@@ -3806,6 +4088,25 @@ function registerDiscoverCommand(program2) {
3806
4088
  // src/commands/call.ts
3807
4089
  import { readFileSync, writeFileSync as writeFileSync2 } from "fs";
3808
4090
  var DEFAULT_BASE_URL4 = "https://agents.hot";
4091
+ async function submitRating(baseUrl, token, agentId, callId, rating) {
4092
+ const res = await fetch(`${baseUrl}/api/agents/${agentId}/rate`, {
4093
+ method: "POST",
4094
+ headers: {
4095
+ Authorization: `Bearer ${token}`,
4096
+ "Content-Type": "application/json"
4097
+ },
4098
+ body: JSON.stringify({ call_id: callId, rating })
4099
+ });
4100
+ if (!res.ok) {
4101
+ let msg = `HTTP ${res.status}`;
4102
+ try {
4103
+ const body = await res.json();
4104
+ msg = body.message || body.error || msg;
4105
+ } catch {
4106
+ }
4107
+ throw new Error(msg);
4108
+ }
4109
+ }
3809
4110
  function sleep5(ms) {
3810
4111
  return new Promise((resolve2) => setTimeout(resolve2, ms));
3811
4112
  }
@@ -3878,15 +4179,30 @@ async function asyncCall(opts) {
3878
4179
  }
3879
4180
  const result = task.result || "";
3880
4181
  if (opts.json) {
3881
- console.log(JSON.stringify({ call_id, request_id, status: "completed", result }));
4182
+ console.log(JSON.stringify({
4183
+ call_id,
4184
+ request_id,
4185
+ status: "completed",
4186
+ result,
4187
+ ...task.attachments?.length ? { attachments: task.attachments } : {},
4188
+ rate_hint: `POST /api/agents/${opts.id}/rate body: { call_id: "${call_id}", rating: 1-5 }`
4189
+ }));
3882
4190
  } else {
3883
4191
  process.stdout.write(result + "\n");
4192
+ if (task.attachments?.length) {
4193
+ for (const att of task.attachments) {
4194
+ log.info(` ${GRAY}File:${RESET} ${att.name} ${GRAY}${att.url}${RESET}`);
4195
+ }
4196
+ }
3884
4197
  }
3885
4198
  if (opts.outputFile && result) {
3886
4199
  writeFileSync2(opts.outputFile, result);
3887
4200
  if (!opts.json) log.info(`Saved to ${opts.outputFile}`);
3888
4201
  }
3889
- return;
4202
+ if (!opts.json) {
4203
+ log.info(`${GRAY}Rate this call: agent-mesh rate ${call_id} <1-5> --agent ${opts.id}${RESET}`);
4204
+ }
4205
+ return { callId: call_id };
3890
4206
  }
3891
4207
  if (task.status === "failed") {
3892
4208
  if (!opts.json) {
@@ -3965,6 +4281,7 @@ async function streamCall(opts) {
3965
4281
  let buffer = "";
3966
4282
  let outputBuffer = "";
3967
4283
  let inThinkingBlock = false;
4284
+ let callId = res.headers.get("X-Call-Id") || "";
3968
4285
  while (true) {
3969
4286
  const { done, value } = await reader.read();
3970
4287
  if (done) break;
@@ -3975,6 +4292,9 @@ async function streamCall(opts) {
3975
4292
  if (data === "[DONE]") continue;
3976
4293
  try {
3977
4294
  const event = JSON.parse(data);
4295
+ if (event.type === "start" && event.call_id) {
4296
+ callId = event.call_id;
4297
+ }
3978
4298
  if (opts.json) {
3979
4299
  console.log(JSON.stringify(event));
3980
4300
  } else {
@@ -4035,10 +4355,14 @@ Error: ${event.message}
4035
4355
  if (!opts.json) {
4036
4356
  console.log("\n");
4037
4357
  log.success("Call completed");
4358
+ if (callId) {
4359
+ log.info(`${GRAY}Rate this call: agent-mesh rate ${callId} <1-5> --agent ${opts.id}${RESET}`);
4360
+ }
4038
4361
  }
4362
+ return { callId };
4039
4363
  }
4040
4364
  function registerCallCommand(program2) {
4041
- program2.command("call <agent>").description("Call an agent on the A2A network (default: async polling)").requiredOption("--task <description>", "Task description").option("--input-file <path>", "Read file and append to task description").option("--output-file <path>", "Save response text to file").option("--stream", "Use SSE streaming instead of async polling").option("--json", "Output JSONL events").option("--timeout <seconds>", "Timeout in seconds", "300").action(async (agentInput, opts) => {
4365
+ program2.command("call <agent>").description("Call an agent on the A2A network (default: async polling)").requiredOption("--task <description>", "Task description").option("--input-file <path>", "Read file and append to task description").option("--output-file <path>", "Save response text to file").option("--stream", "Use SSE streaming instead of async polling").option("--json", "Output JSONL events").option("--timeout <seconds>", "Timeout in seconds", "300").option("--rate <rating>", "Rate the agent after call (1-5)", parseInt).action(async (agentInput, opts) => {
4042
4366
  try {
4043
4367
  const token = loadToken();
4044
4368
  if (!token) {
@@ -4069,12 +4393,23 @@ ${content}`;
4069
4393
  outputFile: opts.outputFile,
4070
4394
  signal: abortController.signal
4071
4395
  };
4396
+ let result;
4072
4397
  if (opts.stream) {
4073
- await streamCall(callOpts);
4398
+ result = await streamCall(callOpts);
4074
4399
  } else {
4075
- await asyncCall(callOpts);
4400
+ result = await asyncCall(callOpts);
4076
4401
  }
4077
4402
  clearTimeout(timer);
4403
+ if (opts.rate && result.callId) {
4404
+ try {
4405
+ await submitRating(DEFAULT_BASE_URL4, token, id, result.callId, opts.rate);
4406
+ if (!opts.json) {
4407
+ log.success(`Rated ${opts.rate}/5`);
4408
+ }
4409
+ } catch (rateErr) {
4410
+ log.warn(`Rating failed: ${rateErr.message}`);
4411
+ }
4412
+ }
4078
4413
  } catch (err) {
4079
4414
  if (err.name === "AbortError") {
4080
4415
  log.error("Call timed out");
@@ -4374,6 +4709,30 @@ function registerRegisterCommand(program2) {
4374
4709
  });
4375
4710
  }
4376
4711
 
4712
+ // src/commands/rate.ts
4713
+ var DEFAULT_BASE_URL6 = "https://agents.hot";
4714
+ function registerRateCommand(program2) {
4715
+ program2.command("rate <call-id> <rating>").description("Rate a completed A2A call (1-5)").requiredOption("--agent <agent-id>", "Agent UUID that was called").action(async (callId, ratingStr, opts) => {
4716
+ const token = loadToken();
4717
+ if (!token) {
4718
+ log.error("Not authenticated. Run `agent-mesh login` first.");
4719
+ process.exit(1);
4720
+ }
4721
+ const rating = parseInt(ratingStr, 10);
4722
+ if (isNaN(rating) || rating < 1 || rating > 5) {
4723
+ log.error("Rating must be an integer between 1 and 5");
4724
+ process.exit(1);
4725
+ }
4726
+ try {
4727
+ await submitRating(DEFAULT_BASE_URL6, token, opts.agent, callId, rating);
4728
+ log.success(`Rated ${rating}/5 for call ${callId.slice(0, 8)}...`);
4729
+ } catch (err) {
4730
+ log.error(err.message);
4731
+ process.exit(1);
4732
+ }
4733
+ });
4734
+ }
4735
+
4377
4736
  // src/index.ts
4378
4737
  var require2 = createRequire(import.meta.url);
4379
4738
  var { version } = require2("../package.json");
@@ -4402,4 +4761,5 @@ registerConfigCommand(program);
4402
4761
  registerStatsCommand(program);
4403
4762
  registerSubscribeCommand(program);
4404
4763
  registerRegisterCommand(program);
4764
+ registerRateCommand(program);
4405
4765
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annals/agent-mesh",
3
- "version": "0.15.1",
3
+ "version": "0.16.1",
4
4
  "description": "CLI bridge connecting local AI agents to the Agents.Hot platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "main": "./dist/index.js",
10
10
  "dependencies": {
11
- "@annals/bridge-protocol": "^0.1.0",
11
+ "@annals/bridge-protocol": "^0.2.0",
12
12
  "commander": "^13.0.0",
13
13
  "ws": "^8.18.0"
14
14
  },