td-ai-tools 1.0.8 → 1.1.2
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/bin/cli.js +189 -82
- package/package.json +7 -2
package/bin/cli.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
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
|
|
5
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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,7 @@ function installSkill(name, { replaceExisting = false } = {}) {
|
|
|
61
80
|
}
|
|
62
81
|
copyDir(src, dest);
|
|
63
82
|
const action = replaceExisting ? 'updated' : 'installed';
|
|
64
|
-
|
|
83
|
+
status('success', `skill: ${name} ${action} → ${target.root}/skills/${name}/`);
|
|
65
84
|
}
|
|
66
85
|
return true;
|
|
67
86
|
}
|
|
@@ -72,12 +91,12 @@ function deleteSkill(name) {
|
|
|
72
91
|
const dest = path.join(TARGET_ROOT, target.root, 'skills', name);
|
|
73
92
|
if (fs.existsSync(dest)) {
|
|
74
93
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
75
|
-
|
|
94
|
+
status('success', `skill: ${name} deleted from ${target.root}/skills/${name}/`);
|
|
76
95
|
deletedAny = true;
|
|
77
96
|
}
|
|
78
97
|
}
|
|
79
98
|
if (!deletedAny) {
|
|
80
|
-
|
|
99
|
+
status('error', `Skill "${name}" is not installed.`);
|
|
81
100
|
return false;
|
|
82
101
|
}
|
|
83
102
|
return true;
|
|
@@ -86,13 +105,13 @@ function deleteSkill(name) {
|
|
|
86
105
|
function installAgent(name, { replaceExisting = false } = {}) {
|
|
87
106
|
const src = path.join(AGENTS_DIR, name);
|
|
88
107
|
if (!fs.existsSync(src)) {
|
|
89
|
-
|
|
108
|
+
status('error', `Agent pack "${name}" not found.`);
|
|
90
109
|
return false;
|
|
91
110
|
}
|
|
92
111
|
for (const target of INSTALL_TARGETS) {
|
|
93
112
|
const dest = path.join(TARGET_ROOT, target.root, 'agents', name);
|
|
94
113
|
if (fs.existsSync(dest) && !replaceExisting) {
|
|
95
|
-
|
|
114
|
+
status('warn', `Agent pack "${name}" already exists at ${target.root}/agents/${name}/ (use "update" to replace).`);
|
|
96
115
|
return false;
|
|
97
116
|
}
|
|
98
117
|
if (fs.existsSync(dest) && replaceExisting) {
|
|
@@ -100,7 +119,7 @@ function installAgent(name, { replaceExisting = false } = {}) {
|
|
|
100
119
|
}
|
|
101
120
|
copyDir(src, dest);
|
|
102
121
|
const action = replaceExisting ? 'updated' : 'installed';
|
|
103
|
-
|
|
122
|
+
status('success', `agent: ${name} ${action} → ${target.root}/agents/${name}/`);
|
|
104
123
|
}
|
|
105
124
|
return true;
|
|
106
125
|
}
|
|
@@ -111,12 +130,12 @@ function deleteAgent(name) {
|
|
|
111
130
|
const dest = path.join(TARGET_ROOT, target.root, 'agents', name);
|
|
112
131
|
if (fs.existsSync(dest)) {
|
|
113
132
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
114
|
-
|
|
133
|
+
status('success', `agent: ${name} deleted from ${target.root}/agents/${name}/`);
|
|
115
134
|
deletedAny = true;
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
137
|
if (!deletedAny) {
|
|
119
|
-
|
|
138
|
+
status('error', `Agent pack "${name}" is not installed.`);
|
|
120
139
|
return false;
|
|
121
140
|
}
|
|
122
141
|
return true;
|
|
@@ -137,7 +156,17 @@ function getInstalled(type) {
|
|
|
137
156
|
return [...installed].sort();
|
|
138
157
|
}
|
|
139
158
|
|
|
140
|
-
function
|
|
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() {
|
|
141
170
|
const skills = getAvailable(SKILLS_DIR);
|
|
142
171
|
const agents = getAvailable(AGENTS_DIR);
|
|
143
172
|
|
|
@@ -156,6 +185,47 @@ function printList() {
|
|
|
156
185
|
console.log('');
|
|
157
186
|
}
|
|
158
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
|
+
|
|
218
|
+
function printSelectionControls(action) {
|
|
219
|
+
p.note(
|
|
220
|
+
[
|
|
221
|
+
`Use Up/Down to move through the ${action} list.`,
|
|
222
|
+
'Press Space to select or deselect items.',
|
|
223
|
+
'Press Enter to confirm your choices.'
|
|
224
|
+
].join('\n'),
|
|
225
|
+
'Controls'
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
159
229
|
function buildMenu() {
|
|
160
230
|
const skills = getAvailable(SKILLS_DIR).map(name => ({ type: 'skill', name }));
|
|
161
231
|
const agents = getAvailable(AGENTS_DIR).map(name => ({ type: 'agent', name }));
|
|
@@ -168,6 +238,28 @@ function buildDeleteMenu() {
|
|
|
168
238
|
return [...skills, ...agents];
|
|
169
239
|
}
|
|
170
240
|
|
|
241
|
+
function toGroupOptions(menu, { hintForInstalled = false } = {}) {
|
|
242
|
+
const skillItems = menu.filter(i => i.type === 'skill').map(i => {
|
|
243
|
+
const hint = hintForInstalled ? '' : getSkillHint(i.name);
|
|
244
|
+
return { value: `skill:${i.name}`, label: i.name, hint: hint || undefined };
|
|
245
|
+
});
|
|
246
|
+
const agentItems = menu.filter(i => i.type === 'agent').map(i => {
|
|
247
|
+
const hint = hintForInstalled ? '' : getAgentHint(i.name);
|
|
248
|
+
return { value: `agent:${i.name}`, label: i.name, hint: hint || undefined };
|
|
249
|
+
});
|
|
250
|
+
const out = {};
|
|
251
|
+
if (skillItems.length) out['Skills'] = skillItems;
|
|
252
|
+
if (agentItems.length) out['Agent Packs'] = agentItems;
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function fromGroupValues(values) {
|
|
257
|
+
return values.map(v => {
|
|
258
|
+
const idx = v.indexOf(':');
|
|
259
|
+
return { type: v.slice(0, idx), name: v.slice(idx + 1) };
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
171
263
|
function installItems(items, options = {}) {
|
|
172
264
|
for (const item of items) {
|
|
173
265
|
if (item.type === 'skill') installSkill(item.name, options);
|
|
@@ -185,67 +277,80 @@ function deleteItems(items) {
|
|
|
185
277
|
async function interactiveInstall(mode = 'install') {
|
|
186
278
|
const menu = buildMenu();
|
|
187
279
|
if (menu.length === 0) {
|
|
188
|
-
|
|
280
|
+
status('info', 'No skills or agent packs available.');
|
|
189
281
|
return;
|
|
190
282
|
}
|
|
283
|
+
if (!IS_TTY) {
|
|
284
|
+
status('error', `interactive ${mode} requires a TTY. Use "${mode} --all" or "${mode} <name>...".`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
191
287
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const label = item.type === 'skill' ? 'skill' : 'agent';
|
|
195
|
-
console.log(` [${i + 1}] ${label}: ${item.name}`);
|
|
196
|
-
});
|
|
197
|
-
console.log('');
|
|
288
|
+
p.intro(pc.bgCyan(pc.black(' AgentToolkit ')));
|
|
289
|
+
printSelectionControls(mode);
|
|
198
290
|
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const trimmed = answer.trim().toLowerCase();
|
|
204
|
-
let selected;
|
|
205
|
-
if (trimmed === 'all') {
|
|
206
|
-
selected = menu;
|
|
207
|
-
} else {
|
|
208
|
-
const indices = trimmed.split(/\s+/).map(n => parseInt(n, 10) - 1);
|
|
209
|
-
selected = indices.filter(i => i >= 0 && i < menu.length).map(i => menu[i]);
|
|
210
|
-
}
|
|
211
|
-
console.log('');
|
|
212
|
-
installItems(selected, { replaceExisting: mode === 'update' });
|
|
213
|
-
resolve();
|
|
214
|
-
});
|
|
291
|
+
const selection = await p.groupMultiselect({
|
|
292
|
+
message: `Select items to ${mode}`,
|
|
293
|
+
options: toGroupOptions(menu),
|
|
294
|
+
required: false,
|
|
215
295
|
});
|
|
296
|
+
|
|
297
|
+
if (p.isCancel(selection)) {
|
|
298
|
+
p.cancel('Cancelled.');
|
|
299
|
+
process.exit(0);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const items = fromGroupValues(selection);
|
|
303
|
+
if (items.length === 0) {
|
|
304
|
+
p.outro(pc.dim('Nothing selected.'));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
installItems(items, { replaceExisting: mode === 'update' });
|
|
309
|
+
p.outro(mode === 'update' ? pc.green('Update complete.') : pc.green('Install complete.'));
|
|
216
310
|
}
|
|
217
311
|
|
|
218
312
|
async function interactiveDelete() {
|
|
219
313
|
const menu = buildDeleteMenu();
|
|
220
314
|
if (menu.length === 0) {
|
|
221
|
-
|
|
315
|
+
status('info', 'No installed skills or agent packs to delete.');
|
|
222
316
|
return;
|
|
223
317
|
}
|
|
318
|
+
if (!IS_TTY) {
|
|
319
|
+
status('error', 'interactive delete requires a TTY. Use "delete --all" or "delete <name>...".');
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
224
322
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
323
|
+
p.intro(pc.bgCyan(pc.black(' AgentToolkit ')));
|
|
324
|
+
printSelectionControls('delete');
|
|
325
|
+
|
|
326
|
+
const selection = await p.groupMultiselect({
|
|
327
|
+
message: 'Select items to delete',
|
|
328
|
+
options: toGroupOptions(menu, { hintForInstalled: true }),
|
|
329
|
+
required: false,
|
|
229
330
|
});
|
|
230
|
-
console.log('');
|
|
231
331
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
resolve();
|
|
247
|
-
});
|
|
332
|
+
if (p.isCancel(selection)) {
|
|
333
|
+
p.cancel('Cancelled.');
|
|
334
|
+
process.exit(0);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const items = fromGroupValues(selection);
|
|
338
|
+
if (items.length === 0) {
|
|
339
|
+
p.outro(pc.dim('Nothing selected.'));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const confirmed = await p.confirm({
|
|
344
|
+
message: `Delete ${items.length} item(s)?`,
|
|
345
|
+
initialValue: false,
|
|
248
346
|
});
|
|
347
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
348
|
+
p.cancel('Cancelled.');
|
|
349
|
+
process.exit(0);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
deleteItems(items);
|
|
353
|
+
p.outro(pc.green('Delete complete.'));
|
|
249
354
|
}
|
|
250
355
|
|
|
251
356
|
function resolveNames(names) {
|
|
@@ -258,7 +363,7 @@ function resolveNames(names) {
|
|
|
258
363
|
} else if (agents.includes(name)) {
|
|
259
364
|
items.push({ type: 'agent', name });
|
|
260
365
|
} else {
|
|
261
|
-
|
|
366
|
+
status('error', `"${name}" not found as a skill or agent pack.`);
|
|
262
367
|
}
|
|
263
368
|
}
|
|
264
369
|
return items;
|
|
@@ -274,18 +379,13 @@ function resolveDeleteNames(names) {
|
|
|
274
379
|
} else if (installedAgents.includes(name)) {
|
|
275
380
|
items.push({ type: 'agent', name });
|
|
276
381
|
} else {
|
|
277
|
-
|
|
382
|
+
status('error', `"${name}" is not installed.`);
|
|
278
383
|
}
|
|
279
384
|
}
|
|
280
385
|
return items;
|
|
281
386
|
}
|
|
282
387
|
|
|
283
|
-
|
|
284
|
-
const args = process.argv.slice(2);
|
|
285
|
-
|
|
286
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
287
|
-
console.log(`
|
|
288
|
-
Usage:
|
|
388
|
+
const HELP_TEXT = `Usage:
|
|
289
389
|
npx td-ai-tools Interactive install
|
|
290
390
|
npx td-ai-tools list List available skills and agent packs
|
|
291
391
|
npx td-ai-tools install Interactive install
|
|
@@ -299,18 +399,31 @@ Usage:
|
|
|
299
399
|
npx td-ai-tools delete <name...> Delete specific skills or agent packs
|
|
300
400
|
|
|
301
401
|
Skills are installed to: .claude/skills/<name>/
|
|
302
|
-
|
|
402
|
+
.agents/skills/<name>/
|
|
303
403
|
Agent packs installed to: .claude/agents/<name>/
|
|
304
|
-
|
|
305
|
-
|
|
404
|
+
.agents/agents/<name>/`;
|
|
405
|
+
|
|
406
|
+
function printHelp() {
|
|
407
|
+
if (IS_TTY) {
|
|
408
|
+
p.intro(pc.bgCyan(pc.black(' AgentToolkit ')));
|
|
409
|
+
p.note(HELP_TEXT, 'Usage');
|
|
410
|
+
p.outro(pc.dim('Run with no arguments for interactive install.'));
|
|
411
|
+
} else {
|
|
412
|
+
console.log('\n' + HELP_TEXT + '\n');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function main() {
|
|
417
|
+
const args = process.argv.slice(2);
|
|
418
|
+
|
|
419
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
420
|
+
printHelp();
|
|
306
421
|
return;
|
|
307
422
|
}
|
|
308
423
|
|
|
309
424
|
const cmd = args[0];
|
|
310
|
-
console.log('AgentToolkit');
|
|
311
|
-
console.log('------------');
|
|
312
425
|
|
|
313
|
-
if (!cmd || cmd === 'install' && args.length === 1) {
|
|
426
|
+
if (!cmd || (cmd === 'install' && args.length === 1)) {
|
|
314
427
|
await interactiveInstall('install');
|
|
315
428
|
return;
|
|
316
429
|
}
|
|
@@ -323,10 +436,8 @@ Agent packs installed to: .claude/agents/<name>/
|
|
|
323
436
|
if (cmd === 'install') {
|
|
324
437
|
const rest = args.slice(1);
|
|
325
438
|
if (rest[0] === '--all') {
|
|
326
|
-
console.log('');
|
|
327
439
|
installItems(buildMenu());
|
|
328
440
|
} else {
|
|
329
|
-
console.log('');
|
|
330
441
|
installItems(resolveNames(rest));
|
|
331
442
|
}
|
|
332
443
|
return;
|
|
@@ -339,10 +450,8 @@ Agent packs installed to: .claude/agents/<name>/
|
|
|
339
450
|
return;
|
|
340
451
|
}
|
|
341
452
|
if (rest[0] === '--all') {
|
|
342
|
-
console.log('');
|
|
343
453
|
installItems(buildMenu(), { replaceExisting: true });
|
|
344
454
|
} else {
|
|
345
|
-
console.log('');
|
|
346
455
|
installItems(resolveNames(rest), { replaceExisting: true });
|
|
347
456
|
}
|
|
348
457
|
return;
|
|
@@ -355,20 +464,18 @@ Agent packs installed to: .claude/agents/<name>/
|
|
|
355
464
|
return;
|
|
356
465
|
}
|
|
357
466
|
if (rest[0] === '--all') {
|
|
358
|
-
console.log('');
|
|
359
467
|
deleteItems(buildDeleteMenu());
|
|
360
468
|
} else {
|
|
361
|
-
console.log('');
|
|
362
469
|
deleteItems(resolveDeleteNames(rest));
|
|
363
470
|
}
|
|
364
471
|
return;
|
|
365
472
|
}
|
|
366
473
|
|
|
367
|
-
|
|
474
|
+
status('error', `Unknown command: "${cmd}". Run with --help for usage.`);
|
|
368
475
|
process.exit(1);
|
|
369
476
|
}
|
|
370
477
|
|
|
371
478
|
main().catch(err => {
|
|
372
|
-
|
|
479
|
+
status('error', err.message);
|
|
373
480
|
process.exit(1);
|
|
374
481
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "td-ai-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
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": ">=
|
|
24
|
+
"node": ">=20.12"
|
|
20
25
|
},
|
|
21
26
|
"keywords": [
|
|
22
27
|
"claude",
|