akm-cli 0.2.2 → 0.3.0-rc2
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 +106 -140
- package/dist/config-cli.js +26 -8
- package/dist/config.js +12 -3
- package/dist/db.js +58 -1
- package/dist/embedder.js +32 -3
- package/dist/indexer.js +95 -19
- package/dist/info.js +10 -1
- package/dist/installed-kits.js +111 -34
- package/dist/local-search.js +35 -2
- package/dist/paths.js +3 -0
- package/dist/semantic-status.js +137 -0
- package/dist/setup.js +90 -23
- package/dist/stash-add.js +0 -18
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -11,16 +11,16 @@ 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 {
|
|
14
|
+
import { akmListSources, akmRemove, akmUpdate } from "./installed-kits";
|
|
15
15
|
import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
|
|
16
16
|
import { buildRegistryIndex, writeRegistryIndex } from "./registry-build-index";
|
|
17
17
|
import { searchRegistry } from "./registry-search";
|
|
18
18
|
import { checkForUpdate, performUpgrade } from "./self-update";
|
|
19
|
-
import { akmAdd
|
|
19
|
+
import { akmAdd } from "./stash-add";
|
|
20
20
|
import { akmClone } from "./stash-clone";
|
|
21
21
|
import { akmSearch, parseSearchSource } from "./stash-search";
|
|
22
22
|
import { akmShowUnified } from "./stash-show";
|
|
23
|
-
import { addStash
|
|
23
|
+
import { addStash } from "./stash-source-manage";
|
|
24
24
|
import { insertUsageEvent } from "./usage-events";
|
|
25
25
|
import { pkgVersion } from "./version";
|
|
26
26
|
import { setQuiet, warn } from "./warn";
|
|
@@ -138,6 +138,7 @@ function shapeSearchOutput(result, detail, forAgent = false) {
|
|
|
138
138
|
source: result.source,
|
|
139
139
|
hits: shapedHits,
|
|
140
140
|
...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}),
|
|
141
|
+
...(result.semanticSearch ? { semanticSearch: result.semanticSearch } : {}),
|
|
141
142
|
...(result.tip ? { tip: result.tip } : {}),
|
|
142
143
|
...(result.warnings ? { warnings: result.warnings } : {}),
|
|
143
144
|
...(result.timing ? { timing: result.timing } : {}),
|
|
@@ -146,8 +147,8 @@ function shapeSearchOutput(result, detail, forAgent = false) {
|
|
|
146
147
|
return {
|
|
147
148
|
hits: shapedHits,
|
|
148
149
|
...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}),
|
|
149
|
-
...(result.tip ? { tip: result.tip } : {}),
|
|
150
150
|
...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
|
|
151
|
+
...(result.tip ? { tip: result.tip } : {}),
|
|
151
152
|
};
|
|
152
153
|
}
|
|
153
154
|
function shapeRegistrySearchOutput(result, detail) {
|
|
@@ -337,6 +338,20 @@ function formatPlain(command, result, detail) {
|
|
|
337
338
|
case "search": {
|
|
338
339
|
return formatSearchPlain(r, detail);
|
|
339
340
|
}
|
|
341
|
+
case "list": {
|
|
342
|
+
const sources = Array.isArray(r.sources) ? r.sources : [];
|
|
343
|
+
if (sources.length === 0)
|
|
344
|
+
return "No sources configured. Use `akm add` to add a source.";
|
|
345
|
+
const lines = [];
|
|
346
|
+
for (const src of sources) {
|
|
347
|
+
const kind = typeof src.kind === "string" ? src.kind : "unknown";
|
|
348
|
+
const name = typeof src.name === "string" ? src.name : "unnamed";
|
|
349
|
+
const ver = typeof src.version === "string" ? ` v${src.version}` : "";
|
|
350
|
+
const prov = typeof src.provider === "string" ? ` (${src.provider})` : "";
|
|
351
|
+
lines.push(`[${kind}] ${name}${ver}${prov}`);
|
|
352
|
+
}
|
|
353
|
+
return lines.join("\n");
|
|
354
|
+
}
|
|
340
355
|
case "add": {
|
|
341
356
|
const index = r.index;
|
|
342
357
|
const scanned = index?.directoriesScanned ?? 0;
|
|
@@ -450,11 +465,11 @@ function formatSearchPlain(r, detail) {
|
|
|
450
465
|
return lines.join("\n").trimEnd();
|
|
451
466
|
}
|
|
452
467
|
/**
|
|
453
|
-
* Naming
|
|
454
|
-
* - stash-*
|
|
468
|
+
* Module Naming:
|
|
469
|
+
* - stash-* : Asset operations (search, show, add, clone)
|
|
455
470
|
* - stash-provider-* : Runtime data source providers (filesystem, openviking)
|
|
456
|
-
* - registry-*
|
|
457
|
-
* - installed-kits
|
|
471
|
+
* - registry-* : Discovery from remote registries (npm, GitHub)
|
|
472
|
+
* - installed-kits : Unified source operations (list, remove, update)
|
|
458
473
|
*/
|
|
459
474
|
const setupCommand = defineCommand({
|
|
460
475
|
meta: {
|
|
@@ -536,17 +551,25 @@ const searchCommand = defineCommand({
|
|
|
536
551
|
},
|
|
537
552
|
});
|
|
538
553
|
const addCommand = defineCommand({
|
|
539
|
-
meta: {
|
|
554
|
+
meta: {
|
|
555
|
+
name: "add",
|
|
556
|
+
description: "Add a source (local directory, npm package, GitHub repo, git URL, or remote provider)",
|
|
557
|
+
},
|
|
540
558
|
args: {
|
|
541
559
|
ref: {
|
|
542
560
|
type: "positional",
|
|
543
|
-
description: "
|
|
561
|
+
description: "Path, URL, or registry ref (npm package, owner/repo, git URL, or local directory)",
|
|
544
562
|
required: true,
|
|
545
563
|
},
|
|
564
|
+
provider: { type: "string", description: "Provider type (e.g. openviking). Required for URL sources." },
|
|
565
|
+
options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
|
|
566
|
+
name: { type: "string", description: "Human-friendly name for the source" },
|
|
546
567
|
},
|
|
547
568
|
async run({ args }) {
|
|
548
569
|
await runWithJsonErrors(async () => {
|
|
549
|
-
|
|
570
|
+
const ref = args.ref.trim();
|
|
571
|
+
// Context-hub convenience alias
|
|
572
|
+
if (ref === CONTEXT_HUB_ALIAS_REF) {
|
|
550
573
|
const result = addStash({
|
|
551
574
|
target: CONTEXT_HUB_ALIAS_URL,
|
|
552
575
|
providerType: "context-hub",
|
|
@@ -555,24 +578,69 @@ const addCommand = defineCommand({
|
|
|
555
578
|
output("stash-add", result);
|
|
556
579
|
return;
|
|
557
580
|
}
|
|
558
|
-
|
|
581
|
+
// URL with --provider → stash source (remote or git provider)
|
|
582
|
+
if (args.provider) {
|
|
583
|
+
if (ref.startsWith("http://")) {
|
|
584
|
+
warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
585
|
+
}
|
|
586
|
+
let parsedOptions;
|
|
587
|
+
if (args.options) {
|
|
588
|
+
try {
|
|
589
|
+
const parsed = JSON.parse(args.options);
|
|
590
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
591
|
+
throw new UsageError("--options must be a JSON object");
|
|
592
|
+
}
|
|
593
|
+
parsedOptions = parsed;
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
if (err instanceof UsageError)
|
|
597
|
+
throw err;
|
|
598
|
+
throw new UsageError("--options must be valid JSON");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const result = addStash({
|
|
602
|
+
target: ref,
|
|
603
|
+
name: args.name,
|
|
604
|
+
providerType: args.provider,
|
|
605
|
+
options: parsedOptions,
|
|
606
|
+
});
|
|
607
|
+
output("stash-add", result);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const result = await akmAdd({ ref });
|
|
559
611
|
output("add", result);
|
|
560
612
|
});
|
|
561
613
|
},
|
|
562
614
|
});
|
|
615
|
+
const VALID_SOURCE_KINDS = new Set(["local", "managed", "remote"]);
|
|
616
|
+
function parseKindFilter(raw) {
|
|
617
|
+
if (!raw)
|
|
618
|
+
return undefined;
|
|
619
|
+
const kinds = raw.split(",").map((s) => s.trim());
|
|
620
|
+
for (const k of kinds) {
|
|
621
|
+
if (!VALID_SOURCE_KINDS.has(k)) {
|
|
622
|
+
throw new UsageError(`Invalid --kind value: "${k}". Expected one of: local, managed, remote`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return kinds;
|
|
626
|
+
}
|
|
563
627
|
const listCommand = defineCommand({
|
|
564
|
-
meta: { name: "list", description: "List
|
|
565
|
-
|
|
628
|
+
meta: { name: "list", description: "List all sources (local directories, managed packages, remote providers)" },
|
|
629
|
+
args: {
|
|
630
|
+
kind: { type: "string", description: "Filter by source kind (local, managed, remote). Comma-separated." },
|
|
631
|
+
},
|
|
632
|
+
async run({ args }) {
|
|
566
633
|
await runWithJsonErrors(async () => {
|
|
567
|
-
const
|
|
634
|
+
const kind = parseKindFilter(args.kind);
|
|
635
|
+
const result = await akmListSources({ kind });
|
|
568
636
|
output("list", result);
|
|
569
637
|
});
|
|
570
638
|
},
|
|
571
639
|
});
|
|
572
640
|
const removeCommand = defineCommand({
|
|
573
|
-
meta: { name: "remove", description: "Remove
|
|
641
|
+
meta: { name: "remove", description: "Remove a source by id, ref, path, URL, or name" },
|
|
574
642
|
args: {
|
|
575
|
-
target: { type: "positional", description: "
|
|
643
|
+
target: { type: "positional", description: "Source to remove (id, ref, path, URL, or name)", required: true },
|
|
576
644
|
},
|
|
577
645
|
async run({ args }) {
|
|
578
646
|
await runWithJsonErrors(async () => {
|
|
@@ -582,9 +650,9 @@ const removeCommand = defineCommand({
|
|
|
582
650
|
},
|
|
583
651
|
});
|
|
584
652
|
const updateCommand = defineCommand({
|
|
585
|
-
meta: { name: "update", description: "Update one or all
|
|
653
|
+
meta: { name: "update", description: "Update one or all managed sources" },
|
|
586
654
|
args: {
|
|
587
|
-
target: { type: "positional", description: "
|
|
655
|
+
target: { type: "positional", description: "Source to update (id or ref)", required: false },
|
|
588
656
|
all: { type: "boolean", description: "Update all installed entries", default: false },
|
|
589
657
|
force: { type: "boolean", description: "Force fresh download even if version is unchanged", default: false },
|
|
590
658
|
},
|
|
@@ -595,31 +663,6 @@ const updateCommand = defineCommand({
|
|
|
595
663
|
});
|
|
596
664
|
},
|
|
597
665
|
});
|
|
598
|
-
const kitAddCommand = defineCommand({
|
|
599
|
-
meta: { name: "add", description: "Install a kit from npm, GitHub, or any git host" },
|
|
600
|
-
args: {
|
|
601
|
-
ref: {
|
|
602
|
-
type: "positional",
|
|
603
|
-
description: "Registry ref (npm package, owner/repo, or git URL)",
|
|
604
|
-
required: true,
|
|
605
|
-
},
|
|
606
|
-
},
|
|
607
|
-
async run({ args }) {
|
|
608
|
-
await runWithJsonErrors(async () => {
|
|
609
|
-
const result = await akmKitAdd({ ref: args.ref });
|
|
610
|
-
output("add", result);
|
|
611
|
-
});
|
|
612
|
-
},
|
|
613
|
-
});
|
|
614
|
-
const kitCommand = defineCommand({
|
|
615
|
-
meta: { name: "kit", description: "Manage installed kits" },
|
|
616
|
-
subCommands: {
|
|
617
|
-
add: kitAddCommand,
|
|
618
|
-
list: listCommand,
|
|
619
|
-
remove: removeCommand,
|
|
620
|
-
update: updateCommand,
|
|
621
|
-
},
|
|
622
|
-
});
|
|
623
666
|
const upgradeCommand = defineCommand({
|
|
624
667
|
meta: { name: "upgrade", description: "Upgrade akm to the latest release" },
|
|
625
668
|
args: {
|
|
@@ -930,75 +973,6 @@ const registryCommand = defineCommand({
|
|
|
930
973
|
}),
|
|
931
974
|
},
|
|
932
975
|
});
|
|
933
|
-
/**
|
|
934
|
-
* Subcommand definitions for managing additional stashes.
|
|
935
|
-
*/
|
|
936
|
-
function buildSourceSubCommands(outputPrefix) {
|
|
937
|
-
return {
|
|
938
|
-
list: defineCommand({
|
|
939
|
-
meta: { name: "list", description: "List all stashes in search order" },
|
|
940
|
-
run() {
|
|
941
|
-
return runWithJsonErrors(() => {
|
|
942
|
-
output(`${outputPrefix}`, listStashes());
|
|
943
|
-
});
|
|
944
|
-
},
|
|
945
|
-
}),
|
|
946
|
-
add: defineCommand({
|
|
947
|
-
meta: { name: "add", description: "Register an additional stash (filesystem path or remote URL)" },
|
|
948
|
-
args: {
|
|
949
|
-
target: { type: "positional", description: "Path or URL to add", required: true },
|
|
950
|
-
name: { type: "string", description: "Human-friendly name for the source" },
|
|
951
|
-
provider: { type: "string", description: "Provider type (e.g. openviking). Required for URLs." },
|
|
952
|
-
options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
|
|
953
|
-
},
|
|
954
|
-
run({ args }) {
|
|
955
|
-
return runWithJsonErrors(() => {
|
|
956
|
-
if (args.target.startsWith("http://")) {
|
|
957
|
-
warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
958
|
-
}
|
|
959
|
-
let parsedOptions;
|
|
960
|
-
if (args.options) {
|
|
961
|
-
try {
|
|
962
|
-
const parsed = JSON.parse(args.options);
|
|
963
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
964
|
-
throw new UsageError("--options must be a JSON object");
|
|
965
|
-
}
|
|
966
|
-
parsedOptions = parsed;
|
|
967
|
-
}
|
|
968
|
-
catch (err) {
|
|
969
|
-
if (err instanceof UsageError)
|
|
970
|
-
throw err;
|
|
971
|
-
throw new UsageError("--options must be valid JSON");
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
const result = addStash({
|
|
975
|
-
target: args.target,
|
|
976
|
-
name: args.name,
|
|
977
|
-
providerType: args.provider,
|
|
978
|
-
options: parsedOptions,
|
|
979
|
-
});
|
|
980
|
-
output(`${outputPrefix}-add`, result);
|
|
981
|
-
});
|
|
982
|
-
},
|
|
983
|
-
}),
|
|
984
|
-
remove: defineCommand({
|
|
985
|
-
meta: { name: "remove", description: "Remove an additional stash by URL, path, or name" },
|
|
986
|
-
args: {
|
|
987
|
-
target: { type: "positional", description: "Source URL, path, or name to remove", required: true },
|
|
988
|
-
},
|
|
989
|
-
run({ args }) {
|
|
990
|
-
return runWithJsonErrors(() => {
|
|
991
|
-
const result = removeStash(args.target);
|
|
992
|
-
output(`${outputPrefix}-remove`, result);
|
|
993
|
-
});
|
|
994
|
-
},
|
|
995
|
-
}),
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
const stashCommand = defineCommand({
|
|
999
|
-
meta: { name: "stash", description: "Manage additional stashes (local directories and remote providers)" },
|
|
1000
|
-
subCommands: buildSourceSubCommands("stash"),
|
|
1001
|
-
});
|
|
1002
976
|
const feedbackCommand = defineCommand({
|
|
1003
977
|
meta: {
|
|
1004
978
|
name: "feedback",
|
|
@@ -1105,12 +1079,10 @@ const main = defineCommand({
|
|
|
1105
1079
|
list: listCommand,
|
|
1106
1080
|
remove: removeCommand,
|
|
1107
1081
|
update: updateCommand,
|
|
1108
|
-
kit: kitCommand,
|
|
1109
1082
|
upgrade: upgradeCommand,
|
|
1110
1083
|
search: searchCommand,
|
|
1111
1084
|
show: showCommand,
|
|
1112
1085
|
clone: cloneCommand,
|
|
1113
|
-
stash: stashCommand,
|
|
1114
1086
|
registry: registryCommand,
|
|
1115
1087
|
config: configCommand,
|
|
1116
1088
|
feedback: feedbackCommand,
|
|
@@ -1160,8 +1132,8 @@ function buildHint(message) {
|
|
|
1160
1132
|
return "Use `akm update --all` or pass a target like `akm update npm:@scope/pkg`.";
|
|
1161
1133
|
if (message.includes("Specify either <target> or --all"))
|
|
1162
1134
|
return "Use only one: a positional target or `--all`.";
|
|
1163
|
-
if (message.includes("No
|
|
1164
|
-
return "Run `akm list` to view
|
|
1135
|
+
if (message.includes("No matching source"))
|
|
1136
|
+
return "Run `akm list` to view your sources, then retry with one of those values.";
|
|
1165
1137
|
if (message.includes("remote package fetched but asset not found"))
|
|
1166
1138
|
return "The remote package was fetched but doesn't contain the requested asset. Check the asset name and type.";
|
|
1167
1139
|
if (message.includes("Invalid value for --source"))
|
|
@@ -1267,17 +1239,16 @@ function loadHints(detail = "normal") {
|
|
|
1267
1239
|
}
|
|
1268
1240
|
const EMBEDDED_HINTS = `# akm CLI
|
|
1269
1241
|
|
|
1270
|
-
You have access to a searchable library of scripts, skills, commands, agents, and knowledge documents via \`akm\`. Search your
|
|
1242
|
+
You have access to a searchable library of scripts, skills, commands, agents, and knowledge documents via \`akm\`. Search your sources first before writing something from scratch.
|
|
1271
1243
|
|
|
1272
1244
|
## Quick Reference
|
|
1273
1245
|
|
|
1274
1246
|
\`\`\`sh
|
|
1275
|
-
akm search "<query>" # Search
|
|
1247
|
+
akm search "<query>" # Search all sources
|
|
1276
1248
|
akm search "<query>" --type skill # Filter by type
|
|
1277
|
-
akm search "<query>" --source both # Also search registries
|
|
1249
|
+
akm search "<query>" --source both # Also search registries
|
|
1278
1250
|
akm show <ref> # View asset details
|
|
1279
|
-
akm add <ref> #
|
|
1280
|
-
akm add context-hub # Shortcut for adding Context Hub as a stash provider
|
|
1251
|
+
akm add <ref> # Add a source (npm, GitHub, git, local dir)
|
|
1281
1252
|
akm clone <ref> # Copy an asset to the working stash (optional --dest arg to clone to specific location)
|
|
1282
1253
|
akm registry search "<query>" # Search all registries
|
|
1283
1254
|
\`\`\`
|
|
@@ -1296,14 +1267,14 @@ Run \`akm -h\` for the full command reference.
|
|
|
1296
1267
|
`;
|
|
1297
1268
|
const EMBEDDED_HINTS_FULL = `# akm CLI — Full Reference
|
|
1298
1269
|
|
|
1299
|
-
You have access to a searchable library of scripts, skills, commands, agents, and knowledge documents via \`akm\`. Search your
|
|
1270
|
+
You have access to a searchable library of scripts, skills, commands, agents, and knowledge documents via \`akm\`. Search your sources first before writing something from scratch.
|
|
1300
1271
|
|
|
1301
1272
|
## Search
|
|
1302
1273
|
|
|
1303
1274
|
\`\`\`sh
|
|
1304
|
-
akm search "<query>" # Search
|
|
1275
|
+
akm search "<query>" # Search all sources
|
|
1305
1276
|
akm search "<query>" --type skill # Filter by asset type
|
|
1306
|
-
akm search "<query>" --source both # Also search registries
|
|
1277
|
+
akm search "<query>" --source both # Also search registries
|
|
1307
1278
|
akm search "<query>" --source registry # Search registries only
|
|
1308
1279
|
akm search "<query>" --limit 10 # Limit results
|
|
1309
1280
|
akm search "<query>" --detail full # Include scores, paths, timing
|
|
@@ -1342,21 +1313,17 @@ akm show knowledge:my-doc # Show content (local or remote)
|
|
|
1342
1313
|
| knowledge | \`content\` (with view modes: \`full\`, \`toc\`, \`frontmatter\`, \`section\`, \`lines\`) |
|
|
1343
1314
|
| memory | \`content\` (recalled context) |
|
|
1344
1315
|
|
|
1345
|
-
##
|
|
1316
|
+
## Add & Manage Sources
|
|
1346
1317
|
|
|
1347
1318
|
\`\`\`sh
|
|
1348
|
-
akm add <ref> #
|
|
1349
|
-
akm add @scope/kit # From npm
|
|
1350
|
-
akm add owner/repo # From GitHub
|
|
1351
|
-
akm add ./path/to/local/kit #
|
|
1352
|
-
akm
|
|
1353
|
-
akm
|
|
1354
|
-
akm
|
|
1355
|
-
akm
|
|
1356
|
-
akm kit update --all # Update all kits
|
|
1357
|
-
akm list # List installed kits
|
|
1358
|
-
akm remove <target> # Remove by id or ref
|
|
1359
|
-
akm update --all # Update all installed kits
|
|
1319
|
+
akm add <ref> # Add a source
|
|
1320
|
+
akm add @scope/kit # From npm (managed)
|
|
1321
|
+
akm add owner/repo # From GitHub (managed)
|
|
1322
|
+
akm add ./path/to/local/kit # Local directory
|
|
1323
|
+
akm list # List all sources
|
|
1324
|
+
akm list --kind managed # List managed sources only
|
|
1325
|
+
akm remove <target> # Remove by id, ref, path, or name
|
|
1326
|
+
akm update --all # Update all managed sources
|
|
1360
1327
|
akm update <target> --force # Force re-download
|
|
1361
1328
|
\`\`\`
|
|
1362
1329
|
|
|
@@ -1404,8 +1371,7 @@ akm config path --all # Show all config paths
|
|
|
1404
1371
|
akm init # Initialize working stash
|
|
1405
1372
|
akm index # Rebuild search index
|
|
1406
1373
|
akm index --full # Full reindex
|
|
1407
|
-
akm
|
|
1408
|
-
akm kit # Kit management (add, list, remove, update)
|
|
1374
|
+
akm list # List all sources
|
|
1409
1375
|
akm upgrade # Upgrade akm binary
|
|
1410
1376
|
akm upgrade --check # Check for updates
|
|
1411
1377
|
akm hints # Print this reference
|
package/dist/config-cli.js
CHANGED
|
@@ -4,11 +4,16 @@ export function parseConfigValue(key, value) {
|
|
|
4
4
|
switch (key) {
|
|
5
5
|
case "stashDir":
|
|
6
6
|
return { stashDir: requireNonEmptyString(value, key) };
|
|
7
|
-
case "
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
case "semanticSearchMode":
|
|
8
|
+
// Accept legacy boolean-style strings from CLI
|
|
9
|
+
if (value === "true")
|
|
10
|
+
return { semanticSearchMode: "auto" };
|
|
11
|
+
if (value === "false")
|
|
12
|
+
return { semanticSearchMode: "off" };
|
|
13
|
+
if (value !== "off" && value !== "auto") {
|
|
14
|
+
throw new UsageError(`Invalid value for semanticSearchMode: expected "off" or "auto"`);
|
|
10
15
|
}
|
|
11
|
-
return {
|
|
16
|
+
return { semanticSearchMode: value };
|
|
12
17
|
case "embedding":
|
|
13
18
|
return { embedding: parseEmbeddingConnectionValue(value) };
|
|
14
19
|
case "llm":
|
|
@@ -29,8 +34,8 @@ export function getConfigValue(config, key) {
|
|
|
29
34
|
switch (key) {
|
|
30
35
|
case "stashDir":
|
|
31
36
|
return config.stashDir ?? null;
|
|
32
|
-
case "
|
|
33
|
-
return config.
|
|
37
|
+
case "semanticSearchMode":
|
|
38
|
+
return config.semanticSearchMode;
|
|
34
39
|
case "embedding":
|
|
35
40
|
return config.embedding ?? null;
|
|
36
41
|
case "llm":
|
|
@@ -50,7 +55,7 @@ export function getConfigValue(config, key) {
|
|
|
50
55
|
export function setConfigValue(config, key, rawValue) {
|
|
51
56
|
switch (key) {
|
|
52
57
|
case "stashDir":
|
|
53
|
-
case "
|
|
58
|
+
case "semanticSearchMode":
|
|
54
59
|
case "embedding":
|
|
55
60
|
case "llm":
|
|
56
61
|
case "registries":
|
|
@@ -84,7 +89,7 @@ export function unsetConfigValue(config, key) {
|
|
|
84
89
|
}
|
|
85
90
|
export function listConfig(config) {
|
|
86
91
|
const result = {
|
|
87
|
-
|
|
92
|
+
semanticSearchMode: config.semanticSearchMode,
|
|
88
93
|
registries: config.registries ?? DEFAULT_CONFIG.registries ?? [],
|
|
89
94
|
output: mergeOutputConfig(DEFAULT_CONFIG.output, config.output) ?? null,
|
|
90
95
|
stashDir: config.stashDir ?? null,
|
|
@@ -163,6 +168,17 @@ function parseEmbeddingConnectionValue(value) {
|
|
|
163
168
|
endpoint: "http://localhost:11434/v1/embeddings",
|
|
164
169
|
model: "nomic-embed-text",
|
|
165
170
|
});
|
|
171
|
+
const localModel = typeof parsed.localModel === "string" && parsed.localModel ? parsed.localModel : undefined;
|
|
172
|
+
const endpoint = typeof parsed.endpoint === "string" ? parsed.endpoint : "";
|
|
173
|
+
if (!endpoint) {
|
|
174
|
+
if (!localModel) {
|
|
175
|
+
throw new UsageError(`Invalid value for embedding: endpoint/model are required for remote embeddings, or provide localModel`);
|
|
176
|
+
}
|
|
177
|
+
const localOnly = { endpoint: "", model: "", localModel };
|
|
178
|
+
if (typeof parsed.provider === "string" && parsed.provider)
|
|
179
|
+
localOnly.provider = parsed.provider;
|
|
180
|
+
return localOnly;
|
|
181
|
+
}
|
|
166
182
|
const result = {
|
|
167
183
|
endpoint: asRequiredString(parsed.endpoint, "embedding", "endpoint"),
|
|
168
184
|
model: asRequiredString(parsed.model, "embedding", "model"),
|
|
@@ -173,6 +189,8 @@ function parseEmbeddingConnectionValue(value) {
|
|
|
173
189
|
result.dimension = parseUnknownPositiveInteger(parsed.dimension, "embedding.dimension");
|
|
174
190
|
if (typeof parsed.apiKey === "string" && parsed.apiKey)
|
|
175
191
|
result.apiKey = parsed.apiKey;
|
|
192
|
+
if (localModel)
|
|
193
|
+
result.localModel = localModel;
|
|
176
194
|
return result;
|
|
177
195
|
}
|
|
178
196
|
function parseLlmConnectionValue(value) {
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { getConfigDir as _getConfigDir, getConfigPath as _getConfigPath } from "./paths";
|
|
4
4
|
// ── Defaults ────────────────────────────────────────────────────────────────
|
|
5
5
|
export const DEFAULT_CONFIG = {
|
|
6
|
-
|
|
6
|
+
semanticSearchMode: "auto",
|
|
7
7
|
registries: [
|
|
8
8
|
{ url: "https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json", name: "official" },
|
|
9
9
|
{ url: "https://skills.sh", name: "skills.sh", provider: "skills-sh" },
|
|
@@ -122,8 +122,17 @@ function pickKnownKeys(raw) {
|
|
|
122
122
|
if (typeof raw.stashDir === "string" && raw.stashDir.trim()) {
|
|
123
123
|
config.stashDir = raw.stashDir.trim();
|
|
124
124
|
}
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
// Backward compatibility: coerce legacy boolean values to string
|
|
126
|
+
if (typeof raw.semanticSearchMode === "boolean") {
|
|
127
|
+
config.semanticSearchMode = raw.semanticSearchMode ? "auto" : "off";
|
|
128
|
+
}
|
|
129
|
+
else if (raw.semanticSearchMode === "off" || raw.semanticSearchMode === "auto") {
|
|
130
|
+
config.semanticSearchMode = raw.semanticSearchMode;
|
|
131
|
+
}
|
|
132
|
+
else if (typeof raw.semanticSearch === "boolean") {
|
|
133
|
+
// Legacy config: older versions used `semanticSearch` (boolean) instead of `semanticSearchMode`
|
|
134
|
+
const legacySemanticSearch = raw.semanticSearch;
|
|
135
|
+
config.semanticSearchMode = legacySemanticSearch ? "auto" : "off";
|
|
127
136
|
}
|
|
128
137
|
// Migrate legacy searchPaths into stashes
|
|
129
138
|
if (Array.isArray(raw.searchPaths)) {
|
package/dist/db.js
CHANGED
|
@@ -82,9 +82,19 @@ function ensureSchema(db, embeddingDim) {
|
|
|
82
82
|
value TEXT NOT NULL
|
|
83
83
|
);
|
|
84
84
|
`);
|
|
85
|
-
// Check stored version — if it differs from DB_VERSION, drop and recreate all tables
|
|
85
|
+
// Check stored version — if it differs from DB_VERSION, drop and recreate all tables.
|
|
86
|
+
// Usage events are preserved across version upgrades so that utility score
|
|
87
|
+
// history is not silently lost.
|
|
86
88
|
const storedVersion = getMeta(db, "version");
|
|
87
89
|
if (storedVersion && storedVersion !== String(DB_VERSION)) {
|
|
90
|
+
// Back up usage_events before dropping tables
|
|
91
|
+
let usageBackup = [];
|
|
92
|
+
try {
|
|
93
|
+
usageBackup = db.prepare("SELECT * FROM usage_events").all();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* table may not exist in older versions */
|
|
97
|
+
}
|
|
88
98
|
db.exec("DROP TABLE IF EXISTS utility_scores");
|
|
89
99
|
db.exec("DROP TABLE IF EXISTS usage_events");
|
|
90
100
|
db.exec("DROP TABLE IF EXISTS embeddings");
|
|
@@ -94,6 +104,9 @@ function ensureSchema(db, embeddingDim) {
|
|
|
94
104
|
db.exec("DROP INDEX IF EXISTS idx_entries_type");
|
|
95
105
|
db.exec("DROP TABLE IF EXISTS entries");
|
|
96
106
|
db.exec("DELETE FROM index_meta");
|
|
107
|
+
// Store backup for restoration after ensureUsageEventsSchema runs
|
|
108
|
+
db.__usageBackup = usageBackup;
|
|
109
|
+
console.warn("[akm] Index rebuilt due to version upgrade. Run 'akm index' to repopulate.");
|
|
97
110
|
}
|
|
98
111
|
db.exec(`
|
|
99
112
|
CREATE TABLE IF NOT EXISTS entries (
|
|
@@ -172,6 +185,7 @@ function ensureSchema(db, embeddingDim) {
|
|
|
172
185
|
catch {
|
|
173
186
|
/* ignore */
|
|
174
187
|
}
|
|
188
|
+
setMeta(db, "hasEmbeddings", "0");
|
|
175
189
|
}
|
|
176
190
|
const vecExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='entries_vec'").get();
|
|
177
191
|
if (!vecExists) {
|
|
@@ -187,8 +201,51 @@ function ensureSchema(db, embeddingDim) {
|
|
|
187
201
|
}
|
|
188
202
|
setMeta(db, "embeddingDim", String(embeddingDim));
|
|
189
203
|
}
|
|
204
|
+
else {
|
|
205
|
+
// Also purge BLOB embeddings on dimension change (JS fallback path).
|
|
206
|
+
// When sqlite-vec is unavailable, entries_vec doesn't exist but the BLOB
|
|
207
|
+
// embeddings table still stores vectors. If the configured dimension
|
|
208
|
+
// changes, those stored BLOBs become silently incompatible.
|
|
209
|
+
const storedDim = getMeta(db, "embeddingDim");
|
|
210
|
+
if (storedDim && storedDim !== String(embeddingDim)) {
|
|
211
|
+
try {
|
|
212
|
+
db.exec("DELETE FROM embeddings");
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/* ignore */
|
|
216
|
+
}
|
|
217
|
+
setMeta(db, "hasEmbeddings", "0");
|
|
218
|
+
}
|
|
219
|
+
setMeta(db, "embeddingDim", String(embeddingDim));
|
|
220
|
+
}
|
|
190
221
|
// Usage telemetry table
|
|
191
222
|
ensureUsageEventsSchema(db);
|
|
223
|
+
// Restore usage_events that were backed up during a version upgrade.
|
|
224
|
+
// Wrapped in outer try/catch because schema changes across versions may
|
|
225
|
+
// make the backup incompatible with the new table definition.
|
|
226
|
+
const dbAny = db;
|
|
227
|
+
const backup = dbAny.__usageBackup;
|
|
228
|
+
if (backup && backup.length > 0) {
|
|
229
|
+
try {
|
|
230
|
+
db.transaction(() => {
|
|
231
|
+
const cols = Object.keys(backup[0]);
|
|
232
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
233
|
+
const insert = db.prepare(`INSERT INTO usage_events (${cols.join(", ")}) VALUES (${placeholders})`);
|
|
234
|
+
for (const row of backup) {
|
|
235
|
+
try {
|
|
236
|
+
insert.run(...cols.map((c) => row[c]));
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
/* skip rows that fail */
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
})();
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
/* schema changed too much — discard backup gracefully */
|
|
246
|
+
}
|
|
247
|
+
delete dbAny.__usageBackup;
|
|
248
|
+
}
|
|
192
249
|
}
|
|
193
250
|
// ── Meta helpers ────────────────────────────────────────────────────────────
|
|
194
251
|
export function getMeta(db, key) {
|
package/dist/embedder.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { fetchWithTimeout, isHttpUrl } from "./common";
|
|
3
|
+
import { getCacheDir } from "./paths";
|
|
2
4
|
import { warn } from "./warn";
|
|
3
5
|
// ── Default local model ─────────────────────────────────────────────────────
|
|
4
6
|
/**
|
|
@@ -15,6 +17,8 @@ export const DEFAULT_LOCAL_MODEL = "Xenova/bge-small-en-v1.5";
|
|
|
15
17
|
function getLocalModelName(overrideModel) {
|
|
16
18
|
return overrideModel || DEFAULT_LOCAL_MODEL;
|
|
17
19
|
}
|
|
20
|
+
const LOCAL_EMBEDDER_DTYPE = "fp32";
|
|
21
|
+
const LOCAL_EMBEDDER_FALLBACK_DTYPE = "auto";
|
|
18
22
|
// Cache the promise itself (not the resolved result) so concurrent calls share
|
|
19
23
|
// the same initialisation work and never download the model twice.
|
|
20
24
|
// The cache is keyed by model name so switching models gets a fresh pipeline.
|
|
@@ -30,16 +34,25 @@ async function getLocalEmbedder(modelName) {
|
|
|
30
34
|
if (!localEmbedderPromise) {
|
|
31
35
|
localEmbedderModelName = resolvedModel;
|
|
32
36
|
localEmbedderPromise = (async () => {
|
|
37
|
+
// Ensure HuggingFace model cache lives in a stable location outside
|
|
38
|
+
// node_modules so it survives package reinstalls.
|
|
39
|
+
if (!process.env.HF_HOME) {
|
|
40
|
+
process.env.HF_HOME = path.join(getCacheDir(), "models");
|
|
41
|
+
}
|
|
33
42
|
let pipeline;
|
|
34
43
|
try {
|
|
35
44
|
const mod = await import("@huggingface/transformers");
|
|
36
45
|
pipeline = mod.pipeline;
|
|
37
46
|
}
|
|
38
|
-
catch {
|
|
39
|
-
|
|
47
|
+
catch (importError) {
|
|
48
|
+
const msg = importError instanceof Error ? importError.message : String(importError);
|
|
49
|
+
if (/Cannot find module|MODULE_NOT_FOUND|Cannot resolve/i.test(msg)) {
|
|
50
|
+
throw new Error("Semantic search requires @huggingface/transformers. Install it with: bun add @huggingface/transformers");
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Failed to load embedding runtime: ${msg}. Check platform compatibility.`);
|
|
40
53
|
}
|
|
41
54
|
const pipelineFn = pipeline;
|
|
42
|
-
return pipelineFn
|
|
55
|
+
return createLocalPipeline(pipelineFn, resolvedModel);
|
|
43
56
|
})();
|
|
44
57
|
// HI-13: Clear the cached promise on failure so the next call retries
|
|
45
58
|
// instead of permanently rejecting every subsequent call with the same error.
|
|
@@ -50,6 +63,22 @@ async function getLocalEmbedder(modelName) {
|
|
|
50
63
|
}
|
|
51
64
|
return localEmbedderPromise;
|
|
52
65
|
}
|
|
66
|
+
async function createLocalPipeline(pipelineFn, modelName) {
|
|
67
|
+
try {
|
|
68
|
+
return await pipelineFn("feature-extraction", modelName, { dtype: LOCAL_EMBEDDER_DTYPE });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!shouldRetryWithoutExplicitDtype(error)) {
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
warn('Local embedding model "%s" rejected explicit dtype "%s"; retrying with explicit fallback dtype "%s".', modelName, LOCAL_EMBEDDER_DTYPE, LOCAL_EMBEDDER_FALLBACK_DTYPE);
|
|
75
|
+
return pipelineFn("feature-extraction", modelName, { dtype: LOCAL_EMBEDDER_FALLBACK_DTYPE });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function shouldRetryWithoutExplicitDtype(error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
return /dtype|fp32|precision|quant/i.test(message);
|
|
81
|
+
}
|
|
53
82
|
export function resetLocalEmbedder() {
|
|
54
83
|
localEmbedderPromise = undefined;
|
|
55
84
|
localEmbedderModelName = undefined;
|