akm-cli 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +602 -8
- package/dist/common.js +5 -0
- package/dist/config-cli.js +87 -0
- package/dist/config.js +197 -25
- package/dist/embedder.js +26 -4
- package/dist/init.js +2 -2
- package/dist/install-audit.js +324 -0
- package/dist/installed-kits.js +2 -2
- package/dist/matchers.js +25 -22
- package/dist/registry-install.js +46 -7
- package/dist/setup.js +2 -2
- package/dist/stash-add.js +4 -3
- package/dist/stash-source-manage.js +3 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveAssetPathFromName } from "./asset-spec";
|
|
6
|
+
import { isWithin, resolveStashDir } from "./common";
|
|
6
7
|
import { generateBashCompletions, installBashCompletions } from "./completions";
|
|
7
|
-
import { DEFAULT_CONFIG, getConfigPath, loadConfig, saveConfig } from "./config";
|
|
8
|
+
import { DEFAULT_CONFIG, getConfigPath, loadConfig, loadUserConfig, saveConfig } from "./config";
|
|
8
9
|
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./config-cli";
|
|
9
10
|
import { closeDatabase, openDatabase } from "./db";
|
|
10
11
|
import { ConfigError, NotFoundError, UsageError } from "./errors";
|
|
11
12
|
import { akmIndex } from "./indexer";
|
|
12
13
|
import { assembleInfo } from "./info";
|
|
13
14
|
import { akmInit } from "./init";
|
|
15
|
+
import { formatInstallAuditSummary } from "./install-audit";
|
|
14
16
|
import { akmListSources, akmRemove, akmUpdate } from "./installed-kits";
|
|
15
17
|
import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
|
|
16
18
|
import { buildRegistryIndex, writeRegistryIndex } from "./registry-build-index";
|
|
@@ -27,8 +29,12 @@ import { setQuiet, warn } from "./warn";
|
|
|
27
29
|
const OUTPUT_FORMATS = ["json", "yaml", "text", "jsonl"];
|
|
28
30
|
const DETAIL_LEVELS = ["brief", "normal", "full", "summary"];
|
|
29
31
|
const NORMAL_DESCRIPTION_LIMIT = 250;
|
|
32
|
+
const MAX_CAPTURED_ASSET_SLUG_LENGTH = 64;
|
|
30
33
|
const CONTEXT_HUB_ALIAS_REF = "context-hub";
|
|
31
34
|
const CONTEXT_HUB_ALIAS_URL = "https://github.com/andrewyng/context-hub";
|
|
35
|
+
const SKILLS_SH_NAME = "skills.sh";
|
|
36
|
+
const SKILLS_SH_URL = "https://skills.sh";
|
|
37
|
+
const SKILLS_SH_PROVIDER = "skills-sh";
|
|
32
38
|
import { stringify as yamlStringify } from "yaml";
|
|
33
39
|
function parseOutputFormat(value) {
|
|
34
40
|
if (!value)
|
|
@@ -338,6 +344,9 @@ function formatPlain(command, result, detail) {
|
|
|
338
344
|
case "search": {
|
|
339
345
|
return formatSearchPlain(r, detail);
|
|
340
346
|
}
|
|
347
|
+
case "curate": {
|
|
348
|
+
return formatCuratePlain(r, detail);
|
|
349
|
+
}
|
|
341
350
|
case "list": {
|
|
342
351
|
const sources = Array.isArray(r.sources) ? r.sources : [];
|
|
343
352
|
if (sources.length === 0)
|
|
@@ -356,7 +365,13 @@ function formatPlain(command, result, detail) {
|
|
|
356
365
|
const index = r.index;
|
|
357
366
|
const scanned = index?.directoriesScanned ?? 0;
|
|
358
367
|
const total = index?.totalEntries ?? 0;
|
|
359
|
-
|
|
368
|
+
const lines = [`Installed ${r.ref} (${scanned} directories scanned, ${total} total assets indexed)`];
|
|
369
|
+
const installed = r.installed;
|
|
370
|
+
const audit = installed?.audit;
|
|
371
|
+
if (audit && typeof audit === "object") {
|
|
372
|
+
lines.push(formatInstallAuditSummary(audit));
|
|
373
|
+
}
|
|
374
|
+
return lines.join("\n");
|
|
360
375
|
}
|
|
361
376
|
case "remove": {
|
|
362
377
|
const target = r.target ?? r.ref ?? "";
|
|
@@ -464,6 +479,275 @@ function formatSearchPlain(r, detail) {
|
|
|
464
479
|
}
|
|
465
480
|
return lines.join("\n").trimEnd();
|
|
466
481
|
}
|
|
482
|
+
function formatCuratePlain(r, detail) {
|
|
483
|
+
const query = typeof r.query === "string" ? r.query : "";
|
|
484
|
+
const summary = typeof r.summary === "string" ? r.summary : "";
|
|
485
|
+
const items = Array.isArray(r.items) ? r.items : [];
|
|
486
|
+
const lines = [`Curated results for "${query}"`];
|
|
487
|
+
if (summary)
|
|
488
|
+
lines.push(summary);
|
|
489
|
+
if (items.length === 0) {
|
|
490
|
+
if (r.tip)
|
|
491
|
+
lines.push(String(r.tip));
|
|
492
|
+
return lines.join("\n");
|
|
493
|
+
}
|
|
494
|
+
for (const item of items) {
|
|
495
|
+
const type = typeof item.type === "string" ? item.type : "unknown";
|
|
496
|
+
const name = typeof item.name === "string" ? item.name : "unnamed";
|
|
497
|
+
lines.push("");
|
|
498
|
+
lines.push(`[${type}] ${name}`);
|
|
499
|
+
if (item.description)
|
|
500
|
+
lines.push(` ${String(item.description)}`);
|
|
501
|
+
if (item.preview)
|
|
502
|
+
lines.push(` preview: ${String(item.preview)}`);
|
|
503
|
+
if (item.ref)
|
|
504
|
+
lines.push(` ref: ${String(item.ref)}`);
|
|
505
|
+
if (item.id)
|
|
506
|
+
lines.push(` id: ${String(item.id)}`);
|
|
507
|
+
if (Array.isArray(item.parameters) && item.parameters.length > 0) {
|
|
508
|
+
lines.push(` parameters: ${item.parameters.join(", ")}`);
|
|
509
|
+
}
|
|
510
|
+
if (item.run)
|
|
511
|
+
lines.push(` run: ${String(item.run)}`);
|
|
512
|
+
if (item.followUp)
|
|
513
|
+
lines.push(` show: ${String(item.followUp)}`);
|
|
514
|
+
if (detail !== "brief" && item.reason)
|
|
515
|
+
lines.push(` why: ${String(item.reason)}`);
|
|
516
|
+
}
|
|
517
|
+
const warnings = Array.isArray(r.warnings) ? r.warnings : [];
|
|
518
|
+
if (warnings.length > 0) {
|
|
519
|
+
lines.push("");
|
|
520
|
+
lines.push("Warnings:");
|
|
521
|
+
for (const warning of warnings) {
|
|
522
|
+
lines.push(`- ${String(warning)}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return lines.join("\n");
|
|
526
|
+
}
|
|
527
|
+
const CURATE_FALLBACK_FILTER_WORDS = new Set([
|
|
528
|
+
"a",
|
|
529
|
+
"an",
|
|
530
|
+
"and",
|
|
531
|
+
"for",
|
|
532
|
+
"how",
|
|
533
|
+
"i",
|
|
534
|
+
"in",
|
|
535
|
+
"of",
|
|
536
|
+
"or",
|
|
537
|
+
"the",
|
|
538
|
+
"to",
|
|
539
|
+
"with",
|
|
540
|
+
]);
|
|
541
|
+
const CURATED_TYPE_FALLBACK_ORDER = ["skill", "command", "script", "knowledge", "agent", "memory"];
|
|
542
|
+
const CURATED_TYPE_FALLBACK_INDEX = new Map(CURATED_TYPE_FALLBACK_ORDER.map((type, index) => [type, index]));
|
|
543
|
+
const MIN_CURATE_FALLBACK_TOKEN_LENGTH = 3;
|
|
544
|
+
const MAX_CURATE_FALLBACK_KEYWORDS = 6;
|
|
545
|
+
const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
|
|
546
|
+
const MIN_CURATE_SEARCH_LIMIT = 12;
|
|
547
|
+
async function curateSearchResults(query, result, limit, selectedType) {
|
|
548
|
+
const stashHits = result.hits.filter((hit) => hit.type !== "registry");
|
|
549
|
+
const registryHits = result.registryHits ?? [];
|
|
550
|
+
let selectedStashHits;
|
|
551
|
+
if (selectedType && selectedType !== "any") {
|
|
552
|
+
selectedStashHits = stashHits.slice(0, limit);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
const bestByType = new Map();
|
|
556
|
+
for (const hit of stashHits) {
|
|
557
|
+
if (!bestByType.has(hit.type))
|
|
558
|
+
bestByType.set(hit.type, hit);
|
|
559
|
+
}
|
|
560
|
+
const orderedTypes = orderCuratedTypes(query, Array.from(bestByType.keys()));
|
|
561
|
+
selectedStashHits = orderedTypes
|
|
562
|
+
.map((type) => bestByType.get(type))
|
|
563
|
+
.filter((hit) => Boolean(hit));
|
|
564
|
+
}
|
|
565
|
+
const selectedRegistryHits = selectedStashHits.length >= limit ? [] : registryHits.slice(0, Math.min(2, limit - selectedStashHits.length));
|
|
566
|
+
const items = [
|
|
567
|
+
...(await Promise.all(selectedStashHits.slice(0, limit).map((hit) => enrichCuratedStashHit(query, hit)))),
|
|
568
|
+
...selectedRegistryHits.map((hit) => buildCuratedRegistryItem(query, hit)),
|
|
569
|
+
].slice(0, limit);
|
|
570
|
+
return {
|
|
571
|
+
query,
|
|
572
|
+
summary: buildCurateSummary(query, items),
|
|
573
|
+
items,
|
|
574
|
+
...(result.warnings?.length ? { warnings: result.warnings } : {}),
|
|
575
|
+
...(result.tip ? { tip: result.tip } : {}),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function orderCuratedTypes(query, types) {
|
|
579
|
+
const lower = query.toLowerCase();
|
|
580
|
+
const boosts = new Map();
|
|
581
|
+
const addBoost = (type, amount) => boosts.set(type, (boosts.get(type) ?? 0) + amount);
|
|
582
|
+
if (/(run|script|bash|shell|cli|execute|automation|deploy|build|test|lint)/.test(lower)) {
|
|
583
|
+
addBoost("script", 6);
|
|
584
|
+
addBoost("command", 4);
|
|
585
|
+
}
|
|
586
|
+
if (/(guide|docs?|readme|reference|how|explain|learn|why)/.test(lower)) {
|
|
587
|
+
addBoost("knowledge", 6);
|
|
588
|
+
addBoost("skill", 4);
|
|
589
|
+
}
|
|
590
|
+
if (/(agent|assistant|planner|review|analy[sz]e|architect|prompt)/.test(lower)) {
|
|
591
|
+
addBoost("agent", 6);
|
|
592
|
+
addBoost("skill", 3);
|
|
593
|
+
}
|
|
594
|
+
if (/(config|template|release|generate|command)/.test(lower)) {
|
|
595
|
+
addBoost("command", 5);
|
|
596
|
+
}
|
|
597
|
+
if (/(memory|context|recall|remember)/.test(lower)) {
|
|
598
|
+
addBoost("memory", 6);
|
|
599
|
+
}
|
|
600
|
+
return [...types].sort((a, b) => {
|
|
601
|
+
const boostDiff = (boosts.get(b) ?? 0) - (boosts.get(a) ?? 0);
|
|
602
|
+
if (boostDiff !== 0)
|
|
603
|
+
return boostDiff;
|
|
604
|
+
return ((CURATED_TYPE_FALLBACK_INDEX.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
|
605
|
+
(CURATED_TYPE_FALLBACK_INDEX.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
async function enrichCuratedStashHit(query, hit) {
|
|
609
|
+
let shown;
|
|
610
|
+
try {
|
|
611
|
+
shown = await akmShowUnified({ ref: hit.ref });
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
shown = undefined;
|
|
615
|
+
}
|
|
616
|
+
const description = shown?.description ?? hit.description;
|
|
617
|
+
const preview = buildCuratedPreview(shown, hit);
|
|
618
|
+
return {
|
|
619
|
+
source: "stash",
|
|
620
|
+
type: shown?.type ?? hit.type,
|
|
621
|
+
name: shown?.name ?? hit.name,
|
|
622
|
+
ref: hit.ref,
|
|
623
|
+
...(description ? { description } : {}),
|
|
624
|
+
...(preview ? { preview } : {}),
|
|
625
|
+
...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
|
|
626
|
+
...(shown?.run ? { run: shown.run } : {}),
|
|
627
|
+
followUp: `akm show ${hit.ref}`,
|
|
628
|
+
reason: buildCuratedReason(query, shown?.type ?? hit.type),
|
|
629
|
+
...(hit.score !== undefined ? { score: hit.score } : {}),
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function buildCuratedRegistryItem(query, hit) {
|
|
633
|
+
return {
|
|
634
|
+
source: "registry",
|
|
635
|
+
type: "registry",
|
|
636
|
+
name: hit.name,
|
|
637
|
+
id: hit.id,
|
|
638
|
+
...(hit.description ? { description: hit.description } : {}),
|
|
639
|
+
followUp: hit.action ?? `akm add ${hit.id}`,
|
|
640
|
+
reason: `Useful external source to explore for ${query}.`,
|
|
641
|
+
...(hit.score !== undefined ? { score: hit.score } : {}),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function firstNonEmpty(values) {
|
|
645
|
+
return values.find((value) => typeof value === "string" && value.trim().length > 0);
|
|
646
|
+
}
|
|
647
|
+
function buildCuratedPreview(shown, hit) {
|
|
648
|
+
if (shown?.run)
|
|
649
|
+
return truncateDescription(`run ${shown.run}`, 160);
|
|
650
|
+
const payload = firstNonEmpty([shown?.template, shown?.prompt, shown?.content, hit.description])
|
|
651
|
+
?.replace(/\s+/g, " ")
|
|
652
|
+
.trim();
|
|
653
|
+
return payload ? truncateDescription(payload, 160) : undefined;
|
|
654
|
+
}
|
|
655
|
+
function buildCuratedReason(query, type) {
|
|
656
|
+
switch (type) {
|
|
657
|
+
case "script":
|
|
658
|
+
return `Best runnable script match for "${query}".`;
|
|
659
|
+
case "command":
|
|
660
|
+
return `Best reusable command/template match for "${query}".`;
|
|
661
|
+
case "knowledge":
|
|
662
|
+
return `Best reference document match for "${query}".`;
|
|
663
|
+
case "skill":
|
|
664
|
+
return `Best instructions/workflow match for "${query}".`;
|
|
665
|
+
case "agent":
|
|
666
|
+
return `Best specialized agent prompt match for "${query}".`;
|
|
667
|
+
case "memory":
|
|
668
|
+
return `Best saved context match for "${query}".`;
|
|
669
|
+
default:
|
|
670
|
+
return `Best ${type} match for "${query}".`;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function buildCurateSummary(query, items) {
|
|
674
|
+
if (items.length === 0) {
|
|
675
|
+
return `No curated assets were selected for "${query}".`;
|
|
676
|
+
}
|
|
677
|
+
const labels = items.map((item) => `${item.type}:${item.name}`);
|
|
678
|
+
return `Selected ${items.length} high-signal result${items.length === 1 ? "" : "s"}: ${labels.join(", ")}.`;
|
|
679
|
+
}
|
|
680
|
+
function hasSearchResults(result) {
|
|
681
|
+
return result.hits.length > 0 || (result.registryHits?.length ?? 0) > 0;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Extract a small set of fallback keywords when a prompt-style curate query
|
|
685
|
+
* returns no hits as a whole phrase.
|
|
686
|
+
*
|
|
687
|
+
* We keep up to MAX_CURATE_FALLBACK_KEYWORDS distinct keywords and drop short
|
|
688
|
+
* or common filler words so follow-up searches stay inexpensive while focusing
|
|
689
|
+
* on higher-signal terms.
|
|
690
|
+
*/
|
|
691
|
+
function deriveCurateFallbackQueries(query) {
|
|
692
|
+
return Array.from(new Set(query
|
|
693
|
+
.toLowerCase()
|
|
694
|
+
.split(/[^a-z0-9]+/)
|
|
695
|
+
.map((token) => token.trim())
|
|
696
|
+
// Keep longer tokens so fallback stays focused on higher-signal terms
|
|
697
|
+
// and avoids broad one- and two-letter matches that overwhelm curation.
|
|
698
|
+
.filter((token) => token.length >= MIN_CURATE_FALLBACK_TOKEN_LENGTH && !CURATE_FALLBACK_FILTER_WORDS.has(token)))).slice(0, MAX_CURATE_FALLBACK_KEYWORDS);
|
|
699
|
+
}
|
|
700
|
+
function mergeCurateSearchResponses(base, extras) {
|
|
701
|
+
const hitsByRef = new Map();
|
|
702
|
+
for (const hit of base.hits.filter((entry) => entry.type !== "registry")) {
|
|
703
|
+
hitsByRef.set(hit.ref, hit);
|
|
704
|
+
}
|
|
705
|
+
for (const result of extras) {
|
|
706
|
+
for (const hit of result.hits.filter((entry) => entry.type !== "registry")) {
|
|
707
|
+
const existing = hitsByRef.get(hit.ref);
|
|
708
|
+
if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
|
|
709
|
+
hitsByRef.set(hit.ref, hit);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const registryById = new Map();
|
|
714
|
+
for (const hit of base.registryHits ?? []) {
|
|
715
|
+
registryById.set(hit.id, hit);
|
|
716
|
+
}
|
|
717
|
+
for (const result of extras) {
|
|
718
|
+
for (const hit of result.registryHits ?? []) {
|
|
719
|
+
const existing = registryById.get(hit.id);
|
|
720
|
+
if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
|
|
721
|
+
registryById.set(hit.id, hit);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const warnings = Array.from(new Set([...(base.warnings ?? []), ...extras.flatMap((result) => result.warnings ?? [])]));
|
|
726
|
+
const mergedHits = [...hitsByRef.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
727
|
+
const mergedRegistryHits = [...registryById.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
728
|
+
return {
|
|
729
|
+
...base,
|
|
730
|
+
hits: mergedHits,
|
|
731
|
+
...(mergedRegistryHits.length > 0 ? { registryHits: mergedRegistryHits } : {}),
|
|
732
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
733
|
+
...(mergedHits.length > 0 || mergedRegistryHits.length > 0 ? { tip: undefined } : {}),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
async function searchForCuration(input) {
|
|
737
|
+
const initial = await akmSearch(input);
|
|
738
|
+
if (hasSearchResults(initial))
|
|
739
|
+
return initial;
|
|
740
|
+
const fallbackQueries = deriveCurateFallbackQueries(input.query);
|
|
741
|
+
if (fallbackQueries.length <= 1)
|
|
742
|
+
return initial;
|
|
743
|
+
const fallbackResults = await Promise.all(fallbackQueries.map((token) => akmSearch({
|
|
744
|
+
query: token,
|
|
745
|
+
type: input.type,
|
|
746
|
+
limit: input.limit,
|
|
747
|
+
source: input.source,
|
|
748
|
+
})));
|
|
749
|
+
return mergeCurateSearchResponses(initial, fallbackResults);
|
|
750
|
+
}
|
|
467
751
|
/**
|
|
468
752
|
* Module Naming:
|
|
469
753
|
* - stash-* : Asset operations (search, show, add, clone)
|
|
@@ -550,6 +834,39 @@ const searchCommand = defineCommand({
|
|
|
550
834
|
});
|
|
551
835
|
},
|
|
552
836
|
});
|
|
837
|
+
const curateCommand = defineCommand({
|
|
838
|
+
meta: { name: "curate", description: "Curate the best matching assets for a task or prompt" },
|
|
839
|
+
args: {
|
|
840
|
+
query: { type: "positional", description: "Task or prompt to curate assets for", required: true },
|
|
841
|
+
type: {
|
|
842
|
+
type: "string",
|
|
843
|
+
description: "Asset type filter (e.g. skill, command, agent, knowledge, script, memory, or any).",
|
|
844
|
+
},
|
|
845
|
+
limit: { type: "string", description: "Maximum number of curated results", default: "4" },
|
|
846
|
+
source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
|
|
847
|
+
},
|
|
848
|
+
async run({ args }) {
|
|
849
|
+
await runWithJsonErrors(async () => {
|
|
850
|
+
const type = args.type;
|
|
851
|
+
const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
|
|
852
|
+
if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
|
|
853
|
+
throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
|
|
854
|
+
}
|
|
855
|
+
const limit = limitRaw && limitRaw > 0 ? limitRaw : 4;
|
|
856
|
+
const source = parseSearchSource(args.source ?? "stash");
|
|
857
|
+
const searchResult = await searchForCuration({
|
|
858
|
+
query: args.query,
|
|
859
|
+
type,
|
|
860
|
+
// Search deeper than the final curated count so we can pick one strong
|
|
861
|
+
// match per type and still have room for fallback retries.
|
|
862
|
+
limit: Math.max(limit * CURATE_SEARCH_LIMIT_MULTIPLIER, MIN_CURATE_SEARCH_LIMIT),
|
|
863
|
+
source,
|
|
864
|
+
});
|
|
865
|
+
const curated = await curateSearchResults(args.query, searchResult, limit, type);
|
|
866
|
+
output("curate", curated);
|
|
867
|
+
});
|
|
868
|
+
},
|
|
869
|
+
});
|
|
553
870
|
const addCommand = defineCommand({
|
|
554
871
|
meta: {
|
|
555
872
|
name: "add",
|
|
@@ -826,7 +1143,7 @@ const configCommand = defineCommand({
|
|
|
826
1143
|
},
|
|
827
1144
|
run({ args }) {
|
|
828
1145
|
return runWithJsonErrors(() => {
|
|
829
|
-
const updated = setConfigValue(
|
|
1146
|
+
const updated = setConfigValue(loadUserConfig(), args.key, args.value);
|
|
830
1147
|
saveConfig(updated);
|
|
831
1148
|
output("config", listConfig(updated));
|
|
832
1149
|
});
|
|
@@ -839,7 +1156,7 @@ const configCommand = defineCommand({
|
|
|
839
1156
|
},
|
|
840
1157
|
run({ args }) {
|
|
841
1158
|
return runWithJsonErrors(() => {
|
|
842
|
-
const updated = unsetConfigValue(
|
|
1159
|
+
const updated = unsetConfigValue(loadUserConfig(), args.key);
|
|
843
1160
|
saveConfig(updated);
|
|
844
1161
|
output("config", listConfig(updated));
|
|
845
1162
|
});
|
|
@@ -888,7 +1205,7 @@ const registryCommand = defineCommand({
|
|
|
888
1205
|
meta: { name: "list", description: "List configured registries" },
|
|
889
1206
|
run() {
|
|
890
1207
|
return runWithJsonErrors(() => {
|
|
891
|
-
const config =
|
|
1208
|
+
const config = loadUserConfig();
|
|
892
1209
|
const registries = config.registries ?? DEFAULT_CONFIG.registries;
|
|
893
1210
|
output("registry-list", { registries });
|
|
894
1211
|
});
|
|
@@ -910,7 +1227,7 @@ const registryCommand = defineCommand({
|
|
|
910
1227
|
if (args.url.startsWith("http://")) {
|
|
911
1228
|
warn("Warning: registry URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
912
1229
|
}
|
|
913
|
-
const config =
|
|
1230
|
+
const config = loadUserConfig();
|
|
914
1231
|
const registries = [...(config.registries ?? [])];
|
|
915
1232
|
// Deduplicate by URL
|
|
916
1233
|
if (registries.some((r) => r.url === args.url)) {
|
|
@@ -943,7 +1260,7 @@ const registryCommand = defineCommand({
|
|
|
943
1260
|
},
|
|
944
1261
|
run({ args }) {
|
|
945
1262
|
return runWithJsonErrors(() => {
|
|
946
|
-
const config =
|
|
1263
|
+
const config = loadUserConfig();
|
|
947
1264
|
const registries = [...(config.registries ?? [])];
|
|
948
1265
|
const idx = registries.findIndex((r) => r.url === args.target || r.name === args.target);
|
|
949
1266
|
if (idx === -1) {
|
|
@@ -1044,6 +1361,164 @@ const feedbackCommand = defineCommand({
|
|
|
1044
1361
|
});
|
|
1045
1362
|
},
|
|
1046
1363
|
});
|
|
1364
|
+
function tryReadStdinText() {
|
|
1365
|
+
if (process.stdin.isTTY)
|
|
1366
|
+
return undefined;
|
|
1367
|
+
const input = fs.readFileSync(0, "utf8");
|
|
1368
|
+
return input.length > 0 ? input : undefined;
|
|
1369
|
+
}
|
|
1370
|
+
function normalizeMarkdownAssetName(name, fallback) {
|
|
1371
|
+
const trimmed = (name ?? fallback)
|
|
1372
|
+
.trim()
|
|
1373
|
+
.replace(/\\/g, "/")
|
|
1374
|
+
.replace(/^\/+|\/+$/g, "")
|
|
1375
|
+
.replace(/\.md$/i, "");
|
|
1376
|
+
if (!trimmed)
|
|
1377
|
+
throw new UsageError("Asset name cannot be empty.");
|
|
1378
|
+
const segments = trimmed.split("/");
|
|
1379
|
+
if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
1380
|
+
throw new UsageError("Asset name must be a relative path without '.' or '..' segments.");
|
|
1381
|
+
}
|
|
1382
|
+
return trimmed;
|
|
1383
|
+
}
|
|
1384
|
+
function slugifyAssetName(value, fallbackPrefix) {
|
|
1385
|
+
const slug = value
|
|
1386
|
+
.toLowerCase()
|
|
1387
|
+
.replace(/^[#>\-\s]+/, "")
|
|
1388
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1389
|
+
.replace(/^-+|-+$/g, "")
|
|
1390
|
+
.slice(0, MAX_CAPTURED_ASSET_SLUG_LENGTH);
|
|
1391
|
+
return slug || `${fallbackPrefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
1392
|
+
}
|
|
1393
|
+
function inferAssetName(content, fallbackPrefix, preferred) {
|
|
1394
|
+
const firstNonEmptyLine = content
|
|
1395
|
+
.split(/\r?\n/)
|
|
1396
|
+
.map((line) => line.trim())
|
|
1397
|
+
.find((line) => line.length > 0);
|
|
1398
|
+
const basis = preferred?.trim() || firstNonEmptyLine || fallbackPrefix;
|
|
1399
|
+
return slugifyAssetName(basis, fallbackPrefix);
|
|
1400
|
+
}
|
|
1401
|
+
function readMemoryContent(contentArg) {
|
|
1402
|
+
const content = contentArg ?? tryReadStdinText();
|
|
1403
|
+
if (!content?.trim()) {
|
|
1404
|
+
throw new UsageError("Memory content is required. Pass quoted text or pipe markdown into stdin.");
|
|
1405
|
+
}
|
|
1406
|
+
return content;
|
|
1407
|
+
}
|
|
1408
|
+
function readKnowledgeContent(source) {
|
|
1409
|
+
if (source === "-") {
|
|
1410
|
+
const content = tryReadStdinText();
|
|
1411
|
+
if (!content?.trim()) {
|
|
1412
|
+
throw new UsageError("No stdin content received. Pipe a document into stdin or pass a file path.");
|
|
1413
|
+
}
|
|
1414
|
+
return { content };
|
|
1415
|
+
}
|
|
1416
|
+
const resolvedSource = path.resolve(source);
|
|
1417
|
+
let stat;
|
|
1418
|
+
try {
|
|
1419
|
+
stat = fs.statSync(resolvedSource);
|
|
1420
|
+
}
|
|
1421
|
+
catch {
|
|
1422
|
+
throw new UsageError(`Knowledge source not found: "${source}". Pass a readable file path or "-" for stdin.`);
|
|
1423
|
+
}
|
|
1424
|
+
if (!stat.isFile()) {
|
|
1425
|
+
throw new UsageError(`Knowledge source must be a file: "${source}".`);
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
content: fs.readFileSync(resolvedSource, "utf8"),
|
|
1429
|
+
preferredName: path.basename(resolvedSource, path.extname(resolvedSource)),
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function writeMarkdownAsset(options) {
|
|
1433
|
+
const stashDir = resolveStashDir();
|
|
1434
|
+
const typeRoot = path.join(stashDir, options.type === "knowledge" ? "knowledge" : "memories");
|
|
1435
|
+
fs.mkdirSync(typeRoot, { recursive: true });
|
|
1436
|
+
const normalizedName = normalizeMarkdownAssetName(options.name, inferAssetName(options.content, options.fallbackPrefix, options.preferredName));
|
|
1437
|
+
const assetPath = resolveAssetPathFromName(options.type, typeRoot, normalizedName);
|
|
1438
|
+
if (!isWithin(assetPath, typeRoot)) {
|
|
1439
|
+
throw new UsageError(`Resolved ${options.type} path escapes the stash: "${normalizedName}"`);
|
|
1440
|
+
}
|
|
1441
|
+
if (fs.existsSync(assetPath) && !options.force) {
|
|
1442
|
+
throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`);
|
|
1443
|
+
}
|
|
1444
|
+
fs.mkdirSync(path.dirname(assetPath), { recursive: true });
|
|
1445
|
+
fs.writeFileSync(assetPath, options.content.endsWith("\n") ? options.content : `${options.content}\n`, "utf8");
|
|
1446
|
+
return {
|
|
1447
|
+
ref: `${options.type}:${normalizedName}`,
|
|
1448
|
+
path: assetPath,
|
|
1449
|
+
stashDir,
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
const rememberCommand = defineCommand({
|
|
1453
|
+
meta: {
|
|
1454
|
+
name: "remember",
|
|
1455
|
+
description: "Record a memory in the default stash",
|
|
1456
|
+
},
|
|
1457
|
+
args: {
|
|
1458
|
+
content: {
|
|
1459
|
+
type: "positional",
|
|
1460
|
+
description: "Memory content. Omit to read markdown from stdin.",
|
|
1461
|
+
required: false,
|
|
1462
|
+
},
|
|
1463
|
+
name: {
|
|
1464
|
+
type: "string",
|
|
1465
|
+
description: "Memory name (defaults to a slug from the content)",
|
|
1466
|
+
},
|
|
1467
|
+
force: {
|
|
1468
|
+
type: "boolean",
|
|
1469
|
+
description: "Overwrite an existing memory with the same name",
|
|
1470
|
+
default: false,
|
|
1471
|
+
},
|
|
1472
|
+
},
|
|
1473
|
+
run({ args }) {
|
|
1474
|
+
return runWithJsonErrors(() => {
|
|
1475
|
+
const result = writeMarkdownAsset({
|
|
1476
|
+
type: "memory",
|
|
1477
|
+
content: readMemoryContent(args.content),
|
|
1478
|
+
name: args.name,
|
|
1479
|
+
fallbackPrefix: "memory",
|
|
1480
|
+
force: args.force,
|
|
1481
|
+
});
|
|
1482
|
+
output("remember", { ok: true, ...result });
|
|
1483
|
+
});
|
|
1484
|
+
},
|
|
1485
|
+
});
|
|
1486
|
+
const importKnowledgeCommand = defineCommand({
|
|
1487
|
+
meta: {
|
|
1488
|
+
name: "import",
|
|
1489
|
+
description: "Import a knowledge document into the default stash",
|
|
1490
|
+
},
|
|
1491
|
+
args: {
|
|
1492
|
+
source: {
|
|
1493
|
+
type: "positional",
|
|
1494
|
+
description: 'Source file path, or "-" to read from stdin',
|
|
1495
|
+
required: true,
|
|
1496
|
+
},
|
|
1497
|
+
name: {
|
|
1498
|
+
type: "string",
|
|
1499
|
+
description: "Knowledge name (defaults to the source filename or content slug)",
|
|
1500
|
+
},
|
|
1501
|
+
force: {
|
|
1502
|
+
type: "boolean",
|
|
1503
|
+
description: "Overwrite an existing knowledge document with the same name",
|
|
1504
|
+
default: false,
|
|
1505
|
+
},
|
|
1506
|
+
},
|
|
1507
|
+
run({ args }) {
|
|
1508
|
+
return runWithJsonErrors(() => {
|
|
1509
|
+
const { content, preferredName } = readKnowledgeContent(args.source);
|
|
1510
|
+
const result = writeMarkdownAsset({
|
|
1511
|
+
type: "knowledge",
|
|
1512
|
+
content,
|
|
1513
|
+
name: args.name,
|
|
1514
|
+
fallbackPrefix: "knowledge",
|
|
1515
|
+
preferredName,
|
|
1516
|
+
force: args.force,
|
|
1517
|
+
});
|
|
1518
|
+
output("import", { ok: true, source: args.source, ...result });
|
|
1519
|
+
});
|
|
1520
|
+
},
|
|
1521
|
+
});
|
|
1047
1522
|
const hintsCommand = defineCommand({
|
|
1048
1523
|
meta: {
|
|
1049
1524
|
name: "hints",
|
|
@@ -1089,6 +1564,83 @@ const completionsCommand = defineCommand({
|
|
|
1089
1564
|
}
|
|
1090
1565
|
},
|
|
1091
1566
|
});
|
|
1567
|
+
function normalizeToggleTarget(target) {
|
|
1568
|
+
const normalized = target.trim().toLowerCase();
|
|
1569
|
+
if (normalized === "skills.sh" || normalized === "skills-sh")
|
|
1570
|
+
return "skills.sh";
|
|
1571
|
+
if (normalized === "context-hub")
|
|
1572
|
+
return "context-hub";
|
|
1573
|
+
throw new UsageError(`Unsupported target "${target}". Supported targets: skills.sh, context-hub`);
|
|
1574
|
+
}
|
|
1575
|
+
function toggleSkillsShRegistry(enabled) {
|
|
1576
|
+
const config = loadUserConfig();
|
|
1577
|
+
const registries = (config.registries ?? DEFAULT_CONFIG.registries ?? []).map((registry) => ({ ...registry }));
|
|
1578
|
+
const idx = registries.findIndex((registry) => registry.provider === SKILLS_SH_PROVIDER || registry.name === SKILLS_SH_NAME || registry.url === SKILLS_SH_URL);
|
|
1579
|
+
if (idx >= 0) {
|
|
1580
|
+
const existing = registries[idx];
|
|
1581
|
+
const wasEnabled = existing.enabled !== false;
|
|
1582
|
+
existing.enabled = enabled;
|
|
1583
|
+
saveConfig({ ...config, registries });
|
|
1584
|
+
return { changed: wasEnabled !== enabled, component: SKILLS_SH_NAME, enabled };
|
|
1585
|
+
}
|
|
1586
|
+
if (!enabled) {
|
|
1587
|
+
// Materialize the skills.sh registry explicitly if absent.
|
|
1588
|
+
registries.push({ url: SKILLS_SH_URL, name: SKILLS_SH_NAME, provider: SKILLS_SH_PROVIDER, enabled: false });
|
|
1589
|
+
saveConfig({ ...config, registries });
|
|
1590
|
+
return { changed: true, component: SKILLS_SH_NAME, enabled: false };
|
|
1591
|
+
}
|
|
1592
|
+
registries.push({ url: SKILLS_SH_URL, name: SKILLS_SH_NAME, provider: SKILLS_SH_PROVIDER, enabled: true });
|
|
1593
|
+
saveConfig({ ...config, registries });
|
|
1594
|
+
return { changed: true, component: SKILLS_SH_NAME, enabled: true };
|
|
1595
|
+
}
|
|
1596
|
+
function toggleContextHubStash(enabled) {
|
|
1597
|
+
const config = loadUserConfig();
|
|
1598
|
+
const stashes = [...(config.stashes ?? [])];
|
|
1599
|
+
const idx = stashes.findIndex((stash) => stash.name === CONTEXT_HUB_ALIAS_REF || stash.url === CONTEXT_HUB_ALIAS_URL);
|
|
1600
|
+
if (idx >= 0) {
|
|
1601
|
+
const existing = stashes[idx];
|
|
1602
|
+
const wasEnabled = existing.enabled !== false;
|
|
1603
|
+
existing.enabled = enabled;
|
|
1604
|
+
saveConfig({ ...config, stashes });
|
|
1605
|
+
return { changed: wasEnabled !== enabled, component: CONTEXT_HUB_ALIAS_REF, enabled };
|
|
1606
|
+
}
|
|
1607
|
+
if (!enabled) {
|
|
1608
|
+
return { changed: false, component: CONTEXT_HUB_ALIAS_REF, enabled: false };
|
|
1609
|
+
}
|
|
1610
|
+
stashes.push({ type: "git", url: CONTEXT_HUB_ALIAS_URL, name: CONTEXT_HUB_ALIAS_REF, enabled: true });
|
|
1611
|
+
saveConfig({ ...config, stashes });
|
|
1612
|
+
return { changed: true, component: CONTEXT_HUB_ALIAS_REF, enabled: true };
|
|
1613
|
+
}
|
|
1614
|
+
function toggleComponent(targetRaw, enabled) {
|
|
1615
|
+
const target = normalizeToggleTarget(targetRaw);
|
|
1616
|
+
if (target === "skills.sh")
|
|
1617
|
+
return toggleSkillsShRegistry(enabled);
|
|
1618
|
+
return toggleContextHubStash(enabled);
|
|
1619
|
+
}
|
|
1620
|
+
const enableCommand = defineCommand({
|
|
1621
|
+
meta: { name: "enable", description: "Enable an optional component (skills.sh or context-hub)" },
|
|
1622
|
+
args: {
|
|
1623
|
+
target: { type: "positional", description: "Component to enable (skills.sh|context-hub)", required: true },
|
|
1624
|
+
},
|
|
1625
|
+
run({ args }) {
|
|
1626
|
+
return runWithJsonErrors(() => {
|
|
1627
|
+
const result = toggleComponent(args.target, true);
|
|
1628
|
+
output("enable", result);
|
|
1629
|
+
});
|
|
1630
|
+
},
|
|
1631
|
+
});
|
|
1632
|
+
const disableCommand = defineCommand({
|
|
1633
|
+
meta: { name: "disable", description: "Disable an optional component (skills.sh or context-hub)" },
|
|
1634
|
+
args: {
|
|
1635
|
+
target: { type: "positional", description: "Component to disable (skills.sh|context-hub)", required: true },
|
|
1636
|
+
},
|
|
1637
|
+
run({ args }) {
|
|
1638
|
+
return runWithJsonErrors(() => {
|
|
1639
|
+
const result = toggleComponent(args.target, false);
|
|
1640
|
+
output("disable", result);
|
|
1641
|
+
});
|
|
1642
|
+
},
|
|
1643
|
+
});
|
|
1092
1644
|
const main = defineCommand({
|
|
1093
1645
|
meta: {
|
|
1094
1646
|
name: "akm",
|
|
@@ -1111,10 +1663,15 @@ const main = defineCommand({
|
|
|
1111
1663
|
update: updateCommand,
|
|
1112
1664
|
upgrade: upgradeCommand,
|
|
1113
1665
|
search: searchCommand,
|
|
1666
|
+
curate: curateCommand,
|
|
1114
1667
|
show: showCommand,
|
|
1668
|
+
remember: rememberCommand,
|
|
1669
|
+
import: importKnowledgeCommand,
|
|
1115
1670
|
clone: cloneCommand,
|
|
1116
1671
|
registry: registryCommand,
|
|
1117
1672
|
config: configCommand,
|
|
1673
|
+
enable: enableCommand,
|
|
1674
|
+
disable: disableCommand,
|
|
1118
1675
|
feedback: feedbackCommand,
|
|
1119
1676
|
hints: hintsCommand,
|
|
1120
1677
|
completions: completionsCommand,
|
|
@@ -1275,9 +1832,13 @@ You have access to a searchable library of scripts, skills, commands, agents, an
|
|
|
1275
1832
|
|
|
1276
1833
|
\`\`\`sh
|
|
1277
1834
|
akm search "<query>" # Search all sources
|
|
1835
|
+
akm curate "<task>" # Curate the best matches for a task
|
|
1278
1836
|
akm search "<query>" --type skill # Filter by type
|
|
1279
1837
|
akm search "<query>" --source both # Also search registries
|
|
1280
1838
|
akm show <ref> # View asset details
|
|
1839
|
+
akm remember "Deployment needs VPN access" # Record a memory in your stash
|
|
1840
|
+
akm import ./notes/release-checklist.md # Import a knowledge doc into your stash
|
|
1841
|
+
akm feedback <ref> --positive|--negative # Record whether an asset helped
|
|
1281
1842
|
akm add <ref> # Add a source (npm, GitHub, git, local dir)
|
|
1282
1843
|
akm clone <ref> # Copy an asset to the working stash (optional --dest arg to clone to specific location)
|
|
1283
1844
|
akm registry search "<query>" # Search all registries
|
|
@@ -1292,6 +1853,10 @@ akm registry search "<query>" # Search all registries
|
|
|
1292
1853
|
| command | A prompt template with placeholders to fill in |
|
|
1293
1854
|
| agent | A system prompt with model and tool hints |
|
|
1294
1855
|
| knowledge | A reference doc (use \`toc\` or \`section "..."\` to navigate) |
|
|
1856
|
+
| memory | Recalled context (read the content for background information) |
|
|
1857
|
+
|
|
1858
|
+
When an asset meaningfully helps or fails, record that with \`akm feedback\` so
|
|
1859
|
+
future search ranking can learn from real usage.
|
|
1295
1860
|
|
|
1296
1861
|
Run \`akm -h\` for the full command reference.
|
|
1297
1862
|
`;
|
|
@@ -1303,6 +1868,7 @@ You have access to a searchable library of scripts, skills, commands, agents, an
|
|
|
1303
1868
|
|
|
1304
1869
|
\`\`\`sh
|
|
1305
1870
|
akm search "<query>" # Search all sources
|
|
1871
|
+
akm curate "<task>" # Curate the best matches for a task
|
|
1306
1872
|
akm search "<query>" --type skill # Filter by asset type
|
|
1307
1873
|
akm search "<query>" --source both # Also search registries
|
|
1308
1874
|
akm search "<query>" --source registry # Search registries only
|
|
@@ -1319,6 +1885,16 @@ akm search "<query>" --detail full # Include scores, paths, timing
|
|
|
1319
1885
|
| \`--detail\` | \`brief\`, \`normal\`, \`full\`, \`summary\` | \`brief\` |
|
|
1320
1886
|
| \`--for-agent\` | boolean | \`false\` |
|
|
1321
1887
|
|
|
1888
|
+
## Curate
|
|
1889
|
+
|
|
1890
|
+
Combine search + follow-up hints into a dense summary for a task or prompt.
|
|
1891
|
+
|
|
1892
|
+
\`\`\`sh
|
|
1893
|
+
akm curate "plan a release" # Pick top matches across asset types
|
|
1894
|
+
akm curate "deploy a Bun app" --limit 3 # Keep the summary shorter
|
|
1895
|
+
akm curate "review architecture" --type skill # Restrict to one asset type
|
|
1896
|
+
\`\`\`
|
|
1897
|
+
|
|
1322
1898
|
## Show
|
|
1323
1899
|
|
|
1324
1900
|
Display an asset by ref. Knowledge assets support view modes as positional arguments.
|
|
@@ -1343,6 +1919,20 @@ akm show knowledge:my-doc # Show content (local or remote)
|
|
|
1343
1919
|
| knowledge | \`content\` (with view modes: \`full\`, \`toc\`, \`frontmatter\`, \`section\`, \`lines\`) |
|
|
1344
1920
|
| memory | \`content\` (recalled context) |
|
|
1345
1921
|
|
|
1922
|
+
## Capture Knowledge While You Work
|
|
1923
|
+
|
|
1924
|
+
\`\`\`sh
|
|
1925
|
+
akm remember "Deployment needs VPN access" # Record a memory in your stash
|
|
1926
|
+
akm remember --name release-retro < notes.md # Save multiline memory from stdin
|
|
1927
|
+
akm import ./docs/auth-flow.md # Import a file as knowledge
|
|
1928
|
+
akm import - --name scratch-notes < notes.md # Import stdin as a knowledge doc
|
|
1929
|
+
akm feedback skill:code-review --positive # Record that an asset helped
|
|
1930
|
+
akm feedback agent:reviewer --negative # Record that an asset missed the mark
|
|
1931
|
+
\`\`\`
|
|
1932
|
+
|
|
1933
|
+
Use \`akm feedback\` whenever an asset materially helps or fails so future search
|
|
1934
|
+
ranking can learn from actual usage.
|
|
1935
|
+
|
|
1346
1936
|
## Add & Manage Sources
|
|
1347
1937
|
|
|
1348
1938
|
\`\`\`sh
|
|
@@ -1350,6 +1940,10 @@ akm add <ref> # Add a source
|
|
|
1350
1940
|
akm add @scope/kit # From npm (managed)
|
|
1351
1941
|
akm add owner/repo # From GitHub (managed)
|
|
1352
1942
|
akm add ./path/to/local/kit # Local directory
|
|
1943
|
+
akm enable skills.sh # Enable the skills.sh registry
|
|
1944
|
+
akm disable skills.sh # Disable the skills.sh registry
|
|
1945
|
+
akm enable context-hub # Add/enable the context-hub source
|
|
1946
|
+
akm disable context-hub # Disable the context-hub source
|
|
1353
1947
|
akm list # List all sources
|
|
1354
1948
|
akm list --kind managed # List managed sources only
|
|
1355
1949
|
akm remove <target> # Remove by id, ref, path, or name
|