ht-skills 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/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # ht-skills
2
+
3
+ CLI for installing and submitting skills from HT Skills Marketplace.
4
+
5
+ ## Usage
6
+
7
+ ```powershell
8
+ npx ht-skills install repo-bug-analyze --registry http://skills.ic.aeroht.local
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ```text
14
+ ht-skills search <query> [--registry <url>] [--limit <n>]
15
+ ht-skills submit <skillDir> [--registry <url>] [--submitter <name>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
16
+ ht-skills install <slug[@version]> [--registry <url>] [--target <dir>] [--tool codex|claude|vscode]
17
+ ```
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ const cli = require("../lib/cli");
4
+
5
+ cli.main().catch((error) => {
6
+ // eslint-disable-next-line no-console
7
+ console.error(`error: ${error.message}`);
8
+ process.exitCode = 1;
9
+ });
package/lib/cli.js ADDED
@@ -0,0 +1,637 @@
1
+ const fs = require("fs/promises");
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const readline = require("readline/promises");
5
+
6
+ const INSTALL_TARGETS = {
7
+ codex: {
8
+ label: "Codex",
9
+ resolveTarget(homeDir, slug) {
10
+ return path.join(homeDir, ".codex", "skills", slug);
11
+ },
12
+ },
13
+ claude: {
14
+ label: "Claude",
15
+ resolveTarget(homeDir, slug) {
16
+ return path.join(homeDir, ".claude", "skills", slug);
17
+ },
18
+ },
19
+ vscode: {
20
+ label: "VS Code",
21
+ resolveTarget(homeDir, slug) {
22
+ return path.join(homeDir, ".copilot", "skills", slug);
23
+ },
24
+ },
25
+ };
26
+
27
+ const TOOL_ALIASES = {
28
+ "claude-code": "claude",
29
+ "github-copilot": "vscode",
30
+ };
31
+
32
+ function printHelp() {
33
+ // eslint-disable-next-line no-console
34
+ console.log(`Usage:
35
+ ht-skills search <query> [--registry <url>] [--limit <n>]
36
+ ht-skills submit <skillDir> [--registry <url>] [--submitter <name>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
37
+ ht-skills install <slug[@version]> [--registry <url>] [--target <dir>] [--tool codex|claude|vscode]
38
+
39
+ Examples:
40
+ ht-skills search openai --registry http://localhost:8787
41
+ ht-skills submit ./examples/hello-skill --registry http://localhost:8787
42
+ ht-skills install hello-skill@1.0.0 --registry http://localhost:8787 --tool codex
43
+ ht-skills install hello-skill@1.0.0 --registry http://localhost:8787 --tool codex,claude,vscode
44
+ ht-skills install hello-skill --registry http://localhost:8787`);
45
+ }
46
+
47
+ function parseArgs(argv) {
48
+ const result = { _: [] };
49
+ for (let i = 0; i < argv.length; i += 1) {
50
+ const token = argv[i];
51
+ if (!token.startsWith("--")) {
52
+ result._.push(token);
53
+ continue;
54
+ }
55
+
56
+ const [key, inlineValue] = token.slice(2).split("=", 2);
57
+ if (inlineValue !== undefined) {
58
+ result[key] = inlineValue;
59
+ continue;
60
+ }
61
+
62
+ const next = argv[i + 1];
63
+ if (!next || next.startsWith("--")) {
64
+ result[key] = true;
65
+ continue;
66
+ }
67
+ result[key] = next;
68
+ i += 1;
69
+ }
70
+ return result;
71
+ }
72
+
73
+ function getRegistryUrl(flags) {
74
+ return String(flags.registry || process.env.SKILLS_REGISTRY_URL || "http://localhost:8787").replace(/\/$/, "");
75
+ }
76
+
77
+ async function requestJson(url, options = {}) {
78
+ const res = await fetch(url, options);
79
+ const text = await res.text();
80
+ let payload;
81
+ try {
82
+ payload = text ? JSON.parse(text) : {};
83
+ } catch {
84
+ payload = { raw: text };
85
+ }
86
+ if (!res.ok) {
87
+ const message = payload.error || text || `HTTP ${res.status}`;
88
+ throw new Error(message);
89
+ }
90
+ return payload;
91
+ }
92
+
93
+ async function walkFiles(baseDir) {
94
+ const results = [];
95
+ const ignored = new Set([".git", "node_modules", ".DS_Store"]);
96
+
97
+ async function walk(currentDir) {
98
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
99
+ for (const entry of entries) {
100
+ if (ignored.has(entry.name)) {
101
+ continue;
102
+ }
103
+
104
+ const absolutePath = path.join(currentDir, entry.name);
105
+ if (entry.isDirectory()) {
106
+ await walk(absolutePath);
107
+ } else if (entry.isFile()) {
108
+ results.push(absolutePath);
109
+ }
110
+ }
111
+ }
112
+
113
+ await walk(baseDir);
114
+ return results;
115
+ }
116
+
117
+ function parseSpec(spec) {
118
+ const [slug, version] = String(spec).split("@", 2);
119
+ if (!slug) {
120
+ throw new Error("install requires <slug> or <slug>@<version>");
121
+ }
122
+ return { slug, version: version || null };
123
+ }
124
+
125
+ function getAvailableInstallTargets(slug, { homeDir = os.homedir() } = {}) {
126
+ return Object.entries(INSTALL_TARGETS).map(([id, target]) => ({
127
+ id,
128
+ label: target.label,
129
+ target: target.resolveTarget(homeDir, slug),
130
+ }));
131
+ }
132
+
133
+ function normalizeToolIds(rawTool) {
134
+ if (!rawTool) return [];
135
+ const values = Array.isArray(rawTool) ? rawTool : [rawTool];
136
+ const output = [];
137
+ const seen = new Set();
138
+
139
+ for (const value of values) {
140
+ const parts = String(value)
141
+ .split(",")
142
+ .map((item) => item.trim().toLowerCase())
143
+ .filter(Boolean);
144
+
145
+ for (const part of parts) {
146
+ if (part === "all" || part === "*") {
147
+ return Object.keys(INSTALL_TARGETS);
148
+ }
149
+ const normalizedPart = TOOL_ALIASES[part] || part;
150
+ if (!INSTALL_TARGETS[normalizedPart]) {
151
+ throw new Error(`unknown tool: ${part}`);
152
+ }
153
+ if (!seen.has(normalizedPart)) {
154
+ seen.add(normalizedPart);
155
+ output.push(normalizedPart);
156
+ }
157
+ }
158
+ }
159
+
160
+ return output;
161
+ }
162
+
163
+ function parseInstallSelection(input, availableTargets) {
164
+ const trimmed = String(input || "").trim().toLowerCase();
165
+ if (!trimmed) {
166
+ return [];
167
+ }
168
+ if (trimmed === "all" || trimmed === "*") {
169
+ return availableTargets.map((item) => item.id);
170
+ }
171
+
172
+ const byNumber = new Map(availableTargets.map((item, index) => [String(index + 1), item.id]));
173
+ const byId = new Map(availableTargets.map((item) => [item.id, item.id]));
174
+ const values = trimmed
175
+ .split(/[,\s]+/)
176
+ .map((item) => item.trim())
177
+ .filter(Boolean);
178
+
179
+ const result = [];
180
+ const seen = new Set();
181
+ for (const value of values) {
182
+ const resolved = byNumber.get(value) || byId.get(value);
183
+ if (!resolved) {
184
+ throw new Error(`unknown selection: ${value}`);
185
+ }
186
+ if (!seen.has(resolved)) {
187
+ seen.add(resolved);
188
+ result.push(resolved);
189
+ }
190
+ }
191
+ return result;
192
+ }
193
+
194
+ function resolveInstallTargets({ toolIds, target, slug, version, homeDir = os.homedir() }) {
195
+ if (target && toolIds.length > 1) {
196
+ throw new Error("--target can only be used with a single install destination");
197
+ }
198
+
199
+ if (target) {
200
+ return [{
201
+ id: toolIds[0] || "custom",
202
+ label: toolIds[0] ? INSTALL_TARGETS[toolIds[0]].label : "Custom",
203
+ target: path.resolve(target),
204
+ }];
205
+ }
206
+
207
+ if (toolIds.length > 0) {
208
+ return toolIds.map((toolId) => ({
209
+ id: toolId,
210
+ label: INSTALL_TARGETS[toolId].label,
211
+ target: INSTALL_TARGETS[toolId].resolveTarget(homeDir, slug),
212
+ }));
213
+ }
214
+
215
+ return [{
216
+ id: "local",
217
+ label: "Local",
218
+ target: path.resolve(process.cwd(), "installed-skills", slug, version || "latest"),
219
+ }];
220
+ }
221
+
222
+ function formatCount(value, noun) {
223
+ return `${value} ${noun}${value === 1 ? "" : "s"}`;
224
+ }
225
+
226
+ function formatInstallSummary(target, colorize = (text) => text) {
227
+ return `${colorize(target.label, "accent")} ${colorize("->", "muted")} ${target.target}`;
228
+ }
229
+
230
+ function formatTargetsNote(installTargets, colorize = (text) => text) {
231
+ return installTargets
232
+ .map((target, index) => `${colorize(`${index + 1}.`, "muted")} ${formatInstallSummary(target, colorize)}`)
233
+ .join("\n");
234
+ }
235
+
236
+ function printFallbackIntro({ registry, slug, version, skillName, skillDescription, installTargets }, log = console.log) {
237
+ log("");
238
+ log("ht-skills");
239
+ log("");
240
+ log(`Source: ${registry}`);
241
+ log(`Skill: ${skillName} (${slug}@${version})`);
242
+ if (skillDescription) {
243
+ log(skillDescription);
244
+ }
245
+ log("");
246
+ log(`Found ${installTargets.length} install target${installTargets.length === 1 ? "" : "s"}`);
247
+ for (const line of formatTargetsNote(installTargets).split("\n")) {
248
+ log(` ${line}`);
249
+ }
250
+ log("");
251
+ }
252
+
253
+ async function loadTerminalUi() {
254
+ const [{ intro, outro, note, multiselect, spinner, isCancel, cancel }, pcModule] = await Promise.all([
255
+ import("@clack/prompts"),
256
+ import("picocolors"),
257
+ ]);
258
+ const pc = pcModule.default || pcModule;
259
+ return {
260
+ intro,
261
+ outro,
262
+ note,
263
+ multiselect,
264
+ spinner,
265
+ isCancel,
266
+ cancel,
267
+ pc,
268
+ };
269
+ }
270
+
271
+ function createUiColorizer(pc) {
272
+ return (text, tone = "default") => {
273
+ const value = String(text);
274
+ switch (tone) {
275
+ case "accent":
276
+ return pc.cyan(pc.bold(value));
277
+ case "muted":
278
+ return pc.dim(value);
279
+ case "success":
280
+ return pc.green(value);
281
+ case "warn":
282
+ return pc.yellow(value);
283
+ default:
284
+ return value;
285
+ }
286
+ };
287
+ }
288
+
289
+ async function selectInstallTargets({ availableTargets, ask = null, ui = null }) {
290
+ if (ui) {
291
+ const selection = await ui.multiselect({
292
+ message: "Which tools do you want to install to?",
293
+ options: availableTargets.map((target) => ({
294
+ value: target.id,
295
+ label: target.label,
296
+ hint: target.target,
297
+ })),
298
+ required: true,
299
+ });
300
+
301
+ if (ui.isCancel(selection)) {
302
+ ui.cancel("Install cancelled.");
303
+ const error = new Error("install cancelled");
304
+ error.cancelled = true;
305
+ throw error;
306
+ }
307
+
308
+ return selection;
309
+ }
310
+
311
+ while (true) {
312
+ const answer = await ask("Which tools do you want to install to? (number, name, or 'all'): ");
313
+ let selected;
314
+ try {
315
+ selected = parseInstallSelection(answer, availableTargets);
316
+ } catch (error) {
317
+ // eslint-disable-next-line no-console
318
+ console.log(`Invalid selection: ${error.message}`);
319
+ continue;
320
+ }
321
+ if (selected.length === 0) {
322
+ // eslint-disable-next-line no-console
323
+ console.log("Please choose at least one install target.");
324
+ continue;
325
+ }
326
+ return selected;
327
+ }
328
+ }
329
+
330
+ async function writeBundleToTarget(bundle, targetPath) {
331
+ await fs.mkdir(targetPath, { recursive: true });
332
+
333
+ for (const file of bundle.files) {
334
+ const absolutePath = path.join(targetPath, file.path);
335
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
336
+ await fs.writeFile(absolutePath, Buffer.from(file.content_base64, "base64"));
337
+ }
338
+ }
339
+
340
+ async function fetchResolvedVersion(registry, parsed, requestJsonImpl) {
341
+ if (parsed.version) {
342
+ return parsed.version;
343
+ }
344
+ const latest = await requestJsonImpl(`${registry}/api/skills/${encodeURIComponent(parsed.slug)}`);
345
+ if (!latest.latestVersion) {
346
+ throw new Error(`failed to resolve latest version for ${parsed.slug}`);
347
+ }
348
+ return latest.latestVersion;
349
+ }
350
+
351
+ async function cmdSearch(flags, deps = {}) {
352
+ const log = deps.log || ((message) => console.log(message));
353
+ const requestJsonImpl = deps.requestJson || requestJson;
354
+ const query = flags._.join(" ").trim();
355
+ if (!query) {
356
+ throw new Error("search requires a keyword");
357
+ }
358
+
359
+ const registry = getRegistryUrl(flags);
360
+ const limit = Number(flags.limit || 20);
361
+ const url = `${registry}/api/skills/search?q=${encodeURIComponent(query)}&limit=${encodeURIComponent(limit)}`;
362
+ const data = await requestJsonImpl(url);
363
+
364
+ if (!data.items || data.items.length === 0) {
365
+ log("no results");
366
+ return;
367
+ }
368
+
369
+ for (const item of data.items) {
370
+ log(`${item.slug}@${item.latestVersion} ${item.name} ${item.description || ""}`);
371
+ }
372
+ }
373
+
374
+ async function cmdInstall(flags, deps = {}) {
375
+ const requestJsonImpl = deps.requestJson || requestJson;
376
+ const ask = deps.ask || null;
377
+ const isInteractive = typeof deps.isInteractive === "boolean"
378
+ ? deps.isInteractive
379
+ : Boolean(process.stdin.isTTY && process.stdout.isTTY);
380
+ const homeDir = deps.homeDir || os.homedir();
381
+ const log = deps.log || ((message) => console.log(message));
382
+ const canUseFancyUi = Boolean(isInteractive && !ask && !deps.disableUi);
383
+ const ui = canUseFancyUi ? await loadTerminalUi().catch(() => null) : null;
384
+ const colorize = ui ? createUiColorizer(ui.pc) : (text) => String(text);
385
+ const spec = flags._[0];
386
+ if (!spec) {
387
+ throw new Error("install requires a skill spec, for example my-skill@1.0.0");
388
+ }
389
+
390
+ const registry = getRegistryUrl(flags);
391
+ const parsed = parseSpec(spec);
392
+
393
+ const resolveSpinner = ui ? ui.spinner() : null;
394
+ if (resolveSpinner) {
395
+ ui.intro(ui.pc.bgCyan(ui.pc.black(" ht-skills ")));
396
+ resolveSpinner.start(`Resolving ${parsed.slug}`);
397
+ }
398
+ const version = await fetchResolvedVersion(registry, parsed, requestJsonImpl);
399
+ if (resolveSpinner) {
400
+ resolveSpinner.stop(`Resolved ${parsed.slug}@${version}`);
401
+ }
402
+
403
+ let toolIds = normalizeToolIds(flags.tool);
404
+ const renderInstallLine = deps.renderInstallLine || ((target) => `installed ${parsed.slug}@${version} -> ${target.target}`);
405
+ let metadata = null;
406
+
407
+ const metadataSpinner = ui ? ui.spinner() : null;
408
+ if (metadataSpinner) {
409
+ metadataSpinner.start("Loading skill metadata");
410
+ }
411
+ try {
412
+ metadata = await requestJsonImpl(
413
+ `${registry}/api/skills/${encodeURIComponent(parsed.slug)}/${encodeURIComponent(version)}`,
414
+ );
415
+ } catch {
416
+ metadata = null;
417
+ }
418
+ if (metadataSpinner) {
419
+ metadataSpinner.stop(metadata?.manifest?.name
420
+ ? `Loaded ${metadata.manifest.name}`
421
+ : "Loaded install metadata");
422
+ }
423
+
424
+ const skillName = metadata?.manifest?.name || parsed.slug;
425
+ const skillDescription = metadata?.manifest?.description || "";
426
+
427
+ if (toolIds.length === 0 && !flags.target && isInteractive) {
428
+ const availableTargets = getAvailableInstallTargets(parsed.slug, { homeDir });
429
+ if (availableTargets.length > 0 && ui) {
430
+ ui.note(
431
+ [
432
+ `${colorize("Source", "muted")}: ${registry}`,
433
+ `${colorize("Skill", "muted")}: ${colorize(skillName, "accent")} ${colorize(`(${parsed.slug}@${version})`, "muted")}`,
434
+ skillDescription ? `${colorize("Summary", "muted")}: ${skillDescription}` : "",
435
+ "",
436
+ `${colorize("Available targets", "muted")}:`,
437
+ formatTargetsNote(availableTargets, colorize),
438
+ ].filter(Boolean).join("\n"),
439
+ "Install plan",
440
+ );
441
+ } else if (availableTargets.length > 0) {
442
+ printFallbackIntro({
443
+ registry,
444
+ slug: parsed.slug,
445
+ version,
446
+ skillName,
447
+ skillDescription,
448
+ installTargets: availableTargets,
449
+ }, log);
450
+ }
451
+
452
+ if (availableTargets.length > 0) {
453
+ toolIds = await selectInstallTargets({
454
+ availableTargets,
455
+ ask: ask || (async (promptText) => {
456
+ const rl = readline.createInterface({
457
+ input: process.stdin,
458
+ output: process.stdout,
459
+ });
460
+ try {
461
+ return await rl.question(promptText);
462
+ } finally {
463
+ rl.close();
464
+ }
465
+ }),
466
+ ui,
467
+ });
468
+ }
469
+ }
470
+
471
+ const installTargets = resolveInstallTargets({
472
+ toolIds,
473
+ target: flags.target,
474
+ slug: parsed.slug,
475
+ version,
476
+ homeDir,
477
+ });
478
+
479
+ if (!ui) {
480
+ printFallbackIntro({
481
+ registry,
482
+ slug: parsed.slug,
483
+ version,
484
+ skillName,
485
+ skillDescription,
486
+ installTargets,
487
+ }, log);
488
+ }
489
+
490
+ const bundleSpinner = ui ? ui.spinner() : null;
491
+ if (bundleSpinner) {
492
+ bundleSpinner.start("Downloading skill bundle");
493
+ }
494
+ const bundle = await requestJsonImpl(
495
+ `${registry}/api/skills/${encodeURIComponent(parsed.slug)}/${encodeURIComponent(version)}/bundle`,
496
+ );
497
+ if (bundleSpinner) {
498
+ bundleSpinner.stop(`Downloaded ${parsed.slug}@${version}`);
499
+ }
500
+
501
+ const installSpinner = ui ? ui.spinner() : null;
502
+ if (installSpinner) {
503
+ installSpinner.start(`Installing to ${formatCount(installTargets.length, "target")}`);
504
+ }
505
+
506
+ for (const target of installTargets) {
507
+ await writeBundleToTarget(bundle, target.target);
508
+
509
+ const metadataPath = path.join(target.target, ".marketplace-install.json");
510
+ await fs.writeFile(
511
+ metadataPath,
512
+ `${JSON.stringify(
513
+ {
514
+ slug: parsed.slug,
515
+ version,
516
+ installedAt: new Date().toISOString(),
517
+ registry,
518
+ checksum: bundle.checksum,
519
+ tool: target.id,
520
+ },
521
+ null,
522
+ 2,
523
+ )}\n`,
524
+ "utf8",
525
+ );
526
+
527
+ if (!ui) {
528
+ log(renderInstallLine(target));
529
+ }
530
+ }
531
+
532
+ if (installSpinner) {
533
+ installSpinner.stop(`Installed to ${formatCount(installTargets.length, "target")}`);
534
+ }
535
+
536
+ if (installTargets.length > 0) {
537
+ if (ui) {
538
+ ui.note(formatTargetsNote(installTargets, colorize), "Installed to");
539
+ ui.outro(`Installed ${ui.pc.cyan(parsed.slug)} to ${installTargets.length} target${installTargets.length === 1 ? "" : "s"}.`);
540
+ } else {
541
+ log(`Installed ${parsed.slug} to ${formatCount(installTargets.length, "target")}`);
542
+ }
543
+ }
544
+
545
+ return installTargets;
546
+ }
547
+
548
+ async function cmdSubmit(flags, deps = {}) {
549
+ const requestJsonImpl = deps.requestJson || requestJson;
550
+ const log = deps.log || ((message) => console.log(message));
551
+ const skillDirArg = flags._[0] || ".";
552
+ const skillDir = path.resolve(skillDirArg);
553
+ const manifestPath = path.resolve(flags.manifest || path.join(skillDir, "skill.json"));
554
+ const manifestRaw = await fs.readFile(manifestPath, "utf8");
555
+ const manifest = JSON.parse(manifestRaw);
556
+
557
+ const filePaths = await walkFiles(skillDir);
558
+ const files = [];
559
+ for (const absolutePath of filePaths) {
560
+ if (path.resolve(absolutePath) === manifestPath) {
561
+ continue;
562
+ }
563
+ const content = await fs.readFile(absolutePath);
564
+ files.push({
565
+ path: path.relative(skillDir, absolutePath).replace(/\\/g, "/"),
566
+ content_base64: content.toString("base64"),
567
+ });
568
+ }
569
+
570
+ const body = {
571
+ manifest,
572
+ files,
573
+ submitter: String(flags.submitter || process.env.USERNAME || os.userInfo().username || "anonymous"),
574
+ };
575
+
576
+ if (flags.visibility) {
577
+ body.visibility = String(flags.visibility);
578
+ }
579
+ if (flags["shared-with"]) {
580
+ body.shared_with = String(flags["shared-with"])
581
+ .split(",")
582
+ .map((item) => item.trim())
583
+ .filter(Boolean);
584
+ }
585
+ if (flags["publish-now"]) {
586
+ body.publish_now = true;
587
+ }
588
+
589
+ const registry = getRegistryUrl(flags);
590
+ const result = await requestJsonImpl(`${registry}/api/skills/submit`, {
591
+ method: "POST",
592
+ headers: {
593
+ "content-type": "application/json",
594
+ },
595
+ body: JSON.stringify(body),
596
+ });
597
+ log(JSON.stringify(result, null, 2));
598
+ }
599
+
600
+ async function main(argv = process.argv) {
601
+ const [, , command, ...rest] = argv;
602
+ const flags = parseArgs(rest);
603
+
604
+ if (!command || command === "help" || command === "--help") {
605
+ printHelp();
606
+ return;
607
+ }
608
+ if (command === "search") {
609
+ await cmdSearch(flags);
610
+ return;
611
+ }
612
+ if (command === "install") {
613
+ await cmdInstall(flags);
614
+ return;
615
+ }
616
+ if (command === "submit") {
617
+ await cmdSubmit(flags);
618
+ return;
619
+ }
620
+
621
+ throw new Error(`unknown command: ${command}`);
622
+ }
623
+
624
+ module.exports = {
625
+ INSTALL_TARGETS,
626
+ parseArgs,
627
+ parseSpec,
628
+ normalizeToolIds,
629
+ parseInstallSelection,
630
+ getAvailableInstallTargets,
631
+ resolveInstallTargets,
632
+ fetchResolvedVersion,
633
+ cmdInstall,
634
+ cmdSearch,
635
+ cmdSubmit,
636
+ main,
637
+ };
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "ht-skills",
3
+ "version": "0.1.0",
4
+ "description": "CLI for installing and submitting skills from HT Skills Marketplace.",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "ht-skills": "./bin/ht-skills.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.17.0"
16
+ },
17
+ "dependencies": {
18
+ "@clack/prompts": "^1.1.0",
19
+ "picocolors": "^1.1.1"
20
+ }
21
+ }