@annals/agent-mesh 0.16.0 → 0.16.2

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 +388 -109
  2. package/package.json +1 -1
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;
@@ -3127,11 +3152,11 @@ function registerChatCommand(program2) {
3127
3152
  }
3128
3153
 
3129
3154
  // src/commands/skills.ts
3130
- 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";
3131
3156
  import { join as join8, resolve, relative as relative4 } from "path";
3132
3157
 
3133
3158
  // src/utils/skill-parser.ts
3134
- import { readFile as readFile3, stat as stat2 } from "fs/promises";
3159
+ import { readFile as readFile3, writeFile as writeFile2, stat as stat2 } from "fs/promises";
3135
3160
  import { join as join7 } from "path";
3136
3161
  function parseSkillMd(raw) {
3137
3162
  const trimmed = raw.trimStart();
@@ -3192,36 +3217,13 @@ function parseSkillMd(raw) {
3192
3217
  return { frontmatter, content };
3193
3218
  }
3194
3219
  async function loadSkillManifest(dir) {
3195
- const skillJsonPath = join7(dir, "skill.json");
3196
- try {
3197
- const raw = await readFile3(skillJsonPath, "utf-8");
3198
- const data = JSON.parse(raw);
3199
- if (!data.name) throw new Error("skill.json missing required field: name");
3200
- if (!data.version) throw new Error("skill.json missing required field: version");
3201
- return {
3202
- name: data.name,
3203
- version: data.version,
3204
- description: data.description,
3205
- main: data.main || "SKILL.md",
3206
- category: data.category,
3207
- tags: data.tags,
3208
- author: data.author,
3209
- source_url: data.source_url,
3210
- private: data.private,
3211
- files: data.files
3212
- };
3213
- } catch (err) {
3214
- if (err.code !== "ENOENT") {
3215
- throw err;
3216
- }
3217
- }
3218
3220
  const skillMdPath = join7(dir, "SKILL.md");
3219
3221
  try {
3220
3222
  const raw = await readFile3(skillMdPath, "utf-8");
3221
3223
  const { frontmatter } = parseSkillMd(raw);
3222
3224
  const name = frontmatter.name;
3223
3225
  if (!name) {
3224
- 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');
3225
3227
  }
3226
3228
  return {
3227
3229
  name,
@@ -3236,7 +3238,7 @@ async function loadSkillManifest(dir) {
3236
3238
  };
3237
3239
  } catch (err) {
3238
3240
  if (err.code === "ENOENT") {
3239
- throw new Error(`No skill.json or SKILL.md found in ${dir}`);
3241
+ throw new Error(`No SKILL.md found in ${dir}`);
3240
3242
  }
3241
3243
  throw err;
3242
3244
  }
@@ -3249,9 +3251,33 @@ async function pathExists(p) {
3249
3251
  return false;
3250
3252
  }
3251
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
+ }
3252
3278
 
3253
3279
  // src/utils/zip.ts
3254
- import { deflateRawSync } from "zlib";
3280
+ import { deflateRawSync, inflateRawSync } from "zlib";
3255
3281
  function dosTime(date) {
3256
3282
  return {
3257
3283
  time: date.getHours() << 11 | date.getMinutes() << 5 | date.getSeconds() >> 1,
@@ -3347,6 +3373,55 @@ function createZipBuffer(entries) {
3347
3373
  chunks.push(eocd);
3348
3374
  return Buffer.concat(chunks);
3349
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
+ }
3350
3425
 
3351
3426
  // src/commands/skills.ts
3352
3427
  var slog = {
@@ -3379,31 +3454,37 @@ function outputError(error, message, hint) {
3379
3454
  function resolveSkillDir(pathArg) {
3380
3455
  return pathArg ? resolve(pathArg) : process.cwd();
3381
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 claudeDir = join8(projectRoot, ".claude", "skills");
3473
+ if (await pathExists(claudeDir)) {
3474
+ return { projectRoot, skillsDir: claudeDir, convention: "claude" };
3475
+ }
3476
+ const agentsDir = join8(projectRoot, ".agents", "skills");
3477
+ if (await pathExists(agentsDir)) {
3478
+ return { projectRoot, skillsDir: agentsDir, convention: "agents" };
3479
+ }
3480
+ return { projectRoot, skillsDir: claudeDir, convention: "claude" };
3481
+ }
3382
3482
  async function collectPackFiles(dir, manifest) {
3383
3483
  const results = [];
3384
- if (manifest.files && manifest.files.length > 0) {
3385
- for (const pattern of manifest.files) {
3386
- const fullPath = join8(dir, pattern);
3387
- try {
3388
- const s = await stat3(fullPath);
3389
- if (s.isDirectory()) {
3390
- const sub = await walkDir(fullPath);
3391
- for (const f of sub) {
3392
- results.push(relative4(dir, f));
3393
- }
3394
- } else {
3395
- results.push(pattern);
3396
- }
3397
- } catch {
3398
- }
3399
- }
3400
- } else {
3401
- const all = await walkDir(dir);
3402
- for (const f of all) {
3403
- const rel = relative4(dir, f);
3404
- if (rel === "skill.json") continue;
3405
- results.push(rel);
3406
- }
3484
+ const all = await walkDir(dir);
3485
+ for (const f of all) {
3486
+ const rel = relative4(dir, f);
3487
+ results.push(rel);
3407
3488
  }
3408
3489
  const mainFile = manifest.main || "SKILL.md";
3409
3490
  if (!results.includes(mainFile)) {
@@ -3474,8 +3555,42 @@ function bumpVersion(current, bump) {
3474
3555
  throw new Error(`Invalid bump type: ${bump}. Use major, minor, patch, or a version string.`);
3475
3556
  }
3476
3557
  }
3558
+ async function downloadAndInstallSkill(client, authorLogin, slug, skillsDir) {
3559
+ const meta = await client.get(skillApiPath(authorLogin, slug));
3560
+ const targetDir = join8(skillsDir, slug);
3561
+ await mkdir2(targetDir, { recursive: true });
3562
+ if (meta.has_files) {
3563
+ const res = await client.getRaw(`${skillApiPath(authorLogin, slug)}/download`);
3564
+ const arrayBuf = await res.arrayBuffer();
3565
+ const buf = Buffer.from(arrayBuf);
3566
+ const entries = extractZipBuffer(buf);
3567
+ for (const entry of entries) {
3568
+ const filePath = join8(targetDir, entry.path);
3569
+ const dir = join8(filePath, "..");
3570
+ await mkdir2(dir, { recursive: true });
3571
+ await writeFile3(filePath, entry.data);
3572
+ }
3573
+ return {
3574
+ slug,
3575
+ name: meta.name,
3576
+ version: meta.version || "1.0.0",
3577
+ files_count: entries.length
3578
+ };
3579
+ } else {
3580
+ const res = await client.getRaw(`${skillApiPath(authorLogin, slug)}/raw`);
3581
+ const content = await res.text();
3582
+ await writeFile3(join8(targetDir, "SKILL.md"), content);
3583
+ return {
3584
+ slug,
3585
+ name: meta.name,
3586
+ version: meta.version || "1.0.0",
3587
+ files_count: 1
3588
+ };
3589
+ }
3590
+ }
3477
3591
  var SKILL_MD_TEMPLATE = `---
3478
3592
  name: {{name}}
3593
+ description: "{{description}}"
3479
3594
  version: 1.0.0
3480
3595
  ---
3481
3596
 
@@ -3487,62 +3602,31 @@ version: 1.0.0
3487
3602
 
3488
3603
  Describe how to use this skill.
3489
3604
  `;
3490
- var SKILL_JSON_TEMPLATE = (name, description) => ({
3491
- name,
3492
- version: "1.0.0",
3493
- description,
3494
- main: "SKILL.md",
3495
- category: "general",
3496
- tags: [],
3497
- files: ["SKILL.md"]
3498
- });
3499
3605
  function registerSkillsCommand(program2) {
3500
- const skills = program2.command("skills").description("Manage skill packages (publish, pack, version)");
3606
+ const skills = program2.command("skills").description("Manage skill packages (publish, install, pack, version)");
3501
3607
  skills.command("init [path]").description("Initialize a new skill project").option("--name <name>", "Skill name").option("--description <desc>", "Skill description").action(async (pathArg, opts) => {
3502
3608
  try {
3503
3609
  const dir = resolveSkillDir(pathArg);
3504
3610
  await mkdir2(dir, { recursive: true });
3505
- let name = opts.name;
3506
- let description = opts.description || "";
3507
3611
  const skillMdPath = join8(dir, "SKILL.md");
3508
- const skillJsonPath = join8(dir, "skill.json");
3509
- if (await pathExists(skillJsonPath)) {
3510
- outputError("already_exists", "skill.json already exists in this directory");
3511
- }
3512
3612
  if (await pathExists(skillMdPath)) {
3513
3613
  const raw = await readFile4(skillMdPath, "utf-8");
3514
3614
  const { frontmatter } = parseSkillMd(raw);
3515
3615
  if (frontmatter.name) {
3516
- name = name || frontmatter.name;
3517
- description = description || frontmatter.description || "";
3518
- const manifest2 = {
3519
- name,
3520
- version: frontmatter.version || "1.0.0",
3521
- description,
3522
- main: "SKILL.md",
3523
- category: frontmatter.category || "general",
3524
- tags: frontmatter.tags || [],
3525
- author: frontmatter.author,
3526
- source_url: frontmatter.source_url,
3527
- files: ["SKILL.md"]
3528
- };
3529
- await writeFile2(skillJsonPath, JSON.stringify(manifest2, null, 2) + "\n");
3530
- slog.info(`Migrated frontmatter from SKILL.md to skill.json`);
3531
- outputJson({ success: true, path: skillJsonPath, migrated: true });
3616
+ slog.info(`SKILL.md already exists with name: ${frontmatter.name}`);
3617
+ outputJson({ success: true, exists: true, path: skillMdPath });
3532
3618
  return;
3533
3619
  }
3534
3620
  }
3621
+ let name = opts.name;
3622
+ const description = opts.description || "";
3535
3623
  if (!name) {
3536
3624
  name = dir.split("/").pop()?.replace(/[^a-z0-9-]/gi, "-").toLowerCase() || "my-skill";
3537
3625
  }
3538
- const manifest = SKILL_JSON_TEMPLATE(name, description);
3539
- await writeFile2(skillJsonPath, JSON.stringify(manifest, null, 2) + "\n");
3540
- if (!await pathExists(skillMdPath)) {
3541
- const content = SKILL_MD_TEMPLATE.replace(/\{\{name\}\}/g, name).replace(/\{\{description\}\}/g, description || "A new skill.");
3542
- await writeFile2(skillMdPath, content);
3543
- }
3626
+ const content = SKILL_MD_TEMPLATE.replace(/\{\{name\}\}/g, name).replace(/\{\{description\}\}/g, description || "A new skill.");
3627
+ await writeFile3(skillMdPath, content);
3544
3628
  slog.info(`Initialized skill: ${name}`);
3545
- outputJson({ success: true, path: skillJsonPath });
3629
+ outputJson({ success: true, path: skillMdPath });
3546
3630
  } catch (err) {
3547
3631
  if (err instanceof Error && err.message.includes("already_exists")) throw err;
3548
3632
  outputError("init_failed", err.message);
@@ -3554,7 +3638,7 @@ function registerSkillsCommand(program2) {
3554
3638
  const manifest = await loadSkillManifest(dir);
3555
3639
  const result = await packSkill(dir, manifest);
3556
3640
  const outPath = join8(dir, result.filename);
3557
- await writeFile2(outPath, result.buffer);
3641
+ await writeFile3(outPath, result.buffer);
3558
3642
  slog.info(`Packed ${result.files.length} files \u2192 ${result.filename} (${result.size} bytes)`);
3559
3643
  outputJson({
3560
3644
  success: true,
@@ -3624,11 +3708,13 @@ function registerSkillsCommand(program2) {
3624
3708
  const client = createClient();
3625
3709
  const result = await client.postFormData("/api/skills/publish", formData);
3626
3710
  slog.success(`Skill ${result.action}: ${manifest.name}`);
3711
+ const authorLogin = result.skill.author_login;
3712
+ const skillUrl = authorLogin ? `https://agents.hot/skills/${authorLogin}/${result.skill.slug}` : `https://agents.hot/skills/${result.skill.slug}`;
3627
3713
  outputJson({
3628
3714
  success: true,
3629
3715
  action: result.action,
3630
3716
  skill: result.skill,
3631
- url: `https://agents.hot/skills/${result.skill.slug}`
3717
+ url: skillUrl
3632
3718
  });
3633
3719
  } catch (err) {
3634
3720
  if (err instanceof PlatformApiError) {
@@ -3637,16 +3723,17 @@ function registerSkillsCommand(program2) {
3637
3723
  outputError("publish_failed", err.message);
3638
3724
  }
3639
3725
  });
3640
- skills.command("info <slug>").description("View skill details").option("--human", "Human-readable output").action(async (slug, opts) => {
3726
+ skills.command("info <ref>").description("View skill details (use author/slug format)").option("--human", "Human-readable output").action(async (ref, opts) => {
3641
3727
  try {
3728
+ const { authorLogin, slug } = parseSkillRef(ref);
3642
3729
  const client = createClient();
3643
- const data = await client.get(`/api/skills/${encodeURIComponent(slug)}`);
3730
+ const data = await client.get(skillApiPath(authorLogin, slug));
3644
3731
  if (opts.human) {
3645
3732
  console.log("");
3646
3733
  console.log(` ${BOLD}${data.name}${RESET} v${data.version || "?"}`);
3647
3734
  if (data.description) console.log(` ${data.description}`);
3648
- console.log(` ${GRAY}slug${RESET} ${data.slug}`);
3649
- console.log(` ${GRAY}author${RESET} ${data.author || "\u2014"}`);
3735
+ console.log(` ${GRAY}ref${RESET} ${authorLogin}/${data.slug}`);
3736
+ console.log(` ${GRAY}author${RESET} ${data.author_login || data.author || "\u2014"}`);
3650
3737
  console.log(` ${GRAY}category${RESET} ${data.category || "\u2014"}`);
3651
3738
  console.log(` ${GRAY}installs${RESET} ${data.installs ?? 0}`);
3652
3739
  console.log(` ${GRAY}private${RESET} ${data.is_private ? "yes" : "no"}`);
@@ -3675,12 +3762,14 @@ function registerSkillsCommand(program2) {
3675
3762
  const table = renderTable(
3676
3763
  [
3677
3764
  { key: "name", label: "NAME", width: 24 },
3765
+ { key: "author", label: "AUTHOR", width: 16 },
3678
3766
  { key: "version", label: "VERSION", width: 12 },
3679
3767
  { key: "installs", label: "INSTALLS", width: 12, align: "right" },
3680
3768
  { key: "private", label: "PRIVATE", width: 10 }
3681
3769
  ],
3682
3770
  data.owned.map((s) => ({
3683
3771
  name: s.name,
3772
+ author: s.author_login || s.author || "\u2014",
3684
3773
  version: s.version || "\u2014",
3685
3774
  installs: String(s.installs ?? 0),
3686
3775
  private: s.is_private ? "yes" : `${GREEN}no${RESET}`
@@ -3698,7 +3787,7 @@ function registerSkillsCommand(program2) {
3698
3787
  ],
3699
3788
  data.authorized.map((s) => ({
3700
3789
  name: s.name,
3701
- author: s.author || "\u2014",
3790
+ author: s.author_login || s.author || "\u2014",
3702
3791
  version: s.version || "\u2014"
3703
3792
  }))
3704
3793
  );
@@ -3714,11 +3803,12 @@ function registerSkillsCommand(program2) {
3714
3803
  outputError("list_failed", err.message);
3715
3804
  }
3716
3805
  });
3717
- skills.command("unpublish <slug>").description("Unpublish a skill").action(async (slug) => {
3806
+ skills.command("unpublish <ref>").description("Unpublish a skill (use author/slug format)").action(async (ref) => {
3718
3807
  try {
3808
+ const { authorLogin, slug } = parseSkillRef(ref);
3719
3809
  const client = createClient();
3720
- const result = await client.del(`/api/skills/${encodeURIComponent(slug)}`);
3721
- slog.success(`Skill unpublished: ${slug}`);
3810
+ const result = await client.del(skillApiPath(authorLogin, slug));
3811
+ slog.success(`Skill unpublished: ${authorLogin}/${slug}`);
3722
3812
  outputJson(result);
3723
3813
  } catch (err) {
3724
3814
  if (err instanceof PlatformApiError) {
@@ -3730,16 +3820,15 @@ function registerSkillsCommand(program2) {
3730
3820
  skills.command("version <bump> [path]").description("Bump skill version (patch | minor | major | x.y.z)").action(async (bump, pathArg) => {
3731
3821
  try {
3732
3822
  const dir = resolveSkillDir(pathArg);
3733
- const skillJsonPath = join8(dir, "skill.json");
3734
- if (!await pathExists(skillJsonPath)) {
3735
- outputError("not_found", "No skill.json found. Run `agent-mesh skills init` first.");
3823
+ const skillMdPath = join8(dir, "SKILL.md");
3824
+ if (!await pathExists(skillMdPath)) {
3825
+ outputError("not_found", "No SKILL.md found. Run `agent-mesh skills init` first.");
3736
3826
  }
3737
- const raw = await readFile4(skillJsonPath, "utf-8");
3738
- const data = JSON.parse(raw);
3739
- const oldVersion = data.version || "0.0.0";
3827
+ const raw = await readFile4(skillMdPath, "utf-8");
3828
+ const { frontmatter } = parseSkillMd(raw);
3829
+ const oldVersion = frontmatter.version || "0.0.0";
3740
3830
  const newVersion = bumpVersion(oldVersion, bump);
3741
- data.version = newVersion;
3742
- await writeFile2(skillJsonPath, JSON.stringify(data, null, 2) + "\n");
3831
+ await updateFrontmatterField(skillMdPath, "version", newVersion);
3743
3832
  slog.success(`${oldVersion} \u2192 ${newVersion}`);
3744
3833
  outputJson({ success: true, old: oldVersion, new: newVersion });
3745
3834
  } catch (err) {
@@ -3747,6 +3836,196 @@ function registerSkillsCommand(program2) {
3747
3836
  outputError("version_failed", err.message);
3748
3837
  }
3749
3838
  });
3839
+ 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) => {
3840
+ try {
3841
+ const { authorLogin, slug } = parseSkillRef(ref);
3842
+ const { skillsDir } = await resolveSkillsRootAsync(pathArg);
3843
+ const targetDir = join8(skillsDir, slug);
3844
+ if (await pathExists(targetDir)) {
3845
+ if (!opts.force) {
3846
+ outputError("already_installed", `Skill "${slug}" is already installed at ${targetDir}. Use --force to overwrite.`);
3847
+ }
3848
+ await rm(targetDir, { recursive: true, force: true });
3849
+ }
3850
+ slog.info(`Installing ${authorLogin}/${slug}...`);
3851
+ const client = createClient();
3852
+ const result = await downloadAndInstallSkill(client, authorLogin, slug, skillsDir);
3853
+ slog.success(`Installed ${result.name} (${result.files_count} files)`);
3854
+ outputJson({
3855
+ success: true,
3856
+ skill: {
3857
+ author: authorLogin,
3858
+ slug: result.slug,
3859
+ name: result.name,
3860
+ version: result.version
3861
+ },
3862
+ installed_to: targetDir,
3863
+ files_count: result.files_count
3864
+ });
3865
+ } catch (err) {
3866
+ if (err instanceof PlatformApiError) {
3867
+ outputError(err.errorCode, err.message);
3868
+ }
3869
+ outputError("install_failed", err.message);
3870
+ }
3871
+ });
3872
+ skills.command("update [ref] [path]").description("Update installed skill(s) from agents.hot").action(async (ref, pathArg) => {
3873
+ try {
3874
+ const { skillsDir } = await resolveSkillsRootAsync(pathArg);
3875
+ const client = createClient();
3876
+ const updated = [];
3877
+ const skipped = [];
3878
+ const failed = [];
3879
+ if (ref) {
3880
+ const { authorLogin, slug } = parseSkillRef(ref);
3881
+ const targetDir = join8(skillsDir, slug);
3882
+ if (!await pathExists(targetDir)) {
3883
+ outputError("not_installed", `Skill "${slug}" is not installed. Use "skills install ${ref}" first.`);
3884
+ }
3885
+ const skillMdPath = join8(targetDir, "SKILL.md");
3886
+ let localVersion = "0.0.0";
3887
+ if (await pathExists(skillMdPath)) {
3888
+ const raw = await readFile4(skillMdPath, "utf-8");
3889
+ const { frontmatter } = parseSkillMd(raw);
3890
+ localVersion = frontmatter.version || "0.0.0";
3891
+ }
3892
+ const remote = await client.get(skillApiPath(authorLogin, slug));
3893
+ const remoteVersion = remote.version || "0.0.0";
3894
+ if (remoteVersion === localVersion) {
3895
+ slog.info(`${slug} is already up to date (v${localVersion})`);
3896
+ skipped.push({ slug, reason: "up_to_date" });
3897
+ } else {
3898
+ slog.info(`Updating ${slug}: v${localVersion} \u2192 v${remoteVersion}...`);
3899
+ await rm(targetDir, { recursive: true, force: true });
3900
+ await downloadAndInstallSkill(client, authorLogin, slug, skillsDir);
3901
+ updated.push({ slug, name: remote.name, old_version: localVersion, new_version: remoteVersion });
3902
+ slog.success(`Updated ${slug} to v${remoteVersion}`);
3903
+ }
3904
+ } else {
3905
+ if (!await pathExists(skillsDir)) {
3906
+ outputError("no_skills_dir", `Skills directory not found: ${skillsDir}`);
3907
+ }
3908
+ const entries = await readdir2(skillsDir, { withFileTypes: true });
3909
+ for (const entry of entries) {
3910
+ if (!entry.isDirectory()) continue;
3911
+ const slug = entry.name;
3912
+ const skillMdPath = join8(skillsDir, slug, "SKILL.md");
3913
+ if (!await pathExists(skillMdPath)) {
3914
+ skipped.push({ slug, reason: "no_skill_md" });
3915
+ continue;
3916
+ }
3917
+ const raw = await readFile4(skillMdPath, "utf-8");
3918
+ const { frontmatter } = parseSkillMd(raw);
3919
+ const localVersion = frontmatter.version || "0.0.0";
3920
+ const authorLogin = frontmatter.author;
3921
+ if (!authorLogin) {
3922
+ skipped.push({ slug, reason: "no_author_in_frontmatter" });
3923
+ continue;
3924
+ }
3925
+ try {
3926
+ const remote = await client.get(skillApiPath(authorLogin, slug));
3927
+ const remoteVersion = remote.version || "0.0.0";
3928
+ if (remoteVersion === localVersion) {
3929
+ skipped.push({ slug, reason: "up_to_date" });
3930
+ } else {
3931
+ slog.info(`Updating ${slug}: v${localVersion} \u2192 v${remoteVersion}...`);
3932
+ await rm(join8(skillsDir, slug), { recursive: true, force: true });
3933
+ await downloadAndInstallSkill(client, authorLogin, slug, skillsDir);
3934
+ updated.push({ slug, name: remote.name, old_version: localVersion, new_version: remoteVersion });
3935
+ }
3936
+ } catch (err) {
3937
+ failed.push({ slug, error: err.message });
3938
+ }
3939
+ }
3940
+ }
3941
+ slog.success(`Update complete: ${updated.length} updated, ${skipped.length} skipped, ${failed.length} failed`);
3942
+ outputJson({ success: true, updated, skipped, failed });
3943
+ } catch (err) {
3944
+ if (err instanceof PlatformApiError) {
3945
+ outputError(err.errorCode, err.message);
3946
+ }
3947
+ outputError("update_failed", err.message);
3948
+ }
3949
+ });
3950
+ skills.command("remove <slug> [path]").description("Remove a locally installed skill").action(async (slug, pathArg) => {
3951
+ try {
3952
+ const { skillsDir } = await resolveSkillsRootAsync(pathArg);
3953
+ const targetDir = join8(skillsDir, slug);
3954
+ if (!await pathExists(targetDir)) {
3955
+ outputError("not_installed", `Skill "${slug}" is not installed at ${targetDir}`);
3956
+ }
3957
+ await rm(targetDir, { recursive: true, force: true });
3958
+ slog.success(`Removed skill: ${slug}`);
3959
+ outputJson({ success: true, removed: slug, path: targetDir });
3960
+ } catch (err) {
3961
+ outputError("remove_failed", err.message);
3962
+ }
3963
+ });
3964
+ 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) => {
3965
+ try {
3966
+ const { skillsDir } = await resolveSkillsRootAsync(pathArg);
3967
+ if (!await pathExists(skillsDir)) {
3968
+ if (opts.human) {
3969
+ slog.info(`No skills directory found at ${skillsDir}`);
3970
+ return;
3971
+ }
3972
+ outputJson({ skills_dir: skillsDir, skills: [] });
3973
+ return;
3974
+ }
3975
+ const entries = await readdir2(skillsDir, { withFileTypes: true });
3976
+ const skills2 = [];
3977
+ for (const entry of entries) {
3978
+ if (!entry.isDirectory()) continue;
3979
+ const slug = entry.name;
3980
+ const skillMdPath = join8(skillsDir, slug, "SKILL.md");
3981
+ if (!await pathExists(skillMdPath)) continue;
3982
+ const raw = await readFile4(skillMdPath, "utf-8");
3983
+ const { frontmatter } = parseSkillMd(raw);
3984
+ const skillInfo = {
3985
+ slug,
3986
+ name: frontmatter.name || slug,
3987
+ version: frontmatter.version || "0.0.0",
3988
+ author: frontmatter.author
3989
+ };
3990
+ if (opts.checkUpdates && skillInfo.author) {
3991
+ try {
3992
+ const client = createClient();
3993
+ const remote = await client.get(skillApiPath(skillInfo.author, slug));
3994
+ skillInfo.remote_version = remote.version || "0.0.0";
3995
+ skillInfo.has_update = skillInfo.remote_version !== skillInfo.version;
3996
+ } catch {
3997
+ }
3998
+ }
3999
+ skills2.push(skillInfo);
4000
+ }
4001
+ if (opts.human) {
4002
+ if (skills2.length === 0) {
4003
+ slog.info("No skills installed.");
4004
+ return;
4005
+ }
4006
+ const columns = [
4007
+ { key: "name", label: "NAME", width: 24 },
4008
+ { key: "version", label: "VERSION", width: 12 },
4009
+ { key: "author", label: "AUTHOR", width: 16 }
4010
+ ];
4011
+ if (opts.checkUpdates) {
4012
+ columns.push({ key: "update", label: "UPDATE", width: 14 });
4013
+ }
4014
+ const rows = skills2.map((s) => ({
4015
+ name: s.name,
4016
+ version: s.version,
4017
+ author: s.author || "\u2014",
4018
+ update: s.has_update ? `${GREEN}${s.remote_version}${RESET}` : "\u2014"
4019
+ }));
4020
+ slog.banner("Installed Skills");
4021
+ console.log(renderTable(columns, rows));
4022
+ return;
4023
+ }
4024
+ outputJson({ skills_dir: skillsDir, skills: skills2 });
4025
+ } catch (err) {
4026
+ outputError("installed_failed", err.message);
4027
+ }
4028
+ });
3750
4029
  }
3751
4030
 
3752
4031
  // src/commands/discover.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annals/agent-mesh",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "CLI bridge connecting local AI agents to the Agents.Hot platform",
5
5
  "type": "module",
6
6
  "bin": {