skillmux 0.1.0 → 0.1.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.
package/dist/cli.js CHANGED
@@ -3,27 +3,6 @@
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
5
 
6
- // src/commands/agents.ts
7
- import { homedir } from "os";
8
-
9
- // src/config/resolve-skillmux-home.ts
10
- import { join, resolve } from "path";
11
- function buildConfigPath(skillmuxHome) {
12
- return join(resolve(skillmuxHome), "config.json");
13
- }
14
- function resolveSkillmuxHome(homeDir) {
15
- const resolvedHomeDir = resolve(homeDir);
16
- const skillmuxHome = join(resolvedHomeDir, ".skillmux");
17
- return {
18
- skillmuxHome,
19
- configPath: buildConfigPath(skillmuxHome)
20
- };
21
- }
22
-
23
- // src/discovery/discover-agents.ts
24
- import * as fs2 from "fs/promises";
25
- import { join as join2, resolve as resolve2 } from "path";
26
-
27
6
  // src/config/default-agent-rules.ts
28
7
  var supportedPlatforms = ["win32", "linux", "darwin"];
29
8
  var builtInAgentIds = [
@@ -84,6 +63,27 @@ var defaultAgentRuleMap = Object.fromEntries(
84
63
  defaultAgentRules.map((rule) => [rule.id, rule])
85
64
  );
86
65
 
66
+ // src/commands/agents.ts
67
+ import { homedir } from "os";
68
+
69
+ // src/config/resolve-skillmux-home.ts
70
+ import { join, resolve } from "path";
71
+ function buildConfigPath(skillmuxHome) {
72
+ return join(resolve(skillmuxHome), "config.json");
73
+ }
74
+ function resolveSkillmuxHome(homeDir) {
75
+ const resolvedHomeDir = resolve(homeDir);
76
+ const skillmuxHome = join(resolvedHomeDir, ".skillmux");
77
+ return {
78
+ skillmuxHome,
79
+ configPath: buildConfigPath(skillmuxHome)
80
+ };
81
+ }
82
+
83
+ // src/discovery/discover-agents.ts
84
+ import * as fs2 from "fs/promises";
85
+ import { join as join2, resolve as resolve2 } from "path";
86
+
87
87
  // src/config/load-user-config.ts
88
88
  import * as fs from "fs/promises";
89
89
  import { z } from "zod";
@@ -95,6 +95,15 @@ var SkillMuxError = class extends Error {
95
95
  this.name = new.target.name;
96
96
  }
97
97
  };
98
+ var InvalidIdentifierError = class extends SkillMuxError {
99
+ constructor(kind, value) {
100
+ super(`Invalid ${kind}: ${value}`);
101
+ this.kind = kind;
102
+ this.value = value;
103
+ }
104
+ kind;
105
+ value;
106
+ };
98
107
  var ManifestValidationError = class extends SkillMuxError {
99
108
  constructor(message) {
100
109
  super(message);
@@ -125,6 +134,9 @@ function createEmptyUserConfig() {
125
134
  agents: {}
126
135
  };
127
136
  }
137
+ function stripUtf8Bom(contents) {
138
+ return contents.charCodeAt(0) === 65279 ? contents.slice(1) : contents;
139
+ }
128
140
  function formatValidationIssues(error) {
129
141
  return error.issues.map((issue) => {
130
142
  const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
@@ -135,7 +147,7 @@ async function loadUserConfig(skillmuxHome) {
135
147
  const configPath = buildConfigPath(skillmuxHome);
136
148
  try {
137
149
  const contents = await fs.readFile(configPath, "utf8");
138
- const parsed = JSON.parse(contents);
150
+ const parsed = JSON.parse(stripUtf8Bom(contents));
139
151
  const validated = userConfigSchema.safeParse(parsed);
140
152
  if (!validated.success) {
141
153
  throw new UserConfigValidationError(
@@ -301,9 +313,212 @@ async function runAgents(options = {}) {
301
313
  };
302
314
  }
303
315
 
304
- // src/commands/config.ts
316
+ // src/commands/config-add-agent.ts
305
317
  import { homedir as homedir2 } from "os";
318
+ import { isAbsolute } from "path";
319
+
320
+ // src/core/ids.ts
321
+ var ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
322
+ function normalizeId(value) {
323
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
324
+ return normalized.length > 0 ? normalized : "skill";
325
+ }
326
+ function isValidId(value) {
327
+ return ID_PATTERN.test(value);
328
+ }
329
+
330
+ // src/config/write-user-config.ts
331
+ import * as fs3 from "fs/promises";
332
+ async function writeUserConfig(skillmuxHome, config) {
333
+ const configPath = buildConfigPath(skillmuxHome);
334
+ await fs3.mkdir(skillmuxHome, { recursive: true });
335
+ await fs3.writeFile(configPath, `${JSON.stringify(config, null, 2)}
336
+ `, "utf8");
337
+ return {
338
+ skillmuxHome,
339
+ configPath
340
+ };
341
+ }
342
+
343
+ // src/commands/config-add-agent.ts
344
+ function normalizeRelativePath(value, field) {
345
+ const trimmed = value.trim();
346
+ if (trimmed.length === 0) {
347
+ throw new UserConfigValidationError(`${field} must not be empty`);
348
+ }
349
+ if (isAbsolute(trimmed)) {
350
+ throw new UserConfigValidationError(`${field} must be a relative path`);
351
+ }
352
+ const normalized = trimmed.replaceAll("\\", "/");
353
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
354
+ throw new UserConfigValidationError(`${field} must stay within the configured home-relative tree`);
355
+ }
356
+ return normalized.replace(/^\.\/+/, "");
357
+ }
358
+ function normalizeAgentId(value) {
359
+ const trimmed = value.trim();
360
+ if (trimmed.length === 0 || /[a-z0-9]/i.test(trimmed) === false) {
361
+ throw new InvalidIdentifierError("agent id", value);
362
+ }
363
+ return normalizeId(trimmed);
364
+ }
365
+ function normalizePlatforms(value) {
366
+ if (value === void 0 || value.length === 0) {
367
+ return [process.platform];
368
+ }
369
+ const normalized = [...new Set(value.map((entry) => entry.trim().toLowerCase()))];
370
+ const invalid = normalized.filter(
371
+ (entry) => supportedPlatforms.includes(entry) === false
372
+ );
373
+ if (invalid.length > 0) {
374
+ throw new UserConfigValidationError(
375
+ `platform must be one of: ${supportedPlatforms.join(", ")}`
376
+ );
377
+ }
378
+ return normalized;
379
+ }
380
+ function buildAgentOverride(options) {
381
+ const agentId = normalizeAgentId(options.id);
382
+ const agent = {
383
+ supportedPlatforms: normalizePlatforms(options.platforms),
384
+ homeRelativeRootPath: normalizeRelativePath(options.root, "root"),
385
+ skillsDirectoryPath: normalizeRelativePath(options.skills ?? "skills", "skills")
386
+ };
387
+ if (options.name !== void 0 && options.name.trim().length > 0) {
388
+ agent.stableName = options.name.trim();
389
+ }
390
+ if (options.disabledByDefault === true) {
391
+ agent.enabledByDefault = false;
392
+ }
393
+ return { agentId, agent };
394
+ }
306
395
  function buildTableOutput2(result) {
396
+ const summary = printTable(
397
+ [
398
+ {
399
+ agentId: result.agentId,
400
+ configPath: result.configPath,
401
+ changed: String(result.changed)
402
+ }
403
+ ],
404
+ [
405
+ { key: "agentId", label: "Agent" },
406
+ { key: "configPath", label: "Config Path" },
407
+ { key: "changed", label: "Changed" }
408
+ ]
409
+ );
410
+ const detail = printTable(
411
+ [
412
+ {
413
+ stableName: result.agent.stableName ?? "",
414
+ platforms: (result.agent.supportedPlatforms ?? []).join(","),
415
+ root: result.agent.homeRelativeRootPath ?? "",
416
+ skills: result.agent.skillsDirectoryPath ?? "",
417
+ enabledByDefault: result.agent.enabledByDefault === void 0 ? "" : String(result.agent.enabledByDefault)
418
+ }
419
+ ],
420
+ [
421
+ { key: "stableName", label: "Name" },
422
+ { key: "platforms", label: "Platforms" },
423
+ { key: "root", label: "Root" },
424
+ { key: "skills", label: "Skills Dir" },
425
+ { key: "enabledByDefault", label: "Enabled By Default" }
426
+ ]
427
+ );
428
+ return `${summary}${detail}`;
429
+ }
430
+ async function runConfigAddAgent(options) {
431
+ const homeDir = options.homeDir ?? homedir2();
432
+ const resolvedPaths = resolveSkillmuxHome(homeDir);
433
+ const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
434
+ const configPath = buildConfigPath(skillmuxHome);
435
+ const config = await loadUserConfig(skillmuxHome);
436
+ const { agentId, agent } = buildAgentOverride(options);
437
+ const previous = config.agents[agentId];
438
+ const changed = JSON.stringify(previous ?? null) !== JSON.stringify(agent);
439
+ const nextConfig = {
440
+ ...config,
441
+ agents: {
442
+ ...config.agents,
443
+ [agentId]: agent
444
+ }
445
+ };
446
+ await writeUserConfig(skillmuxHome, nextConfig);
447
+ const resultWithoutOutput = {
448
+ skillmuxHome,
449
+ configPath,
450
+ agentId,
451
+ changed,
452
+ agent,
453
+ config: nextConfig
454
+ };
455
+ return {
456
+ ...resultWithoutOutput,
457
+ output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput2(resultWithoutOutput)
458
+ };
459
+ }
460
+
461
+ // src/commands/config-remove-agent.ts
462
+ import { homedir as homedir3 } from "os";
463
+ function normalizeAgentId2(value) {
464
+ const trimmed = value.trim();
465
+ if (trimmed.length === 0 || /[a-z0-9]/i.test(trimmed) === false) {
466
+ throw new InvalidIdentifierError("agent id", value);
467
+ }
468
+ return normalizeId(trimmed);
469
+ }
470
+ function buildTableOutput3(result) {
471
+ return printTable(
472
+ [
473
+ {
474
+ agentId: result.agentId,
475
+ configPath: result.configPath,
476
+ changed: String(result.changed),
477
+ removed: String(result.removed)
478
+ }
479
+ ],
480
+ [
481
+ { key: "agentId", label: "Agent" },
482
+ { key: "configPath", label: "Config Path" },
483
+ { key: "changed", label: "Changed" },
484
+ { key: "removed", label: "Removed" }
485
+ ]
486
+ );
487
+ }
488
+ async function runConfigRemoveAgent(options) {
489
+ const homeDir = options.homeDir ?? homedir3();
490
+ const resolvedPaths = resolveSkillmuxHome(homeDir);
491
+ const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
492
+ const configPath = buildConfigPath(skillmuxHome);
493
+ const config = await loadUserConfig(skillmuxHome);
494
+ const agentId = normalizeAgentId2(options.id);
495
+ const removed = agentId in config.agents;
496
+ const nextConfig = {
497
+ ...config,
498
+ agents: Object.fromEntries(
499
+ Object.entries(config.agents).filter(([currentAgentId]) => currentAgentId !== agentId)
500
+ )
501
+ };
502
+ if (removed) {
503
+ await writeUserConfig(skillmuxHome, nextConfig);
504
+ }
505
+ const resultWithoutOutput = {
506
+ skillmuxHome,
507
+ configPath,
508
+ agentId,
509
+ changed: removed,
510
+ removed,
511
+ config: nextConfig
512
+ };
513
+ return {
514
+ ...resultWithoutOutput,
515
+ output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput3(resultWithoutOutput)
516
+ };
517
+ }
518
+
519
+ // src/commands/config.ts
520
+ import { homedir as homedir4 } from "os";
521
+ function buildTableOutput4(result) {
307
522
  const summary = printTable(
308
523
  [
309
524
  {
@@ -338,35 +553,35 @@ ${printTable(agentRows, [
338
553
  ])}`;
339
554
  }
340
555
  async function runConfig(options = {}) {
341
- const homeDir = options.homeDir ?? homedir2();
556
+ const homeDir = options.homeDir ?? homedir4();
342
557
  const resolvedPaths = resolveSkillmuxHome(homeDir);
343
558
  const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
344
559
  const config = await loadUserConfig(skillmuxHome);
345
560
  const resultWithoutOutput = {
346
561
  skillmuxHome,
347
- configPath: resolvedPaths.configPath,
562
+ configPath: buildConfigPath(skillmuxHome),
348
563
  config
349
564
  };
350
565
  return {
351
566
  ...resultWithoutOutput,
352
- output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput2(resultWithoutOutput)
567
+ output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput4(resultWithoutOutput)
353
568
  };
354
569
  }
355
570
 
356
571
  // src/commands/doctor.ts
357
- import * as fs8 from "fs/promises";
358
- import { homedir as homedir3 } from "os";
572
+ import * as fs9 from "fs/promises";
573
+ import { homedir as homedir5 } from "os";
359
574
  import { join as join5 } from "path";
360
575
 
361
576
  // src/discovery/scan-agent-skills.ts
362
- import * as fs5 from "fs/promises";
577
+ import * as fs6 from "fs/promises";
363
578
 
364
579
  // src/discovery/infer-skill-entry.ts
365
- import * as fs4 from "fs/promises";
580
+ import * as fs5 from "fs/promises";
366
581
  import { basename, resolve as resolve4 } from "path";
367
582
 
368
583
  // src/fs/path-utils.ts
369
- import * as fs3 from "fs/promises";
584
+ import * as fs4 from "fs/promises";
370
585
  import { dirname, parse, relative, resolve as resolve3, sep } from "path";
371
586
  function normalizeAbsolutePath(path) {
372
587
  const normalized = resolve3(path);
@@ -397,7 +612,7 @@ async function assertNoSymlinkAncestors(path, options) {
397
612
  let current = options?.includeLeaf === true ? resolve3(path) : dirname(resolve3(path));
398
613
  while (true) {
399
614
  try {
400
- const entry = await fs3.lstat(current);
615
+ const entry = await fs4.lstat(current);
401
616
  if (entry.isSymbolicLink()) {
402
617
  throw new Error(`Refusing to use path with symlink ancestor at ${current}`);
403
618
  }
@@ -421,10 +636,10 @@ function buildIssue(code, severity, message, path) {
421
636
  async function inferSkillEntry(options) {
422
637
  const absolutePath = resolve4(options.path);
423
638
  const skillName = basename(absolutePath);
424
- const stats = await fs4.lstat(absolutePath);
639
+ const stats = await fs5.lstat(absolutePath);
425
640
  if (stats.isSymbolicLink()) {
426
641
  try {
427
- const targetPath = await fs4.realpath(absolutePath);
642
+ const targetPath = await fs5.realpath(absolutePath);
428
643
  if (isPathInside(options.skillmuxHome, targetPath)) {
429
644
  return {
430
645
  entry: {
@@ -510,7 +725,7 @@ async function scanAgentSkills(agent, skillmuxHome) {
510
725
  issues: []
511
726
  };
512
727
  }
513
- const directoryEntries = await fs5.readdir(agent.absoluteSkillsDirectoryPath, {
728
+ const directoryEntries = await fs6.readdir(agent.absoluteSkillsDirectoryPath, {
514
729
  withFileTypes: true
515
730
  });
516
731
  const sortedDirectoryEntries = [...directoryEntries].sort(
@@ -537,7 +752,7 @@ async function scanAgentSkills(agent, skillmuxHome) {
537
752
  }
538
753
 
539
754
  // src/manifest/read-manifest.ts
540
- import * as fs7 from "fs/promises";
755
+ import * as fs8 from "fs/promises";
541
756
  import { join as join4, resolve as resolve5 } from "path";
542
757
 
543
758
  // src/manifest/build-empty-manifest.ts
@@ -557,18 +772,6 @@ function buildEmptyManifest(skillmuxHome) {
557
772
 
558
773
  // src/manifest/manifest-schema.ts
559
774
  import { z as z2 } from "zod";
560
-
561
- // src/core/ids.ts
562
- var ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
563
- function normalizeId(value) {
564
- const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
565
- return normalized.length > 0 ? normalized : "skill";
566
- }
567
- function isValidId(value) {
568
- return ID_PATTERN.test(value);
569
- }
570
-
571
- // src/manifest/manifest-schema.ts
572
775
  var idSchema = z2.string().min(1).refine(isValidId, "Expected a canonical lowercase slug identifier");
573
776
  var scanIssueSchema = z2.object({
574
777
  code: z2.string().min(1),
@@ -675,7 +878,7 @@ var manifestSchema = z2.object({
675
878
 
676
879
  // src/manifest/write-manifest.ts
677
880
  import { randomUUID } from "crypto";
678
- import * as fs6 from "fs/promises";
881
+ import * as fs7 from "fs/promises";
679
882
  import { join as join3 } from "path";
680
883
  function getManifestPath(home) {
681
884
  return join3(home, "manifest.json");
@@ -684,16 +887,16 @@ function createManifestTempPath(manifestPath) {
684
887
  return `${manifestPath}.${process.pid}.${randomUUID()}.tmp`;
685
888
  }
686
889
  async function writeManifest(home, manifest) {
687
- await fs6.mkdir(home, { recursive: true });
890
+ await fs7.mkdir(home, { recursive: true });
688
891
  const manifestPath = getManifestPath(home);
689
892
  const tempPath = createManifestTempPath(manifestPath);
690
893
  const contents = `${JSON.stringify(manifest, null, 2)}
691
894
  `;
692
- await fs6.writeFile(tempPath, contents, "utf8");
895
+ await fs7.writeFile(tempPath, contents, "utf8");
693
896
  try {
694
- await fs6.rename(tempPath, manifestPath);
897
+ await fs7.rename(tempPath, manifestPath);
695
898
  } catch (error) {
696
- await fs6.unlink(tempPath).catch(() => void 0);
899
+ await fs7.unlink(tempPath).catch(() => void 0);
697
900
  throw error;
698
901
  }
699
902
  }
@@ -715,7 +918,7 @@ function formatValidationIssues2(error) {
715
918
  async function readManifest(home) {
716
919
  const manifestPath = getManifestPath2(home);
717
920
  try {
718
- const contents = await fs7.readFile(manifestPath, "utf8");
921
+ const contents = await fs8.readFile(manifestPath, "utf8");
719
922
  const parsedJson = JSON.parse(contents);
720
923
  const parsedManifest = manifestSchema.safeParse(parsedJson);
721
924
  if (!parsedManifest.success) {
@@ -750,7 +953,7 @@ function buildIssue2(code, severity, message, path) {
750
953
  }
751
954
  async function pathExists2(path) {
752
955
  try {
753
- await fs8.access(path);
956
+ await fs9.access(path);
754
957
  return true;
755
958
  } catch (error) {
756
959
  if (error.code === "ENOENT") {
@@ -814,7 +1017,7 @@ function addConflictingAgentPathIssues(agents, issues) {
814
1017
  );
815
1018
  }
816
1019
  }
817
- function buildTableOutput3(issues) {
1020
+ function buildTableOutput5(issues) {
818
1021
  if (issues.length === 0) {
819
1022
  return "No doctor issues found.\n";
820
1023
  }
@@ -846,7 +1049,7 @@ function buildJsonOutput(result) {
846
1049
  });
847
1050
  }
848
1051
  async function runDoctor(options = {}) {
849
- const homeDir = options.homeDir ?? homedir3();
1052
+ const homeDir = options.homeDir ?? homedir5();
850
1053
  const resolvedPaths = resolveSkillmuxHome(homeDir);
851
1054
  const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
852
1055
  const [manifest, config, agents] = await Promise.all([
@@ -883,51 +1086,127 @@ async function runDoctor(options = {}) {
883
1086
  };
884
1087
  return {
885
1088
  ...resultWithoutOutput,
886
- output: options.json === true ? buildJsonOutput(resultWithoutOutput) : buildTableOutput3(dedupedIssues)
1089
+ output: options.json === true ? buildJsonOutput(resultWithoutOutput) : buildTableOutput5(dedupedIssues)
887
1090
  };
888
1091
  }
889
1092
 
890
1093
  // src/commands/disable.ts
891
- import * as fs11 from "fs/promises";
892
- import { homedir as homedir4 } from "os";
893
- import { join as join6 } from "path";
1094
+ import * as fs13 from "fs/promises";
1095
+ import { homedir as homedir6 } from "os";
1096
+ import { join as join7, resolve as resolve8 } from "path";
1097
+
1098
+ // src/fs/safe-copy.ts
1099
+ import * as fs10 from "fs/promises";
1100
+ import { dirname as dirname2, join as join6, resolve as resolve6 } from "path";
1101
+ async function assertDirectory(path) {
1102
+ const entry = await fs10.lstat(path);
1103
+ if (!entry.isDirectory()) {
1104
+ throw new Error(`Expected a directory at ${path}`);
1105
+ }
1106
+ }
1107
+ async function assertRegularFile(path, label) {
1108
+ const entry = await fs10.lstat(path);
1109
+ if (!entry.isFile()) {
1110
+ throw new Error(`Expected ${label} to be a regular file at ${path}`);
1111
+ }
1112
+ }
1113
+ async function assertTargetDoesNotExist(path) {
1114
+ try {
1115
+ await fs10.lstat(path);
1116
+ throw new Error(`Refusing to overwrite existing path at ${path}`);
1117
+ } catch (error) {
1118
+ if (error.code !== "ENOENT") {
1119
+ throw error;
1120
+ }
1121
+ }
1122
+ }
1123
+ async function copyDirectoryContents(sourcePath, targetPath) {
1124
+ await fs10.mkdir(targetPath, { recursive: true });
1125
+ const entries = await fs10.readdir(sourcePath, { withFileTypes: true });
1126
+ for (const entry of entries) {
1127
+ const sourceEntryPath = join6(sourcePath, entry.name);
1128
+ const targetEntryPath = join6(targetPath, entry.name);
1129
+ const entryStats = await fs10.lstat(sourceEntryPath);
1130
+ if (entryStats.isSymbolicLink()) {
1131
+ throw new Error(`Refusing to copy source symlink at ${sourceEntryPath}`);
1132
+ }
1133
+ if (entryStats.isDirectory()) {
1134
+ await copyDirectoryContents(sourceEntryPath, targetEntryPath);
1135
+ continue;
1136
+ }
1137
+ if (entryStats.isFile()) {
1138
+ await fs10.mkdir(dirname2(targetEntryPath), { recursive: true });
1139
+ await fs10.copyFile(sourceEntryPath, targetEntryPath);
1140
+ continue;
1141
+ }
1142
+ throw new Error(`Unsupported filesystem entry at ${sourceEntryPath}`);
1143
+ }
1144
+ }
1145
+ async function assertSkillSourceLayout(sourcePath) {
1146
+ const resolvedSourcePath = resolve6(sourcePath);
1147
+ const skillFilePath = join6(resolvedSourcePath, "SKILL.md");
1148
+ await assertNoSymlinkAncestors(resolvedSourcePath, { includeLeaf: true });
1149
+ await assertDirectory(resolvedSourcePath);
1150
+ try {
1151
+ await assertRegularFile(skillFilePath, "SKILL.md");
1152
+ } catch (error) {
1153
+ if (error.code === "ENOENT") {
1154
+ throw new Error(`Refusing to import ${resolvedSourcePath} without a root SKILL.md`);
1155
+ }
1156
+ throw error;
1157
+ }
1158
+ }
1159
+ async function copySkillContentsToManagedStore(sourcePath, targetPath) {
1160
+ const resolvedSourcePath = resolve6(sourcePath);
1161
+ const resolvedTargetPath = resolve6(targetPath);
1162
+ if (pathsAreEqual(resolvedSourcePath, resolvedTargetPath)) {
1163
+ throw new Error("Source and target paths must differ");
1164
+ }
1165
+ if (isPathInside(resolvedSourcePath, resolvedTargetPath)) {
1166
+ throw new Error("Refusing to copy into a child of the source directory");
1167
+ }
1168
+ await assertSkillSourceLayout(resolvedSourcePath);
1169
+ await assertNoSymlinkAncestors(resolvedTargetPath);
1170
+ await assertTargetDoesNotExist(resolvedTargetPath);
1171
+ await copyDirectoryContents(resolvedSourcePath, resolvedTargetPath);
1172
+ }
894
1173
 
895
1174
  // src/fs/link-ops.ts
896
- import * as fs9 from "fs/promises";
897
- import { dirname as dirname2, resolve as resolve6 } from "path";
1175
+ import * as fs11 from "fs/promises";
1176
+ import { dirname as dirname3, resolve as resolve7 } from "path";
898
1177
  var directoryLinkType = process.platform === "win32" ? "junction" : "dir";
899
1178
  async function createManagedLink(linkPath, targetPath) {
900
- const resolvedLinkPath = resolve6(linkPath);
901
- const resolvedTargetPath = resolve6(targetPath);
1179
+ const resolvedLinkPath = resolve7(linkPath);
1180
+ const resolvedTargetPath = resolve7(targetPath);
902
1181
  await assertNoSymlinkAncestors(resolvedLinkPath);
903
1182
  await assertNoSymlinkAncestors(resolvedTargetPath, { includeLeaf: true });
904
- await fs9.mkdir(dirname2(resolvedLinkPath), { recursive: true });
1183
+ await fs11.mkdir(dirname3(resolvedLinkPath), { recursive: true });
905
1184
  try {
906
- const existingEntry = await fs9.lstat(resolvedLinkPath);
1185
+ const existingEntry = await fs11.lstat(resolvedLinkPath);
907
1186
  if (!existingEntry.isSymbolicLink()) {
908
1187
  throw new Error(`Refusing to replace non-link entry at ${resolvedLinkPath}`);
909
1188
  }
910
- const currentTargetPath = await fs9.realpath(resolvedLinkPath);
1189
+ const currentTargetPath = await fs11.realpath(resolvedLinkPath);
911
1190
  if (pathsAreEqual(currentTargetPath, resolvedTargetPath)) {
912
1191
  return;
913
1192
  }
914
1193
  throw new Error(`Refusing to replace link at ${resolvedLinkPath}`);
915
1194
  } catch (error) {
916
- if (error.code === "ENOENT" && await fs9.lstat(resolvedLinkPath).then((entry) => entry.isSymbolicLink()).catch(() => false)) {
917
- await fs9.rm(resolvedLinkPath, { recursive: true, force: false });
1195
+ if (error.code === "ENOENT" && await fs11.lstat(resolvedLinkPath).then((entry) => entry.isSymbolicLink()).catch(() => false)) {
1196
+ await fs11.rm(resolvedLinkPath, { recursive: true, force: false });
918
1197
  } else if (error.code !== "ENOENT") {
919
1198
  throw error;
920
1199
  }
921
1200
  }
922
- await fs9.symlink(resolvedTargetPath, resolvedLinkPath, directoryLinkType);
1201
+ await fs11.symlink(resolvedTargetPath, resolvedLinkPath, directoryLinkType);
923
1202
  }
924
1203
  async function isLinkPointingToTarget(linkPath, targetPath) {
925
1204
  try {
926
- const entry = await fs9.lstat(linkPath);
1205
+ const entry = await fs11.lstat(linkPath);
927
1206
  if (!entry.isSymbolicLink()) {
928
1207
  return false;
929
1208
  }
930
- const resolvedTargetPath = await fs9.realpath(linkPath);
1209
+ const resolvedTargetPath = await fs11.realpath(linkPath);
931
1210
  return pathsAreEqual(resolvedTargetPath, targetPath);
932
1211
  } catch (error) {
933
1212
  if (error.code === "ENOENT") {
@@ -938,14 +1217,14 @@ async function isLinkPointingToTarget(linkPath, targetPath) {
938
1217
  }
939
1218
 
940
1219
  // src/fs/safe-remove-link.ts
941
- import * as fs10 from "fs/promises";
1220
+ import * as fs12 from "fs/promises";
942
1221
  async function safeRemoveLink(path) {
943
1222
  try {
944
- const entry = await fs10.lstat(path);
1223
+ const entry = await fs12.lstat(path);
945
1224
  if (!entry.isSymbolicLink()) {
946
1225
  return false;
947
1226
  }
948
- await fs10.rm(path, { recursive: true, force: false });
1227
+ await fs12.rm(path, { recursive: true, force: false });
949
1228
  return true;
950
1229
  } catch (error) {
951
1230
  if (error.code === "ENOENT") {
@@ -985,6 +1264,38 @@ function upsertActivation(manifest, activation) {
985
1264
  }
986
1265
  manifest.activations[index] = activation;
987
1266
  }
1267
+ function buildManagedSkillPath(skillmuxHome, skillId) {
1268
+ return resolve8(skillmuxHome, "skills", skillId);
1269
+ }
1270
+ async function tryAdoptManagedSkill(manifest, skillmuxHome, skillId, skillName, linkPath, timestamp) {
1271
+ try {
1272
+ const entry = await fs13.lstat(linkPath);
1273
+ if (!entry.isSymbolicLink()) {
1274
+ return void 0;
1275
+ }
1276
+ } catch (error) {
1277
+ if (error.code === "ENOENT") {
1278
+ return void 0;
1279
+ }
1280
+ throw error;
1281
+ }
1282
+ const sourcePath = await fs13.realpath(linkPath);
1283
+ await assertSkillSourceLayout(sourcePath);
1284
+ const managedSkillPath = buildManagedSkillPath(skillmuxHome, skillId);
1285
+ await copySkillContentsToManagedStore(sourcePath, managedSkillPath);
1286
+ const skill = {
1287
+ id: skillId,
1288
+ name: skillName,
1289
+ path: managedSkillPath,
1290
+ source: {
1291
+ kind: "imported",
1292
+ path: sourcePath
1293
+ },
1294
+ importedAt: timestamp
1295
+ };
1296
+ manifest.skills[skillId] = skill;
1297
+ return { skill, sourcePath };
1298
+ }
988
1299
  async function resolveTargetAgent(homeDir, skillmuxHome, agentName) {
989
1300
  const agentId = normalizeId(agentName);
990
1301
  const agents = await discoverAgents({ homeDir, skillmuxHome });
@@ -999,7 +1310,7 @@ async function resolveTargetAgent(homeDir, skillmuxHome, agentName) {
999
1310
  }
1000
1311
  async function pathExists3(path) {
1001
1312
  try {
1002
- await fs11.lstat(path);
1313
+ await fs13.lstat(path);
1003
1314
  return true;
1004
1315
  } catch (error) {
1005
1316
  if (error.code === "ENOENT") {
@@ -1009,28 +1320,38 @@ async function pathExists3(path) {
1009
1320
  }
1010
1321
  }
1011
1322
  async function runDisable(options) {
1012
- const homeDir = options.homeDir ?? homedir4();
1323
+ const homeDir = options.homeDir ?? homedir6();
1013
1324
  const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
1014
1325
  const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
1015
1326
  const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
1016
1327
  const manifest = await readManifest(skillmuxHome);
1017
1328
  const skillId = normalizeId(options.skill);
1018
- const skill = manifest.skills[skillId];
1329
+ const agent = await resolveTargetAgent(homeDir, skillmuxHome, options.agent);
1330
+ const linkPath = join7(agent.absoluteSkillsDirectoryPath, skillId);
1331
+ const adoption = manifest.skills[skillId] ? void 0 : await tryAdoptManagedSkill(
1332
+ manifest,
1333
+ skillmuxHome,
1334
+ skillId,
1335
+ options.skill,
1336
+ linkPath,
1337
+ timestamp
1338
+ );
1339
+ const skill = manifest.skills[skillId] ?? adoption?.skill;
1019
1340
  if (skill === void 0) {
1020
1341
  throw new Error(`Managed skill not found: ${skillId}`);
1021
1342
  }
1022
- const agent = await resolveTargetAgent(homeDir, skillmuxHome, options.agent);
1023
1343
  const currentActivation = manifest.activations.find(
1024
1344
  (entry) => entry.skillId === skill.id && entry.agentId === agent.id
1025
1345
  );
1026
- const linkPath = currentActivation?.linkPath ?? join6(agent.absoluteSkillsDirectoryPath, skill.id);
1346
+ const activationLinkPath = currentActivation?.linkPath ?? linkPath;
1027
1347
  const agentRecord = buildAgentRecord(agent, timestamp);
1028
1348
  manifest.agents[agent.id] = agentRecord;
1029
- const linkMatchesSkill = await isLinkPointingToTarget(linkPath, skill.path);
1030
- if (!linkMatchesSkill && await pathExists3(linkPath)) {
1349
+ const adoptedLinkRemoved = adoption !== void 0 ? await safeRemoveLink(linkPath) : false;
1350
+ const linkMatchesSkill = adoption === void 0 ? await isLinkPointingToTarget(activationLinkPath, skill.path) : false;
1351
+ if (adoption === void 0 && !linkMatchesSkill && await pathExists3(activationLinkPath)) {
1031
1352
  throw new Error(`Refusing to disable non-managed entry at ${linkPath}`);
1032
1353
  }
1033
- const removedLink = linkMatchesSkill ? await safeRemoveLink(linkPath) : false;
1354
+ const removedLink = adoptedLinkRemoved ? true : linkMatchesSkill ? await safeRemoveLink(activationLinkPath) : false;
1034
1355
  if (removedLink === false && currentActivation?.state !== "enabled") {
1035
1356
  return {
1036
1357
  changed: false,
@@ -1057,9 +1378,9 @@ async function runDisable(options) {
1057
1378
  }
1058
1379
 
1059
1380
  // src/commands/enable.ts
1060
- import * as fs12 from "fs/promises";
1061
- import { homedir as homedir5 } from "os";
1062
- import { join as join7 } from "path";
1381
+ import * as fs14 from "fs/promises";
1382
+ import { homedir as homedir7 } from "os";
1383
+ import { join as join8 } from "path";
1063
1384
  function buildAgentRecord2(agent, timestamp) {
1064
1385
  return {
1065
1386
  id: agent.id,
@@ -1102,7 +1423,7 @@ async function resolveTargetAgent2(homeDir, skillmuxHome, agentName) {
1102
1423
  return agent;
1103
1424
  }
1104
1425
  async function runEnable(options) {
1105
- const homeDir = options.homeDir ?? homedir5();
1426
+ const homeDir = options.homeDir ?? homedir7();
1106
1427
  const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
1107
1428
  const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
1108
1429
  const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
@@ -1113,13 +1434,13 @@ async function runEnable(options) {
1113
1434
  throw new Error(`Managed skill not found: ${skillId}`);
1114
1435
  }
1115
1436
  const agent = await resolveTargetAgent2(homeDir, skillmuxHome, options.agent);
1116
- const linkPath = join7(agent.absoluteSkillsDirectoryPath, skill.id);
1437
+ const linkPath = join8(agent.absoluteSkillsDirectoryPath, skill.id);
1117
1438
  const currentActivation = manifest.activations.find(
1118
1439
  (entry) => entry.skillId === skill.id && entry.agentId === agent.id
1119
1440
  );
1120
1441
  const agentRecord = buildAgentRecord2(agent, timestamp);
1121
1442
  manifest.agents[agent.id] = agentRecord;
1122
- await fs12.mkdir(agent.absoluteSkillsDirectoryPath, { recursive: true });
1443
+ await fs14.mkdir(agent.absoluteSkillsDirectoryPath, { recursive: true });
1123
1444
  const linkAlreadyEnabled = await isLinkPointingToTarget(linkPath, skill.path);
1124
1445
  const activationAlreadyEnabled = currentActivation?.state === "enabled" && currentActivation.linkPath === linkPath;
1125
1446
  if (linkAlreadyEnabled && activationAlreadyEnabled) {
@@ -1155,98 +1476,20 @@ async function runEnable(options) {
1155
1476
  }
1156
1477
 
1157
1478
  // src/commands/import.ts
1158
- import { resolve as resolve8 } from "path";
1159
- import { homedir as homedir6 } from "os";
1160
-
1161
- // src/fs/safe-copy.ts
1162
- import * as fs13 from "fs/promises";
1163
- import { dirname as dirname3, join as join8, resolve as resolve7 } from "path";
1164
- async function assertDirectory(path) {
1165
- const entry = await fs13.lstat(path);
1166
- if (!entry.isDirectory()) {
1167
- throw new Error(`Expected a directory at ${path}`);
1168
- }
1169
- }
1170
- async function assertRegularFile(path, label) {
1171
- const entry = await fs13.lstat(path);
1172
- if (!entry.isFile()) {
1173
- throw new Error(`Expected ${label} to be a regular file at ${path}`);
1174
- }
1175
- }
1176
- async function assertTargetDoesNotExist(path) {
1177
- try {
1178
- await fs13.lstat(path);
1179
- throw new Error(`Refusing to overwrite existing path at ${path}`);
1180
- } catch (error) {
1181
- if (error.code !== "ENOENT") {
1182
- throw error;
1183
- }
1184
- }
1185
- }
1186
- async function copyDirectoryContents(sourcePath, targetPath) {
1187
- await fs13.mkdir(targetPath, { recursive: true });
1188
- const entries = await fs13.readdir(sourcePath, { withFileTypes: true });
1189
- for (const entry of entries) {
1190
- const sourceEntryPath = join8(sourcePath, entry.name);
1191
- const targetEntryPath = join8(targetPath, entry.name);
1192
- const entryStats = await fs13.lstat(sourceEntryPath);
1193
- if (entryStats.isSymbolicLink()) {
1194
- throw new Error(`Refusing to copy source symlink at ${sourceEntryPath}`);
1195
- }
1196
- if (entryStats.isDirectory()) {
1197
- await copyDirectoryContents(sourceEntryPath, targetEntryPath);
1198
- continue;
1199
- }
1200
- if (entryStats.isFile()) {
1201
- await fs13.mkdir(dirname3(targetEntryPath), { recursive: true });
1202
- await fs13.copyFile(sourceEntryPath, targetEntryPath);
1203
- continue;
1204
- }
1205
- throw new Error(`Unsupported filesystem entry at ${sourceEntryPath}`);
1206
- }
1207
- }
1208
- async function assertSkillSourceLayout(sourcePath) {
1209
- const resolvedSourcePath = resolve7(sourcePath);
1210
- const skillFilePath = join8(resolvedSourcePath, "SKILL.md");
1211
- await assertNoSymlinkAncestors(resolvedSourcePath, { includeLeaf: true });
1212
- await assertDirectory(resolvedSourcePath);
1213
- try {
1214
- await assertRegularFile(skillFilePath, "SKILL.md");
1215
- } catch (error) {
1216
- if (error.code === "ENOENT") {
1217
- throw new Error(`Refusing to import ${resolvedSourcePath} without a root SKILL.md`);
1218
- }
1219
- throw error;
1220
- }
1221
- }
1222
- async function copySkillContentsToManagedStore(sourcePath, targetPath) {
1223
- const resolvedSourcePath = resolve7(sourcePath);
1224
- const resolvedTargetPath = resolve7(targetPath);
1225
- if (pathsAreEqual(resolvedSourcePath, resolvedTargetPath)) {
1226
- throw new Error("Source and target paths must differ");
1227
- }
1228
- if (isPathInside(resolvedSourcePath, resolvedTargetPath)) {
1229
- throw new Error("Refusing to copy into a child of the source directory");
1230
- }
1231
- await assertSkillSourceLayout(resolvedSourcePath);
1232
- await assertNoSymlinkAncestors(resolvedTargetPath);
1233
- await assertTargetDoesNotExist(resolvedTargetPath);
1234
- await copyDirectoryContents(resolvedSourcePath, resolvedTargetPath);
1235
- }
1236
-
1237
- // src/commands/import.ts
1238
- function buildManagedSkillPath(skillmuxHome, skillId) {
1239
- return resolve8(skillmuxHome, "skills", skillId);
1479
+ import { resolve as resolve9 } from "path";
1480
+ import { homedir as homedir8 } from "os";
1481
+ function buildManagedSkillPath2(skillmuxHome, skillId) {
1482
+ return resolve9(skillmuxHome, "skills", skillId);
1240
1483
  }
1241
1484
  async function runImport(options) {
1242
- const homeDir = options.homeDir ?? homedir6();
1485
+ const homeDir = options.homeDir ?? homedir8();
1243
1486
  const resolvedPaths = resolveSkillmuxHome(homeDir);
1244
1487
  const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
1245
- const sourcePath = resolve8(options.sourcePath);
1488
+ const sourcePath = resolve9(options.sourcePath);
1246
1489
  const skillId = normalizeId(options.skillName);
1247
1490
  const importedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
1248
1491
  const manifest = await readManifest(skillmuxHome);
1249
- const managedSkillPath = buildManagedSkillPath(skillmuxHome, skillId);
1492
+ const managedSkillPath = buildManagedSkillPath2(skillmuxHome, skillId);
1250
1493
  await assertSkillSourceLayout(sourcePath);
1251
1494
  if (manifest.skills[skillId] !== void 0) {
1252
1495
  throw new Error(`Managed skill already exists for ${skillId}`);
@@ -1273,7 +1516,7 @@ async function runImport(options) {
1273
1516
  }
1274
1517
 
1275
1518
  // src/commands/scan.ts
1276
- import { homedir as homedir7 } from "os";
1519
+ import { homedir as homedir9 } from "os";
1277
1520
 
1278
1521
  // src/output/format-issue.ts
1279
1522
  function formatIssue(issue) {
@@ -1334,7 +1577,7 @@ ${result.issues.map(formatIssue).join("\n")}
1334
1577
  `;
1335
1578
  }
1336
1579
  async function runScan(options = {}) {
1337
- const homeDir = options.homeDir ?? homedir7();
1580
+ const homeDir = options.homeDir ?? homedir9();
1338
1581
  const resolvedPaths = resolveSkillmuxHome(homeDir);
1339
1582
  const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
1340
1583
  const manifest = await readManifest(skillmuxHome);
@@ -1383,6 +1626,13 @@ function buildRecordsView(scanResult) {
1383
1626
  }
1384
1627
  function buildAgentsView(scanResult) {
1385
1628
  const groups = /* @__PURE__ */ new Map();
1629
+ for (const agent of scanResult.agents) {
1630
+ groups.set(agent.id, {
1631
+ agentId: agent.id,
1632
+ agentName: agent.stableName,
1633
+ entries: []
1634
+ });
1635
+ }
1386
1636
  for (const entry of scanResult.entries) {
1387
1637
  const current = groups.get(entry.agentId) ?? {
1388
1638
  agentId: entry.agentId,
@@ -1402,6 +1652,12 @@ function buildAgentsView(scanResult) {
1402
1652
  }
1403
1653
  function buildSkillsView(scanResult) {
1404
1654
  const groups = /* @__PURE__ */ new Map();
1655
+ for (const skill of Object.values(scanResult.manifest.skills)) {
1656
+ groups.set(skill.id, {
1657
+ skillName: skill.id,
1658
+ entries: []
1659
+ });
1660
+ }
1405
1661
  for (const entry of scanResult.entries) {
1406
1662
  const current = groups.get(entry.skillName) ?? {
1407
1663
  skillName: entry.skillName,
@@ -1427,7 +1683,7 @@ function buildListData(scanResult, view) {
1427
1683
  }
1428
1684
  return buildRecordsView(scanResult);
1429
1685
  }
1430
- function buildTableOutput4(data, view) {
1686
+ function buildTableOutput6(data, view) {
1431
1687
  if (view === "agents") {
1432
1688
  const agentRows = data.agents;
1433
1689
  return printTable(
@@ -1477,7 +1733,7 @@ async function runList(options = {}) {
1477
1733
  const data = buildListData(scanResult, view);
1478
1734
  return {
1479
1735
  data,
1480
- output: format === "json" ? printJson(data) : buildTableOutput4(data, view)
1736
+ output: format === "json" ? printJson(data) : buildTableOutput6(data, view)
1481
1737
  };
1482
1738
  }
1483
1739
 
@@ -1511,10 +1767,37 @@ function buildCli() {
1511
1767
  const result = await runDoctor({ json: options.json === true });
1512
1768
  process.stdout.write(result.output);
1513
1769
  });
1514
- program.command("config").option("--json", "Emit structured JSON output").action(async (options) => {
1770
+ const configCommand = program.command("config");
1771
+ configCommand.option("--json", "Emit structured JSON output").action(async (options) => {
1515
1772
  const result = await runConfig({ json: options.json === true });
1516
1773
  process.stdout.write(result.output);
1517
1774
  });
1775
+ configCommand.command("add-agent").requiredOption("--id <id>", "Agent id").requiredOption("--root <path>", "Home-relative root path").option("--skills <path>", "Skills directory path relative to the agent root", "skills").option("--name <name>", "Stable display name").option(
1776
+ "--platform <platform>",
1777
+ `Supported platform (${supportedPlatforms.join(", ")})`,
1778
+ (value, previous = []) => [...previous, value],
1779
+ []
1780
+ ).option("--disabled-by-default", "Mark this custom agent as disabled by default").option("--json", "Emit structured JSON output").action(
1781
+ async (options) => {
1782
+ const result = await runConfigAddAgent({
1783
+ id: options.id,
1784
+ root: options.root,
1785
+ skills: options.skills,
1786
+ name: options.name,
1787
+ platforms: options.platform,
1788
+ disabledByDefault: options.disabledByDefault === true,
1789
+ json: options.json === true
1790
+ });
1791
+ process.stdout.write(result.output);
1792
+ }
1793
+ );
1794
+ configCommand.command("remove-agent").requiredOption("--id <id>", "Agent id").option("--json", "Emit structured JSON output").action(async (options) => {
1795
+ const result = await runConfigRemoveAgent({
1796
+ id: options.id,
1797
+ json: options.json === true
1798
+ });
1799
+ process.stdout.write(result.output);
1800
+ });
1518
1801
  program.command("enable").requiredOption("--skill <skill>", "Managed skill name or id").requiredOption("--agent <agent>", "Target agent id").action(async (options) => {
1519
1802
  const result = await runEnable({
1520
1803
  skill: options.skill,