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