akm-cli 0.2.1 → 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/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;
package/dist/indexer.js CHANGED
@@ -5,6 +5,7 @@ import { closeDatabase, deleteEntriesByDir, deleteEntriesByStashDir, getEmbeddin
5
5
  import { generateMetadataFlat, loadStashFile } from "./metadata";
6
6
  import { getDbPath } from "./paths";
7
7
  import { buildSearchText } from "./search-fields";
8
+ import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./semantic-status";
8
9
  import { ensureUsageEventsSchema, purgeOldUsageEvents } from "./usage-events";
9
10
  import { walkStashFlat } from "./walker";
10
11
  import { warn } from "./warn";
@@ -36,7 +37,7 @@ export async function akmIndex(options) {
36
37
  message: buildIndexSummaryMessage({
37
38
  mode: isIncremental ? "incremental" : "full",
38
39
  stashSources: allStashDirs.length,
39
- semanticSearch: config.semanticSearch,
40
+ semanticSearchMode: config.semanticSearchMode,
40
41
  embeddingProvider: getEmbeddingProvider(config.embedding),
41
42
  llmEnabled: !!config.llm,
42
43
  vecAvailable: isVecAvailable(db),
@@ -97,21 +98,52 @@ export async function akmIndex(options) {
97
98
  rebuildFts(db);
98
99
  onProgress({ phase: "fts", message: "Rebuilt full-text search index." });
99
100
  const tFtsEnd = Date.now();
101
+ // Re-link detached usage_events to their new entry_ids via entry_ref.
102
+ // entry_ref is "type:name" (e.g., "skill:code-review"), entry_key is "stashDir:type:name".
103
+ // Use substr to extract the "type:name" suffix from entry_key for exact comparison
104
+ // (avoids LIKE which would require escaping % and _ in user-facing names).
105
+ try {
106
+ db.exec(`
107
+ UPDATE usage_events SET entry_id = (
108
+ SELECT e.id FROM entries e
109
+ WHERE substr(e.entry_key, length(e.entry_key) - length(usage_events.entry_ref)) = ':' || usage_events.entry_ref
110
+ LIMIT 1
111
+ )
112
+ WHERE entry_id IS NULL AND entry_ref IS NOT NULL
113
+ `);
114
+ }
115
+ catch {
116
+ /* ignore if table doesn't exist yet */
117
+ }
100
118
  // Recompute utility scores from usage_events after FTS rebuild
101
119
  recomputeUtilityScores(db);
102
120
  // Generate embeddings if semantic search is enabled
103
- const hasEmbeddings = await generateEmbeddingsForDb(db, config, onProgress);
121
+ const embeddingResult = await generateEmbeddingsForDb(db, config, onProgress);
104
122
  const tEmbedEnd = Date.now();
105
123
  // Update metadata
106
124
  setMeta(db, "builtAt", new Date().toISOString());
107
125
  setMeta(db, "stashDir", stashDir);
108
126
  setMeta(db, "stashDirs", JSON.stringify(allStashDirs));
109
- setMeta(db, "hasEmbeddings", hasEmbeddings ? "1" : "0");
127
+ setMeta(db, "hasEmbeddings", embeddingResult.success ? "1" : "0");
110
128
  const totalEntries = getEntryCount(db);
111
129
  // Warn on every index run if using JS fallback with many entries
112
130
  warnIfVecMissing(db);
113
131
  const tEnd = Date.now();
114
- const verification = verifyIndexState(db, config, totalEntries);
132
+ const verification = verifyIndexState(db, config, totalEntries, embeddingResult);
133
+ if (config.semanticSearchMode === "off") {
134
+ clearSemanticStatus();
135
+ }
136
+ else {
137
+ writeSemanticStatus({
138
+ status: verification.semanticStatus === "disabled" ? "pending" : verification.semanticStatus,
139
+ ...(embeddingResult.reason ? { reason: embeddingResult.reason } : {}),
140
+ ...(embeddingResult.message ? { message: embeddingResult.message } : {}),
141
+ providerFingerprint: deriveSemanticProviderFingerprint(config.embedding),
142
+ lastCheckedAt: new Date().toISOString(),
143
+ entryCount: verification.entryCount,
144
+ embeddingCount: verification.embeddingCount,
145
+ });
146
+ }
115
147
  onProgress({ phase: "verify", message: verification.message });
116
148
  return {
117
149
  stashDir,
@@ -222,7 +254,14 @@ async function indexEntries(db, allStashDirs, isIncremental, builtAtMs, doFullDe
222
254
  }
223
255
  db.exec("DELETE FROM entries_fts");
224
256
  db.exec("DELETE FROM utility_scores");
225
- db.exec("DELETE FROM usage_events");
257
+ // Detach usage_events from entries about to be deleted — null out entry_id
258
+ // but keep entry_ref so events can be re-linked after entries are rebuilt.
259
+ try {
260
+ db.exec("UPDATE usage_events SET entry_id = NULL WHERE entry_id IS NOT NULL");
261
+ }
262
+ catch {
263
+ /* ignore if table doesn't exist */
264
+ }
226
265
  db.exec("DELETE FROM entries");
227
266
  }
228
267
  for (const { dirPath, currentStashDir, files, stash, skip } of dirRecords) {
@@ -283,16 +322,38 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm) {
283
322
  }
284
323
  }
285
324
  async function generateEmbeddingsForDb(db, config, onProgress) {
286
- if (!config.semanticSearch) {
325
+ if (config.semanticSearchMode === "off") {
287
326
  onProgress({ phase: "embeddings", message: "Semantic search disabled; skipping embeddings." });
288
- return false;
327
+ return { success: false, reason: "index-missing", message: "Semantic search is disabled." };
328
+ }
329
+ // Detect embedding model/provider changes and purge stale embeddings
330
+ // so that incremental reindex regenerates all vectors with the new model.
331
+ const currentFingerprint = deriveSemanticProviderFingerprint(config.embedding);
332
+ const storedFingerprint = getMeta(db, "embeddingFingerprint");
333
+ if (storedFingerprint && storedFingerprint !== currentFingerprint) {
334
+ try {
335
+ db.exec("DELETE FROM embeddings");
336
+ }
337
+ catch {
338
+ /* ignore */
339
+ }
340
+ if (isVecAvailable(db)) {
341
+ try {
342
+ db.exec("DELETE FROM entries_vec");
343
+ }
344
+ catch {
345
+ /* ignore */
346
+ }
347
+ }
348
+ setMeta(db, "hasEmbeddings", "0");
289
349
  }
290
350
  try {
291
351
  const { embedBatch } = await import("./embedder.js");
292
352
  const allEntries = getAllEntriesForEmbedding(db);
293
353
  if (allEntries.length === 0) {
294
354
  onProgress({ phase: "embeddings", message: "Embeddings already up to date." });
295
- return true;
355
+ setMeta(db, "embeddingFingerprint", currentFingerprint);
356
+ return { success: true };
296
357
  }
297
358
  onProgress({
298
359
  phase: "embeddings",
@@ -311,15 +372,21 @@ async function generateEmbeddingsForDb(db, config, onProgress) {
311
372
  phase: "embeddings",
312
373
  message: `Stored ${embeddings.length} embedding${embeddings.length === 1 ? "" : "s"}.`,
313
374
  });
314
- return true;
375
+ setMeta(db, "embeddingFingerprint", currentFingerprint);
376
+ return { success: true };
315
377
  }
316
378
  catch (error) {
317
- warn("Embedding generation failed, continuing without:", error instanceof Error ? error.message : String(error));
379
+ const message = error instanceof Error ? error.message : String(error);
380
+ warn("Embedding generation failed, continuing without:", message);
318
381
  onProgress({
319
382
  phase: "embeddings",
320
- message: `Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`,
383
+ message: `Embedding generation failed: ${message}`,
321
384
  });
322
- return false;
385
+ return {
386
+ success: false,
387
+ reason: classifySemanticFailure(message),
388
+ message: `Semantic search verification failed: ${message}`,
389
+ };
323
390
  }
324
391
  }
325
392
  // ── Helpers ─────────────────────────────────────────────────────────────────
@@ -341,18 +408,18 @@ function attachFileSize(entry, entryPath) {
341
408
  }
342
409
  function buildIndexSummaryMessage(options) {
343
410
  const stashSourceLabel = options.stashSources === 1 ? "stash source" : "stash sources";
344
- const semanticDetail = getSemanticSearchLabel(options.semanticSearch, options.embeddingProvider, options.vecAvailable);
411
+ const semanticDetail = getSemanticSearchLabel(options.semanticSearchMode, options.embeddingProvider, options.vecAvailable);
345
412
  return `Starting ${options.mode} index (${options.stashSources} ${stashSourceLabel}, semantic search: ${semanticDetail}, LLM: ${options.llmEnabled ? "enabled" : "disabled"}).`;
346
413
  }
347
414
  function getEmbeddingProvider(embedding) {
348
415
  return isHttpUrl(embedding?.endpoint) ? "remote" : "local";
349
416
  }
350
- function getSemanticSearchLabel(semanticSearch, embeddingProvider, vecAvailable) {
351
- if (!semanticSearch)
417
+ function getSemanticSearchLabel(semanticSearchMode, embeddingProvider, vecAvailable) {
418
+ if (semanticSearchMode === "off")
352
419
  return "disabled";
353
420
  return `${embeddingProvider} embeddings, ${vecAvailable ? "sqlite-vec" : "JS fallback"}`;
354
421
  }
355
- function verifyIndexState(db, config, totalEntries) {
422
+ function verifyIndexState(db, config, totalEntries, embeddingResult) {
356
423
  const embeddingCount = getEmbeddingCount(db);
357
424
  const vecAvailable = isVecAvailable(db);
358
425
  const embeddingProvider = getEmbeddingProvider(config.embedding);
@@ -360,18 +427,22 @@ function verifyIndexState(db, config, totalEntries) {
360
427
  return {
361
428
  ok: true,
362
429
  message: "Index ready. No assets were found yet.",
363
- semanticSearchEnabled: config.semanticSearch,
430
+ semanticSearchEnabled: config.semanticSearchMode === "auto",
431
+ semanticSearchMode: config.semanticSearchMode,
432
+ semanticStatus: config.semanticSearchMode === "off" ? "disabled" : "pending",
364
433
  embeddingProvider,
365
434
  entryCount: totalEntries,
366
435
  embeddingCount,
367
436
  vecAvailable,
368
437
  };
369
438
  }
370
- if (!config.semanticSearch) {
439
+ if (config.semanticSearchMode === "off") {
371
440
  return {
372
441
  ok: true,
373
442
  message: "Keyword index ready. Semantic search is disabled.",
374
443
  semanticSearchEnabled: false,
444
+ semanticSearchMode: config.semanticSearchMode,
445
+ semanticStatus: "disabled",
375
446
  embeddingProvider,
376
447
  entryCount: totalEntries,
377
448
  embeddingCount,
@@ -383,6 +454,8 @@ function verifyIndexState(db, config, totalEntries) {
383
454
  ok: true,
384
455
  message: `Semantic search ready (${embeddingCount}/${totalEntries} embeddings, ${vecAvailable ? "sqlite-vec active" : "JS fallback active"}).`,
385
456
  semanticSearchEnabled: true,
457
+ semanticSearchMode: config.semanticSearchMode,
458
+ semanticStatus: vecAvailable ? "ready-vec" : "ready-js",
386
459
  embeddingProvider,
387
460
  entryCount: totalEntries,
388
461
  embeddingCount,
@@ -391,11 +464,14 @@ function verifyIndexState(db, config, totalEntries) {
391
464
  }
392
465
  return {
393
466
  ok: false,
394
- message: `Semantic search verification failed (${embeddingCount}/${totalEntries} embeddings available).`,
467
+ message: embeddingResult.message ??
468
+ `Semantic search verification failed (${embeddingCount}/${totalEntries} embeddings available).`,
395
469
  guidance: embeddingProvider === "remote"
396
470
  ? "Check your embedding endpoint and credentials, then retry `akm index --full --verbose`."
397
471
  : "Retry `akm index --full --verbose`. If it still fails, confirm local model downloads are permitted and see docs/configuration.md for local embedding dependency setup.",
398
472
  semanticSearchEnabled: true,
473
+ semanticSearchMode: config.semanticSearchMode,
474
+ semanticStatus: "blocked",
399
475
  embeddingProvider,
400
476
  entryCount: totalEntries,
401
477
  embeddingCount,
package/dist/info.js CHANGED
@@ -3,6 +3,7 @@ import { getAssetTypes } from "./asset-spec";
3
3
  import { loadConfig } from "./config";
4
4
  import { closeDatabase, getEntryCount, getMeta, isVecAvailable, openDatabase } from "./db";
5
5
  import { getDbPath } from "./paths";
6
+ import { getEffectiveSemanticStatus, readSemanticStatus } from "./semantic-status";
6
7
  import { pkgVersion } from "./version";
7
8
  /**
8
9
  * Assemble system info describing the current capabilities, configuration,
@@ -14,9 +15,11 @@ export function assembleInfo(options) {
14
15
  const config = loadConfig();
15
16
  // Asset types
16
17
  const assetTypes = getAssetTypes();
18
+ const semanticRuntime = readSemanticStatus();
19
+ const semanticStatus = getEffectiveSemanticStatus(config, semanticRuntime);
17
20
  // Search modes
18
21
  const searchModes = ["fts"];
19
- if (config.semanticSearch) {
22
+ if (semanticStatus === "ready-js" || semanticStatus === "ready-vec") {
20
23
  searchModes.push("semantic", "hybrid");
21
24
  }
22
25
  // Registries (strip sensitive fields like apiKey from options)
@@ -41,6 +44,12 @@ export function assembleInfo(options) {
41
44
  version: pkgVersion,
42
45
  assetTypes,
43
46
  searchModes,
47
+ semanticSearch: {
48
+ mode: config.semanticSearchMode,
49
+ status: semanticStatus,
50
+ ...(semanticRuntime?.reason ? { reason: semanticRuntime.reason } : {}),
51
+ ...(semanticRuntime?.message ? { message: semanticRuntime.message } : {}),
52
+ },
44
53
  registries,
45
54
  stashProviders,
46
55
  indexStats,
@@ -1,15 +1,11 @@
1
1
  /**
2
- * Installed-kit operations: list, remove, update.
2
+ * Source operations: list, remove, update.
3
3
  *
4
- * Manages the set of kits that have been added to the local stash via
5
- * `akm add`. Each installed kit has a cache directory and a stash root that
6
- * is added to the search path.
7
- *
8
- * Not to be confused with:
9
- * - registry-factory.ts — factory map for kit-discovery registry providers
10
- * - stash-provider-factory.ts — factory map for runtime stash data sources
4
+ * Provides unified operations across all source kinds (local, managed, remote).
5
+ * The CLI's `akm list`, `akm remove`, and `akm update` commands are wired here.
11
6
  */
12
7
  import fs from "node:fs";
8
+ import path from "node:path";
13
9
  import { resolveStashDir } from "./common";
14
10
  import { loadConfig } from "./config";
15
11
  import { NotFoundError, UsageError } from "./errors";
@@ -17,49 +13,107 @@ import { akmIndex } from "./indexer";
17
13
  import { removeLockEntry, upsertLockEntry } from "./lockfile";
18
14
  import { installRegistryRef, removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./registry-install";
19
15
  import { parseRegistryRef } from "./registry-resolve";
20
- export async function akmList(input) {
16
+ import { removeStash } from "./stash-source-manage";
17
+ export async function akmListSources(input) {
21
18
  const stashDir = input?.stashDir ?? resolveStashDir();
22
19
  const config = loadConfig();
23
- const installed = config.installed ?? [];
20
+ const kindFilter = input?.kind;
21
+ const sources = [];
22
+ // Stash entries → local or remote sources
23
+ for (const stash of config.stashes ?? []) {
24
+ const isRemote = stash.url != null;
25
+ const kind = isRemote ? "remote" : "local";
26
+ if (kindFilter && !kindFilter.includes(kind))
27
+ continue;
28
+ const name = stash.name ?? stash.path ?? stash.url ?? "unknown";
29
+ sources.push({
30
+ name,
31
+ kind,
32
+ path: stash.path,
33
+ provider: isRemote ? stash.type : undefined,
34
+ updatable: false,
35
+ status: { exists: stash.path ? directoryExists(stash.path) : true },
36
+ });
37
+ }
38
+ // Installed entries → managed sources
39
+ for (const entry of config.installed ?? []) {
40
+ const kind = "managed";
41
+ if (kindFilter && !kindFilter.includes(kind))
42
+ continue;
43
+ sources.push({
44
+ name: entry.id,
45
+ kind,
46
+ path: entry.stashRoot,
47
+ ref: entry.ref,
48
+ version: entry.resolvedVersion,
49
+ updatable: true,
50
+ status: { exists: directoryExists(entry.stashRoot) },
51
+ });
52
+ }
24
53
  return {
25
54
  schemaVersion: 1,
26
55
  stashDir,
27
- installed: installed.map((entry) => ({
28
- ...entry,
29
- status: {
30
- cacheDirExists: directoryExists(entry.cacheDir),
31
- stashRootExists: directoryExists(entry.stashRoot),
32
- },
33
- })),
34
- totalInstalled: installed.length,
56
+ sources,
57
+ totalSources: sources.length,
35
58
  };
36
59
  }
37
60
  export async function akmRemove(input) {
38
61
  const target = input.target.trim();
39
62
  if (!target)
40
- throw new UsageError("Target is required. Provide the kit id or ref (e.g. `akm remove npm:@scope/kit` or `akm remove owner/repo`).");
63
+ throw new UsageError("Target is required. Provide the source id, ref, path, URL, or name (e.g. `akm remove npm:@scope/kit` or `akm remove ~/my-stash`).");
41
64
  const stashDir = input.stashDir ?? resolveStashDir();
42
65
  const config = loadConfig();
43
66
  const installed = config.installed ?? [];
44
- const entry = resolveInstalledTarget(installed, target);
45
- const updatedConfig = removeInstalledRegistryEntry(entry.id);
46
- await removeLockEntry(entry.id);
47
- // Only clean up cache for non-local sources — local sources point to the
48
- // user's real directory on disk and must never be deleted.
49
- if (entry.source !== "local") {
50
- cleanupDirectoryBestEffort(entry.cacheDir);
67
+ // Try installed[] first (managed sources)
68
+ const entry = tryResolveInstalledTarget(installed, target);
69
+ if (entry) {
70
+ const updatedConfig = removeInstalledRegistryEntry(entry.id);
71
+ await removeLockEntry(entry.id);
72
+ if (entry.source !== "local") {
73
+ cleanupDirectoryBestEffort(entry.cacheDir);
74
+ }
75
+ const index = await akmIndex({ stashDir });
76
+ return {
77
+ schemaVersion: 1,
78
+ stashDir,
79
+ target,
80
+ removed: {
81
+ id: entry.id,
82
+ source: entry.source,
83
+ ref: entry.ref,
84
+ cacheDir: entry.cacheDir,
85
+ stashRoot: entry.stashRoot,
86
+ },
87
+ config: {
88
+ stashCount: updatedConfig.stashes?.length ?? 0,
89
+ installedKitCount: updatedConfig.installed?.length ?? 0,
90
+ },
91
+ index: {
92
+ mode: index.mode,
93
+ totalEntries: index.totalEntries,
94
+ directoriesScanned: index.directoriesScanned,
95
+ directoriesSkipped: index.directoriesSkipped,
96
+ },
97
+ };
51
98
  }
99
+ // Fall through to stashes[] (local/remote sources)
100
+ const stashResult = removeStash(target);
101
+ if (!stashResult.removed || !stashResult.entry) {
102
+ throw new NotFoundError(`No matching source for target: ${target}`);
103
+ }
104
+ const removedEntry = stashResult.entry;
52
105
  const index = await akmIndex({ stashDir });
106
+ const updatedConfig = loadConfig();
53
107
  return {
54
108
  schemaVersion: 1,
55
109
  stashDir,
56
110
  target,
57
111
  removed: {
58
- id: entry.id,
59
- source: entry.source,
60
- ref: entry.ref,
61
- cacheDir: entry.cacheDir,
62
- stashRoot: entry.stashRoot,
112
+ id: removedEntry.name ?? removedEntry.path ?? removedEntry.url ?? target,
113
+ source: removedEntry.type,
114
+ ref: removedEntry.path ?? removedEntry.url ?? target,
115
+ cacheDir: "",
116
+ stashRoot: removedEntry.path ?? "",
63
117
  },
64
118
  config: {
65
119
  stashCount: updatedConfig.stashes?.length ?? 0,
@@ -146,9 +200,32 @@ function selectTargets(installed, target, all) {
146
200
  if (!target) {
147
201
  throw new UsageError("Either <target> or --all is required.");
148
202
  }
149
- return [resolveInstalledTarget(installed, target)];
203
+ const found = tryResolveInstalledTarget(installed, target);
204
+ if (found)
205
+ return [found];
206
+ // Check if target matches a stash source and give a helpful message
207
+ const config = loadConfig();
208
+ const stashes = config.stashes ?? [];
209
+ const isUrl = target.startsWith("http://") || target.startsWith("https://");
210
+ const resolvedPath = !isUrl ? path.resolve(target) : undefined;
211
+ const stashMatch = stashes.find((s) => {
212
+ if (isUrl && s.url === target)
213
+ return true;
214
+ if (resolvedPath && s.path && path.resolve(s.path) === resolvedPath)
215
+ return true;
216
+ if (s.name === target)
217
+ return true;
218
+ return false;
219
+ });
220
+ if (stashMatch) {
221
+ if (stashMatch.url) {
222
+ throw new UsageError(`"${target}" is a remote provider — it queries live data and has nothing to update.`);
223
+ }
224
+ throw new UsageError(`"${target}" is a local directory — it reflects your files in place. To refresh the search index, run: akm index`);
225
+ }
226
+ throw new NotFoundError(`No matching source for target: ${target}`);
150
227
  }
151
- function resolveInstalledTarget(installed, target) {
228
+ function tryResolveInstalledTarget(installed, target) {
152
229
  const byId = installed.find((entry) => entry.id === target);
153
230
  if (byId)
154
231
  return byId;
@@ -167,7 +244,7 @@ function resolveInstalledTarget(installed, target) {
167
244
  if (byParsedId)
168
245
  return byParsedId;
169
246
  }
170
- throw new NotFoundError(`No installed kit matched target: ${target}`);
247
+ return undefined;
171
248
  }
172
249
  function toInstalledEntry(status) {
173
250
  // KitInstallStatus extends InstalledKitEntry; omit the extra extractedDir field.
@@ -17,6 +17,7 @@ import { buildSearchText } from "./indexer";
17
17
  import { generateMetadataFlat, loadStashFile } from "./metadata";
18
18
  import { getDbPath } from "./paths";
19
19
  import { buildEditHint, findSourceForPath, isEditable } from "./search-source";
20
+ import { deriveSemanticProviderFingerprint, getEffectiveSemanticStatus, isSemanticRuntimeReady, readSemanticStatus, } from "./semantic-status";
20
21
  import { makeAssetRef } from "./stash-ref";
21
22
  import { walkStashFlat } from "./walker";
22
23
  import { warn } from "./warn";
@@ -32,6 +33,22 @@ export function buildLocalAction(type, ref) {
32
33
  export async function searchLocal(input) {
33
34
  const { query, searchType, limit, stashDir, sources, config } = input;
34
35
  const allStashDirs = sources.map((s) => s.path);
36
+ const rawStatus = readSemanticStatus();
37
+ const semanticStatus = getEffectiveSemanticStatus(config, rawStatus);
38
+ const warnings = [];
39
+ if (config.semanticSearchMode === "auto" && semanticStatus === "pending") {
40
+ // Distinguish between fingerprint mismatch (config changed) and never-set-up.
41
+ const currentFingerprint = deriveSemanticProviderFingerprint(config.embedding);
42
+ if (rawStatus && rawStatus.providerFingerprint !== currentFingerprint) {
43
+ warnings.push("Embedding config changed. Run 'akm index --full' to rebuild the semantic index with the new provider.");
44
+ }
45
+ else {
46
+ warnings.push("Semantic search is pending verification. Run 'akm setup' or 'akm index --full' to enable semantic search.");
47
+ }
48
+ }
49
+ if (config.semanticSearchMode === "auto" && semanticStatus === "blocked") {
50
+ warnings.push("Semantic search is currently blocked. Using keyword search until the semantic backend is healthy again.");
51
+ }
35
52
  // Try to open the database
36
53
  const dbPath = getDbPath();
37
54
  try {
@@ -41,13 +58,27 @@ export async function searchLocal(input) {
41
58
  try {
42
59
  const entryCount = getEntryCount(db);
43
60
  const storedStashDir = getMeta(db, "stashDir");
44
- if (entryCount > 0 && storedStashDir === stashDir) {
61
+ // Accept the index if the incoming stashDir matches the primary OR
62
+ // appears anywhere in the stored stashDirs array. This prevents
63
+ // unnecessary substring fallback when only the primary dir changes.
64
+ let stashDirMatch = storedStashDir === stashDir;
65
+ if (!stashDirMatch) {
66
+ try {
67
+ const storedDirs = JSON.parse(getMeta(db, "stashDirs") ?? "[]");
68
+ stashDirMatch = storedDirs.includes(stashDir);
69
+ }
70
+ catch {
71
+ /* ignore malformed stashDirs */
72
+ }
73
+ }
74
+ if (entryCount > 0 && stashDirMatch) {
45
75
  const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, sources);
46
76
  return {
47
77
  hits,
48
78
  tip: hits.length === 0
49
79
  ? "No matching stash assets were found. Try running 'akm index' to rebuild."
50
80
  : undefined,
81
+ warnings: warnings.length > 0 ? warnings : undefined,
51
82
  embedMs,
52
83
  rankMs,
53
84
  };
@@ -66,6 +97,7 @@ export async function searchLocal(input) {
66
97
  return {
67
98
  hits,
68
99
  tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
100
+ warnings: warnings.length > 0 ? warnings : undefined,
69
101
  };
70
102
  }
71
103
  // ── Database search ─────────────────────────────────────────────────────────
@@ -335,7 +367,8 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
335
367
  }
336
368
  // ── Vector scorer ───────────────────────────────────────────────────────────
337
369
  async function tryVecScores(db, query, k, config) {
338
- if (!config.semanticSearch)
370
+ const semanticStatus = getEffectiveSemanticStatus(config, readSemanticStatus());
371
+ if (!isSemanticRuntimeReady(semanticStatus))
339
372
  return null;
340
373
  const hasEmbeddings = getMeta(db, "hasEmbeddings");
341
374
  if (hasEmbeddings !== "1")
package/dist/paths.js CHANGED
@@ -68,6 +68,9 @@ export function getCacheDir() {
68
68
  export function getDbPath() {
69
69
  return path.join(getCacheDir(), "index.db");
70
70
  }
71
+ export function getSemanticStatusPath() {
72
+ return path.join(getCacheDir(), "semantic-status.json");
73
+ }
71
74
  export function getRegistryCacheDir() {
72
75
  return path.join(getCacheDir(), "registry");
73
76
  }
@@ -13,7 +13,7 @@ const DEFAULT_MANUAL_ENTRIES_PATH = path.resolve("manual-entries.json");
13
13
  const DEFAULT_OUTPUT_PATH = path.resolve("index.json");
14
14
  const REQUIRED_KEYWORDS = ["akm-kit"];
15
15
  const GITHUB_TOPICS = ["akm-kit"];
16
- const EXCLUDED_REPOS = new Set(["itlackey/agentikit"]);
16
+ const EXCLUDED_REPOS = new Set(["itlackey/akm"]);
17
17
  const EXCLUDED_NPM_PACKAGES = new Set(["akm-cli"]);
18
18
  const EMPTY_INSPECTION = {};
19
19
  export async function buildRegistryIndex(options) {