akm-cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +366 -11
- package/dist/common.js +5 -0
- package/dist/config-cli.js +87 -0
- package/dist/config.js +197 -25
- package/dist/indexer.js +2 -2
- 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/search-source.js +33 -6
- package/dist/setup.js +2 -2
- package/dist/stash-add.js +75 -3
- package/dist/stash-providers/index.js +1 -0
- package/dist/stash-providers/website.js +443 -0
- package/dist/stash-source-manage.js +3 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,13 +4,14 @@ import path from "node:path";
|
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
5
|
import { resolveStashDir } from "./common";
|
|
6
6
|
import { generateBashCompletions, installBashCompletions } from "./completions";
|
|
7
|
-
import { DEFAULT_CONFIG, getConfigPath, loadConfig, saveConfig } from "./config";
|
|
7
|
+
import { DEFAULT_CONFIG, getConfigPath, loadConfig, loadUserConfig, saveConfig } from "./config";
|
|
8
8
|
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./config-cli";
|
|
9
9
|
import { closeDatabase, openDatabase } from "./db";
|
|
10
10
|
import { ConfigError, NotFoundError, UsageError } from "./errors";
|
|
11
11
|
import { akmIndex } from "./indexer";
|
|
12
12
|
import { assembleInfo } from "./info";
|
|
13
13
|
import { akmInit } from "./init";
|
|
14
|
+
import { formatInstallAuditSummary } from "./install-audit";
|
|
14
15
|
import { akmListSources, akmRemove, akmUpdate } from "./installed-kits";
|
|
15
16
|
import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
|
|
16
17
|
import { buildRegistryIndex, writeRegistryIndex } from "./registry-build-index";
|
|
@@ -338,6 +339,9 @@ function formatPlain(command, result, detail) {
|
|
|
338
339
|
case "search": {
|
|
339
340
|
return formatSearchPlain(r, detail);
|
|
340
341
|
}
|
|
342
|
+
case "curate": {
|
|
343
|
+
return formatCuratePlain(r, detail);
|
|
344
|
+
}
|
|
341
345
|
case "list": {
|
|
342
346
|
const sources = Array.isArray(r.sources) ? r.sources : [];
|
|
343
347
|
if (sources.length === 0)
|
|
@@ -356,7 +360,13 @@ function formatPlain(command, result, detail) {
|
|
|
356
360
|
const index = r.index;
|
|
357
361
|
const scanned = index?.directoriesScanned ?? 0;
|
|
358
362
|
const total = index?.totalEntries ?? 0;
|
|
359
|
-
|
|
363
|
+
const lines = [`Installed ${r.ref} (${scanned} directories scanned, ${total} total assets indexed)`];
|
|
364
|
+
const installed = r.installed;
|
|
365
|
+
const audit = installed?.audit;
|
|
366
|
+
if (audit && typeof audit === "object") {
|
|
367
|
+
lines.push(formatInstallAuditSummary(audit));
|
|
368
|
+
}
|
|
369
|
+
return lines.join("\n");
|
|
360
370
|
}
|
|
361
371
|
case "remove": {
|
|
362
372
|
const target = r.target ?? r.ref ?? "";
|
|
@@ -464,6 +474,275 @@ function formatSearchPlain(r, detail) {
|
|
|
464
474
|
}
|
|
465
475
|
return lines.join("\n").trimEnd();
|
|
466
476
|
}
|
|
477
|
+
function formatCuratePlain(r, detail) {
|
|
478
|
+
const query = typeof r.query === "string" ? r.query : "";
|
|
479
|
+
const summary = typeof r.summary === "string" ? r.summary : "";
|
|
480
|
+
const items = Array.isArray(r.items) ? r.items : [];
|
|
481
|
+
const lines = [`Curated results for "${query}"`];
|
|
482
|
+
if (summary)
|
|
483
|
+
lines.push(summary);
|
|
484
|
+
if (items.length === 0) {
|
|
485
|
+
if (r.tip)
|
|
486
|
+
lines.push(String(r.tip));
|
|
487
|
+
return lines.join("\n");
|
|
488
|
+
}
|
|
489
|
+
for (const item of items) {
|
|
490
|
+
const type = typeof item.type === "string" ? item.type : "unknown";
|
|
491
|
+
const name = typeof item.name === "string" ? item.name : "unnamed";
|
|
492
|
+
lines.push("");
|
|
493
|
+
lines.push(`[${type}] ${name}`);
|
|
494
|
+
if (item.description)
|
|
495
|
+
lines.push(` ${String(item.description)}`);
|
|
496
|
+
if (item.preview)
|
|
497
|
+
lines.push(` preview: ${String(item.preview)}`);
|
|
498
|
+
if (item.ref)
|
|
499
|
+
lines.push(` ref: ${String(item.ref)}`);
|
|
500
|
+
if (item.id)
|
|
501
|
+
lines.push(` id: ${String(item.id)}`);
|
|
502
|
+
if (Array.isArray(item.parameters) && item.parameters.length > 0) {
|
|
503
|
+
lines.push(` parameters: ${item.parameters.join(", ")}`);
|
|
504
|
+
}
|
|
505
|
+
if (item.run)
|
|
506
|
+
lines.push(` run: ${String(item.run)}`);
|
|
507
|
+
if (item.followUp)
|
|
508
|
+
lines.push(` show: ${String(item.followUp)}`);
|
|
509
|
+
if (detail !== "brief" && item.reason)
|
|
510
|
+
lines.push(` why: ${String(item.reason)}`);
|
|
511
|
+
}
|
|
512
|
+
const warnings = Array.isArray(r.warnings) ? r.warnings : [];
|
|
513
|
+
if (warnings.length > 0) {
|
|
514
|
+
lines.push("");
|
|
515
|
+
lines.push("Warnings:");
|
|
516
|
+
for (const warning of warnings) {
|
|
517
|
+
lines.push(`- ${String(warning)}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return lines.join("\n");
|
|
521
|
+
}
|
|
522
|
+
const CURATE_FALLBACK_FILTER_WORDS = new Set([
|
|
523
|
+
"a",
|
|
524
|
+
"an",
|
|
525
|
+
"and",
|
|
526
|
+
"for",
|
|
527
|
+
"how",
|
|
528
|
+
"i",
|
|
529
|
+
"in",
|
|
530
|
+
"of",
|
|
531
|
+
"or",
|
|
532
|
+
"the",
|
|
533
|
+
"to",
|
|
534
|
+
"with",
|
|
535
|
+
]);
|
|
536
|
+
const CURATED_TYPE_FALLBACK_ORDER = ["skill", "command", "script", "knowledge", "agent", "memory"];
|
|
537
|
+
const CURATED_TYPE_FALLBACK_INDEX = new Map(CURATED_TYPE_FALLBACK_ORDER.map((type, index) => [type, index]));
|
|
538
|
+
const MIN_CURATE_FALLBACK_TOKEN_LENGTH = 3;
|
|
539
|
+
const MAX_CURATE_FALLBACK_KEYWORDS = 6;
|
|
540
|
+
const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
|
|
541
|
+
const MIN_CURATE_SEARCH_LIMIT = 12;
|
|
542
|
+
async function curateSearchResults(query, result, limit, selectedType) {
|
|
543
|
+
const stashHits = result.hits.filter((hit) => hit.type !== "registry");
|
|
544
|
+
const registryHits = result.registryHits ?? [];
|
|
545
|
+
let selectedStashHits;
|
|
546
|
+
if (selectedType && selectedType !== "any") {
|
|
547
|
+
selectedStashHits = stashHits.slice(0, limit);
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
const bestByType = new Map();
|
|
551
|
+
for (const hit of stashHits) {
|
|
552
|
+
if (!bestByType.has(hit.type))
|
|
553
|
+
bestByType.set(hit.type, hit);
|
|
554
|
+
}
|
|
555
|
+
const orderedTypes = orderCuratedTypes(query, Array.from(bestByType.keys()));
|
|
556
|
+
selectedStashHits = orderedTypes
|
|
557
|
+
.map((type) => bestByType.get(type))
|
|
558
|
+
.filter((hit) => Boolean(hit));
|
|
559
|
+
}
|
|
560
|
+
const selectedRegistryHits = selectedStashHits.length >= limit ? [] : registryHits.slice(0, Math.min(2, limit - selectedStashHits.length));
|
|
561
|
+
const items = [
|
|
562
|
+
...(await Promise.all(selectedStashHits.slice(0, limit).map((hit) => enrichCuratedStashHit(query, hit)))),
|
|
563
|
+
...selectedRegistryHits.map((hit) => buildCuratedRegistryItem(query, hit)),
|
|
564
|
+
].slice(0, limit);
|
|
565
|
+
return {
|
|
566
|
+
query,
|
|
567
|
+
summary: buildCurateSummary(query, items),
|
|
568
|
+
items,
|
|
569
|
+
...(result.warnings?.length ? { warnings: result.warnings } : {}),
|
|
570
|
+
...(result.tip ? { tip: result.tip } : {}),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function orderCuratedTypes(query, types) {
|
|
574
|
+
const lower = query.toLowerCase();
|
|
575
|
+
const boosts = new Map();
|
|
576
|
+
const addBoost = (type, amount) => boosts.set(type, (boosts.get(type) ?? 0) + amount);
|
|
577
|
+
if (/(run|script|bash|shell|cli|execute|automation|deploy|build|test|lint)/.test(lower)) {
|
|
578
|
+
addBoost("script", 6);
|
|
579
|
+
addBoost("command", 4);
|
|
580
|
+
}
|
|
581
|
+
if (/(guide|docs?|readme|reference|how|explain|learn|why)/.test(lower)) {
|
|
582
|
+
addBoost("knowledge", 6);
|
|
583
|
+
addBoost("skill", 4);
|
|
584
|
+
}
|
|
585
|
+
if (/(agent|assistant|planner|review|analy[sz]e|architect|prompt)/.test(lower)) {
|
|
586
|
+
addBoost("agent", 6);
|
|
587
|
+
addBoost("skill", 3);
|
|
588
|
+
}
|
|
589
|
+
if (/(config|template|release|generate|command)/.test(lower)) {
|
|
590
|
+
addBoost("command", 5);
|
|
591
|
+
}
|
|
592
|
+
if (/(memory|context|recall|remember)/.test(lower)) {
|
|
593
|
+
addBoost("memory", 6);
|
|
594
|
+
}
|
|
595
|
+
return [...types].sort((a, b) => {
|
|
596
|
+
const boostDiff = (boosts.get(b) ?? 0) - (boosts.get(a) ?? 0);
|
|
597
|
+
if (boostDiff !== 0)
|
|
598
|
+
return boostDiff;
|
|
599
|
+
return ((CURATED_TYPE_FALLBACK_INDEX.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
|
600
|
+
(CURATED_TYPE_FALLBACK_INDEX.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
async function enrichCuratedStashHit(query, hit) {
|
|
604
|
+
let shown;
|
|
605
|
+
try {
|
|
606
|
+
shown = await akmShowUnified({ ref: hit.ref });
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
shown = undefined;
|
|
610
|
+
}
|
|
611
|
+
const description = shown?.description ?? hit.description;
|
|
612
|
+
const preview = buildCuratedPreview(shown, hit);
|
|
613
|
+
return {
|
|
614
|
+
source: "stash",
|
|
615
|
+
type: shown?.type ?? hit.type,
|
|
616
|
+
name: shown?.name ?? hit.name,
|
|
617
|
+
ref: hit.ref,
|
|
618
|
+
...(description ? { description } : {}),
|
|
619
|
+
...(preview ? { preview } : {}),
|
|
620
|
+
...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
|
|
621
|
+
...(shown?.run ? { run: shown.run } : {}),
|
|
622
|
+
followUp: `akm show ${hit.ref}`,
|
|
623
|
+
reason: buildCuratedReason(query, shown?.type ?? hit.type),
|
|
624
|
+
...(hit.score !== undefined ? { score: hit.score } : {}),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function buildCuratedRegistryItem(query, hit) {
|
|
628
|
+
return {
|
|
629
|
+
source: "registry",
|
|
630
|
+
type: "registry",
|
|
631
|
+
name: hit.name,
|
|
632
|
+
id: hit.id,
|
|
633
|
+
...(hit.description ? { description: hit.description } : {}),
|
|
634
|
+
followUp: hit.action ?? `akm add ${hit.id}`,
|
|
635
|
+
reason: `Useful external source to explore for ${query}.`,
|
|
636
|
+
...(hit.score !== undefined ? { score: hit.score } : {}),
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function firstNonEmpty(values) {
|
|
640
|
+
return values.find((value) => typeof value === "string" && value.trim().length > 0);
|
|
641
|
+
}
|
|
642
|
+
function buildCuratedPreview(shown, hit) {
|
|
643
|
+
if (shown?.run)
|
|
644
|
+
return truncateDescription(`run ${shown.run}`, 160);
|
|
645
|
+
const payload = firstNonEmpty([shown?.template, shown?.prompt, shown?.content, hit.description])
|
|
646
|
+
?.replace(/\s+/g, " ")
|
|
647
|
+
.trim();
|
|
648
|
+
return payload ? truncateDescription(payload, 160) : undefined;
|
|
649
|
+
}
|
|
650
|
+
function buildCuratedReason(query, type) {
|
|
651
|
+
switch (type) {
|
|
652
|
+
case "script":
|
|
653
|
+
return `Best runnable script match for "${query}".`;
|
|
654
|
+
case "command":
|
|
655
|
+
return `Best reusable command/template match for "${query}".`;
|
|
656
|
+
case "knowledge":
|
|
657
|
+
return `Best reference document match for "${query}".`;
|
|
658
|
+
case "skill":
|
|
659
|
+
return `Best instructions/workflow match for "${query}".`;
|
|
660
|
+
case "agent":
|
|
661
|
+
return `Best specialized agent prompt match for "${query}".`;
|
|
662
|
+
case "memory":
|
|
663
|
+
return `Best saved context match for "${query}".`;
|
|
664
|
+
default:
|
|
665
|
+
return `Best ${type} match for "${query}".`;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function buildCurateSummary(query, items) {
|
|
669
|
+
if (items.length === 0) {
|
|
670
|
+
return `No curated assets were selected for "${query}".`;
|
|
671
|
+
}
|
|
672
|
+
const labels = items.map((item) => `${item.type}:${item.name}`);
|
|
673
|
+
return `Selected ${items.length} high-signal result${items.length === 1 ? "" : "s"}: ${labels.join(", ")}.`;
|
|
674
|
+
}
|
|
675
|
+
function hasSearchResults(result) {
|
|
676
|
+
return result.hits.length > 0 || (result.registryHits?.length ?? 0) > 0;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Extract a small set of fallback keywords when a prompt-style curate query
|
|
680
|
+
* returns no hits as a whole phrase.
|
|
681
|
+
*
|
|
682
|
+
* We keep up to MAX_CURATE_FALLBACK_KEYWORDS distinct keywords and drop short
|
|
683
|
+
* or common filler words so follow-up searches stay inexpensive while focusing
|
|
684
|
+
* on higher-signal terms.
|
|
685
|
+
*/
|
|
686
|
+
function deriveCurateFallbackQueries(query) {
|
|
687
|
+
return Array.from(new Set(query
|
|
688
|
+
.toLowerCase()
|
|
689
|
+
.split(/[^a-z0-9]+/)
|
|
690
|
+
.map((token) => token.trim())
|
|
691
|
+
// Keep longer tokens so fallback stays focused on higher-signal terms
|
|
692
|
+
// and avoids broad one- and two-letter matches that overwhelm curation.
|
|
693
|
+
.filter((token) => token.length >= MIN_CURATE_FALLBACK_TOKEN_LENGTH && !CURATE_FALLBACK_FILTER_WORDS.has(token)))).slice(0, MAX_CURATE_FALLBACK_KEYWORDS);
|
|
694
|
+
}
|
|
695
|
+
function mergeCurateSearchResponses(base, extras) {
|
|
696
|
+
const hitsByRef = new Map();
|
|
697
|
+
for (const hit of base.hits.filter((entry) => entry.type !== "registry")) {
|
|
698
|
+
hitsByRef.set(hit.ref, hit);
|
|
699
|
+
}
|
|
700
|
+
for (const result of extras) {
|
|
701
|
+
for (const hit of result.hits.filter((entry) => entry.type !== "registry")) {
|
|
702
|
+
const existing = hitsByRef.get(hit.ref);
|
|
703
|
+
if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
|
|
704
|
+
hitsByRef.set(hit.ref, hit);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const registryById = new Map();
|
|
709
|
+
for (const hit of base.registryHits ?? []) {
|
|
710
|
+
registryById.set(hit.id, hit);
|
|
711
|
+
}
|
|
712
|
+
for (const result of extras) {
|
|
713
|
+
for (const hit of result.registryHits ?? []) {
|
|
714
|
+
const existing = registryById.get(hit.id);
|
|
715
|
+
if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
|
|
716
|
+
registryById.set(hit.id, hit);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const warnings = Array.from(new Set([...(base.warnings ?? []), ...extras.flatMap((result) => result.warnings ?? [])]));
|
|
721
|
+
const mergedHits = [...hitsByRef.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
722
|
+
const mergedRegistryHits = [...registryById.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
723
|
+
return {
|
|
724
|
+
...base,
|
|
725
|
+
hits: mergedHits,
|
|
726
|
+
...(mergedRegistryHits.length > 0 ? { registryHits: mergedRegistryHits } : {}),
|
|
727
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
728
|
+
...(mergedHits.length > 0 || mergedRegistryHits.length > 0 ? { tip: undefined } : {}),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
async function searchForCuration(input) {
|
|
732
|
+
const initial = await akmSearch(input);
|
|
733
|
+
if (hasSearchResults(initial))
|
|
734
|
+
return initial;
|
|
735
|
+
const fallbackQueries = deriveCurateFallbackQueries(input.query);
|
|
736
|
+
if (fallbackQueries.length <= 1)
|
|
737
|
+
return initial;
|
|
738
|
+
const fallbackResults = await Promise.all(fallbackQueries.map((token) => akmSearch({
|
|
739
|
+
query: token,
|
|
740
|
+
type: input.type,
|
|
741
|
+
limit: input.limit,
|
|
742
|
+
source: input.source,
|
|
743
|
+
})));
|
|
744
|
+
return mergeCurateSearchResponses(initial, fallbackResults);
|
|
745
|
+
}
|
|
467
746
|
/**
|
|
468
747
|
* Module Naming:
|
|
469
748
|
* - stash-* : Asset operations (search, show, add, clone)
|
|
@@ -550,20 +829,55 @@ const searchCommand = defineCommand({
|
|
|
550
829
|
});
|
|
551
830
|
},
|
|
552
831
|
});
|
|
832
|
+
const curateCommand = defineCommand({
|
|
833
|
+
meta: { name: "curate", description: "Curate the best matching assets for a task or prompt" },
|
|
834
|
+
args: {
|
|
835
|
+
query: { type: "positional", description: "Task or prompt to curate assets for", required: true },
|
|
836
|
+
type: {
|
|
837
|
+
type: "string",
|
|
838
|
+
description: "Asset type filter (e.g. skill, command, agent, knowledge, script, memory, or any).",
|
|
839
|
+
},
|
|
840
|
+
limit: { type: "string", description: "Maximum number of curated results", default: "4" },
|
|
841
|
+
source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
|
|
842
|
+
},
|
|
843
|
+
async run({ args }) {
|
|
844
|
+
await runWithJsonErrors(async () => {
|
|
845
|
+
const type = args.type;
|
|
846
|
+
const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
|
|
847
|
+
if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
|
|
848
|
+
throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
|
|
849
|
+
}
|
|
850
|
+
const limit = limitRaw && limitRaw > 0 ? limitRaw : 4;
|
|
851
|
+
const source = parseSearchSource(args.source ?? "stash");
|
|
852
|
+
const searchResult = await searchForCuration({
|
|
853
|
+
query: args.query,
|
|
854
|
+
type,
|
|
855
|
+
// Search deeper than the final curated count so we can pick one strong
|
|
856
|
+
// match per type and still have room for fallback retries.
|
|
857
|
+
limit: Math.max(limit * CURATE_SEARCH_LIMIT_MULTIPLIER, MIN_CURATE_SEARCH_LIMIT),
|
|
858
|
+
source,
|
|
859
|
+
});
|
|
860
|
+
const curated = await curateSearchResults(args.query, searchResult, limit, type);
|
|
861
|
+
output("curate", curated);
|
|
862
|
+
});
|
|
863
|
+
},
|
|
864
|
+
});
|
|
553
865
|
const addCommand = defineCommand({
|
|
554
866
|
meta: {
|
|
555
867
|
name: "add",
|
|
556
|
-
description: "Add a source (local directory, npm package, GitHub repo, git URL, or remote provider)",
|
|
868
|
+
description: "Add a source (local directory, website, npm package, GitHub repo, git URL, or remote provider)",
|
|
557
869
|
},
|
|
558
870
|
args: {
|
|
559
871
|
ref: {
|
|
560
872
|
type: "positional",
|
|
561
|
-
description: "Path, URL, or registry ref (npm package, owner/repo, git URL, or local directory)",
|
|
873
|
+
description: "Path, URL, or registry ref (website URL, npm package, owner/repo, git URL, or local directory)",
|
|
562
874
|
required: true,
|
|
563
875
|
},
|
|
564
876
|
provider: { type: "string", description: "Provider type (e.g. openviking). Required for URL sources." },
|
|
565
877
|
options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
|
|
566
878
|
name: { type: "string", description: "Human-friendly name for the source" },
|
|
879
|
+
"max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
|
|
880
|
+
"max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
|
|
567
881
|
},
|
|
568
882
|
async run({ args }) {
|
|
569
883
|
await runWithJsonErrors(async () => {
|
|
@@ -580,7 +894,7 @@ const addCommand = defineCommand({
|
|
|
580
894
|
}
|
|
581
895
|
// URL with --provider → stash source (remote or git provider)
|
|
582
896
|
if (args.provider) {
|
|
583
|
-
if (ref
|
|
897
|
+
if (shouldWarnOnPlainHttp(ref)) {
|
|
584
898
|
warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
585
899
|
}
|
|
586
900
|
let parsedOptions;
|
|
@@ -607,7 +921,19 @@ const addCommand = defineCommand({
|
|
|
607
921
|
output("stash-add", result);
|
|
608
922
|
return;
|
|
609
923
|
}
|
|
610
|
-
|
|
924
|
+
if (shouldWarnOnPlainHttp(ref)) {
|
|
925
|
+
warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
926
|
+
}
|
|
927
|
+
const websiteOptions = {};
|
|
928
|
+
if (args["max-pages"])
|
|
929
|
+
websiteOptions.maxPages = args["max-pages"];
|
|
930
|
+
if (args["max-depth"])
|
|
931
|
+
websiteOptions.maxDepth = args["max-depth"];
|
|
932
|
+
const result = await akmAdd({
|
|
933
|
+
ref,
|
|
934
|
+
name: args.name,
|
|
935
|
+
options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
|
|
936
|
+
});
|
|
611
937
|
output("add", result);
|
|
612
938
|
});
|
|
613
939
|
},
|
|
@@ -624,6 +950,22 @@ function parseKindFilter(raw) {
|
|
|
624
950
|
}
|
|
625
951
|
return kinds;
|
|
626
952
|
}
|
|
953
|
+
function shouldWarnOnPlainHttp(ref) {
|
|
954
|
+
if (!ref.startsWith("http://"))
|
|
955
|
+
return false;
|
|
956
|
+
try {
|
|
957
|
+
const hostname = new URL(ref).hostname.toLowerCase();
|
|
958
|
+
return (hostname !== "localhost" &&
|
|
959
|
+
hostname !== "127.0.0.1" &&
|
|
960
|
+
hostname !== "0.0.0.0" &&
|
|
961
|
+
hostname !== "::1" &&
|
|
962
|
+
hostname !== "[::1]" &&
|
|
963
|
+
!hostname.endsWith(".localhost"));
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
627
969
|
const listCommand = defineCommand({
|
|
628
970
|
meta: { name: "list", description: "List all sources (local directories, managed packages, remote providers)" },
|
|
629
971
|
args: {
|
|
@@ -796,7 +1138,7 @@ const configCommand = defineCommand({
|
|
|
796
1138
|
},
|
|
797
1139
|
run({ args }) {
|
|
798
1140
|
return runWithJsonErrors(() => {
|
|
799
|
-
const updated = setConfigValue(
|
|
1141
|
+
const updated = setConfigValue(loadUserConfig(), args.key, args.value);
|
|
800
1142
|
saveConfig(updated);
|
|
801
1143
|
output("config", listConfig(updated));
|
|
802
1144
|
});
|
|
@@ -809,7 +1151,7 @@ const configCommand = defineCommand({
|
|
|
809
1151
|
},
|
|
810
1152
|
run({ args }) {
|
|
811
1153
|
return runWithJsonErrors(() => {
|
|
812
|
-
const updated = unsetConfigValue(
|
|
1154
|
+
const updated = unsetConfigValue(loadUserConfig(), args.key);
|
|
813
1155
|
saveConfig(updated);
|
|
814
1156
|
output("config", listConfig(updated));
|
|
815
1157
|
});
|
|
@@ -858,7 +1200,7 @@ const registryCommand = defineCommand({
|
|
|
858
1200
|
meta: { name: "list", description: "List configured registries" },
|
|
859
1201
|
run() {
|
|
860
1202
|
return runWithJsonErrors(() => {
|
|
861
|
-
const config =
|
|
1203
|
+
const config = loadUserConfig();
|
|
862
1204
|
const registries = config.registries ?? DEFAULT_CONFIG.registries;
|
|
863
1205
|
output("registry-list", { registries });
|
|
864
1206
|
});
|
|
@@ -880,7 +1222,7 @@ const registryCommand = defineCommand({
|
|
|
880
1222
|
if (args.url.startsWith("http://")) {
|
|
881
1223
|
warn("Warning: registry URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
882
1224
|
}
|
|
883
|
-
const config =
|
|
1225
|
+
const config = loadUserConfig();
|
|
884
1226
|
const registries = [...(config.registries ?? [])];
|
|
885
1227
|
// Deduplicate by URL
|
|
886
1228
|
if (registries.some((r) => r.url === args.url)) {
|
|
@@ -913,7 +1255,7 @@ const registryCommand = defineCommand({
|
|
|
913
1255
|
},
|
|
914
1256
|
run({ args }) {
|
|
915
1257
|
return runWithJsonErrors(() => {
|
|
916
|
-
const config =
|
|
1258
|
+
const config = loadUserConfig();
|
|
917
1259
|
const registries = [...(config.registries ?? [])];
|
|
918
1260
|
const idx = registries.findIndex((r) => r.url === args.target || r.name === args.target);
|
|
919
1261
|
if (idx === -1) {
|
|
@@ -1081,6 +1423,7 @@ const main = defineCommand({
|
|
|
1081
1423
|
update: updateCommand,
|
|
1082
1424
|
upgrade: upgradeCommand,
|
|
1083
1425
|
search: searchCommand,
|
|
1426
|
+
curate: curateCommand,
|
|
1084
1427
|
show: showCommand,
|
|
1085
1428
|
clone: cloneCommand,
|
|
1086
1429
|
registry: registryCommand,
|
|
@@ -1245,6 +1588,7 @@ You have access to a searchable library of scripts, skills, commands, agents, an
|
|
|
1245
1588
|
|
|
1246
1589
|
\`\`\`sh
|
|
1247
1590
|
akm search "<query>" # Search all sources
|
|
1591
|
+
akm curate "<task>" # Curate the best matches for a task
|
|
1248
1592
|
akm search "<query>" --type skill # Filter by type
|
|
1249
1593
|
akm search "<query>" --source both # Also search registries
|
|
1250
1594
|
akm show <ref> # View asset details
|
|
@@ -1273,6 +1617,7 @@ You have access to a searchable library of scripts, skills, commands, agents, an
|
|
|
1273
1617
|
|
|
1274
1618
|
\`\`\`sh
|
|
1275
1619
|
akm search "<query>" # Search all sources
|
|
1620
|
+
akm curate "<task>" # Curate the best matches for a task
|
|
1276
1621
|
akm search "<query>" --type skill # Filter by asset type
|
|
1277
1622
|
akm search "<query>" --source both # Also search registries
|
|
1278
1623
|
akm search "<query>" --source registry # Search registries only
|
|
@@ -1289,6 +1634,16 @@ akm search "<query>" --detail full # Include scores, paths, timing
|
|
|
1289
1634
|
| \`--detail\` | \`brief\`, \`normal\`, \`full\`, \`summary\` | \`brief\` |
|
|
1290
1635
|
| \`--for-agent\` | boolean | \`false\` |
|
|
1291
1636
|
|
|
1637
|
+
## Curate
|
|
1638
|
+
|
|
1639
|
+
Combine search + follow-up hints into a dense summary for a task or prompt.
|
|
1640
|
+
|
|
1641
|
+
\`\`\`sh
|
|
1642
|
+
akm curate "plan a release" # Pick top matches across asset types
|
|
1643
|
+
akm curate "deploy a Bun app" --limit 3 # Keep the summary shorter
|
|
1644
|
+
akm curate "review architecture" --type skill # Restrict to one asset type
|
|
1645
|
+
\`\`\`
|
|
1646
|
+
|
|
1292
1647
|
## Show
|
|
1293
1648
|
|
|
1294
1649
|
Display an asset by ref. Knowledge assets support view modes as positional arguments.
|
package/dist/common.js
CHANGED
|
@@ -8,6 +8,11 @@ export const IS_WINDOWS = process.platform === "win32";
|
|
|
8
8
|
export function isHttpUrl(value) {
|
|
9
9
|
return !!value && /^https?:\/\//.test(value);
|
|
10
10
|
}
|
|
11
|
+
export function filterNonEmptyStrings(value) {
|
|
12
|
+
if (!Array.isArray(value))
|
|
13
|
+
return undefined;
|
|
14
|
+
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
15
|
+
}
|
|
11
16
|
// ── Validators ──────────────────────────────────────────────────────────────
|
|
12
17
|
export function isAssetType(type) {
|
|
13
18
|
return Object.hasOwn(TYPE_DIRS, type);
|
package/dist/config-cli.js
CHANGED
|
@@ -26,6 +26,16 @@ export function parseConfigValue(key, value) {
|
|
|
26
26
|
return { output: { format: parseOutputFormat(value) } };
|
|
27
27
|
case "output.detail":
|
|
28
28
|
return { output: { detail: parseOutputDetail(value) } };
|
|
29
|
+
case "security.installAudit.enabled":
|
|
30
|
+
return { security: { installAudit: { enabled: parseBooleanValue(value, key) } } };
|
|
31
|
+
case "security.installAudit.blockOnCritical":
|
|
32
|
+
return { security: { installAudit: { blockOnCritical: parseBooleanValue(value, key) } } };
|
|
33
|
+
case "security.installAudit.blockUnlistedRegistries":
|
|
34
|
+
return { security: { installAudit: { blockUnlistedRegistries: parseBooleanValue(value, key) } } };
|
|
35
|
+
case "security.installAudit.registryAllowlist":
|
|
36
|
+
return { security: { installAudit: { registryAllowlist: parseStringArrayValue(value, key) } } };
|
|
37
|
+
case "security.installAudit.registryWhitelist":
|
|
38
|
+
return { security: { installAudit: { registryAllowlist: parseStringArrayValue(value, key) } } };
|
|
29
39
|
default:
|
|
30
40
|
throw new UsageError(`Unknown config key: ${key}`);
|
|
31
41
|
}
|
|
@@ -48,6 +58,18 @@ export function getConfigValue(config, key) {
|
|
|
48
58
|
return config.output?.format ?? null;
|
|
49
59
|
case "output.detail":
|
|
50
60
|
return config.output?.detail ?? null;
|
|
61
|
+
case "security":
|
|
62
|
+
return config.security ?? null;
|
|
63
|
+
case "security.installAudit.enabled":
|
|
64
|
+
return config.security?.installAudit?.enabled ?? null;
|
|
65
|
+
case "security.installAudit.blockOnCritical":
|
|
66
|
+
return config.security?.installAudit?.blockOnCritical ?? null;
|
|
67
|
+
case "security.installAudit.blockUnlistedRegistries":
|
|
68
|
+
return config.security?.installAudit?.blockUnlistedRegistries ?? null;
|
|
69
|
+
case "security.installAudit.registryAllowlist":
|
|
70
|
+
return getInstallAuditAllowlist(config);
|
|
71
|
+
case "security.installAudit.registryWhitelist":
|
|
72
|
+
return getInstallAuditAllowlist(config);
|
|
51
73
|
default:
|
|
52
74
|
throw new UsageError(`Unknown config key: ${key}`);
|
|
53
75
|
}
|
|
@@ -62,6 +84,11 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
62
84
|
case "stashes":
|
|
63
85
|
case "output.format":
|
|
64
86
|
case "output.detail":
|
|
87
|
+
case "security.installAudit.enabled":
|
|
88
|
+
case "security.installAudit.blockOnCritical":
|
|
89
|
+
case "security.installAudit.blockUnlistedRegistries":
|
|
90
|
+
case "security.installAudit.registryAllowlist":
|
|
91
|
+
case "security.installAudit.registryWhitelist":
|
|
65
92
|
return mergeConfigValue(config, parseConfigValue(key, rawValue));
|
|
66
93
|
default:
|
|
67
94
|
throw new UsageError(`Unknown config key: ${key}`);
|
|
@@ -83,6 +110,28 @@ export function unsetConfigValue(config, key) {
|
|
|
83
110
|
return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
|
|
84
111
|
case "output.detail":
|
|
85
112
|
return { ...config, output: mergeOutputConfig(config.output, { detail: undefined }) };
|
|
113
|
+
case "security":
|
|
114
|
+
return { ...config, security: undefined };
|
|
115
|
+
case "security.installAudit.enabled":
|
|
116
|
+
return { ...config, security: mergeSecurityConfig(config.security, { installAudit: { enabled: undefined } }) };
|
|
117
|
+
case "security.installAudit.blockOnCritical":
|
|
118
|
+
return {
|
|
119
|
+
...config,
|
|
120
|
+
security: mergeSecurityConfig(config.security, { installAudit: { blockOnCritical: undefined } }),
|
|
121
|
+
};
|
|
122
|
+
case "security.installAudit.blockUnlistedRegistries":
|
|
123
|
+
return {
|
|
124
|
+
...config,
|
|
125
|
+
security: mergeSecurityConfig(config.security, { installAudit: { blockUnlistedRegistries: undefined } }),
|
|
126
|
+
};
|
|
127
|
+
case "security.installAudit.registryAllowlist":
|
|
128
|
+
case "security.installAudit.registryWhitelist":
|
|
129
|
+
return {
|
|
130
|
+
...config,
|
|
131
|
+
security: mergeSecurityConfig(config.security, {
|
|
132
|
+
installAudit: { registryAllowlist: undefined, registryWhitelist: undefined },
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
86
135
|
default:
|
|
87
136
|
throw new UsageError(`Unknown or unsupported unset key: ${key}`);
|
|
88
137
|
}
|
|
@@ -100,6 +149,8 @@ export function listConfig(config) {
|
|
|
100
149
|
result.embedding = config.embedding;
|
|
101
150
|
if (config.llm)
|
|
102
151
|
result.llm = config.llm;
|
|
152
|
+
if (config.security)
|
|
153
|
+
result.security = config.security;
|
|
103
154
|
return result;
|
|
104
155
|
}
|
|
105
156
|
function mergeConfigValue(config, partial) {
|
|
@@ -107,6 +158,7 @@ function mergeConfigValue(config, partial) {
|
|
|
107
158
|
...config,
|
|
108
159
|
...partial,
|
|
109
160
|
output: mergeOutputConfig(config.output, partial.output),
|
|
161
|
+
security: mergeSecurityConfig(config.security, partial.security),
|
|
110
162
|
};
|
|
111
163
|
}
|
|
112
164
|
function mergeOutputConfig(base, override) {
|
|
@@ -116,6 +168,18 @@ function mergeOutputConfig(base, override) {
|
|
|
116
168
|
};
|
|
117
169
|
return merged.format || merged.detail ? merged : undefined;
|
|
118
170
|
}
|
|
171
|
+
function mergeSecurityConfig(base, override) {
|
|
172
|
+
const mergedInstallAudit = mergeInstallAuditConfig(base?.installAudit, override?.installAudit);
|
|
173
|
+
return mergedInstallAudit ? { installAudit: mergedInstallAudit } : undefined;
|
|
174
|
+
}
|
|
175
|
+
function mergeInstallAuditConfig(base, override) {
|
|
176
|
+
const merged = {
|
|
177
|
+
...(base ?? {}),
|
|
178
|
+
...(override ?? {}),
|
|
179
|
+
};
|
|
180
|
+
const hasValue = Object.values(merged).some((value) => value !== undefined);
|
|
181
|
+
return hasValue ? merged : undefined;
|
|
182
|
+
}
|
|
119
183
|
function parseOutputFormat(value) {
|
|
120
184
|
if (value === "json" || value === "yaml" || value === "text")
|
|
121
185
|
return value;
|
|
@@ -126,6 +190,29 @@ function parseOutputDetail(value) {
|
|
|
126
190
|
return value;
|
|
127
191
|
throw new UsageError(`Invalid value for output.detail: expected one of brief|normal|full`);
|
|
128
192
|
}
|
|
193
|
+
function parseBooleanValue(value, key) {
|
|
194
|
+
if (value === "true")
|
|
195
|
+
return true;
|
|
196
|
+
if (value === "false")
|
|
197
|
+
return false;
|
|
198
|
+
throw new UsageError(`Invalid value for ${key}: expected true or false`);
|
|
199
|
+
}
|
|
200
|
+
function parseStringArrayValue(value, key) {
|
|
201
|
+
let parsed;
|
|
202
|
+
try {
|
|
203
|
+
parsed = JSON.parse(value);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
throw new UsageError(`Invalid value for ${key}: expected a JSON array of strings`);
|
|
207
|
+
}
|
|
208
|
+
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
|
|
209
|
+
throw new UsageError(`Invalid value for ${key}: expected a JSON array of strings`);
|
|
210
|
+
}
|
|
211
|
+
return parsed;
|
|
212
|
+
}
|
|
213
|
+
function getInstallAuditAllowlist(config) {
|
|
214
|
+
return config.security?.installAudit?.registryAllowlist ?? config.security?.installAudit?.registryWhitelist ?? null;
|
|
215
|
+
}
|
|
129
216
|
function parseRegistriesValue(value) {
|
|
130
217
|
if (value === "null" || value === "")
|
|
131
218
|
return undefined;
|