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/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
|
-
|
|
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
|
|
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",
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
375
|
+
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
376
|
+
return { success: true };
|
|
315
377
|
}
|
|
316
378
|
catch (error) {
|
|
317
|
-
|
|
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: ${
|
|
383
|
+
message: `Embedding generation failed: ${message}`,
|
|
321
384
|
});
|
|
322
|
-
return
|
|
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.
|
|
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(
|
|
351
|
-
if (
|
|
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.
|
|
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 (
|
|
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:
|
|
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 (
|
|
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,
|
package/dist/installed-kits.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Source operations: list, remove, update.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `akm
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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:
|
|
59
|
-
source:
|
|
60
|
-
ref:
|
|
61
|
-
cacheDir:
|
|
62
|
-
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
|
-
|
|
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
|
|
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
|
-
|
|
247
|
+
return undefined;
|
|
171
248
|
}
|
|
172
249
|
function toInstalledEntry(status) {
|
|
173
250
|
// KitInstallStatus extends InstalledKitEntry; omit the extra extractedDir field.
|
package/dist/local-search.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|