tools-manager 0.1.0

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 ADDED
@@ -0,0 +1,2007 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/core/init.ts
5
+ import { dirname as dirname2, join as join6 } from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // src/core/db.ts
9
+ import { Database } from "bun:sqlite";
10
+
11
+ // src/core/paths.ts
12
+ import { homedir } from "os";
13
+ import { join } from "path";
14
+ function managerRoot() {
15
+ return process.env.TOOLS_MANAGER_HOME || join(homedir(), ".tools-manager");
16
+ }
17
+ function paths() {
18
+ const root = managerRoot();
19
+ return {
20
+ root,
21
+ skillsDir: join(root, "skills"),
22
+ cacheGitDir: join(root, "cache", "git"),
23
+ logsDir: join(root, "logs"),
24
+ dbPath: join(root, "tools-manager.db"),
25
+ configPath: join(root, "config.toml")
26
+ };
27
+ }
28
+ function expandHome(input) {
29
+ if (input === "~")
30
+ return homedir();
31
+ if (input.startsWith("~/"))
32
+ return join(homedir(), input.slice(2));
33
+ return input;
34
+ }
35
+
36
+ // src/core/fs.ts
37
+ import { mkdir, readdir, readFile, rm, stat, symlink, copyFile, lstat, writeFile } from "fs/promises";
38
+ import { dirname, join as join2 } from "path";
39
+ async function pathExists(path) {
40
+ try {
41
+ await stat(path);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ async function ensureDir(path) {
48
+ await mkdir(path, { recursive: true });
49
+ }
50
+ async function readTextIfExists(path) {
51
+ try {
52
+ return await readFile(path, "utf8");
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ async function writeText(path, contents) {
58
+ await ensureDir(dirname(path));
59
+ await writeFile(path, contents, "utf8");
60
+ }
61
+ async function backupFile(path) {
62
+ if (!await pathExists(path))
63
+ return null;
64
+ const backup = `${path}.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`;
65
+ await copyFile(path, backup);
66
+ return backup;
67
+ }
68
+ async function copyDir(src, dst) {
69
+ await rm(dst, { recursive: true, force: true });
70
+ await ensureDir(dst);
71
+ for (const entry of await readdir(src, { withFileTypes: true })) {
72
+ if (entry.name === ".git")
73
+ continue;
74
+ const source = join2(src, entry.name);
75
+ const target = join2(dst, entry.name);
76
+ if (entry.isDirectory()) {
77
+ await copyDir(source, target);
78
+ } else if (entry.isSymbolicLink()) {
79
+ const real = await Bun.file(source).arrayBuffer();
80
+ await Bun.write(target, real);
81
+ } else {
82
+ await copyFile(source, target);
83
+ }
84
+ }
85
+ }
86
+ async function removePath(path) {
87
+ await rm(path, { recursive: true, force: true });
88
+ }
89
+ async function syncDir(src, dst, mode) {
90
+ await ensureDir(dirname(dst));
91
+ await rm(dst, { recursive: true, force: true });
92
+ if (mode === "symlink") {
93
+ try {
94
+ await symlink(src, dst, "dir");
95
+ return "symlink";
96
+ } catch {
97
+ await copyDir(src, dst);
98
+ return "copy";
99
+ }
100
+ }
101
+ await copyDir(src, dst);
102
+ return "copy";
103
+ }
104
+ async function replaceWithSymlink(src, dst) {
105
+ await rm(dst, { recursive: true, force: true });
106
+ await ensureDir(dirname(dst));
107
+ await symlink(src, dst, "dir");
108
+ }
109
+
110
+ // src/core/db.ts
111
+ var database = null;
112
+ async function openDb() {
113
+ if (database)
114
+ return database;
115
+ const p = paths();
116
+ await ensureDir(p.root);
117
+ database = new Database(p.dbPath);
118
+ database.exec("PRAGMA journal_mode = WAL");
119
+ migrate(database);
120
+ ensureDefaultPreset(database);
121
+ return database;
122
+ }
123
+ function closeDb() {
124
+ database?.close();
125
+ database = null;
126
+ }
127
+ function migrate(db) {
128
+ db.exec(`
129
+ CREATE TABLE IF NOT EXISTS skills (
130
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
131
+ name TEXT NOT NULL UNIQUE,
132
+ description TEXT,
133
+ path TEXT NOT NULL,
134
+ source_type TEXT NOT NULL,
135
+ source_url TEXT,
136
+ source_ref TEXT,
137
+ source_subpath TEXT,
138
+ source_commit TEXT,
139
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
140
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
141
+ );
142
+
143
+ CREATE TABLE IF NOT EXISTS presets (
144
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
145
+ name TEXT NOT NULL UNIQUE,
146
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
147
+ );
148
+
149
+ CREATE TABLE IF NOT EXISTS preset_skills (
150
+ preset_id INTEGER NOT NULL,
151
+ skill_id INTEGER NOT NULL,
152
+ PRIMARY KEY (preset_id, skill_id),
153
+ FOREIGN KEY (preset_id) REFERENCES presets(id) ON DELETE CASCADE,
154
+ FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE
155
+ );
156
+
157
+ CREATE TABLE IF NOT EXISTS mcp_servers (
158
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
159
+ name TEXT NOT NULL UNIQUE,
160
+ command TEXT NOT NULL,
161
+ args_json TEXT NOT NULL,
162
+ env_json TEXT NOT NULL,
163
+ target_tools_json TEXT NOT NULL,
164
+ enabled INTEGER NOT NULL DEFAULT 1,
165
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
166
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
167
+ );
168
+ `);
169
+ }
170
+ function ensureDefaultPreset(db) {
171
+ db.query("INSERT OR IGNORE INTO presets (name) VALUES (?)").run("Default");
172
+ }
173
+
174
+ // src/core/config.ts
175
+ async function ensureConfig() {
176
+ const p = paths();
177
+ await ensureDir(p.root);
178
+ if (!await pathExists(p.configPath)) {
179
+ await writeText(p.configPath, [
180
+ "# Tools Manager configuration",
181
+ 'sync_mode = "symlink"',
182
+ "",
183
+ "# Optional remote used by `tm backup`.",
184
+ '# git_remote = "git@github.com:you/tools-manager-skills.git"',
185
+ ""
186
+ ].join(`
187
+ `));
188
+ }
189
+ }
190
+ async function readConfig() {
191
+ await ensureConfig();
192
+ const text = await readTextIfExists(paths().configPath) || "";
193
+ const syncMode = /sync_mode\s*=\s*"copy"/.test(text) ? "copy" : "symlink";
194
+ const remoteMatch = text.match(/git_remote\s*=\s*"([^"]+)"/);
195
+ return {
196
+ syncMode,
197
+ ...remoteMatch?.[1] ? { gitRemote: remoteMatch[1] } : {}
198
+ };
199
+ }
200
+
201
+ // src/core/skill.ts
202
+ import { basename, join as join5, resolve } from "path";
203
+ import { lstat as lstat2, readdir as readdir2, readFile as readFile2, realpath, stat as stat2 } from "fs/promises";
204
+
205
+ // src/core/git.ts
206
+ import { createHash } from "crypto";
207
+ import { join as join3 } from "path";
208
+
209
+ // src/core/errors.ts
210
+ class UsageError extends Error {
211
+ constructor(message) {
212
+ super(message);
213
+ this.name = "UsageError";
214
+ }
215
+ }
216
+ function assertUsage(condition, message) {
217
+ if (!condition) {
218
+ throw new UsageError(message);
219
+ }
220
+ }
221
+
222
+ // src/core/git.ts
223
+ function isGitSource(source) {
224
+ return /^https?:\/\/.+/.test(source) || /^file:\/\/.+/.test(source) || /^ssh:\/\/.+/.test(source) || /^[^@\s]+@[^:\s]+:.+/.test(source) || source.endsWith(".git");
225
+ }
226
+ function parseGitSource(source) {
227
+ const hashIndex = source.indexOf("#");
228
+ if (hashIndex === -1)
229
+ return { url: source };
230
+ const url = source.slice(0, hashIndex);
231
+ const spec = source.slice(hashIndex + 1);
232
+ assertUsage(url.length > 0, "Git URL is empty.");
233
+ assertUsage(spec.length > 0, "Git ref after # is empty.");
234
+ const colonIndex = spec.indexOf(":");
235
+ if (colonIndex === -1)
236
+ return { url, ref: spec };
237
+ const ref = spec.slice(0, colonIndex);
238
+ const subpath = spec.slice(colonIndex + 1);
239
+ assertUsage(ref.length > 0, "Git ref before : is empty.");
240
+ assertUsage(subpath.length > 0, "Git subpath after : is empty.");
241
+ return { url, ref, subpath };
242
+ }
243
+ async function cloneGitSource(source) {
244
+ const p = paths();
245
+ await ensureDir(p.cacheGitDir);
246
+ const key = createHash("sha1").update(`${source.url}#${source.ref || ""}`).digest("hex");
247
+ const checkoutDir = join3(p.cacheGitDir, key);
248
+ await removePath(checkoutDir);
249
+ const args = ["clone", "--depth", "1"];
250
+ if (source.ref)
251
+ args.push("--branch", source.ref);
252
+ args.push(source.url, checkoutDir);
253
+ runGit(args, "Failed to clone Git source.");
254
+ const commitSha = runGit(["-C", checkoutDir, "rev-parse", "HEAD"], "Failed to read Git commit.", true).trim() || null;
255
+ return { checkoutDir, commitSha };
256
+ }
257
+ function runGit(args, message, capture = false) {
258
+ const proc = Bun.spawnSync(["git", ...args], {
259
+ stdout: capture ? "pipe" : "inherit",
260
+ stderr: "pipe"
261
+ });
262
+ if (!proc.success) {
263
+ const stderr = new TextDecoder().decode(proc.stderr).trim();
264
+ throw new Error(`${message}${stderr ? `
265
+ ${stderr}` : ""}${gitFailureHint(args, stderr)}`);
266
+ }
267
+ return capture ? new TextDecoder().decode(proc.stdout) : "";
268
+ }
269
+ function gitFailureHint(args, stderr) {
270
+ if (args[0] !== "clone")
271
+ return "";
272
+ if (!/authentication failed|HTTP Basic: Access denied|Permission denied \(publickey\)/i.test(stderr))
273
+ return "";
274
+ return [
275
+ "",
276
+ "Git authentication failed. Tools Manager uses your local git command and does not store credentials.",
277
+ "For private repositories, make sure git can clone this URL first:",
278
+ " git clone <repo-url>",
279
+ "Use a personal access token for HTTPS repositories with 2FA, or use an SSH URL after configuring your SSH key."
280
+ ].join(`
281
+ `);
282
+ }
283
+
284
+ // src/core/tools.ts
285
+ import { homedir as homedir2 } from "os";
286
+ import { join as join4 } from "path";
287
+ var toolAdapters = [
288
+ {
289
+ key: "codex",
290
+ displayName: "Codex",
291
+ detectPath: join4(homedir2(), ".codex"),
292
+ skillsDir: join4(homedir2(), ".codex", "skills"),
293
+ projectSkillsDir: ".codex/skills",
294
+ mcpKind: "codex-toml",
295
+ mcpPath: join4(homedir2(), ".codex", "config.toml")
296
+ },
297
+ {
298
+ key: "claude_code",
299
+ displayName: "Claude Code",
300
+ detectPath: join4(homedir2(), ".claude"),
301
+ skillsDir: join4(homedir2(), ".claude", "skills"),
302
+ projectSkillsDir: ".claude/skills",
303
+ mcpKind: "claude-json",
304
+ mcpPath: join4(homedir2(), ".claude", "mcp.json")
305
+ },
306
+ {
307
+ key: "cursor",
308
+ displayName: "Cursor",
309
+ detectPath: join4(homedir2(), ".cursor"),
310
+ skillsDir: join4(homedir2(), ".cursor", "skills"),
311
+ projectSkillsDir: ".cursor/skills",
312
+ mcpKind: "cursor-json",
313
+ mcpPath: join4(homedir2(), ".cursor", "mcp.json")
314
+ }
315
+ ];
316
+ function getTool(key) {
317
+ const tool = toolAdapters.find((adapter) => adapter.key === key);
318
+ if (!tool)
319
+ throw new Error(`Unknown tool: ${key}. Expected one of: ${toolAdapters.map((t) => t.key).join(", ")}`);
320
+ return tool;
321
+ }
322
+ function resolveTools(input) {
323
+ if (!input || input === "all")
324
+ return toolAdapters;
325
+ return [getTool(input)];
326
+ }
327
+ async function detectTools() {
328
+ return Promise.all(toolAdapters.map(async (tool) => ({ ...tool, installed: await pathExists(tool.detectPath) })));
329
+ }
330
+
331
+ // src/core/skill.ts
332
+ async function findSkillFile(dir) {
333
+ const canonical = join5(dir, "SKILL.md");
334
+ if (await pathExists(canonical))
335
+ return canonical;
336
+ const legacy = join5(dir, "skill.md");
337
+ if (await pathExists(legacy))
338
+ return legacy;
339
+ return null;
340
+ }
341
+ async function findSkillCandidates(root) {
342
+ const candidates = [];
343
+ await walk(root, candidates);
344
+ return candidates;
345
+ }
346
+ async function walk(dir, candidates) {
347
+ const skillFile = await findSkillFile(dir);
348
+ if (skillFile) {
349
+ candidates.push({ dir, skillFile });
350
+ return;
351
+ }
352
+ let entries;
353
+ try {
354
+ entries = await readdir2(dir, { withFileTypes: true });
355
+ } catch {
356
+ return;
357
+ }
358
+ for (const entry of entries) {
359
+ if (!entry.isDirectory() || entry.name === ".git" || entry.name === "node_modules")
360
+ continue;
361
+ await walk(join5(dir, entry.name), candidates);
362
+ }
363
+ }
364
+ async function addSkill(source) {
365
+ const skills = await addSkills(source);
366
+ return skills[0];
367
+ }
368
+ async function addSkills(source) {
369
+ await ensureDir(paths().skillsDir);
370
+ const resolved = isGitSource(source) ? await resolveGitImport(source) : await resolveLocalImport(source);
371
+ const candidates = await findSkillCandidates(resolved.root);
372
+ assertUsage(candidates.length > 0, `No skill found in ${resolved.root}. Expected SKILL.md or skill.md.`);
373
+ const imported = [];
374
+ for (const candidate of candidates)
375
+ imported.push(await importCandidate(candidate, resolved.importSource));
376
+ return imported;
377
+ }
378
+ async function addCodexSkills(skillsDir = getTool("codex").skillsDir) {
379
+ return addSkillsFromDirectory(skillsDir, "Codex");
380
+ }
381
+ async function addLocalAgentSkills(toolInput, tools = toolAdapters) {
382
+ if (toolInput === "all")
383
+ return addAllLocalAgentSkills(tools);
384
+ const tool = tools.find((adapter) => adapter.key === toolInput) || getTool(toolInput);
385
+ const skills = await addSkillsFromDirectory(tool.skillsDir, tool.displayName);
386
+ return [{ tool: tool.key, skills, skipped: false }];
387
+ }
388
+ async function addAllLocalAgentSkills(tools = toolAdapters) {
389
+ const results = [];
390
+ for (const tool of tools) {
391
+ if (!await pathExists(tool.skillsDir)) {
392
+ results.push({ tool: tool.key, skills: [], skipped: true, reason: `Skills directory does not exist: ${tool.skillsDir}` });
393
+ continue;
394
+ }
395
+ const skills = await addSkillsFromDirectory(tool.skillsDir, tool.displayName, { requireAny: false });
396
+ results.push({ tool: tool.key, skills, skipped: false });
397
+ }
398
+ assertUsage(results.some((result) => result.skills.length > 0), "No local agent skills found.");
399
+ return results;
400
+ }
401
+ async function listAgentSkills(toolInput = "all", tools = toolAdapters) {
402
+ const selected = toolInput === "all" ? tools : [tools.find((adapter) => adapter.key === toolInput) || getTool(toolInput)];
403
+ const results = [];
404
+ for (const tool of selected) {
405
+ const skills = [];
406
+ let entries;
407
+ try {
408
+ entries = await readdir2(tool.skillsDir, { withFileTypes: true });
409
+ } catch {
410
+ results.push({ tool: tool.key, path: tool.skillsDir, skills });
411
+ continue;
412
+ }
413
+ for (const entry of entries) {
414
+ if (!entry.isDirectory() && !entry.isSymbolicLink() || entry.name.startsWith("."))
415
+ continue;
416
+ const dir = join5(tool.skillsDir, entry.name);
417
+ const skillFile = await findSkillFile(dir);
418
+ if (!skillFile)
419
+ continue;
420
+ const meta = await readSkillMetadata(skillFile);
421
+ skills.push({
422
+ tool: tool.key,
423
+ name: slug(meta.name || entry.name),
424
+ description: meta.description,
425
+ path: dir
426
+ });
427
+ }
428
+ skills.sort((a, b) => a.name.localeCompare(b.name));
429
+ results.push({ tool: tool.key, path: tool.skillsDir, skills });
430
+ }
431
+ return results;
432
+ }
433
+ async function addSkillsFromDirectory(skillsDir, label, options = {}) {
434
+ const requireAny = options.requireAny ?? true;
435
+ assertUsage(await pathExists(skillsDir), `${label} skills directory does not exist: ${skillsDir}`);
436
+ const entries = await readdir2(skillsDir, { withFileTypes: true });
437
+ const imported = [];
438
+ for (const entry of entries) {
439
+ if (!entry.isDirectory() && !entry.isSymbolicLink() || entry.name.startsWith("."))
440
+ continue;
441
+ const dir = join5(skillsDir, entry.name);
442
+ if (!await findSkillFile(dir))
443
+ continue;
444
+ const existingManagedDir = await managedSymlinkTarget(dir);
445
+ const skill = existingManagedDir ? await importCandidate({ dir: existingManagedDir, skillFile: await findSkillFile(existingManagedDir) }, { sourceType: "local" }, { copy: false }) : await addSkill(dir);
446
+ await linkManagedSkillToSource(skill, dir);
447
+ imported.push(skill);
448
+ }
449
+ assertUsage(!requireAny || imported.length > 0, `No skills found in ${skillsDir}. Expected direct child directories with SKILL.md or skill.md.`);
450
+ return imported;
451
+ }
452
+ async function managedSymlinkTarget(sourceDir) {
453
+ try {
454
+ if (!(await lstat2(sourceDir)).isSymbolicLink())
455
+ return null;
456
+ const target = await realpath(sourceDir);
457
+ const managedRoot = await realpath(paths().skillsDir).catch(() => resolve(paths().skillsDir));
458
+ if (!target.startsWith(`${managedRoot}/`))
459
+ return null;
460
+ return await findSkillFile(target) ? target : null;
461
+ } catch {
462
+ return null;
463
+ }
464
+ }
465
+ async function linkManagedSkillToSource(skill, sourceDir) {
466
+ const source = resolve(sourceDir);
467
+ const target = resolve(skill.path);
468
+ if (source === target)
469
+ return;
470
+ try {
471
+ const existing = await lstat2(source);
472
+ if (existing.isSymbolicLink()) {
473
+ const linkedPath = await realpath(source);
474
+ const realTarget = await realpath(target);
475
+ if (linkedPath === realTarget)
476
+ return;
477
+ }
478
+ } catch {
479
+ return;
480
+ }
481
+ await replaceWithSymlink(target, source);
482
+ }
483
+ async function resolveLocalImport(source) {
484
+ const root = resolve(expandHome(source));
485
+ assertUsage(await pathExists(root), `Path does not exist: ${root}`);
486
+ assertUsage((await stat2(root)).isDirectory(), `Path is not a directory: ${root}`);
487
+ return { root, importSource: { sourceType: "local" } };
488
+ }
489
+ async function resolveGitImport(source) {
490
+ const parsed = parseGitSource(source);
491
+ const cloned = await cloneGitSource(parsed);
492
+ const root = parsed.subpath ? join5(cloned.checkoutDir, parsed.subpath) : cloned.checkoutDir;
493
+ assertUsage(await pathExists(root), `Git subpath does not exist: ${parsed.subpath}`);
494
+ return {
495
+ root,
496
+ importSource: {
497
+ sourceType: "git",
498
+ url: parsed.url,
499
+ ...parsed.ref ? { ref: parsed.ref } : {},
500
+ ...parsed.subpath ? { subpath: parsed.subpath } : {},
501
+ commitSha: cloned.commitSha
502
+ }
503
+ };
504
+ }
505
+ async function importCandidate(candidate, source, options = {}) {
506
+ const meta = await readSkillMetadata(candidate.skillFile);
507
+ const name = slug(meta.name || basename(candidate.dir));
508
+ const target = join5(paths().skillsDir, name);
509
+ if (options.copy !== false)
510
+ await copyDir(candidate.dir, target);
511
+ const db = await openDb();
512
+ db.query(`
513
+ INSERT INTO skills (name, description, path, source_type, source_url, source_ref, source_subpath, source_commit, updated_at)
514
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
515
+ ON CONFLICT(name) DO UPDATE SET
516
+ description = excluded.description,
517
+ path = excluded.path,
518
+ source_type = excluded.source_type,
519
+ source_url = excluded.source_url,
520
+ source_ref = excluded.source_ref,
521
+ source_subpath = excluded.source_subpath,
522
+ source_commit = excluded.source_commit,
523
+ updated_at = CURRENT_TIMESTAMP
524
+ `).run(name, meta.description, target, source.sourceType, source.url || null, source.ref || null, source.subpath || null, source.commitSha || null);
525
+ const skill = db.query("SELECT * FROM skills WHERE name = ?").get(name);
526
+ await addSkillToPreset(skill.id);
527
+ return skill;
528
+ }
529
+ async function addSkillToPreset(skillId, presetName = "Default") {
530
+ const db = await openDb();
531
+ db.query("INSERT OR IGNORE INTO presets (name) VALUES (?)").run(presetName);
532
+ const preset = db.query("SELECT id FROM presets WHERE name = ?").get(presetName);
533
+ db.query("INSERT OR IGNORE INTO preset_skills (preset_id, skill_id) VALUES (?, ?)").run(preset.id, skillId);
534
+ }
535
+ async function listSkills() {
536
+ const db = await openDb();
537
+ return db.query("SELECT * FROM skills ORDER BY name").all();
538
+ }
539
+ async function getSkill(name) {
540
+ const db = await openDb();
541
+ return db.query("SELECT * FROM skills WHERE name = ?").get(name) || null;
542
+ }
543
+ async function findSkillAgentLinks(skill, tools = toolAdapters) {
544
+ const target = await realpath(skill.path).catch(() => resolve(skill.path));
545
+ const links = [];
546
+ for (const tool of tools) {
547
+ const path = join5(tool.skillsDir, skill.name);
548
+ try {
549
+ if (!(await lstat2(path)).isSymbolicLink())
550
+ continue;
551
+ if (await realpath(path) === target)
552
+ links.push({ tool: tool.key, path });
553
+ } catch {
554
+ continue;
555
+ }
556
+ }
557
+ return links;
558
+ }
559
+ async function removeSkill(name, options = {}) {
560
+ const db = await openDb();
561
+ const skill = await getSkill(name);
562
+ if (!skill)
563
+ throw new Error(`Skill not found: ${name}`);
564
+ const agentLinks = await findSkillAgentLinks(skill, options.tools);
565
+ if (options.removeAgentLinks) {
566
+ for (const link of agentLinks)
567
+ await removePath(link.path);
568
+ }
569
+ db.query("DELETE FROM preset_skills WHERE skill_id = ?").run(skill.id);
570
+ db.query("DELETE FROM skills WHERE id = ?").run(skill.id);
571
+ await removePath(skill.path);
572
+ return { skill, agentLinks };
573
+ }
574
+ async function skillMarkdownSummary(skill) {
575
+ const file = await findSkillFile(skill.path) || join5(skill.path, "SKILL.md");
576
+ const markdown = await readFile2(file, "utf8");
577
+ return markdown.split(`
578
+ `).slice(0, 40).join(`
579
+ `);
580
+ }
581
+ async function readSkillMetadata(skillFile) {
582
+ const text = await readFile2(skillFile, "utf8");
583
+ const frontmatter = text.match(/^---\n([\s\S]*?)\n---/);
584
+ if (!frontmatter?.[1])
585
+ return { name: null, description: null };
586
+ return {
587
+ name: readYamlScalar(frontmatter[1], "name") || null,
588
+ description: readYamlScalar(frontmatter[1], "description") || null
589
+ };
590
+ }
591
+ function readYamlScalar(text, key) {
592
+ const match = text.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
593
+ return match?.[1]?.trim().replace(/^["']|["']$/g, "");
594
+ }
595
+ function slug(input) {
596
+ const value = input.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
597
+ return value || "skill";
598
+ }
599
+
600
+ // src/core/init.ts
601
+ async function initManager() {
602
+ const p = paths();
603
+ await ensureDir(p.root);
604
+ await ensureDir(p.skillsDir);
605
+ await ensureDir(p.cacheGitDir);
606
+ await ensureDir(p.logsDir);
607
+ await ensureConfig();
608
+ await openDb();
609
+ await installBuiltinSkills();
610
+ }
611
+ async function installBuiltinSkills() {
612
+ const builtinDirs = await findBuiltinSkillDirs();
613
+ for (const sourceDir of builtinDirs) {
614
+ await installBuiltinSkill(sourceDir, join6(sourceDir, "SKILL.md"));
615
+ }
616
+ }
617
+ async function findBuiltinSkillDirs() {
618
+ const here = dirname2(fileURLToPath(import.meta.url));
619
+ const candidates = [
620
+ join6(here, "..", ".."),
621
+ join6(here, "..")
622
+ ];
623
+ for (const candidate of candidates) {
624
+ if (await pathExists(join6(candidate, "SKILL.md")))
625
+ return [candidate];
626
+ }
627
+ return [];
628
+ }
629
+ async function installBuiltinSkill(sourceDir, skillFile) {
630
+ const meta = await readSkillMetadata(skillFile);
631
+ const name = slug(meta.name || sourceDir.split("/").at(-1) || "skill");
632
+ const target = join6(paths().skillsDir, name);
633
+ await ensureDir(target);
634
+ await Bun.write(join6(target, "SKILL.md"), Bun.file(skillFile));
635
+ const db = await openDb();
636
+ db.query(`
637
+ INSERT INTO skills (name, description, path, source_type, source_url, source_ref, source_subpath, source_commit, updated_at)
638
+ VALUES (?, ?, ?, 'builtin', NULL, NULL, NULL, NULL, CURRENT_TIMESTAMP)
639
+ ON CONFLICT(name) DO UPDATE SET
640
+ description = excluded.description,
641
+ path = excluded.path,
642
+ source_type = excluded.source_type,
643
+ source_url = excluded.source_url,
644
+ source_ref = excluded.source_ref,
645
+ source_subpath = excluded.source_subpath,
646
+ source_commit = excluded.source_commit,
647
+ updated_at = CURRENT_TIMESTAMP
648
+ `).run(name, meta.description, target);
649
+ const skill = db.query("SELECT id FROM skills WHERE name = ?").get(name);
650
+ await addSkillToPreset(skill.id);
651
+ }
652
+
653
+ // src/core/status.ts
654
+ async function status() {
655
+ const db = await openDb();
656
+ const skillCount = db.query("SELECT COUNT(*) AS count FROM skills").get().count;
657
+ const presetCount = db.query("SELECT COUNT(*) AS count FROM presets").get().count;
658
+ const mcpCount = db.query("SELECT COUNT(*) AS count FROM mcp_servers").get().count;
659
+ return {
660
+ root: paths().root,
661
+ skillsDir: paths().skillsDir,
662
+ dbPath: paths().dbPath,
663
+ configPath: paths().configPath,
664
+ skillCount,
665
+ presetCount,
666
+ mcpCount,
667
+ tools: await detectTools()
668
+ };
669
+ }
670
+
671
+ // src/core/preset.ts
672
+ import { tmpdir } from "os";
673
+ import { join as join7, resolve as resolve2 } from "path";
674
+ async function listPresets() {
675
+ const db = await openDb();
676
+ return db.query(`
677
+ SELECT p.*, COUNT(ps.skill_id) AS skill_count
678
+ FROM presets p
679
+ LEFT JOIN preset_skills ps ON ps.preset_id = p.id
680
+ GROUP BY p.id
681
+ ORDER BY p.name
682
+ `).all();
683
+ }
684
+ async function getPresetSkills(presetName) {
685
+ const db = await openDb();
686
+ return db.query(`
687
+ SELECT s.*
688
+ FROM skills s
689
+ JOIN preset_skills ps ON ps.skill_id = s.id
690
+ JOIN presets p ON p.id = ps.preset_id
691
+ WHERE p.name = ?
692
+ ORDER BY s.name
693
+ `).all(presetName);
694
+ }
695
+ async function movePreset(fromName, toName) {
696
+ const db = await openDb();
697
+ const from = db.query("SELECT id FROM presets WHERE name = ?").get(fromName);
698
+ if (!from)
699
+ throw new Error(`Preset not found: ${fromName}`);
700
+ db.query("INSERT OR IGNORE INTO presets (name) VALUES (?)").run(toName);
701
+ const to = db.query("SELECT id FROM presets WHERE name = ?").get(toName);
702
+ db.query(`
703
+ INSERT OR IGNORE INTO preset_skills (preset_id, skill_id)
704
+ SELECT ?, skill_id
705
+ FROM preset_skills
706
+ WHERE preset_id = ?
707
+ `).run(to.id, from.id);
708
+ db.query("DELETE FROM preset_skills WHERE preset_id = ?").run(from.id);
709
+ const skillCount = db.query("SELECT COUNT(*) AS count FROM preset_skills WHERE preset_id = ?").get(to.id).count;
710
+ return { from: fromName, to: toName, skillCount };
711
+ }
712
+ async function moveSkillPreset(skillName, fromName, toName) {
713
+ const db = await openDb();
714
+ const skill = db.query("SELECT id, name FROM skills WHERE name = ?").get(skillName);
715
+ if (!skill)
716
+ throw new Error(`Skill not found: ${skillName}`);
717
+ const from = db.query("SELECT id FROM presets WHERE name = ?").get(fromName);
718
+ if (!from)
719
+ throw new Error(`Preset not found: ${fromName}`);
720
+ const membership = db.query("SELECT 1 FROM preset_skills WHERE preset_id = ? AND skill_id = ?").get(from.id, skill.id);
721
+ if (!membership)
722
+ throw new Error(`Skill ${skillName} is not in preset ${fromName}`);
723
+ db.query("INSERT OR IGNORE INTO presets (name) VALUES (?)").run(toName);
724
+ const to = db.query("SELECT id FROM presets WHERE name = ?").get(toName);
725
+ db.query("INSERT OR IGNORE INTO preset_skills (preset_id, skill_id) VALUES (?, ?)").run(to.id, skill.id);
726
+ db.query("DELETE FROM preset_skills WHERE preset_id = ? AND skill_id = ?").run(from.id, skill.id);
727
+ return { skill: skill.name, from: fromName, to: toName };
728
+ }
729
+ async function applyPreset(presetName, toolInput, syncMode, adapters) {
730
+ const skills = await getPresetSkills(presetName);
731
+ if (skills.length === 0) {
732
+ throw new Error(`Preset not found or empty: ${presetName}`);
733
+ }
734
+ const config = await readConfig();
735
+ const tools = adapters ? resolveSelectedTools(toolInput, adapters) : resolveTools(toolInput);
736
+ const mode = syncMode || config.syncMode;
737
+ assertNoTempManagerSymlinkToRealAgents(mode, tools);
738
+ const results = [];
739
+ for (const tool of tools) {
740
+ for (const skill of skills) {
741
+ const target = join7(tool.skillsDir, skill.name);
742
+ const appliedMode = await syncDir(skill.path, target, mode);
743
+ results.push({ tool: tool.key, skill: skill.name, mode: appliedMode, target });
744
+ }
745
+ }
746
+ return results;
747
+ }
748
+ function resolveSelectedTools(toolInput, adapters) {
749
+ if (!toolInput || toolInput === "all")
750
+ return adapters;
751
+ const tool = adapters.find((adapter) => adapter.key === toolInput);
752
+ if (!tool)
753
+ throw new Error(`Unknown tool: ${toolInput}. Expected one of: ${adapters.map((adapter) => adapter.key).join(", ")}`);
754
+ return [tool];
755
+ }
756
+ function assertNoTempManagerSymlinkToRealAgents(syncMode, tools) {
757
+ if (syncMode !== "symlink")
758
+ return;
759
+ const root = resolve2(managerRoot());
760
+ const tmp = resolve2(tmpdir());
761
+ if (!root.startsWith(`${tmp}/`))
762
+ return;
763
+ const realAgentSkillDirs = new Set(toolAdapters.map((tool) => resolve2(tool.skillsDir)));
764
+ const selectedRealTools = tools.filter((tool) => realAgentSkillDirs.has(resolve2(tool.skillsDir)));
765
+ if (selectedRealTools.length === 0)
766
+ return;
767
+ throw new UsageError(`Refusing to symlink skills from temporary Tools Manager home (${root}) into real agent directories. ` + `Use a non-temporary TOOLS_MANAGER_HOME, set sync_mode = "copy", or pass test-only tool adapters.`);
768
+ }
769
+
770
+ // src/core/mcp.ts
771
+ import { dirname as dirname3 } from "path";
772
+
773
+ // src/core/json.ts
774
+ function parseJsonArray(value) {
775
+ const parsed = JSON.parse(value);
776
+ return Array.isArray(parsed) ? parsed.map(String) : [];
777
+ }
778
+ function parseJsonRecord(value) {
779
+ const parsed = JSON.parse(value);
780
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
781
+ return {};
782
+ return Object.fromEntries(Object.entries(parsed).map(([key, val]) => [key, String(val)]));
783
+ }
784
+ function stableJson(value) {
785
+ return JSON.stringify(value);
786
+ }
787
+
788
+ // src/core/mcp.ts
789
+ async function addMcpServer(input) {
790
+ const db = await openDb();
791
+ db.query(`
792
+ INSERT INTO mcp_servers (name, command, args_json, env_json, target_tools_json, enabled, updated_at)
793
+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
794
+ ON CONFLICT(name) DO UPDATE SET
795
+ command = excluded.command,
796
+ args_json = excluded.args_json,
797
+ env_json = excluded.env_json,
798
+ target_tools_json = excluded.target_tools_json,
799
+ enabled = excluded.enabled,
800
+ updated_at = CURRENT_TIMESTAMP
801
+ `).run(input.name, input.command, stableJson(input.args), stableJson(input.env), stableJson(input.targetTools), input.enabled ? 1 : 0);
802
+ }
803
+ async function listMcpServers() {
804
+ const db = await openDb();
805
+ const rows = db.query("SELECT * FROM mcp_servers ORDER BY name").all();
806
+ return rows.map(rowToServer);
807
+ }
808
+ async function getMcpServer(name) {
809
+ const db = await openDb();
810
+ const row = db.query("SELECT * FROM mcp_servers WHERE name = ?").get(name);
811
+ return row ? rowToServer(row) : null;
812
+ }
813
+ async function removeMcpServer(name) {
814
+ const db = await openDb();
815
+ const row = db.query("SELECT * FROM mcp_servers WHERE name = ?").get(name);
816
+ if (!row)
817
+ throw new Error(`MCP server not found: ${name}`);
818
+ db.query("DELETE FROM mcp_servers WHERE name = ?").run(name);
819
+ return rowToServer(row);
820
+ }
821
+ async function importMcpFromTools(toolInput) {
822
+ const tools = resolveTools(toolInput);
823
+ const results = [];
824
+ for (const tool of tools) {
825
+ const text = await readTextIfExists(tool.mcpPath);
826
+ const servers = parseToolMcp(tool, text);
827
+ for (const server of servers)
828
+ await addMcpServer(server);
829
+ results.push({ tool: tool.key, path: tool.mcpPath, count: servers.length, servers: servers.map((server) => server.name) });
830
+ }
831
+ return results;
832
+ }
833
+ async function listToolMcpServers(toolInput = "all", adapters) {
834
+ const tools = adapters || resolveTools(toolInput);
835
+ const results = [];
836
+ for (const tool of tools) {
837
+ const text = await readTextIfExists(tool.mcpPath);
838
+ const servers = parseToolMcp(tool, text).sort((a, b) => a.name.localeCompare(b.name));
839
+ results.push({ tool: tool.key, path: tool.mcpPath, servers });
840
+ }
841
+ return results;
842
+ }
843
+ async function syncMcp(toolInput) {
844
+ const servers = (await listMcpServers()).filter((server) => server.enabled);
845
+ const tools = resolveTools(toolInput);
846
+ const results = [];
847
+ for (const tool of tools) {
848
+ const selected = servers.filter((server) => server.targetTools.includes("all") || server.targetTools.includes(tool.key));
849
+ const backup = await renderToolMcp(tool, selected);
850
+ results.push({ tool: tool.key, path: tool.mcpPath, count: selected.length, backup });
851
+ }
852
+ return results;
853
+ }
854
+ function parseToolMcp(tool, text) {
855
+ if (!text?.trim())
856
+ return [];
857
+ return tool.mcpKind === "codex-toml" ? parseCodexToml(text, tool.key) : parseMcpJson(text, tool.key);
858
+ }
859
+ function rowToServer(row) {
860
+ return {
861
+ name: row.name,
862
+ command: row.command,
863
+ args: parseJsonArray(row.args_json),
864
+ env: parseJsonRecord(row.env_json),
865
+ targetTools: parseJsonArray(row.target_tools_json),
866
+ enabled: row.enabled === 1
867
+ };
868
+ }
869
+ async function renderToolMcp(tool, servers) {
870
+ await ensureDir(dirname3(tool.mcpPath));
871
+ const backup = await backupFile(tool.mcpPath);
872
+ if (tool.mcpKind === "codex-toml") {
873
+ await writeText(tool.mcpPath, renderCodexToml(await readTextIfExists(tool.mcpPath), servers));
874
+ return backup;
875
+ }
876
+ await writeText(tool.mcpPath, renderMcpJson(await readTextIfExists(tool.mcpPath), servers));
877
+ return backup;
878
+ }
879
+ function renderMcpJson(existing, servers) {
880
+ let root = {};
881
+ if (existing?.trim()) {
882
+ try {
883
+ const parsed = JSON.parse(existing);
884
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
885
+ root = parsed;
886
+ } catch {
887
+ root = {};
888
+ }
889
+ }
890
+ const mcpServers = normalizeObject(root.mcpServers);
891
+ for (const server of servers) {
892
+ mcpServers[server.name] = serverToJson(server);
893
+ }
894
+ root.mcpServers = mcpServers;
895
+ return `${JSON.stringify(root, null, 2)}
896
+ `;
897
+ }
898
+ function parseMcpJson(text, tool) {
899
+ try {
900
+ const parsed = JSON.parse(text);
901
+ const root = normalizeObject(parsed);
902
+ const mcpServers = normalizeObject(root.mcpServers);
903
+ return Object.entries(mcpServers).flatMap(([name, value]) => {
904
+ const server = normalizeObject(value);
905
+ const command = typeof server.command === "string" ? server.command : "";
906
+ if (!command)
907
+ return [];
908
+ return [{
909
+ name,
910
+ command,
911
+ args: Array.isArray(server.args) ? server.args.map(String) : [],
912
+ env: parseStringRecord(server.env),
913
+ targetTools: [tool],
914
+ enabled: true
915
+ }];
916
+ });
917
+ } catch {
918
+ return [];
919
+ }
920
+ }
921
+ function renderCodexToml(existing, servers) {
922
+ const retained = stripManagedCodexServers(existing || "", servers.map((server) => server.name)).trimEnd();
923
+ const blocks = servers.map((server) => {
924
+ const lines = [
925
+ `[mcp_servers.${tomlQuotedKey(server.name)}]`,
926
+ `command = ${tomlString(server.command)}`,
927
+ `args = [${server.args.map(tomlString).join(", ")}]`
928
+ ];
929
+ const envEntries = Object.entries(server.env);
930
+ if (envEntries.length > 0) {
931
+ lines.push("");
932
+ lines.push(`[mcp_servers.${tomlQuotedKey(server.name)}.env]`);
933
+ for (const [key, value] of envEntries) {
934
+ lines.push(`${key} = ${tomlString(value)}`);
935
+ }
936
+ }
937
+ return lines.join(`
938
+ `);
939
+ });
940
+ return [retained, ...blocks].filter(Boolean).join(`
941
+
942
+ `) + `
943
+ `;
944
+ }
945
+ function parseCodexToml(text, tool) {
946
+ const servers = new Map;
947
+ let currentName = null;
948
+ let currentEnvName = null;
949
+ for (const rawLine of text.split(/\r?\n/)) {
950
+ const line = rawLine.trim();
951
+ if (!line || line.startsWith("#"))
952
+ continue;
953
+ const header = line.match(/^\[mcp_servers\.(?:"([^"]+)"|([^\].]+))(?:\.env)?\]$/);
954
+ if (header) {
955
+ const name = header[1] || header[2] || "";
956
+ currentName = name;
957
+ currentEnvName = line.endsWith(".env]") ? name : null;
958
+ if (!servers.has(name)) {
959
+ servers.set(name, { name, command: "", args: [], env: {}, targetTools: [tool], enabled: true });
960
+ }
961
+ continue;
962
+ }
963
+ if (!currentName)
964
+ continue;
965
+ const server = servers.get(currentName);
966
+ if (!server)
967
+ continue;
968
+ const assignment = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/);
969
+ if (!assignment)
970
+ continue;
971
+ const key = assignment[1];
972
+ const value = assignment[2];
973
+ if (currentEnvName) {
974
+ server.env[key] = parseTomlString(value);
975
+ } else if (key === "command") {
976
+ server.command = parseTomlString(value);
977
+ } else if (key === "args") {
978
+ server.args = parseTomlStringArray(value);
979
+ }
980
+ }
981
+ return Array.from(servers.values()).filter((server) => server.command);
982
+ }
983
+ function stripManagedCodexServers(existing, names) {
984
+ if (names.length === 0)
985
+ return existing;
986
+ const lines = existing.split(/\r?\n/);
987
+ const output = [];
988
+ let skipping = false;
989
+ for (const line of lines) {
990
+ const anyHeader = line.match(/^\[([^\]]+)\]$/);
991
+ if (anyHeader) {
992
+ const header = line.match(/^\[mcp_servers\.(?:"([^"]+)"|([^\].]+))(?:\.env)?\]$/);
993
+ if (header) {
994
+ const name = header[1] || header[2] || "";
995
+ skipping = names.includes(name);
996
+ } else {
997
+ skipping = false;
998
+ }
999
+ }
1000
+ if (!skipping)
1001
+ output.push(line);
1002
+ }
1003
+ return output.join(`
1004
+ `);
1005
+ }
1006
+ function serverToJson(server) {
1007
+ return {
1008
+ command: server.command,
1009
+ args: server.args,
1010
+ ...Object.keys(server.env).length > 0 ? { env: server.env } : {}
1011
+ };
1012
+ }
1013
+ function normalizeObject(value) {
1014
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1015
+ }
1016
+ function parseStringRecord(value) {
1017
+ const record = normalizeObject(value);
1018
+ return Object.fromEntries(Object.entries(record).filter((entry) => typeof entry[1] === "string"));
1019
+ }
1020
+ function parseTomlString(value) {
1021
+ const trimmed = value.trim();
1022
+ if (trimmed.startsWith('"')) {
1023
+ try {
1024
+ return JSON.parse(trimmed);
1025
+ } catch {
1026
+ return trimmed.replace(/^"|"$/g, "");
1027
+ }
1028
+ }
1029
+ return trimmed;
1030
+ }
1031
+ function parseTomlStringArray(value) {
1032
+ const trimmed = value.trim();
1033
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
1034
+ return [];
1035
+ try {
1036
+ const parsed = JSON.parse(trimmed);
1037
+ return Array.isArray(parsed) ? parsed.map(String) : [];
1038
+ } catch {
1039
+ return trimmed.slice(1, -1).split(",").map((item) => parseTomlString(item)).filter(Boolean);
1040
+ }
1041
+ }
1042
+ function tomlString(value) {
1043
+ return JSON.stringify(value);
1044
+ }
1045
+ function tomlQuotedKey(value) {
1046
+ return /^[A-Za-z0-9_-]+$/.test(value) ? value : JSON.stringify(value);
1047
+ }
1048
+
1049
+ // src/core/agent.ts
1050
+ async function listAgentOverview(toolInput = "all", tools) {
1051
+ const selected = tools ? toolInput === "all" ? tools : [tools.find((tool) => tool.key === toolInput) || getTool(toolInput)] : resolveTools(toolInput);
1052
+ const skills = await listAgentSkills(toolInput, selected);
1053
+ const mcpServers = await listToolMcpServers(toolInput, selected);
1054
+ return selected.map((tool) => {
1055
+ const toolSkills = skills.find((item) => item.tool === tool.key);
1056
+ const toolMcp = mcpServers.find((item) => item.tool === tool.key);
1057
+ return {
1058
+ tool: tool.key,
1059
+ displayName: tool.displayName,
1060
+ skillsPath: toolSkills?.path || tool.skillsDir,
1061
+ mcpPath: toolMcp?.path || tool.mcpPath,
1062
+ skills: toolSkills?.skills || [],
1063
+ mcpServers: toolMcp?.servers || []
1064
+ };
1065
+ });
1066
+ }
1067
+
1068
+ // src/core/backup.ts
1069
+ async function backup() {
1070
+ const skillsDir = paths().skillsDir;
1071
+ if (!await pathExists(`${skillsDir}/.git`)) {
1072
+ runGit(["-C", skillsDir, "init"], "Failed to initialize Git repository.");
1073
+ }
1074
+ const config = await readConfig();
1075
+ if (config.gitRemote) {
1076
+ const remotes = runGit(["-C", skillsDir, "remote"], "Failed to inspect Git remotes.", true);
1077
+ if (!remotes.split(/\r?\n/).includes("origin")) {
1078
+ runGit(["-C", skillsDir, "remote", "add", "origin", config.gitRemote], "Failed to add Git remote.");
1079
+ }
1080
+ }
1081
+ runGit(["-C", skillsDir, "add", "-A"], "Failed to stage skills.");
1082
+ const status2 = runGit(["-C", skillsDir, "status", "--porcelain"], "Failed to inspect Git status.", true);
1083
+ if (!status2.trim()) {
1084
+ return { initialized: true, committed: false, pushed: false, message: "No changes to backup." };
1085
+ }
1086
+ const message = `chore: backup skills ${new Date().toISOString()}`;
1087
+ runGit(["-C", skillsDir, "commit", "-m", message], "Failed to commit skills.");
1088
+ let pushed = false;
1089
+ if (config.gitRemote) {
1090
+ runGit(["-C", skillsDir, "push", "-u", "origin", "HEAD"], "Failed to push skills.");
1091
+ pushed = true;
1092
+ }
1093
+ return { initialized: true, committed: true, pushed, message };
1094
+ }
1095
+
1096
+ // src/core/output.ts
1097
+ function printJson(value) {
1098
+ console.log(JSON.stringify(value, null, 2));
1099
+ }
1100
+ function heading(text) {
1101
+ console.log(`
1102
+ ${text}`);
1103
+ console.log("=".repeat(text.length));
1104
+ }
1105
+ function note(text) {
1106
+ console.log(text);
1107
+ }
1108
+ function success(text) {
1109
+ console.log(`OK ${text}`);
1110
+ }
1111
+ function table(rows, options = {}) {
1112
+ if (options.title)
1113
+ heading(options.title);
1114
+ if (rows.length === 0) {
1115
+ console.log(options.empty || "No rows.");
1116
+ return;
1117
+ }
1118
+ const maxWidth = options.maxWidth ?? 64;
1119
+ const columns = Object.keys(rows[0] || {});
1120
+ const widths = columns.map((column) => Math.max(displayWidth(column), ...rows.map((row) => displayWidth(formatCell(row[column], maxWidth)))));
1121
+ const border = `+-${widths.map((width) => "-".repeat(width)).join("-+-")}-+`;
1122
+ console.log(border);
1123
+ console.log(`| ${columns.map((column, index) => padCell(column, widths[index] || 0)).join(" | ")} |`);
1124
+ console.log(border);
1125
+ for (const row of rows) {
1126
+ console.log(`| ${columns.map((column, index) => padCell(formatCell(row[column], maxWidth), widths[index] || 0)).join(" | ")} |`);
1127
+ }
1128
+ console.log(border);
1129
+ }
1130
+ function formatCell(value, maxWidth) {
1131
+ const text = String(value ?? "").replace(/\s+/g, " ").trim();
1132
+ if (displayWidth(text) <= maxWidth)
1133
+ return text;
1134
+ return truncateCell(text, maxWidth);
1135
+ }
1136
+ function padCell(text, width) {
1137
+ return `${text}${" ".repeat(Math.max(0, width - displayWidth(text)))}`;
1138
+ }
1139
+ function truncateCell(text, maxWidth) {
1140
+ const suffix = "...";
1141
+ const target = Math.max(0, maxWidth - suffix.length);
1142
+ let result = "";
1143
+ let width = 0;
1144
+ for (const char of text) {
1145
+ const next = charWidth(char);
1146
+ if (width + next > target)
1147
+ break;
1148
+ result += char;
1149
+ width += next;
1150
+ }
1151
+ return `${result}${suffix}`;
1152
+ }
1153
+ function displayWidth(text) {
1154
+ let width = 0;
1155
+ for (const char of text)
1156
+ width += charWidth(char);
1157
+ return width;
1158
+ }
1159
+ function charWidth(char) {
1160
+ const code = char.codePointAt(0) || 0;
1161
+ if (code === 0)
1162
+ return 0;
1163
+ if (code < 32 || code >= 127 && code < 160)
1164
+ return 0;
1165
+ return code >= 4352 ? 2 : 1;
1166
+ }
1167
+
1168
+ // src/cli.ts
1169
+ async function main() {
1170
+ const argv = process.argv.slice(2);
1171
+ const global = { json: takeFlag(argv, "--json") };
1172
+ const command = argv.shift();
1173
+ if (command === undefined) {
1174
+ if (process.stdin.isTTY && process.stdout.isTTY) {
1175
+ await interactiveMenu(global);
1176
+ } else {
1177
+ printHelp();
1178
+ }
1179
+ return;
1180
+ }
1181
+ switch (command) {
1182
+ case "init":
1183
+ await initManager();
1184
+ success("Initialized tools manager.");
1185
+ break;
1186
+ case "status":
1187
+ await cmdStatus(global);
1188
+ break;
1189
+ case "agents":
1190
+ await cmdAgents(argv, global);
1191
+ break;
1192
+ case "skills":
1193
+ await cmdSkills(argv, global);
1194
+ break;
1195
+ case "presets":
1196
+ await cmdPresets(argv, global);
1197
+ break;
1198
+ case "mcp":
1199
+ await cmdMcp(argv, global);
1200
+ break;
1201
+ case "backup":
1202
+ await cmdBackup(global);
1203
+ break;
1204
+ case "-h":
1205
+ case "--help":
1206
+ printHelp();
1207
+ break;
1208
+ default:
1209
+ throw new UsageError(`Unknown command: ${command}`);
1210
+ }
1211
+ }
1212
+ var menuCommands = [
1213
+ { group: "System", label: "Status", description: "Paths, counts, and detected agents.", argv: ["status"] },
1214
+ { group: "Agents", label: "List agent-side contents", description: "Show skills and MCP currently in agents.", argv: ["agents", "list", "--tool", "$tool?"] },
1215
+ { group: "Skills", label: "Add skill from source", description: "Import a local path or Git URL.", argv: ["skills", "add", "$source"] },
1216
+ { group: "Skills", label: "Import skills from agent", description: "Copy agent-side skills into tm.", argv: ["skills", "add", "--tool", "$tool?"] },
1217
+ { group: "Skills", label: "List managed skills", description: "Browse skills stored in tm.", argv: ["skills", "list"] },
1218
+ { group: "Skills", label: "Show managed skill", description: "Inspect one tm skill document.", argv: ["skills", "show", "$skill"] },
1219
+ { group: "Skills", label: "Remove skill", description: "Delete managed source and agent symlinks.", argv: ["skills", "remove", "$skill"] },
1220
+ { group: "Skills", label: "Sync preset skills", description: "Apply a tm preset to agents.", argv: ["skills", "sync", "$preset?", "--tool", "$tool?", "--mode", "$mode?"] },
1221
+ { group: "Skill Presets", label: "List skill presets", description: "Show skill groups.", argv: ["presets", "list"] },
1222
+ { group: "Skill Presets", label: "Move skill to preset", description: "Move one skill between presets.", argv: ["presets", "move-skill", "$skill", "$from", "$to"] },
1223
+ { group: "Skill Presets", label: "Move whole preset", description: "Move all skills between presets.", argv: ["presets", "move", "$from", "$to"] },
1224
+ { group: "MCP", label: "Add managed MCP server", description: "Register server and target agents.", argv: ["mcp", "add", "$mcp", "--command", "$command", "--arg", "$args?", "--env", "$env?", "--tool", "$tool?"] },
1225
+ { group: "MCP", label: "Import MCP from agent", description: "Copy agent config servers into tm.", argv: ["mcp", "add", "--tool", "$tool?"] },
1226
+ { group: "MCP", label: "List managed MCP servers", description: "Browse MCP servers stored in tm.", argv: ["mcp", "list"] },
1227
+ { group: "MCP", label: "Show managed MCP server", description: "Inspect one tm MCP server.", argv: ["mcp", "show", "$mcp"] },
1228
+ { group: "MCP", label: "Remove MCP server", description: "Delete from tm; sync to update agents.", argv: ["mcp", "remove", "$mcp"] },
1229
+ { group: "MCP", label: "Sync managed MCP", description: "Write tm MCP registry to agents.", argv: ["mcp", "sync", "--tool", "$tool?"] },
1230
+ { group: "System", label: "Backup skills", description: "Commit managed skills with Git.", argv: ["backup"] },
1231
+ { group: "System", label: "Help", description: "Print command reference.", argv: ["--help"] }
1232
+ ];
1233
+ async function interactiveMenu(global) {
1234
+ while (true) {
1235
+ const selected = await selectMenu("Tools Manager", menuCommands);
1236
+ if (!selected)
1237
+ return;
1238
+ const argv = await resolveMenuArgs(selected.argv);
1239
+ if (!argv)
1240
+ continue;
1241
+ note(`
1242
+ $ tm ${argv.join(" ")}`);
1243
+ const command = argv.shift();
1244
+ if (command === "--help") {
1245
+ printHelp();
1246
+ await waitForMenuReturn();
1247
+ continue;
1248
+ }
1249
+ try {
1250
+ switch (command) {
1251
+ case "status":
1252
+ await cmdStatus(global);
1253
+ break;
1254
+ case "agents":
1255
+ await cmdAgents(argv, global);
1256
+ break;
1257
+ case "skills":
1258
+ await cmdSkills(argv, global);
1259
+ break;
1260
+ case "presets":
1261
+ await cmdPresets(argv, global);
1262
+ break;
1263
+ case "mcp":
1264
+ await cmdMcp(argv, global);
1265
+ break;
1266
+ case "backup":
1267
+ await cmdBackup(global);
1268
+ break;
1269
+ default:
1270
+ throw new UsageError(`Unknown command: ${command}`);
1271
+ }
1272
+ } catch (error) {
1273
+ resetTerminalInput();
1274
+ console.error(error instanceof Error ? error.message : String(error));
1275
+ }
1276
+ await waitForMenuReturn();
1277
+ }
1278
+ }
1279
+ async function resolveMenuArgs(argv) {
1280
+ const resolved = [];
1281
+ for (let index = 0;index < argv.length; index += 1) {
1282
+ const arg = argv[index];
1283
+ if (arg === "$skill") {
1284
+ const value = await selectExistingOrCustom("Skill", await skillNameOptions(), "Skill name: ");
1285
+ if (value === null)
1286
+ return null;
1287
+ resolved.push(value);
1288
+ } else if (arg === "$source") {
1289
+ const value = await selectExistingOrCustom("Skill Source", ["~/.codex/skills", "~/.claude/skills", "~/.cursor/skills"], "Skill source path or Git URL: ");
1290
+ if (value === null)
1291
+ return null;
1292
+ resolved.push(value);
1293
+ } else if (arg === "$mcp") {
1294
+ const value = await selectExistingOrCustom("MCP Server", await mcpNameOptions(), "MCP server name: ");
1295
+ if (value === null)
1296
+ return null;
1297
+ resolved.push(value);
1298
+ } else if (arg === "$command") {
1299
+ const value = await selectExistingOrCustom("Command", ["npx", "bunx", "node", "python", "python3"], "Command: ");
1300
+ if (value === null)
1301
+ return null;
1302
+ resolved.push(value);
1303
+ } else if (arg === "$args?") {
1304
+ const previous = resolved[resolved.length - 1];
1305
+ const value = await selectExistingOrCustom("Args", ["none", "@playwright/mcp@latest", "-y @modelcontextprotocol/server-filesystem"], "Args [optional, space separated]: ");
1306
+ if (value === null)
1307
+ return null;
1308
+ if (value && value !== "none") {
1309
+ for (const item of value.split(/\s+/).filter(Boolean)) {
1310
+ resolved.push(item);
1311
+ resolved.push("--arg");
1312
+ }
1313
+ resolved.pop();
1314
+ } else if (previous === "--arg") {
1315
+ resolved.pop();
1316
+ }
1317
+ } else if (arg === "$env?") {
1318
+ const previous = resolved[resolved.length - 1];
1319
+ const value = await selectExistingOrCustom("Env", ["none"], "Env [optional, KEY=VALUE, comma separated]: ");
1320
+ if (value === null)
1321
+ return null;
1322
+ if (value && value !== "none") {
1323
+ for (const item of value.split(",").map((part) => part.trim()).filter(Boolean)) {
1324
+ resolved.push(item);
1325
+ resolved.push("--env");
1326
+ }
1327
+ resolved.pop();
1328
+ } else if (previous === "--env") {
1329
+ resolved.pop();
1330
+ }
1331
+ } else if (arg === "$from") {
1332
+ const value = await selectExistingOrCustom("From Preset", await presetNameOptions(), "From preset: ");
1333
+ if (value === null)
1334
+ return null;
1335
+ resolved.push(value);
1336
+ } else if (arg === "$to") {
1337
+ const value = await selectExistingOrCustom("To Preset", await presetNameOptions(), "To preset: ");
1338
+ if (value === null)
1339
+ return null;
1340
+ resolved.push(value);
1341
+ } else if (arg === "$preset?") {
1342
+ const value = await selectExistingOrCustom("Preset", await presetNameOptions("Default"), "Preset [Default]: ");
1343
+ if (value === null)
1344
+ return null;
1345
+ if (value && value !== "Default")
1346
+ resolved.push(value);
1347
+ } else if (arg === "$tool?") {
1348
+ const previous = resolved[resolved.length - 1];
1349
+ if (previous === "--tool") {
1350
+ const value = await selectExistingOrCustom("Tool", ["all", "codex", "cursor", "claude_code"], "Tool [all|codex|cursor|claude_code]: ");
1351
+ if (value === null)
1352
+ return null;
1353
+ resolved.push(value);
1354
+ }
1355
+ } else if (arg === "$mode?") {
1356
+ const previous = resolved[resolved.length - 1];
1357
+ if (previous === "--mode") {
1358
+ const value = await selectExistingOrCustom("Sync Mode", ["symlink", "copy"], "Sync mode [symlink|copy]: ");
1359
+ if (value === null)
1360
+ return null;
1361
+ if (value)
1362
+ resolved.push(value);
1363
+ else
1364
+ resolved.pop();
1365
+ }
1366
+ } else {
1367
+ resolved.push(arg);
1368
+ }
1369
+ }
1370
+ return resolved.filter(Boolean);
1371
+ }
1372
+ async function cmdStatus(global) {
1373
+ const value = await status();
1374
+ if (global.json)
1375
+ return printJson(value);
1376
+ heading("Tools Manager Status");
1377
+ note(`Root: ${value.root}`);
1378
+ note(`Skills: ${value.skillCount} (${value.skillsDir})`);
1379
+ note(`Skill presets: ${value.presetCount}`);
1380
+ note(`MCP servers: ${value.mcpCount}`);
1381
+ table(value.tools.map((tool) => ({ tool: tool.key, installed: tool.installed ? "yes" : "no", skills: tool.skillsDir, mcp: tool.mcpPath })), { title: "Tools" });
1382
+ }
1383
+ async function cmdAgents(argv, global) {
1384
+ const sub = argv.shift();
1385
+ switch (sub) {
1386
+ case "list": {
1387
+ const tool = takeOption(argv, "--tool") || "all";
1388
+ assertUsage(argv.length === 0, "Usage: tm agents list [--tool <tool|all>] [--json]");
1389
+ const result = await listAgentOverview(tool);
1390
+ if (global.json)
1391
+ return printJson(result);
1392
+ printAgentOverview(result);
1393
+ break;
1394
+ }
1395
+ default:
1396
+ throw new UsageError("Usage: tm agents <list>");
1397
+ }
1398
+ }
1399
+ function printAgentOverview(result) {
1400
+ const rows = result.flatMap((agent) => {
1401
+ const skillRows = agent.skills.map((skill) => ({
1402
+ tool: agent.tool,
1403
+ type: "skill",
1404
+ name: skill.name,
1405
+ detail: skill.description || "",
1406
+ path: skill.path
1407
+ }));
1408
+ const mcpRows = agent.mcpServers.map((server) => ({
1409
+ tool: agent.tool,
1410
+ type: "mcp",
1411
+ name: server.name,
1412
+ detail: [server.command, ...server.args].join(" "),
1413
+ path: agent.mcpPath
1414
+ }));
1415
+ if (skillRows.length === 0 && mcpRows.length === 0) {
1416
+ return [{ tool: agent.tool, type: "", name: "", detail: "No skills or MCP servers found.", path: `${agent.skillsPath} | ${agent.mcpPath}` }];
1417
+ }
1418
+ return [...skillRows, ...mcpRows];
1419
+ });
1420
+ table(rows, { title: "Agent Contents", empty: "No agents found." });
1421
+ }
1422
+ async function cmdSkills(argv, global) {
1423
+ const sub = argv.shift();
1424
+ switch (sub) {
1425
+ case "add": {
1426
+ const tool = takeOption(argv, "--tool");
1427
+ if (tool) {
1428
+ assertUsage(argv.length === 0, "Usage: tm skills add --tool <tool|all>");
1429
+ const result = await addLocalAgentSkills(tool);
1430
+ if (global.json)
1431
+ return printJson(result);
1432
+ printToolSkillImportResult(result);
1433
+ break;
1434
+ }
1435
+ if (takeFlag(argv, "--all")) {
1436
+ assertUsage(argv.length === 0, "Usage: tm skills add --all");
1437
+ const result = await addAllLocalAgentSkills();
1438
+ if (global.json)
1439
+ return printJson(result);
1440
+ printToolSkillImportResult(result);
1441
+ break;
1442
+ }
1443
+ if (takeFlag(argv, "--codex")) {
1444
+ assertUsage(argv.length === 0, "Usage: tm skills add --codex");
1445
+ const skills2 = await addCodexSkills();
1446
+ if (global.json)
1447
+ return printJson(skills2);
1448
+ printSkills(skills2, "Imported Skills");
1449
+ break;
1450
+ }
1451
+ const source = argv.shift();
1452
+ assertUsage(source, "Usage: tm skills add <source>");
1453
+ const skills = await addSkills(source);
1454
+ if (global.json)
1455
+ return printJson(skills);
1456
+ if (skills.length === 1)
1457
+ success(`Added skill ${skills[0].name}`);
1458
+ else
1459
+ printSkills(skills, `Imported Skills (${skills.length})`);
1460
+ break;
1461
+ }
1462
+ case "list": {
1463
+ const tool = takeOption(argv, "--tool");
1464
+ assertUsage(argv.length === 0, "Usage: tm skills list [--tool <tool|all>] [--json]");
1465
+ if (tool) {
1466
+ const result = await listAgentSkills(tool);
1467
+ if (global.json)
1468
+ return printJson(result);
1469
+ printAgentSkills(result);
1470
+ break;
1471
+ }
1472
+ const skills = await listSkills();
1473
+ if (global.json)
1474
+ return printJson(skills);
1475
+ printSkills(skills, `Skills (${skills.length})`);
1476
+ break;
1477
+ }
1478
+ case "show": {
1479
+ const name = argv.shift();
1480
+ assertUsage(name, "Usage: tm skills show <name>");
1481
+ const skill = await getSkill(name);
1482
+ assertUsage(skill, `Skill not found: ${name}`);
1483
+ const markdown = await skillMarkdownSummary(skill);
1484
+ if (global.json)
1485
+ return printJson({ ...skill, markdown });
1486
+ heading(skill.name);
1487
+ note(`Description: ${skill.description || ""}`);
1488
+ note(`Path: ${skill.path}`);
1489
+ note(`Source: ${skill.source_type}${skill.source_url ? ` ${skill.source_url}` : ""}`);
1490
+ note("");
1491
+ note(markdown);
1492
+ break;
1493
+ }
1494
+ case "remove":
1495
+ case "rm": {
1496
+ const yes = takeFlag(argv, "--yes") || takeFlag(argv, "-y");
1497
+ const name = argv.shift();
1498
+ assertUsage(name, "Usage: tm skills remove <name> [--yes]");
1499
+ const skill = await getSkill(name);
1500
+ assertUsage(skill, `Skill not found: ${name}`);
1501
+ const agentLinks = await findSkillAgentLinks(skill);
1502
+ if (global.json) {
1503
+ const result2 = await removeSkill(name, { removeAgentLinks: true });
1504
+ return printJson(result2);
1505
+ }
1506
+ if (agentLinks.length > 0) {
1507
+ note(`Removing ${skill.name} will delete its managed source:`);
1508
+ note(` ${skill.path}`);
1509
+ note("These agent skill symlinks point to that source and will also be deleted:");
1510
+ for (const link of agentLinks)
1511
+ note(` ${link.tool}: ${link.path}`);
1512
+ } else {
1513
+ note(`Removing ${skill.name} will delete its managed source:`);
1514
+ note(` ${skill.path}`);
1515
+ }
1516
+ if (!yes) {
1517
+ const confirmed = await confirm("Continue? [y/N] ");
1518
+ if (!confirmed) {
1519
+ note("Aborted.");
1520
+ break;
1521
+ }
1522
+ }
1523
+ const result = await removeSkill(name, { removeAgentLinks: true });
1524
+ success(`Removed skill ${result.skill.name}`);
1525
+ if (result.agentLinks.length > 0)
1526
+ success(`Removed ${result.agentLinks.length} agent symlink(s).`);
1527
+ break;
1528
+ }
1529
+ case "sync": {
1530
+ const preset = argv[0]?.startsWith("--") ? "Default" : argv.shift() || "Default";
1531
+ const tool = takeOption(argv, "--tool") || "all";
1532
+ const mode = takeSyncMode(argv);
1533
+ assertUsage(argv.length === 0, "Usage: tm skills sync [preset] [--tool <tool|all>] [--mode <symlink|copy>]");
1534
+ const result = await applyPreset(preset, tool, mode);
1535
+ if (global.json)
1536
+ return printJson(result);
1537
+ table(result.map((row) => ({ tool: row.tool, skill: row.skill, mode: row.mode, target: row.target })), { title: `Synced ${result.length} Skills` });
1538
+ break;
1539
+ }
1540
+ default:
1541
+ throw new UsageError("Usage: tm skills <add|list|show|remove|sync>");
1542
+ }
1543
+ }
1544
+ function printToolSkillImportResult(result) {
1545
+ const rows = result.flatMap((tool) => tool.skills.map((skill) => ({ tool: tool.tool, name: skill.name, source: skill.source_type, description: skill.description || "" })));
1546
+ table(rows, { title: `Imported Skills (${rows.length})`, empty: "No skills imported." });
1547
+ }
1548
+ function printSkills(skills, title) {
1549
+ table(skills.map((skill) => ({ name: skill.name, source: skill.source_type, description: skill.description || "" })), { title, empty: "No skills found." });
1550
+ }
1551
+ function printAgentSkills(result) {
1552
+ const rows = result.flatMap((item) => item.skills.map((skill) => ({ tool: item.tool, name: skill.name, description: skill.description || "", path: skill.path })));
1553
+ table(rows, { title: `Agent Skills (${rows.length})`, empty: "No agent skills found." });
1554
+ }
1555
+ async function cmdPresets(argv, global) {
1556
+ const sub = argv.shift();
1557
+ switch (sub) {
1558
+ case "list": {
1559
+ const presets = await listPresets();
1560
+ if (global.json)
1561
+ return printJson(presets);
1562
+ table(presets.map((preset) => ({ name: preset.name, skills: preset.skill_count })), { title: `Skill Presets (${presets.length})` });
1563
+ break;
1564
+ }
1565
+ case "apply": {
1566
+ const preset = argv[0]?.startsWith("--") ? "Default" : argv.shift() || "Default";
1567
+ const tool = takeOption(argv, "--tool") || "all";
1568
+ const mode = takeSyncMode(argv);
1569
+ assertUsage(argv.length === 0, "Usage: tm presets apply [preset] [--tool <tool|all>] [--mode <symlink|copy>]");
1570
+ const result = await applyPreset(preset, tool, mode);
1571
+ if (global.json)
1572
+ return printJson(result);
1573
+ table(result.map((row) => ({ tool: row.tool, skill: row.skill, mode: row.mode, target: row.target })), { title: `Applied ${result.length} Skills` });
1574
+ break;
1575
+ }
1576
+ case "move":
1577
+ case "mv": {
1578
+ const from = argv.shift();
1579
+ const to = argv.shift();
1580
+ assertUsage(from && to, "Usage: tm presets move <from> <to>");
1581
+ const result = await movePreset(from, to);
1582
+ if (global.json)
1583
+ return printJson(result);
1584
+ success(`Moved ${result.skillCount} skills from ${result.from} to ${result.to}.`);
1585
+ break;
1586
+ }
1587
+ case "move-skill": {
1588
+ const skill = argv.shift();
1589
+ const from = argv.shift();
1590
+ const to = argv.shift();
1591
+ assertUsage(skill && from && to, "Usage: tm presets move-skill <skill> <from> <to>");
1592
+ const result = await moveSkillPreset(skill, from, to);
1593
+ if (global.json)
1594
+ return printJson(result);
1595
+ success(`Moved ${result.skill} from ${result.from} to ${result.to}.`);
1596
+ break;
1597
+ }
1598
+ default:
1599
+ throw new UsageError("Usage: tm presets <list|apply|move|move-skill>");
1600
+ }
1601
+ }
1602
+ async function cmdMcp(argv, global) {
1603
+ const sub = argv.shift();
1604
+ switch (sub) {
1605
+ case "add": {
1606
+ const importTool = takeOption(argv, "--tool");
1607
+ if (importTool && argv.length === 0) {
1608
+ const result = await importMcpFromTools(importTool);
1609
+ if (global.json)
1610
+ return printJson(result);
1611
+ table(result.map((row) => ({ tool: row.tool, servers: row.count, path: row.path, names: row.servers.join(",") })), { title: "Imported MCP Servers" });
1612
+ break;
1613
+ }
1614
+ const name = argv.shift();
1615
+ assertUsage(name, "Usage: tm mcp add <name> --command <cmd> [--arg value] [--env K=V] [--tool <tool|all>]");
1616
+ const command = takeOption(argv, "--command");
1617
+ assertUsage(command, "Missing required --command.");
1618
+ const args = takeRepeatedOption(argv, "--arg");
1619
+ const env = Object.assign({}, ...takeRepeatedOption(argv, "--env").map(parseKeyValue));
1620
+ const targetTools = importTool ? [importTool, ...takeRepeatedOption(argv, "--tool")] : takeRepeatedOption(argv, "--tool");
1621
+ await addMcpServer({ name, command, args, env, targetTools: targetTools.length ? targetTools : ["all"], enabled: true });
1622
+ success(`Added MCP server ${name}`);
1623
+ break;
1624
+ }
1625
+ case "list": {
1626
+ const tool = takeOption(argv, "--tool");
1627
+ assertUsage(argv.length === 0, "Usage: tm mcp list [--tool <tool|all>] [--json]");
1628
+ if (tool) {
1629
+ const result = await listToolMcpServers(tool);
1630
+ if (global.json)
1631
+ return printJson(result);
1632
+ printToolMcpServers(result);
1633
+ break;
1634
+ }
1635
+ const servers = await listMcpServers();
1636
+ if (global.json)
1637
+ return printJson(servers);
1638
+ table(servers.map((server) => ({ name: server.name, command: server.command, tools: server.targetTools.join(","), enabled: server.enabled ? "yes" : "no" })), { title: `MCP Servers (${servers.length})` });
1639
+ break;
1640
+ }
1641
+ case "show": {
1642
+ const name = argv.shift();
1643
+ assertUsage(name, "Usage: tm mcp show <name>");
1644
+ const server = await getMcpServer(name);
1645
+ assertUsage(server, `MCP server not found: ${name}`);
1646
+ if (global.json)
1647
+ return printJson(server);
1648
+ heading(server.name);
1649
+ note(`Command: ${server.command}`);
1650
+ note(`Args: ${server.args.join(" ")}`);
1651
+ note(`Tools: ${server.targetTools.join(",")}`);
1652
+ note(`Enabled: ${server.enabled ? "yes" : "no"}`);
1653
+ if (Object.keys(server.env).length > 0) {
1654
+ note("Env:");
1655
+ for (const [key, value] of Object.entries(server.env))
1656
+ note(` ${key}=${value}`);
1657
+ }
1658
+ break;
1659
+ }
1660
+ case "sync": {
1661
+ const tool = takeOption(argv, "--tool") || "all";
1662
+ const result = await syncMcp(tool);
1663
+ if (global.json)
1664
+ return printJson(result);
1665
+ table(result.map((row) => ({ tool: row.tool, servers: row.count, path: row.path, backup: row.backup || "" })), { title: "MCP Sync" });
1666
+ break;
1667
+ }
1668
+ case "remove":
1669
+ case "rm": {
1670
+ const name = argv.shift();
1671
+ assertUsage(name, "Usage: tm mcp remove <name>");
1672
+ const server = await removeMcpServer(name);
1673
+ if (global.json)
1674
+ return printJson(server);
1675
+ success(`Removed MCP server ${server.name}`);
1676
+ break;
1677
+ }
1678
+ default:
1679
+ throw new UsageError("Usage: tm mcp <add|list|show|sync|remove>");
1680
+ }
1681
+ }
1682
+ function printToolMcpServers(result) {
1683
+ const rows = result.flatMap((item) => item.servers.map((server) => ({ tool: item.tool, name: server.name, command: [server.command, ...server.args].join(" "), env: Object.keys(server.env).length, path: item.path })));
1684
+ table(rows, { title: `Agent MCP Servers (${rows.length})`, empty: "No agent MCP servers found." });
1685
+ }
1686
+ async function cmdBackup(global) {
1687
+ const result = await backup();
1688
+ if (global.json)
1689
+ return printJson(result);
1690
+ success(result.message);
1691
+ if (result.pushed)
1692
+ success("Pushed to origin.");
1693
+ }
1694
+ function parseKeyValue(value) {
1695
+ const index = value.indexOf("=");
1696
+ assertUsage(index > 0, `Expected KEY=VALUE, got: ${value}`);
1697
+ return { [value.slice(0, index)]: value.slice(index + 1) };
1698
+ }
1699
+ function takeFlag(argv, flag) {
1700
+ const index = argv.indexOf(flag);
1701
+ if (index === -1)
1702
+ return false;
1703
+ argv.splice(index, 1);
1704
+ return true;
1705
+ }
1706
+ function takeOption(argv, flag) {
1707
+ const index = argv.indexOf(flag);
1708
+ if (index === -1)
1709
+ return;
1710
+ const value = argv[index + 1];
1711
+ assertUsage(value && !value.startsWith("--"), `Missing value for ${flag}.`);
1712
+ argv.splice(index, 2);
1713
+ return value;
1714
+ }
1715
+ function takeSyncMode(argv) {
1716
+ const mode = takeOption(argv, "--mode");
1717
+ if (!mode)
1718
+ return;
1719
+ assertUsage(mode === "symlink" || mode === "copy", "Expected --mode to be one of: symlink, copy.");
1720
+ return mode;
1721
+ }
1722
+ function takeRepeatedOption(argv, flag) {
1723
+ const values = [];
1724
+ let value;
1725
+ while (value = takeOption(argv, flag))
1726
+ values.push(value);
1727
+ return values;
1728
+ }
1729
+ async function selectMenu(title, commands) {
1730
+ let selected = 0;
1731
+ process.stdin.setRawMode(true);
1732
+ process.stdin.resume();
1733
+ process.stdin.setEncoding("utf8");
1734
+ const render = () => {
1735
+ process.stdout.write("\x1B[2J\x1B[H");
1736
+ process.stdout.write(`${ansi.dim("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E")}
1737
+ `);
1738
+ process.stdout.write(`${ansi.dim("\u2502")} ${ansi.signal(title.padEnd(76))} ${ansi.dim("\u2502")}
1739
+ `);
1740
+ process.stdout.write(`${ansi.dim("\u2502")} ${ansi.muted("One local control plane for skills, presets, and MCP servers.".padEnd(76))} ${ansi.dim("\u2502")}
1741
+ `);
1742
+ process.stdout.write(`${ansi.dim("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F")}
1743
+
1744
+ `);
1745
+ process.stdout.write(`${ansi.muted("\u2191/\u2193 choose")} ${ansi.muted("Enter run")} ${ansi.muted("Esc/q quit")}
1746
+
1747
+ `);
1748
+ let group = "";
1749
+ commands.forEach((command, index) => {
1750
+ const active = index === selected;
1751
+ if (command.group !== group) {
1752
+ group = command.group;
1753
+ process.stdout.write(`
1754
+ ${ansi.group(group)}
1755
+ `);
1756
+ }
1757
+ const line = ` ${command.label.padEnd(24)} ${command.description.padEnd(44)} ${commandText(command.argv)}`;
1758
+ process.stdout.write(active ? `${ansi.selected(`\u203A${line}`)}
1759
+ ` : `${ansi.dim(" ")}${line}
1760
+ `);
1761
+ });
1762
+ };
1763
+ render();
1764
+ return new Promise((resolve3) => {
1765
+ const cleanup = () => {
1766
+ process.stdin.setRawMode(false);
1767
+ process.stdin.pause();
1768
+ process.stdin.off("data", onData);
1769
+ process.stdout.write(`
1770
+ `);
1771
+ };
1772
+ const onData = (chunk) => {
1773
+ if (chunk === "\x03" || chunk === "\x1B" || chunk.toLowerCase() === "q") {
1774
+ cleanup();
1775
+ resolve3(null);
1776
+ return;
1777
+ }
1778
+ if (chunk === "\r" || chunk === `
1779
+ `) {
1780
+ const command = commands[selected] || null;
1781
+ cleanup();
1782
+ resolve3(command);
1783
+ return;
1784
+ }
1785
+ if (chunk === "\x1B[A") {
1786
+ selected = (selected - 1 + commands.length) % commands.length;
1787
+ render();
1788
+ } else if (chunk === "\x1B[B") {
1789
+ selected = (selected + 1) % commands.length;
1790
+ render();
1791
+ }
1792
+ };
1793
+ process.stdin.on("data", onData);
1794
+ });
1795
+ }
1796
+ var ansi = {
1797
+ signal: (value) => `\x1B[38;5;191m${value}\x1B[0m`,
1798
+ muted: (value) => `\x1B[38;5;244m${value}\x1B[0m`,
1799
+ dim: (value) => `\x1B[2m${value}\x1B[0m`,
1800
+ group: (value) => `\x1B[38;5;214m${value.toUpperCase()}\x1B[0m`,
1801
+ selected: (value) => `\x1B[38;5;16m\x1B[48;5;191m${value}\x1B[0m`
1802
+ };
1803
+ function commandText(argv) {
1804
+ return ansi.muted(`tm ${argv.filter((arg) => !arg.startsWith("$")).join(" ")}`);
1805
+ }
1806
+ async function selectValue(title, values, options = {}) {
1807
+ const customLabel = "<custom>";
1808
+ const choices = options.custom ? [...values, customLabel] : values;
1809
+ let selected = 0;
1810
+ process.stdin.setRawMode(true);
1811
+ process.stdin.resume();
1812
+ process.stdin.setEncoding("utf8");
1813
+ const render = () => {
1814
+ process.stdout.write("\x1B[2J\x1B[H");
1815
+ process.stdout.write(`${ansi.signal(title)}
1816
+ `);
1817
+ process.stdout.write(`${ansi.muted("\u2191/\u2193 choose")} ${ansi.muted("Enter select")} ${ansi.muted("Esc return")}
1818
+
1819
+ `);
1820
+ choices.forEach((value, index) => {
1821
+ const active = index === selected;
1822
+ const description = value === customLabel ? "custom input" : options.descriptions?.[value] || "";
1823
+ const line = ` ${value.padEnd(18)} ${description}`;
1824
+ process.stdout.write(active ? `${ansi.selected(`\u203A${line}`)}
1825
+ ` : `${ansi.dim(" ")}${line}
1826
+ `);
1827
+ });
1828
+ };
1829
+ render();
1830
+ return new Promise((resolve3) => {
1831
+ const cleanup = () => {
1832
+ process.stdin.setRawMode(false);
1833
+ process.stdin.pause();
1834
+ process.stdin.off("data", onData);
1835
+ process.stdout.write(`
1836
+ `);
1837
+ };
1838
+ const onData = (chunk) => {
1839
+ if (chunk === "\x03" || chunk === "\x1B") {
1840
+ cleanup();
1841
+ resolve3(null);
1842
+ return;
1843
+ }
1844
+ if (chunk === "\r" || chunk === `
1845
+ `) {
1846
+ const value = choices[selected] || null;
1847
+ cleanup();
1848
+ resolve3(value === customLabel ? "__custom__" : value);
1849
+ return;
1850
+ }
1851
+ if (chunk === "\x1B[A") {
1852
+ selected = (selected - 1 + choices.length) % choices.length;
1853
+ render();
1854
+ } else if (chunk === "\x1B[B") {
1855
+ selected = (selected + 1) % choices.length;
1856
+ render();
1857
+ }
1858
+ };
1859
+ process.stdin.on("data", onData);
1860
+ });
1861
+ }
1862
+ async function selectExistingOrCustom(title, values, prompt) {
1863
+ const uniqueValues = [...new Set(values.filter(Boolean))];
1864
+ const selected = await selectValue(title, uniqueValues, { custom: true });
1865
+ if (selected === null)
1866
+ return null;
1867
+ if (selected === "__custom__")
1868
+ return promptText(prompt);
1869
+ return selected;
1870
+ }
1871
+ async function skillNameOptions() {
1872
+ try {
1873
+ return (await listSkills()).map((skill) => skill.name);
1874
+ } catch {
1875
+ return [];
1876
+ }
1877
+ }
1878
+ async function presetNameOptions(fallback) {
1879
+ try {
1880
+ const names = (await listPresets()).map((preset) => preset.name);
1881
+ return fallback && !names.includes(fallback) ? [fallback, ...names] : names;
1882
+ } catch {
1883
+ return fallback ? [fallback] : [];
1884
+ }
1885
+ }
1886
+ async function mcpNameOptions() {
1887
+ try {
1888
+ return (await listMcpServers()).map((server) => server.name);
1889
+ } catch {
1890
+ return [];
1891
+ }
1892
+ }
1893
+ async function promptText(prompt) {
1894
+ if (process.stdin.isTTY)
1895
+ process.stdin.setRawMode(true);
1896
+ process.stdin.resume();
1897
+ process.stdout.write(`${prompt}${ansi.muted("(Esc to return) ")}`);
1898
+ return new Promise((resolve3) => {
1899
+ let value = "";
1900
+ const cleanup = () => {
1901
+ process.stdin.off("data", onData);
1902
+ resetTerminalInput();
1903
+ process.stdout.write(`
1904
+ `);
1905
+ };
1906
+ const onData = (data) => {
1907
+ const chunk = String(data);
1908
+ if (chunk === "\x03" || chunk === "\x1B") {
1909
+ cleanup();
1910
+ resolve3(null);
1911
+ return;
1912
+ }
1913
+ if (chunk === "\r" || chunk === `
1914
+ `) {
1915
+ cleanup();
1916
+ resolve3(value.trim());
1917
+ return;
1918
+ }
1919
+ if (chunk === "\x7F") {
1920
+ if (value.length > 0) {
1921
+ value = value.slice(0, -1);
1922
+ process.stdout.write("\b \b");
1923
+ }
1924
+ return;
1925
+ }
1926
+ if (chunk >= " ") {
1927
+ value += chunk;
1928
+ process.stdout.write(chunk);
1929
+ }
1930
+ };
1931
+ process.stdin.on("data", onData);
1932
+ });
1933
+ }
1934
+ async function waitForMenuReturn() {
1935
+ if (!process.stdin.isTTY)
1936
+ return;
1937
+ process.stdin.setRawMode(true);
1938
+ process.stdin.resume();
1939
+ process.stdout.write(`
1940
+ ${ansi.muted("Press Enter or Esc to return to menu.")}`);
1941
+ await new Promise((resolve3) => {
1942
+ const cleanup = () => {
1943
+ process.stdin.off("data", onData);
1944
+ resetTerminalInput();
1945
+ process.stdout.write(`
1946
+ `);
1947
+ };
1948
+ const onData = (data) => {
1949
+ const chunk = String(data);
1950
+ if (chunk === "\r" || chunk === `
1951
+ ` || chunk === "\x1B" || chunk === "\x03") {
1952
+ cleanup();
1953
+ resolve3();
1954
+ }
1955
+ };
1956
+ process.stdin.on("data", onData);
1957
+ });
1958
+ }
1959
+ function resetTerminalInput() {
1960
+ if (process.stdin.isTTY)
1961
+ process.stdin.setRawMode(false);
1962
+ process.stdin.pause();
1963
+ }
1964
+ async function confirm(prompt) {
1965
+ process.stderr.write(prompt);
1966
+ const line = await new Promise((resolve3) => {
1967
+ process.stdin.once("data", (data) => resolve3(String(data)));
1968
+ });
1969
+ return ["y", "yes"].includes(line.trim().toLowerCase());
1970
+ }
1971
+ function printHelp() {
1972
+ console.log(`Usage:
1973
+ tm init
1974
+ tm status [--json]
1975
+ tm agents list [--tool <tool|all>] [--json]
1976
+ tm skills add <source>
1977
+ tm skills add --tool <tool|all>
1978
+ tm skills add --codex
1979
+ tm skills add --all
1980
+ tm skills list [--json]
1981
+ tm skills list --tool <tool|all> [--json]
1982
+ tm skills show <name> [--json]
1983
+ tm skills remove <name>
1984
+ tm skills sync [preset] [--tool <tool|all>] [--mode <symlink|copy>]
1985
+ tm presets list [--json]
1986
+ tm presets apply [preset] [--tool <tool|all>] [--mode <symlink|copy>]
1987
+ tm presets move-skill <skill> <from> <to>
1988
+ tm presets move <from> <to>
1989
+ tm mcp add <name> --command <cmd> [--arg value] [--env K=V] [--tool <tool|all>]
1990
+ tm mcp add --tool <tool|all>
1991
+ tm mcp list [--json]
1992
+ tm mcp list --tool <tool|all> [--json]
1993
+ tm mcp show <name> [--json]
1994
+ tm mcp sync [--tool <tool|all>]
1995
+ tm mcp remove <name>
1996
+ tm backup`);
1997
+ }
1998
+ main().catch((error) => {
1999
+ resetTerminalInput();
2000
+ if (error instanceof UsageError) {
2001
+ console.error(error.message);
2002
+ process.exitCode = 2;
2003
+ return;
2004
+ }
2005
+ console.error(error instanceof Error ? error.message : String(error));
2006
+ process.exitCode = 1;
2007
+ }).finally(() => closeDb());