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 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 { akmList, akmRemove, akmUpdate } from "./installed-kits";
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, akmKitAdd } from "./stash-add";
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, listStashes, removeStash } from "./stash-source-manage";
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 Conventions:
454
- * - stash-* : Operations on the user's local asset store (stash-show, stash-add, stash-clone)
468
+ * Module Naming:
469
+ * - stash-* : Asset operations (search, show, add, clone)
455
470
  * - stash-provider-* : Runtime data source providers (filesystem, openviking)
456
- * - registry-* : Kit discovery from remote registries (npm, GitHub)
457
- * - installed-kits : Management of kits already installed locally
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: { name: "add", description: "Install a kit from npm, GitHub, any git host, or a local directory" },
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: "Registry ref (npm package, owner/repo, git URL, or local directory)",
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
- if (args.ref.trim() === CONTEXT_HUB_ALIAS_REF) {
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
- const result = await akmAdd({ ref: args.ref });
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 installed kits" },
565
- async run() {
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 result = await akmList();
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 an installed kit by id or ref" },
641
+ meta: { name: "remove", description: "Remove a source by id, ref, path, URL, or name" },
574
642
  args: {
575
- target: { type: "positional", description: "Installed target (id or ref)", required: true },
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 installed kits" },
653
+ meta: { name: "update", description: "Update one or all managed sources" },
586
654
  args: {
587
- target: { type: "positional", description: "Installed target (id or ref)", required: false },
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 installed kit matched target"))
1164
- return "Run `akm list` to view installed ids/refs, then retry with one of those values.";
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 stashes first before writing something from scratch.
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 your stashes and installed kits
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 for installable kits
1249
+ akm search "<query>" --source both # Also search registries
1278
1250
  akm show <ref> # View asset details
1279
- akm add <ref> # Install a kit (npm, GitHub, git, local)
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 stashes first before writing something from scratch.
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 your stashes and installed kits
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 for installable kits
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
- ## Install & Manage Kits
1316
+ ## Add & Manage Sources
1346
1317
 
1347
1318
  \`\`\`sh
1348
- akm add <ref> # Install a kit (smart router: local dirs become stash adds)
1349
- akm add @scope/kit # From npm
1350
- akm add owner/repo # From GitHub
1351
- akm add ./path/to/local/kit # From local directory (adds as stash)
1352
- akm add context-hub # Add the official Context Hub stash
1353
- akm kit add <ref> # Install a kit (explicit)
1354
- akm kit list # List installed kits
1355
- akm kit remove <target> # Remove a kit
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 stash # List all stashes
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
@@ -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 "semanticSearch":
8
- if (value !== "true" && value !== "false") {
9
- throw new UsageError(`Invalid value for semanticSearch: expected "true" or "false"`);
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 { semanticSearch: value === "true" };
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 "semanticSearch":
33
- return config.semanticSearch;
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 "semanticSearch":
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
- semanticSearch: config.semanticSearch,
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
- semanticSearch: true,
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
- if (typeof raw.semanticSearch === "boolean") {
126
- config.semanticSearch = raw.semanticSearch;
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
- throw new Error("Semantic search requires @huggingface/transformers. Install it with: npm install @huggingface/transformers");
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("feature-extraction", resolvedModel);
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;