td-ai-tools 1.0.7 → 1.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.
Files changed (3) hide show
  1. package/README.md +5 -0
  2. package/bin/cli.js +275 -51
  3. package/package.json +7 -2
package/README.md CHANGED
@@ -36,6 +36,9 @@ npx td-ai-tools install pr-solver horizon-component-library
36
36
  npx td-ai-tools update
37
37
  npx td-ai-tools update --all
38
38
  npx td-ai-tools update pr-solver
39
+ npx td-ai-tools delete
40
+ npx td-ai-tools delete --all
41
+ npx td-ai-tools delete pr-solver
39
42
  ```
40
43
 
41
44
  The installer copies requested items into both agent layouts:
@@ -46,6 +49,8 @@ This keeps the installed assets available to both Claude-style and `.agents`-sty
46
49
 
47
50
  `install` now errors when the target item already exists. Use `update` to replace an existing installed skill or agent pack.
48
51
 
52
+ `delete` removes installed items from both `.claude/` and `.agents/` target directories, and works on any installed skill or agent pack regardless of whether it is in the catalogue.
53
+
49
54
  ## Local Verification
50
55
  Run the local smoke test to package the repo and verify installation into a throwaway project:
51
56
 
package/bin/cli.js CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import * as p from '@clack/prompts';
6
+ import pc from 'picocolors';
3
7
 
4
- const fs = require('fs');
5
- const path = require('path');
6
- const readline = require('readline');
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
7
10
 
8
11
  const TOOLKIT_ROOT = path.resolve(__dirname, '..');
9
12
  const SKILLS_DIR = path.join(TOOLKIT_ROOT, 'skills');
@@ -14,6 +17,22 @@ const INSTALL_TARGETS = [
14
17
  { label: 'agents', root: '.agents' }
15
18
  ];
16
19
 
20
+ const IS_TTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
21
+
22
+ function status(kind, msg) {
23
+ if (IS_TTY) {
24
+ if (kind === 'success') p.log.success(msg);
25
+ else if (kind === 'step') p.log.step(msg);
26
+ else if (kind === 'warn') p.log.warn(msg);
27
+ else if (kind === 'error') p.log.error(msg);
28
+ else p.log.info(msg);
29
+ } else {
30
+ const stream = (kind === 'error' || kind === 'warn') ? console.error : console.log;
31
+ const prefix = kind === 'error' ? ' [error] ' : kind === 'warn' ? ' [warn] ' : ' ';
32
+ stream(prefix + msg);
33
+ }
34
+ }
35
+
17
36
  function copyDir(src, dest) {
18
37
  const srcStat = fs.statSync(src);
19
38
  fs.mkdirSync(dest, { recursive: true, mode: srcStat.mode });
@@ -47,13 +66,13 @@ function readFrontmatterField(mdPath, field) {
47
66
  function installSkill(name, { replaceExisting = false } = {}) {
48
67
  const src = path.join(SKILLS_DIR, name);
49
68
  if (!fs.existsSync(src)) {
50
- console.error(` [error] Skill "${name}" not found.`);
69
+ status('error', `Skill "${name}" not found.`);
51
70
  return false;
52
71
  }
53
72
  for (const target of INSTALL_TARGETS) {
54
73
  const dest = path.join(TARGET_ROOT, target.root, 'skills', name);
55
74
  if (fs.existsSync(dest) && !replaceExisting) {
56
- console.error(` [error] Skill "${name}" already exists at ${target.root}/skills/${name}/ (use "update" to replace).`);
75
+ status('warn', `Skill "${name}" already exists at ${target.root}/skills/${name}/ (use "update" to replace).`);
57
76
  return false;
58
77
  }
59
78
  if (fs.existsSync(dest) && replaceExisting) {
@@ -61,7 +80,24 @@ function installSkill(name, { replaceExisting = false } = {}) {
61
80
  }
62
81
  copyDir(src, dest);
63
82
  const action = replaceExisting ? 'updated' : 'installed';
64
- console.log(` [skill] ${name} ${action} -> ${target.root}/skills/${name}/`);
83
+ status('success', `skill: ${name} ${action} ${target.root}/skills/${name}/`);
84
+ }
85
+ return true;
86
+ }
87
+
88
+ function deleteSkill(name) {
89
+ let deletedAny = false;
90
+ for (const target of INSTALL_TARGETS) {
91
+ const dest = path.join(TARGET_ROOT, target.root, 'skills', name);
92
+ if (fs.existsSync(dest)) {
93
+ fs.rmSync(dest, { recursive: true, force: true });
94
+ status('success', `skill: ${name} deleted from ${target.root}/skills/${name}/`);
95
+ deletedAny = true;
96
+ }
97
+ }
98
+ if (!deletedAny) {
99
+ status('error', `Skill "${name}" is not installed.`);
100
+ return false;
65
101
  }
66
102
  return true;
67
103
  }
@@ -69,13 +105,13 @@ function installSkill(name, { replaceExisting = false } = {}) {
69
105
  function installAgent(name, { replaceExisting = false } = {}) {
70
106
  const src = path.join(AGENTS_DIR, name);
71
107
  if (!fs.existsSync(src)) {
72
- console.error(` [error] Agent pack "${name}" not found.`);
108
+ status('error', `Agent pack "${name}" not found.`);
73
109
  return false;
74
110
  }
75
111
  for (const target of INSTALL_TARGETS) {
76
112
  const dest = path.join(TARGET_ROOT, target.root, 'agents', name);
77
113
  if (fs.existsSync(dest) && !replaceExisting) {
78
- console.error(` [error] Agent pack "${name}" already exists at ${target.root}/agents/${name}/ (use "update" to replace).`);
114
+ status('warn', `Agent pack "${name}" already exists at ${target.root}/agents/${name}/ (use "update" to replace).`);
79
115
  return false;
80
116
  }
81
117
  if (fs.existsSync(dest) && replaceExisting) {
@@ -83,12 +119,54 @@ function installAgent(name, { replaceExisting = false } = {}) {
83
119
  }
84
120
  copyDir(src, dest);
85
121
  const action = replaceExisting ? 'updated' : 'installed';
86
- console.log(` [agent] ${name} ${action} -> ${target.root}/agents/${name}/`);
122
+ status('success', `agent: ${name} ${action} ${target.root}/agents/${name}/`);
87
123
  }
88
124
  return true;
89
125
  }
90
126
 
91
- function printList() {
127
+ function deleteAgent(name) {
128
+ let deletedAny = false;
129
+ for (const target of INSTALL_TARGETS) {
130
+ const dest = path.join(TARGET_ROOT, target.root, 'agents', name);
131
+ if (fs.existsSync(dest)) {
132
+ fs.rmSync(dest, { recursive: true, force: true });
133
+ status('success', `agent: ${name} deleted from ${target.root}/agents/${name}/`);
134
+ deletedAny = true;
135
+ }
136
+ }
137
+ if (!deletedAny) {
138
+ status('error', `Agent pack "${name}" is not installed.`);
139
+ return false;
140
+ }
141
+ return true;
142
+ }
143
+
144
+ function getInstalled(type) {
145
+ const installed = new Set();
146
+ for (const target of INSTALL_TARGETS) {
147
+ const dir = path.join(TARGET_ROOT, target.root, type);
148
+ if (fs.existsSync(dir)) {
149
+ for (const entry of fs.readdirSync(dir)) {
150
+ if (fs.statSync(path.join(dir, entry)).isDirectory()) {
151
+ installed.add(entry);
152
+ }
153
+ }
154
+ }
155
+ }
156
+ return [...installed].sort();
157
+ }
158
+
159
+ function getSkillHint(name) {
160
+ const desc = readFrontmatterField(path.join(SKILLS_DIR, name, 'SKILL.md'), 'description');
161
+ return desc.length > 72 ? desc.slice(0, 72) + '…' : desc;
162
+ }
163
+
164
+ function getAgentHint(name) {
165
+ const desc = readFrontmatterField(path.join(AGENTS_DIR, name, 'AGENTS.md'), 'description');
166
+ return desc.length > 72 ? desc.slice(0, 72) + '…' : desc;
167
+ }
168
+
169
+ function printListPlain() {
92
170
  const skills = getAvailable(SKILLS_DIR);
93
171
  const agents = getAvailable(AGENTS_DIR);
94
172
 
@@ -107,12 +185,70 @@ function printList() {
107
185
  console.log('');
108
186
  }
109
187
 
188
+ function printList() {
189
+ if (!IS_TTY) {
190
+ printListPlain();
191
+ return;
192
+ }
193
+
194
+ const skills = getAvailable(SKILLS_DIR);
195
+ const agents = getAvailable(AGENTS_DIR);
196
+
197
+ p.intro(pc.bgCyan(pc.black(' AgentToolkit ')));
198
+
199
+ const skillsBody = skills.length
200
+ ? skills.map(name => {
201
+ const hint = getSkillHint(name);
202
+ return hint ? `${pc.bold(name)}\n ${pc.dim(hint)}` : pc.bold(name);
203
+ }).join('\n')
204
+ : pc.dim('(none)');
205
+ p.note(skillsBody, `Skills (${skills.length})`);
206
+
207
+ const agentsBody = agents.length
208
+ ? agents.map(name => {
209
+ const hint = getAgentHint(name);
210
+ return hint ? `${pc.bold(name)}\n ${pc.dim(hint)}` : pc.bold(name);
211
+ }).join('\n')
212
+ : pc.dim('(none)');
213
+ p.note(agentsBody, `Agent Packs (${agents.length})`);
214
+
215
+ p.outro(pc.dim(`${skills.length} skills, ${agents.length} agent packs available.`));
216
+ }
217
+
110
218
  function buildMenu() {
111
219
  const skills = getAvailable(SKILLS_DIR).map(name => ({ type: 'skill', name }));
112
220
  const agents = getAvailable(AGENTS_DIR).map(name => ({ type: 'agent', name }));
113
221
  return [...skills, ...agents];
114
222
  }
115
223
 
224
+ function buildDeleteMenu() {
225
+ const skills = getInstalled('skills').map(name => ({ type: 'skill', name }));
226
+ const agents = getInstalled('agents').map(name => ({ type: 'agent', name }));
227
+ return [...skills, ...agents];
228
+ }
229
+
230
+ function toGroupOptions(menu, { hintForInstalled = false } = {}) {
231
+ const skillItems = menu.filter(i => i.type === 'skill').map(i => {
232
+ const hint = hintForInstalled ? '' : getSkillHint(i.name);
233
+ return { value: `skill:${i.name}`, label: i.name, hint: hint || undefined };
234
+ });
235
+ const agentItems = menu.filter(i => i.type === 'agent').map(i => {
236
+ const hint = hintForInstalled ? '' : getAgentHint(i.name);
237
+ return { value: `agent:${i.name}`, label: i.name, hint: hint || undefined };
238
+ });
239
+ const out = {};
240
+ if (skillItems.length) out['Skills'] = skillItems;
241
+ if (agentItems.length) out['Agent Packs'] = agentItems;
242
+ return out;
243
+ }
244
+
245
+ function fromGroupValues(values) {
246
+ return values.map(v => {
247
+ const idx = v.indexOf(':');
248
+ return { type: v.slice(0, idx), name: v.slice(idx + 1) };
249
+ });
250
+ }
251
+
116
252
  function installItems(items, options = {}) {
117
253
  for (const item of items) {
118
254
  if (item.type === 'skill') installSkill(item.name, options);
@@ -120,37 +256,88 @@ function installItems(items, options = {}) {
120
256
  }
121
257
  }
122
258
 
259
+ function deleteItems(items) {
260
+ for (const item of items) {
261
+ if (item.type === 'skill') deleteSkill(item.name);
262
+ else deleteAgent(item.name);
263
+ }
264
+ }
265
+
123
266
  async function interactiveInstall(mode = 'install') {
124
267
  const menu = buildMenu();
125
268
  if (menu.length === 0) {
126
- console.log('No skills or agent packs available.');
269
+ status('info', 'No skills or agent packs available.');
127
270
  return;
128
271
  }
272
+ if (!IS_TTY) {
273
+ status('error', `interactive ${mode} requires a TTY. Use "${mode} --all" or "${mode} <name>...".`);
274
+ process.exit(1);
275
+ }
276
+
277
+ p.intro(pc.bgCyan(pc.black(' AgentToolkit ')));
129
278
 
130
- console.log('\nAvailable (enter numbers separated by spaces, or "all"):\n');
131
- menu.forEach((item, i) => {
132
- const label = item.type === 'skill' ? 'skill' : 'agent';
133
- console.log(` [${i + 1}] ${label}: ${item.name}`);
279
+ const selection = await p.groupMultiselect({
280
+ message: `Select items to ${mode}`,
281
+ options: toGroupOptions(menu),
282
+ required: false,
134
283
  });
135
- console.log('');
136
284
 
137
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
138
- return new Promise(resolve => {
139
- rl.question(`Select items to ${mode}: `, answer => {
140
- rl.close();
141
- const trimmed = answer.trim().toLowerCase();
142
- let selected;
143
- if (trimmed === 'all') {
144
- selected = menu;
145
- } else {
146
- const indices = trimmed.split(/\s+/).map(n => parseInt(n, 10) - 1);
147
- selected = indices.filter(i => i >= 0 && i < menu.length).map(i => menu[i]);
148
- }
149
- console.log('');
150
- installItems(selected, { replaceExisting: mode === 'update' });
151
- resolve();
152
- });
285
+ if (p.isCancel(selection)) {
286
+ p.cancel('Cancelled.');
287
+ process.exit(0);
288
+ }
289
+
290
+ const items = fromGroupValues(selection);
291
+ if (items.length === 0) {
292
+ p.outro(pc.dim('Nothing selected.'));
293
+ return;
294
+ }
295
+
296
+ installItems(items, { replaceExisting: mode === 'update' });
297
+ p.outro(mode === 'update' ? pc.green('Update complete.') : pc.green('Install complete.'));
298
+ }
299
+
300
+ async function interactiveDelete() {
301
+ const menu = buildDeleteMenu();
302
+ if (menu.length === 0) {
303
+ status('info', 'No installed skills or agent packs to delete.');
304
+ return;
305
+ }
306
+ if (!IS_TTY) {
307
+ status('error', 'interactive delete requires a TTY. Use "delete --all" or "delete <name>...".');
308
+ process.exit(1);
309
+ }
310
+
311
+ p.intro(pc.bgCyan(pc.black(' AgentToolkit ')));
312
+
313
+ const selection = await p.groupMultiselect({
314
+ message: 'Select items to delete',
315
+ options: toGroupOptions(menu, { hintForInstalled: true }),
316
+ required: false,
317
+ });
318
+
319
+ if (p.isCancel(selection)) {
320
+ p.cancel('Cancelled.');
321
+ process.exit(0);
322
+ }
323
+
324
+ const items = fromGroupValues(selection);
325
+ if (items.length === 0) {
326
+ p.outro(pc.dim('Nothing selected.'));
327
+ return;
328
+ }
329
+
330
+ const confirmed = await p.confirm({
331
+ message: `Delete ${items.length} item(s)?`,
332
+ initialValue: false,
153
333
  });
334
+ if (p.isCancel(confirmed) || !confirmed) {
335
+ p.cancel('Cancelled.');
336
+ process.exit(0);
337
+ }
338
+
339
+ deleteItems(items);
340
+ p.outro(pc.green('Delete complete.'));
154
341
  }
155
342
 
156
343
  function resolveNames(names) {
@@ -163,18 +350,29 @@ function resolveNames(names) {
163
350
  } else if (agents.includes(name)) {
164
351
  items.push({ type: 'agent', name });
165
352
  } else {
166
- console.error(` [error] "${name}" not found as a skill or agent pack.`);
353
+ status('error', `"${name}" not found as a skill or agent pack.`);
167
354
  }
168
355
  }
169
356
  return items;
170
357
  }
171
358
 
172
- async function main() {
173
- const args = process.argv.slice(2);
359
+ function resolveDeleteNames(names) {
360
+ const installedSkills = getInstalled('skills');
361
+ const installedAgents = getInstalled('agents');
362
+ const items = [];
363
+ for (const name of names) {
364
+ if (installedSkills.includes(name)) {
365
+ items.push({ type: 'skill', name });
366
+ } else if (installedAgents.includes(name)) {
367
+ items.push({ type: 'agent', name });
368
+ } else {
369
+ status('error', `"${name}" is not installed.`);
370
+ }
371
+ }
372
+ return items;
373
+ }
174
374
 
175
- if (args.includes('--help') || args.includes('-h')) {
176
- console.log(`
177
- Usage:
375
+ const HELP_TEXT = `Usage:
178
376
  npx td-ai-tools Interactive install
179
377
  npx td-ai-tools list List available skills and agent packs
180
378
  npx td-ai-tools install Interactive install
@@ -183,20 +381,36 @@ Usage:
183
381
  npx td-ai-tools update Interactive update (replaces existing items)
184
382
  npx td-ai-tools update --all Update everything
185
383
  npx td-ai-tools update <name...> Update specific skills or agent packs
384
+ npx td-ai-tools delete Interactive delete
385
+ npx td-ai-tools delete --all Delete everything
386
+ npx td-ai-tools delete <name...> Delete specific skills or agent packs
186
387
 
187
388
  Skills are installed to: .claude/skills/<name>/
188
- .agents/skills/<name>/
389
+ .agents/skills/<name>/
189
390
  Agent packs installed to: .claude/agents/<name>/
190
- .agents/agents/<name>/
191
- `);
391
+ .agents/agents/<name>/`;
392
+
393
+ function printHelp() {
394
+ if (IS_TTY) {
395
+ p.intro(pc.bgCyan(pc.black(' AgentToolkit ')));
396
+ p.note(HELP_TEXT, 'Usage');
397
+ p.outro(pc.dim('Run with no arguments for interactive install.'));
398
+ } else {
399
+ console.log('\n' + HELP_TEXT + '\n');
400
+ }
401
+ }
402
+
403
+ async function main() {
404
+ const args = process.argv.slice(2);
405
+
406
+ if (args.includes('--help') || args.includes('-h')) {
407
+ printHelp();
192
408
  return;
193
409
  }
194
410
 
195
411
  const cmd = args[0];
196
- console.log('AgentToolkit');
197
- console.log('------------');
198
412
 
199
- if (!cmd || cmd === 'install' && args.length === 1) {
413
+ if (!cmd || (cmd === 'install' && args.length === 1)) {
200
414
  await interactiveInstall('install');
201
415
  return;
202
416
  }
@@ -209,10 +423,8 @@ Agent packs installed to: .claude/agents/<name>/
209
423
  if (cmd === 'install') {
210
424
  const rest = args.slice(1);
211
425
  if (rest[0] === '--all') {
212
- console.log('');
213
426
  installItems(buildMenu());
214
427
  } else {
215
- console.log('');
216
428
  installItems(resolveNames(rest));
217
429
  }
218
430
  return;
@@ -225,20 +437,32 @@ Agent packs installed to: .claude/agents/<name>/
225
437
  return;
226
438
  }
227
439
  if (rest[0] === '--all') {
228
- console.log('');
229
440
  installItems(buildMenu(), { replaceExisting: true });
230
441
  } else {
231
- console.log('');
232
442
  installItems(resolveNames(rest), { replaceExisting: true });
233
443
  }
234
444
  return;
235
445
  }
236
446
 
237
- console.error(`Unknown command: "${cmd}". Run with --help for usage.`);
447
+ if (cmd === 'delete') {
448
+ const rest = args.slice(1);
449
+ if (rest.length === 0) {
450
+ await interactiveDelete();
451
+ return;
452
+ }
453
+ if (rest[0] === '--all') {
454
+ deleteItems(buildDeleteMenu());
455
+ } else {
456
+ deleteItems(resolveDeleteNames(rest));
457
+ }
458
+ return;
459
+ }
460
+
461
+ status('error', `Unknown command: "${cmd}". Run with --help for usage.`);
238
462
  process.exit(1);
239
463
  }
240
464
 
241
465
  main().catch(err => {
242
- console.error(err.message);
466
+ status('error', err.message);
243
467
  process.exit(1);
244
468
  });
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.0.7",
3
+ "version": "1.1.1",
4
4
  "description": "Install agent skills and packs into your project",
5
+ "type": "module",
5
6
  "scripts": {
6
7
  "smoke:install": "./scripts/smoke-install.sh"
7
8
  },
@@ -15,8 +16,12 @@
15
16
  "skills/",
16
17
  "agents/"
17
18
  ],
19
+ "dependencies": {
20
+ "@clack/prompts": "^1.4.0",
21
+ "picocolors": "^1.1.0"
22
+ },
18
23
  "engines": {
19
- "node": ">=16"
24
+ "node": ">=20.12"
20
25
  },
21
26
  "keywords": [
22
27
  "claude",