@annals/agent-mesh 0.16.0 → 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 +385 -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,34 @@ 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 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
+ }
3382
3479
  async function collectPackFiles(dir, manifest) {
3383
3480
  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
- }
3481
+ const all = await walkDir(dir);
3482
+ for (const f of all) {
3483
+ const rel = relative4(dir, f);
3484
+ results.push(rel);
3407
3485
  }
3408
3486
  const mainFile = manifest.main || "SKILL.md";
3409
3487
  if (!results.includes(mainFile)) {
@@ -3474,8 +3552,42 @@ function bumpVersion(current, bump) {
3474
3552
  throw new Error(`Invalid bump type: ${bump}. Use major, minor, patch, or a version string.`);
3475
3553
  }
3476
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
+ }
3477
3588
  var SKILL_MD_TEMPLATE = `---
3478
3589
  name: {{name}}
3590
+ description: "{{description}}"
3479
3591
  version: 1.0.0
3480
3592
  ---
3481
3593
 
@@ -3487,62 +3599,31 @@ version: 1.0.0
3487
3599
 
3488
3600
  Describe how to use this skill.
3489
3601
  `;
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
3602
  function registerSkillsCommand(program2) {
3500
- 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)");
3501
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) => {
3502
3605
  try {
3503
3606
  const dir = resolveSkillDir(pathArg);
3504
3607
  await mkdir2(dir, { recursive: true });
3505
- let name = opts.name;
3506
- let description = opts.description || "";
3507
3608
  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
3609
  if (await pathExists(skillMdPath)) {
3513
3610
  const raw = await readFile4(skillMdPath, "utf-8");
3514
3611
  const { frontmatter } = parseSkillMd(raw);
3515
3612
  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 });
3613
+ slog.info(`SKILL.md already exists with name: ${frontmatter.name}`);
3614
+ outputJson({ success: true, exists: true, path: skillMdPath });
3532
3615
  return;
3533
3616
  }
3534
3617
  }
3618
+ let name = opts.name;
3619
+ const description = opts.description || "";
3535
3620
  if (!name) {
3536
3621
  name = dir.split("/").pop()?.replace(/[^a-z0-9-]/gi, "-").toLowerCase() || "my-skill";
3537
3622
  }
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
- }
3623
+ const content = SKILL_MD_TEMPLATE.replace(/\{\{name\}\}/g, name).replace(/\{\{description\}\}/g, description || "A new skill.");
3624
+ await writeFile3(skillMdPath, content);
3544
3625
  slog.info(`Initialized skill: ${name}`);
3545
- outputJson({ success: true, path: skillJsonPath });
3626
+ outputJson({ success: true, path: skillMdPath });
3546
3627
  } catch (err) {
3547
3628
  if (err instanceof Error && err.message.includes("already_exists")) throw err;
3548
3629
  outputError("init_failed", err.message);
@@ -3554,7 +3635,7 @@ function registerSkillsCommand(program2) {
3554
3635
  const manifest = await loadSkillManifest(dir);
3555
3636
  const result = await packSkill(dir, manifest);
3556
3637
  const outPath = join8(dir, result.filename);
3557
- await writeFile2(outPath, result.buffer);
3638
+ await writeFile3(outPath, result.buffer);
3558
3639
  slog.info(`Packed ${result.files.length} files \u2192 ${result.filename} (${result.size} bytes)`);
3559
3640
  outputJson({
3560
3641
  success: true,
@@ -3624,11 +3705,13 @@ function registerSkillsCommand(program2) {
3624
3705
  const client = createClient();
3625
3706
  const result = await client.postFormData("/api/skills/publish", formData);
3626
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}`;
3627
3710
  outputJson({
3628
3711
  success: true,
3629
3712
  action: result.action,
3630
3713
  skill: result.skill,
3631
- url: `https://agents.hot/skills/${result.skill.slug}`
3714
+ url: skillUrl
3632
3715
  });
3633
3716
  } catch (err) {
3634
3717
  if (err instanceof PlatformApiError) {
@@ -3637,16 +3720,17 @@ function registerSkillsCommand(program2) {
3637
3720
  outputError("publish_failed", err.message);
3638
3721
  }
3639
3722
  });
3640
- 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) => {
3641
3724
  try {
3725
+ const { authorLogin, slug } = parseSkillRef(ref);
3642
3726
  const client = createClient();
3643
- const data = await client.get(`/api/skills/${encodeURIComponent(slug)}`);
3727
+ const data = await client.get(skillApiPath(authorLogin, slug));
3644
3728
  if (opts.human) {
3645
3729
  console.log("");
3646
3730
  console.log(` ${BOLD}${data.name}${RESET} v${data.version || "?"}`);
3647
3731
  if (data.description) console.log(` ${data.description}`);
3648
- console.log(` ${GRAY}slug${RESET} ${data.slug}`);
3649
- 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"}`);
3650
3734
  console.log(` ${GRAY}category${RESET} ${data.category || "\u2014"}`);
3651
3735
  console.log(` ${GRAY}installs${RESET} ${data.installs ?? 0}`);
3652
3736
  console.log(` ${GRAY}private${RESET} ${data.is_private ? "yes" : "no"}`);
@@ -3675,12 +3759,14 @@ function registerSkillsCommand(program2) {
3675
3759
  const table = renderTable(
3676
3760
  [
3677
3761
  { key: "name", label: "NAME", width: 24 },
3762
+ { key: "author", label: "AUTHOR", width: 16 },
3678
3763
  { key: "version", label: "VERSION", width: 12 },
3679
3764
  { key: "installs", label: "INSTALLS", width: 12, align: "right" },
3680
3765
  { key: "private", label: "PRIVATE", width: 10 }
3681
3766
  ],
3682
3767
  data.owned.map((s) => ({
3683
3768
  name: s.name,
3769
+ author: s.author_login || s.author || "\u2014",
3684
3770
  version: s.version || "\u2014",
3685
3771
  installs: String(s.installs ?? 0),
3686
3772
  private: s.is_private ? "yes" : `${GREEN}no${RESET}`
@@ -3698,7 +3784,7 @@ function registerSkillsCommand(program2) {
3698
3784
  ],
3699
3785
  data.authorized.map((s) => ({
3700
3786
  name: s.name,
3701
- author: s.author || "\u2014",
3787
+ author: s.author_login || s.author || "\u2014",
3702
3788
  version: s.version || "\u2014"
3703
3789
  }))
3704
3790
  );
@@ -3714,11 +3800,12 @@ function registerSkillsCommand(program2) {
3714
3800
  outputError("list_failed", err.message);
3715
3801
  }
3716
3802
  });
3717
- 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) => {
3718
3804
  try {
3805
+ const { authorLogin, slug } = parseSkillRef(ref);
3719
3806
  const client = createClient();
3720
- const result = await client.del(`/api/skills/${encodeURIComponent(slug)}`);
3721
- slog.success(`Skill unpublished: ${slug}`);
3807
+ const result = await client.del(skillApiPath(authorLogin, slug));
3808
+ slog.success(`Skill unpublished: ${authorLogin}/${slug}`);
3722
3809
  outputJson(result);
3723
3810
  } catch (err) {
3724
3811
  if (err instanceof PlatformApiError) {
@@ -3730,16 +3817,15 @@ function registerSkillsCommand(program2) {
3730
3817
  skills.command("version <bump> [path]").description("Bump skill version (patch | minor | major | x.y.z)").action(async (bump, pathArg) => {
3731
3818
  try {
3732
3819
  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.");
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.");
3736
3823
  }
3737
- const raw = await readFile4(skillJsonPath, "utf-8");
3738
- const data = JSON.parse(raw);
3739
- 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";
3740
3827
  const newVersion = bumpVersion(oldVersion, bump);
3741
- data.version = newVersion;
3742
- await writeFile2(skillJsonPath, JSON.stringify(data, null, 2) + "\n");
3828
+ await updateFrontmatterField(skillMdPath, "version", newVersion);
3743
3829
  slog.success(`${oldVersion} \u2192 ${newVersion}`);
3744
3830
  outputJson({ success: true, old: oldVersion, new: newVersion });
3745
3831
  } catch (err) {
@@ -3747,6 +3833,196 @@ function registerSkillsCommand(program2) {
3747
3833
  outputError("version_failed", err.message);
3748
3834
  }
3749
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
+ });
3750
4026
  }
3751
4027
 
3752
4028
  // 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.1",
4
4
  "description": "CLI bridge connecting local AI agents to the Agents.Hot platform",
5
5
  "type": "module",
6
6
  "bin": {