akm-cli 0.5.0 → 0.6.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/CHANGELOG.md +53 -5
- package/README.md +9 -9
- package/dist/cli.js +379 -1448
- package/dist/{completions.js → commands/completions.js} +1 -1
- package/dist/{config-cli.js → commands/config-cli.js} +109 -11
- package/dist/commands/curate.js +263 -0
- package/dist/{info.js → commands/info.js} +17 -11
- package/dist/{init.js → commands/init.js} +4 -4
- package/dist/{install-audit.js → commands/install-audit.js} +14 -2
- package/dist/{installed-kits.js → commands/installed-stashes.js} +122 -50
- package/dist/commands/migration-help.js +141 -0
- package/dist/{registry-search.js → commands/registry-search.js} +68 -9
- package/dist/commands/remember.js +178 -0
- package/dist/{stash-search.js → commands/search.js} +28 -69
- package/dist/{self-update.js → commands/self-update.js} +3 -3
- package/dist/{stash-show.js → commands/show.js} +106 -81
- package/dist/{stash-add.js → commands/source-add.js} +133 -67
- package/dist/{stash-clone.js → commands/source-clone.js} +15 -13
- package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
- package/dist/{vault.js → commands/vault.js} +43 -0
- package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
- package/dist/{asset-registry.js → core/asset-registry.js} +30 -6
- package/dist/{asset-spec.js → core/asset-spec.js} +13 -6
- package/dist/{common.js → core/common.js} +147 -50
- package/dist/{config.js → core/config.js} +288 -29
- package/dist/core/errors.js +90 -0
- package/dist/{frontmatter.js → core/frontmatter.js} +64 -8
- package/dist/{paths.js → core/paths.js} +4 -4
- package/dist/core/write-source.js +280 -0
- package/dist/{local-search.js → indexer/db-search.js} +49 -32
- package/dist/{db.js → indexer/db.js} +210 -81
- package/dist/{file-context.js → indexer/file-context.js} +3 -3
- package/dist/{indexer.js → indexer/indexer.js} +153 -30
- package/dist/{manifest.js → indexer/manifest.js} +10 -10
- package/dist/{matchers.js → indexer/matchers.js} +4 -7
- package/dist/{metadata.js → indexer/metadata.js} +9 -5
- package/dist/{search-source.js → indexer/search-source.js} +97 -55
- package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
- package/dist/{walker.js → indexer/walker.js} +1 -1
- package/dist/{lockfile.js → integrations/lockfile.js} +29 -2
- package/dist/{llm.js → llm/client.js} +12 -48
- package/dist/llm/embedder.js +127 -0
- package/dist/llm/embedders/cache.js +47 -0
- package/dist/llm/embedders/local.js +152 -0
- package/dist/llm/embedders/remote.js +121 -0
- package/dist/llm/embedders/types.js +39 -0
- package/dist/llm/metadata-enhance.js +53 -0
- package/dist/output/cli-hints.js +301 -0
- package/dist/output/context.js +95 -0
- package/dist/{renderers.js → output/renderers.js} +57 -61
- package/dist/output/shapes.js +212 -0
- package/dist/output/text.js +520 -0
- package/dist/{registry-build-index.js → registry/build-index.js} +48 -32
- package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
- package/dist/registry/factory.js +33 -0
- package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
- package/dist/registry/providers/index.js +11 -0
- package/dist/{providers → registry/providers}/skills-sh.js +60 -4
- package/dist/{providers → registry/providers}/static-index.js +126 -56
- package/dist/registry/providers/types.js +25 -0
- package/dist/{registry-resolve.js → registry/resolve.js} +10 -6
- package/dist/{detect.js → setup/detect.js} +0 -27
- package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
- package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
- package/dist/{setup.js → setup/setup.js} +162 -129
- package/dist/setup/steps.js +45 -0
- package/dist/{kit-include.js → sources/include.js} +1 -1
- package/dist/sources/provider-factory.js +36 -0
- package/dist/sources/provider.js +21 -0
- package/dist/sources/providers/filesystem.js +35 -0
- package/dist/{stash-providers → sources/providers}/git.js +218 -28
- package/dist/{stash-providers → sources/providers}/index.js +4 -4
- package/dist/sources/providers/install-types.js +14 -0
- package/dist/sources/providers/npm.js +160 -0
- package/dist/sources/providers/provider-utils.js +173 -0
- package/dist/sources/providers/sync-from-ref.js +45 -0
- package/dist/sources/providers/tar-utils.js +154 -0
- package/dist/{stash-providers → sources/providers}/website.js +60 -20
- package/dist/{stash-resolve.js → sources/resolve.js} +13 -12
- package/dist/{wiki.js → wiki/wiki.js} +18 -17
- package/dist/{workflow-authoring.js → workflows/authoring.js} +48 -17
- package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
- package/dist/{workflow-db.js → workflows/db.js} +1 -1
- package/dist/workflows/document-cache.js +20 -0
- package/dist/workflows/parser.js +379 -0
- package/dist/workflows/renderer.js +78 -0
- package/dist/{workflow-runs.js → workflows/runs.js} +84 -30
- package/dist/workflows/schema.js +11 -0
- package/dist/workflows/validator.js +48 -0
- package/docs/README.md +30 -0
- package/docs/migration/release-notes/0.0.13.md +4 -0
- package/docs/migration/release-notes/0.1.0.md +6 -0
- package/docs/migration/release-notes/0.2.0.md +6 -0
- package/docs/migration/release-notes/0.3.0.md +5 -0
- package/docs/migration/release-notes/0.5.0.md +6 -0
- package/docs/migration/release-notes/0.6.0.md +75 -0
- package/docs/migration/release-notes/README.md +21 -0
- package/package.json +3 -2
- package/dist/embedder.js +0 -351
- package/dist/errors.js +0 -34
- package/dist/migration-help.js +0 -110
- package/dist/registry-factory.js +0 -19
- package/dist/registry-install.js +0 -532
- package/dist/ripgrep.js +0 -2
- package/dist/stash-provider-factory.js +0 -35
- package/dist/stash-provider.js +0 -1
- package/dist/stash-providers/filesystem.js +0 -41
- package/dist/stash-providers/openviking.js +0 -348
- package/dist/stash-providers/provider-utils.js +0 -11
- package/dist/stash-types.js +0 -1
- package/dist/workflow-markdown.js +0 -251
- /package/dist/{markdown.js → core/markdown.js} +0 -0
- /package/dist/{warn.js → core/warn.js} +0 -0
- /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
- /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
- /package/dist/{github.js → integrations/github.js} +0 -0
- /package/dist/{registry-provider.js → registry/types.js} +0 -0
- /package/dist/{registry-types.js → sources/types.js} +0 -0
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { isHttpUrl, resolveStashDir } from "
|
|
3
|
+
import { isHttpUrl, resolveStashDir, toErrorMessage } from "../core/common";
|
|
4
|
+
import { getDbPath } from "../core/paths";
|
|
5
|
+
import { warn } from "../core/warn";
|
|
6
|
+
import { takeWorkflowDocument } from "../workflows/document-cache";
|
|
4
7
|
import { closeDatabase, deleteEntriesByDir, deleteEntriesByStashDir, getEmbeddingCount, getEntriesByDir, getEntryCount, getMeta, isVecAvailable, openDatabase, rebuildFts, setMeta, upsertEmbedding, upsertEntry, upsertUtilityScore, warnIfVecMissing, } from "./db";
|
|
5
8
|
import { generateMetadataFlat, loadStashFile, shouldIndexStashFile } from "./metadata";
|
|
6
|
-
import { getDbPath } from "./paths";
|
|
7
9
|
import { buildSearchText } from "./search-fields";
|
|
8
10
|
import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./semantic-status";
|
|
9
11
|
import { ensureUsageEventsSchema, purgeOldUsageEvents } from "./usage-events";
|
|
10
12
|
import { walkStashFlat } from "./walker";
|
|
11
|
-
import { warn } from "./warn";
|
|
12
13
|
// ── Indexer ──────────────────────────────────────────────────────────────────
|
|
13
14
|
export async function akmIndex(options) {
|
|
14
15
|
const stashDir = options?.stashDir || resolveStashDir();
|
|
15
16
|
const onProgress = options?.onProgress ?? (() => { });
|
|
16
17
|
// Load config and resolve all stash sources
|
|
17
|
-
const { loadConfig } = await import("
|
|
18
|
+
const { loadConfig } = await import("../core/config.js");
|
|
18
19
|
const config = loadConfig();
|
|
19
20
|
// Ensure git stash caches are extracted before resolving stash dirs,
|
|
20
21
|
// so their content directories exist on disk for the walker to discover.
|
|
21
|
-
const {
|
|
22
|
-
await
|
|
23
|
-
const
|
|
24
|
-
const
|
|
22
|
+
const { ensureSourceCaches, resolveSourceEntries } = await import("./search-source.js");
|
|
23
|
+
await ensureSourceCaches(config);
|
|
24
|
+
const allSourceEntries = resolveSourceEntries(stashDir, config);
|
|
25
|
+
const allSourceDirs = allSourceEntries.map((s) => s.path);
|
|
25
26
|
const t0 = Date.now();
|
|
26
27
|
// Open database — pass embedding dimension from config if available
|
|
27
28
|
const dbPath = getDbPath();
|
|
@@ -37,7 +38,7 @@ export async function akmIndex(options) {
|
|
|
37
38
|
phase: "summary",
|
|
38
39
|
message: buildIndexSummaryMessage({
|
|
39
40
|
mode: isIncremental ? "incremental" : "full",
|
|
40
|
-
|
|
41
|
+
sourcesCount: allSourceDirs.length,
|
|
41
42
|
semanticSearchMode: config.semanticSearchMode,
|
|
42
43
|
embeddingProvider: getEmbeddingProvider(config.embedding),
|
|
43
44
|
llmEnabled: !!config.llm,
|
|
@@ -67,7 +68,7 @@ export async function akmIndex(options) {
|
|
|
67
68
|
catch {
|
|
68
69
|
warn("index_meta stashDirs value is corrupt JSON — treating as empty");
|
|
69
70
|
}
|
|
70
|
-
const currentSet = new Set(
|
|
71
|
+
const currentSet = new Set(allSourceDirs);
|
|
71
72
|
for (const dir of prevStashDirs) {
|
|
72
73
|
if (!currentSet.has(dir)) {
|
|
73
74
|
deleteEntriesByStashDir(db, dir);
|
|
@@ -80,7 +81,7 @@ export async function akmIndex(options) {
|
|
|
80
81
|
// doFullDelete=true merges the wipe into the same transaction as the
|
|
81
82
|
// inserts so readers never see an empty database mid-rebuild.
|
|
82
83
|
const doFullDelete = options?.full || !isIncremental;
|
|
83
|
-
const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm, warnings } = await indexEntries(db,
|
|
84
|
+
const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm, warnings } = await indexEntries(db, allSourceEntries, isIncremental, builtAtMs, doFullDelete);
|
|
84
85
|
onProgress({
|
|
85
86
|
phase: "scan",
|
|
86
87
|
message: `Scanned ${scannedDirs} ${scannedDirs === 1 ? "directory" : "directories"} and skipped ${skippedDirs}.`,
|
|
@@ -95,9 +96,15 @@ export async function akmIndex(options) {
|
|
|
95
96
|
: "LLM enhancement disabled.",
|
|
96
97
|
});
|
|
97
98
|
const tLlmEnd = Date.now();
|
|
98
|
-
// Rebuild FTS after all inserts
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// Rebuild FTS after all inserts. Use incremental mode when this whole
|
|
100
|
+
// index run is incremental — only entries touched by `upsertEntry`
|
|
101
|
+
// since the last rebuild are re-indexed, instead of re-scanning every
|
|
102
|
+
// row on every `akm index` invocation.
|
|
103
|
+
rebuildFts(db, { incremental: isIncremental });
|
|
104
|
+
onProgress({
|
|
105
|
+
phase: "fts",
|
|
106
|
+
message: isIncremental ? "Rebuilt full-text search index (dirty rows only)." : "Rebuilt full-text search index.",
|
|
107
|
+
});
|
|
101
108
|
const tFtsEnd = Date.now();
|
|
102
109
|
// Re-link detached usage_events to their new entry_ids via entry_ref.
|
|
103
110
|
// entry_ref is "type:name" (e.g., "skill:code-review"), entry_key is "stashDir:type:name".
|
|
@@ -124,7 +131,7 @@ export async function akmIndex(options) {
|
|
|
124
131
|
// are read-only caches, and regenerating their indexes would mutate
|
|
125
132
|
// cache content.
|
|
126
133
|
try {
|
|
127
|
-
const { regenerateAllWikiIndexes } = await import("
|
|
134
|
+
const { regenerateAllWikiIndexes } = await import("../wiki/wiki.js");
|
|
128
135
|
regenerateAllWikiIndexes(stashDir);
|
|
129
136
|
}
|
|
130
137
|
catch {
|
|
@@ -136,7 +143,7 @@ export async function akmIndex(options) {
|
|
|
136
143
|
// Update metadata
|
|
137
144
|
setMeta(db, "builtAt", new Date().toISOString());
|
|
138
145
|
setMeta(db, "stashDir", stashDir);
|
|
139
|
-
setMeta(db, "stashDirs", JSON.stringify(
|
|
146
|
+
setMeta(db, "stashDirs", JSON.stringify(allSourceDirs));
|
|
140
147
|
setMeta(db, "hasEmbeddings", embeddingResult.success ? "1" : "0");
|
|
141
148
|
const totalEntries = getEntryCount(db);
|
|
142
149
|
// Warn on every index run if using JS fallback with many entries
|
|
@@ -182,7 +189,7 @@ export async function akmIndex(options) {
|
|
|
182
189
|
}
|
|
183
190
|
}
|
|
184
191
|
// ── Extracted helpers for indexing ────────────────────────────────────────────
|
|
185
|
-
async function indexEntries(db,
|
|
192
|
+
async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, doFullDelete = false) {
|
|
186
193
|
let scannedDirs = 0;
|
|
187
194
|
let skippedDirs = 0;
|
|
188
195
|
let generatedCount = 0;
|
|
@@ -190,12 +197,12 @@ async function indexEntries(db, allStashSources, isIncremental, builtAtMs, doFul
|
|
|
190
197
|
const seenPaths = new Set();
|
|
191
198
|
const dirsNeedingLlm = [];
|
|
192
199
|
const dirRecords = [];
|
|
193
|
-
for (const
|
|
194
|
-
const currentStashDir =
|
|
200
|
+
for (const sourceAdded of allSourceEntries) {
|
|
201
|
+
const currentStashDir = sourceAdded.path;
|
|
195
202
|
const fileContexts = walkStashFlat(currentStashDir);
|
|
196
203
|
// Wiki-root stashes: all .md files are indexed as wiki pages under wikiName
|
|
197
|
-
if (
|
|
198
|
-
const wikiName =
|
|
204
|
+
if (sourceAdded.wikiName) {
|
|
205
|
+
const wikiName = sourceAdded.wikiName;
|
|
199
206
|
const wikiDirGroups = new Map();
|
|
200
207
|
for (const ctx of fileContexts) {
|
|
201
208
|
if (ctx.ext !== ".md")
|
|
@@ -347,7 +354,13 @@ async function indexEntries(db, allStashSources, isIncremental, builtAtMs, doFul
|
|
|
347
354
|
const entryKey = `${currentStashDir}:${entry.type}:${entry.name}`;
|
|
348
355
|
const searchText = buildSearchText(entry);
|
|
349
356
|
const entryWithSize = attachFileSize(entry, entryPath);
|
|
350
|
-
upsertEntry(db, entryKey, dirPath, entryPath, currentStashDir, entryWithSize, searchText);
|
|
357
|
+
const entryId = upsertEntry(db, entryKey, dirPath, entryPath, currentStashDir, entryWithSize, searchText);
|
|
358
|
+
if (entry.type === "workflow") {
|
|
359
|
+
const doc = takeWorkflowDocument(entry);
|
|
360
|
+
if (doc) {
|
|
361
|
+
upsertWorkflowDocument(db, entryId, doc, fs.readFileSync(entryPath));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
351
364
|
}
|
|
352
365
|
// Collect dirs needing LLM enhancement during the first walk
|
|
353
366
|
if (stash.entries.some((e) => e.quality === "generated")) {
|
|
@@ -362,13 +375,17 @@ async function indexEntries(db, allStashSources, isIncremental, builtAtMs, doFul
|
|
|
362
375
|
async function enhanceDirsWithLlm(db, config, dirsNeedingLlm) {
|
|
363
376
|
if (!config.llm || dirsNeedingLlm.length === 0)
|
|
364
377
|
return;
|
|
378
|
+
// Aggregate per-entry failures so a misconfigured LLM endpoint surfaces
|
|
379
|
+
// as a single visible warning instead of silently degrading every entry
|
|
380
|
+
// and leaving the user wondering why nothing got enhanced.
|
|
381
|
+
const summary = { attempted: 0, succeeded: 0, failureSamples: [] };
|
|
365
382
|
for (const { dirPath, files, currentStashDir, stash: originalStash } of dirsNeedingLlm) {
|
|
366
383
|
// Only enhance generated entries; user-provided overrides should not be overwritten
|
|
367
384
|
const generatedEntries = originalStash.entries.filter((e) => e.quality === "generated");
|
|
368
385
|
if (generatedEntries.length === 0)
|
|
369
386
|
continue;
|
|
370
387
|
const generatedStash = { entries: generatedEntries };
|
|
371
|
-
const enhanced = await enhanceStashWithLlm(config.llm, generatedStash, files);
|
|
388
|
+
const enhanced = await enhanceStashWithLlm(config.llm, generatedStash, files, summary);
|
|
372
389
|
// Re-upsert the enhanced entries in a single transaction so a crash
|
|
373
390
|
// cannot leave half the entries updated and the rest stale.
|
|
374
391
|
db.transaction(() => {
|
|
@@ -380,6 +397,16 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm) {
|
|
|
380
397
|
}
|
|
381
398
|
})();
|
|
382
399
|
}
|
|
400
|
+
if (summary.attempted > 0 && summary.succeeded === 0) {
|
|
401
|
+
const sample = summary.failureSamples.length ? ` Example: ${summary.failureSamples[0]}` : "";
|
|
402
|
+
warn(`LLM enhancement failed for all ${summary.attempted} attempted entries — index built without LLM enrichment.` +
|
|
403
|
+
` Check llm.endpoint and llm.model in your config.${sample}`);
|
|
404
|
+
}
|
|
405
|
+
else if (summary.attempted > 0 && summary.succeeded < summary.attempted) {
|
|
406
|
+
const failed = summary.attempted - summary.succeeded;
|
|
407
|
+
const sample = summary.failureSamples.length ? ` Examples: ${summary.failureSamples.join("; ")}` : "";
|
|
408
|
+
warn(`LLM enhancement failed for ${failed}/${summary.attempted} entries — they were left un-enhanced.${sample}`);
|
|
409
|
+
}
|
|
383
410
|
}
|
|
384
411
|
async function generateEmbeddingsForDb(db, config, onProgress) {
|
|
385
412
|
if (config.semanticSearchMode === "off") {
|
|
@@ -408,7 +435,7 @@ async function generateEmbeddingsForDb(db, config, onProgress) {
|
|
|
408
435
|
setMeta(db, "hasEmbeddings", "0");
|
|
409
436
|
}
|
|
410
437
|
try {
|
|
411
|
-
const { embedBatch } = await import("
|
|
438
|
+
const { embedBatch } = await import("../llm/embedder.js");
|
|
412
439
|
const allEntries = getAllEntriesForEmbedding(db);
|
|
413
440
|
if (allEntries.length === 0) {
|
|
414
441
|
onProgress({ phase: "embeddings", message: "Embeddings already up to date." });
|
|
@@ -466,10 +493,32 @@ function attachFileSize(entry, entryPath) {
|
|
|
466
493
|
return entry;
|
|
467
494
|
}
|
|
468
495
|
}
|
|
496
|
+
function upsertWorkflowDocument(db, entryId, doc, content) {
|
|
497
|
+
const sourceHash = computeSourceHash(content);
|
|
498
|
+
db.prepare(`INSERT INTO workflow_documents (entry_id, schema_version, document_json, source_path, source_hash, updated_at)
|
|
499
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
500
|
+
ON CONFLICT(entry_id) DO UPDATE SET
|
|
501
|
+
schema_version = excluded.schema_version,
|
|
502
|
+
document_json = excluded.document_json,
|
|
503
|
+
source_path = excluded.source_path,
|
|
504
|
+
source_hash = excluded.source_hash,
|
|
505
|
+
updated_at = excluded.updated_at`).run(entryId, doc.schemaVersion, JSON.stringify(doc), doc.source.path, sourceHash, new Date().toISOString());
|
|
506
|
+
}
|
|
507
|
+
function computeSourceHash(content) {
|
|
508
|
+
// Cheap, stable identity for the source markdown — used by future
|
|
509
|
+
// incremental fast-paths that skip re-validation when content is unchanged.
|
|
510
|
+
// Not security-sensitive; FNV-1a over the bytes is sufficient.
|
|
511
|
+
let hash = 0x811c9dc5;
|
|
512
|
+
for (let i = 0; i < content.length; i++) {
|
|
513
|
+
hash ^= content[i];
|
|
514
|
+
hash = Math.imul(hash, 0x01000193);
|
|
515
|
+
}
|
|
516
|
+
return (hash >>> 0).toString(16);
|
|
517
|
+
}
|
|
469
518
|
function buildIndexSummaryMessage(options) {
|
|
470
|
-
const stashSourceLabel = options.
|
|
519
|
+
const stashSourceLabel = options.sourcesCount === 1 ? "stash source" : "stash sources";
|
|
471
520
|
const semanticDetail = getSemanticSearchLabel(options.semanticSearchMode, options.embeddingProvider, options.vecAvailable);
|
|
472
|
-
return `Starting ${options.mode} index (${options.
|
|
521
|
+
return `Starting ${options.mode} index (${options.sourcesCount} ${stashSourceLabel}, semantic search: ${semanticDetail}, LLM: ${options.llmEnabled ? "enabled" : "disabled"}).`;
|
|
473
522
|
}
|
|
474
523
|
function getEmbeddingProvider(embedding) {
|
|
475
524
|
return isHttpUrl(embedding?.endpoint) ? "remote" : "local";
|
|
@@ -569,10 +618,11 @@ function isDirStale(dirPath, currentFiles, previousEntries, builtAtMs) {
|
|
|
569
618
|
}
|
|
570
619
|
return false;
|
|
571
620
|
}
|
|
572
|
-
async function enhanceStashWithLlm(llmConfig, stash, files) {
|
|
573
|
-
const { enhanceMetadata } = await import("
|
|
621
|
+
async function enhanceStashWithLlm(llmConfig, stash, files, summary) {
|
|
622
|
+
const { enhanceMetadata } = await import("../llm/metadata-enhance");
|
|
574
623
|
const enhanced = [];
|
|
575
624
|
for (const entry of stash.entries) {
|
|
625
|
+
summary.attempted++;
|
|
576
626
|
try {
|
|
577
627
|
const entryFile = entry.filename
|
|
578
628
|
? (files.find((f) => path.basename(f) === entry.filename) ?? files[0])
|
|
@@ -595,9 +645,16 @@ async function enhanceStashWithLlm(llmConfig, stash, files) {
|
|
|
595
645
|
if (improvements.tags?.length)
|
|
596
646
|
updated.tags = improvements.tags;
|
|
597
647
|
enhanced.push(updated);
|
|
648
|
+
summary.succeeded++;
|
|
598
649
|
}
|
|
599
|
-
catch {
|
|
650
|
+
catch (err) {
|
|
600
651
|
enhanced.push(entry);
|
|
652
|
+
const msg = toErrorMessage(err);
|
|
653
|
+
// failureSamples is bounded to 3 items, so a linear scan is cheaper
|
|
654
|
+
// than maintaining a parallel Set for membership checks (#177 review).
|
|
655
|
+
if (summary.failureSamples.length < 3 && !summary.failureSamples.includes(msg)) {
|
|
656
|
+
summary.failureSamples.push(msg);
|
|
657
|
+
}
|
|
601
658
|
}
|
|
602
659
|
}
|
|
603
660
|
return { entries: enhanced };
|
|
@@ -639,7 +696,73 @@ export function matchEntryToFile(entryName, fileMap, files) {
|
|
|
639
696
|
// Fallback to first file, or null if no files are available
|
|
640
697
|
return files[0] || null;
|
|
641
698
|
}
|
|
642
|
-
|
|
699
|
+
/**
|
|
700
|
+
* Look up a single asset by ref. Spec §6.2 — `akm show` queries this and
|
|
701
|
+
* reads the file from disk. The index is the source of truth for which
|
|
702
|
+
* file corresponds to which ref; the indexer walks `provider.path()` for
|
|
703
|
+
* every configured source, so this query covers all source kinds.
|
|
704
|
+
*
|
|
705
|
+
* Match rules:
|
|
706
|
+
* - `ref.origin === undefined` → first match across all sources (primary
|
|
707
|
+
* source first, then in declared order — same priority as the indexer's
|
|
708
|
+
* write order).
|
|
709
|
+
* - `ref.origin === "local"` → primary source only (entry_key prefix is
|
|
710
|
+
* the primary stash dir).
|
|
711
|
+
* - `ref.origin === <name>` → restrict to the matching source name. We
|
|
712
|
+
* resolve the source's directory and match on `entry_key` prefix.
|
|
713
|
+
*
|
|
714
|
+
* Returns `null` when no row matches — callers translate that into a
|
|
715
|
+
* `NotFoundError` with their own messaging.
|
|
716
|
+
*/
|
|
717
|
+
export async function lookup(ref) {
|
|
718
|
+
const { loadConfig } = await import("../core/config.js");
|
|
719
|
+
const { resolveSourceEntries } = await import("./search-source.js");
|
|
720
|
+
const config = loadConfig();
|
|
721
|
+
const sources = resolveSourceEntries(undefined, config);
|
|
722
|
+
if (sources.length === 0)
|
|
723
|
+
return null;
|
|
724
|
+
const dbPath = getDbPath();
|
|
725
|
+
const db = openDatabase(dbPath);
|
|
726
|
+
try {
|
|
727
|
+
// entry_key shape: `${stashDir}:${type}:${name}`. Suffix-match on
|
|
728
|
+
// `:type:name` so we can scope by source dir as a prefix when origin is
|
|
729
|
+
// supplied. Use parameterised queries throughout — names may include
|
|
730
|
+
// user-supplied glob characters.
|
|
731
|
+
const escapeLike = (value) => value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
732
|
+
const suffix = `:${ref.type}:${ref.name}`;
|
|
733
|
+
const escapedSuffix = escapeLike(suffix);
|
|
734
|
+
const candidateDirs = (() => {
|
|
735
|
+
if (!ref.origin)
|
|
736
|
+
return sources.map((s) => s.path);
|
|
737
|
+
if (ref.origin === "local")
|
|
738
|
+
return [sources[0].path];
|
|
739
|
+
const named = sources.find((s) => s.registryId === ref.origin);
|
|
740
|
+
return named ? [named.path] : [];
|
|
741
|
+
})();
|
|
742
|
+
if (candidateDirs.length === 0)
|
|
743
|
+
return null;
|
|
744
|
+
for (const dir of candidateDirs) {
|
|
745
|
+
const escapedDir = escapeLike(dir);
|
|
746
|
+
const row = db
|
|
747
|
+
.prepare("SELECT entry_key AS entryKey, file_path AS filePath, stash_dir AS stashDir, entry_type AS type FROM entries " +
|
|
748
|
+
"WHERE entry_key LIKE ? ESCAPE '\\' AND entry_type = ? LIMIT 1")
|
|
749
|
+
.get(`${escapedDir}${escapedSuffix}`, ref.type);
|
|
750
|
+
if (row) {
|
|
751
|
+
return {
|
|
752
|
+
entryKey: row.entryKey,
|
|
753
|
+
filePath: row.filePath,
|
|
754
|
+
stashDir: row.stashDir,
|
|
755
|
+
type: row.type,
|
|
756
|
+
name: ref.name,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
finally {
|
|
763
|
+
closeDatabase(db);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
643
766
|
// ── Utility score recomputation ──────────────────────────────────────────────
|
|
644
767
|
/** Retention window for usage events: events older than this are purged. */
|
|
645
768
|
const USAGE_EVENT_RETENTION_DAYS = 90;
|
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { makeAssetRef } from "../core/asset-ref";
|
|
12
|
+
import { deriveCanonicalAssetNameFromStashRoot } from "../core/asset-spec";
|
|
13
|
+
import { resolveStashDir } from "../core/common";
|
|
14
|
+
import { loadConfig } from "../core/config";
|
|
15
|
+
import { getDbPath } from "../core/paths";
|
|
16
|
+
import { warn } from "../core/warn";
|
|
14
17
|
import { closeDatabase, getAllEntries, getEntryCount, getMeta, openDatabase } from "./db";
|
|
15
18
|
import { generateMetadataFlat, loadStashFile } from "./metadata";
|
|
16
|
-
import {
|
|
17
|
-
import { resolveStashSources } from "./search-source";
|
|
18
|
-
import { makeAssetRef } from "./stash-ref";
|
|
19
|
+
import { resolveSourceEntries } from "./search-source";
|
|
19
20
|
import { walkStashFlat } from "./walker";
|
|
20
|
-
import { warn } from "./warn";
|
|
21
21
|
const MAX_DESCRIPTION_LENGTH = 80;
|
|
22
22
|
/**
|
|
23
23
|
* Truncate a description string to a maximum length, appending "..." if truncated.
|
|
@@ -99,9 +99,9 @@ function getManifestFromDb(stashDir, config, sources, type) {
|
|
|
99
99
|
* Get the manifest by walking the stash directory (fallback when no index).
|
|
100
100
|
*/
|
|
101
101
|
async function getManifestFromWalker(sources, type) {
|
|
102
|
-
const
|
|
102
|
+
const allSourceDirs = sources.map((s) => s.path);
|
|
103
103
|
const entries = [];
|
|
104
|
-
for (const currentStashDir of
|
|
104
|
+
for (const currentStashDir of allSourceDirs) {
|
|
105
105
|
const fileContexts = walkStashFlat(currentStashDir);
|
|
106
106
|
// Group by parent directory
|
|
107
107
|
const dirGroups = new Map();
|
|
@@ -154,7 +154,7 @@ export async function akmManifest(options) {
|
|
|
154
154
|
const stashDir = options?.stashDir ?? resolveStashDir();
|
|
155
155
|
const type = options?.type;
|
|
156
156
|
const config = loadConfig();
|
|
157
|
-
const sources =
|
|
157
|
+
const sources = resolveSourceEntries(stashDir, config);
|
|
158
158
|
// Fast path: try database
|
|
159
159
|
const dbEntries = getManifestFromDb(stashDir, config, sources, type);
|
|
160
160
|
if (dbEntries !== null) {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* - `wikiMatcher` (20) -- classifies any `.md` under `wikis/<name>/…` as
|
|
19
19
|
* `wiki`. Registered last so the later-wins tiebreaker beats agent at 20.
|
|
20
20
|
*/
|
|
21
|
-
import { SCRIPT_EXTENSIONS } from "
|
|
21
|
+
import { SCRIPT_EXTENSIONS } from "../core/asset-spec";
|
|
22
|
+
import { looksLikeWorkflow } from "../workflows/parser";
|
|
22
23
|
import { registerMatcher } from "./file-context";
|
|
23
24
|
// ── extensionMatcher (specificity: 3) ────────────────────────────────────────
|
|
24
25
|
/**
|
|
@@ -51,7 +52,7 @@ export function extensionMatcher(ctx) {
|
|
|
51
52
|
* directory segment from the stash root matches a known type name.
|
|
52
53
|
*
|
|
53
54
|
* The first matching type-like ancestor wins. This preserves intuitive
|
|
54
|
-
* behavior for nested
|
|
55
|
+
* behavior for nested stash layouts such as `agent-stash/agents/blog/foo.md`
|
|
55
56
|
* while still honoring earlier type roots like `commands/agents/foo.md`.
|
|
56
57
|
*/
|
|
57
58
|
export function directoryMatcher(ctx) {
|
|
@@ -140,11 +141,7 @@ export function smartMdMatcher(ctx) {
|
|
|
140
141
|
if (ctx.ext !== ".md")
|
|
141
142
|
return null;
|
|
142
143
|
const body = ctx.content();
|
|
143
|
-
|
|
144
|
-
/^##\s+Step:\s+/m.test(body) &&
|
|
145
|
-
/^Step ID:\s+/m.test(body) &&
|
|
146
|
-
/^###\s+Instructions\s*$/m.test(body);
|
|
147
|
-
if (hasWorkflowSignals) {
|
|
144
|
+
if (looksLikeWorkflow(body)) {
|
|
148
145
|
return { type: "workflow", specificity: 19, renderer: "workflow-md" };
|
|
149
146
|
}
|
|
150
147
|
const fm = ctx.frontmatter();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { deriveCanonicalAssetName, deriveCanonicalAssetNameFromStashRoot, isRelevantAssetFile } from "
|
|
4
|
-
import { isAssetType } from "
|
|
3
|
+
import { deriveCanonicalAssetName, deriveCanonicalAssetNameFromStashRoot, isRelevantAssetFile, } from "../core/asset-spec";
|
|
4
|
+
import { isAssetType } from "../core/common";
|
|
5
|
+
import { parseFrontmatter, toStringOrUndefined } from "../core/frontmatter";
|
|
6
|
+
import { warn } from "../core/warn";
|
|
5
7
|
import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "./file-context";
|
|
6
|
-
import { parseFrontmatter, toStringOrUndefined } from "./frontmatter";
|
|
7
|
-
import { warn } from "./warn";
|
|
8
8
|
// ── Load / Write ────────────────────────────────────────────────────────────
|
|
9
9
|
const STASH_FILENAME = ".stash.json";
|
|
10
10
|
export function stashFilePath(dirPath) {
|
|
@@ -568,7 +568,11 @@ export async function generateMetadataFlat(stashRoot, files) {
|
|
|
568
568
|
}
|
|
569
569
|
function buildMetadataSkipWarning(filePath, assetType, error) {
|
|
570
570
|
const detail = error instanceof Error ? error.message : String(error);
|
|
571
|
-
|
|
571
|
+
// Workflow errors are already multi-line `path:line — message` blocks; print
|
|
572
|
+
// them as-is so the author sees a flat list without a redundant prefix.
|
|
573
|
+
const warning = assetType === "workflow"
|
|
574
|
+
? `Skipped workflow ${filePath}:\n${detail}`
|
|
575
|
+
: `Skipped malformed ${assetType} asset at ${filePath}: ${detail}`;
|
|
572
576
|
warn(warning);
|
|
573
577
|
return warning;
|
|
574
578
|
}
|
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { resolveStashDir } from "
|
|
4
|
-
import { loadConfig } from "
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { resolveStashDir } from "../core/common";
|
|
4
|
+
import { loadConfig } from "../core/config";
|
|
5
|
+
import { resolveSourceProviderFactory } from "../sources/provider-factory";
|
|
6
|
+
// Eager side-effect imports so all built-in source providers self-register
|
|
7
|
+
// before resolveEntryContentDir() runs.
|
|
8
|
+
import "../sources/providers/index";
|
|
9
|
+
import { warn } from "../core/warn";
|
|
10
|
+
import { ensureGitMirror, getCachePaths, parseGitRepoUrl } from "../sources/providers/git";
|
|
11
|
+
import { ensureWebsiteMirror } from "../sources/providers/website";
|
|
12
|
+
// Legacy "context-hub" / "github" type aliases are normalized to "git" at
|
|
13
|
+
// config-load time (see src/config.ts), so this set only contains the canonical
|
|
14
|
+
// type.
|
|
15
|
+
const GIT_STASH_TYPES = new Set(["git"]);
|
|
8
16
|
// ── Resolution ──────────────────────────────────────────────────────────────
|
|
9
17
|
/**
|
|
10
|
-
* Build the ordered list of stash sources
|
|
11
|
-
*
|
|
12
|
-
* 2. Additional stashes (filesystem and remote providers)
|
|
13
|
-
* 3. Installed kit paths (cache-managed, from registry)
|
|
18
|
+
* Build the ordered list of stash sources, walking every configured stash
|
|
19
|
+
* once. Iteration order:
|
|
14
20
|
*
|
|
15
|
-
* The
|
|
16
|
-
*
|
|
21
|
+
* 1. The primary stash directory (the entry marked `primary: true`, or the
|
|
22
|
+
* legacy top-level `stashDir`). Always emitted, even when the directory
|
|
23
|
+
* does not yet exist on disk, so callers can use it as the clone target.
|
|
24
|
+
* 2. Each entry in `config.sources ?? config.stashes[]` (in declared order), excluding the
|
|
25
|
+
* one already emitted as the primary.
|
|
26
|
+
* 3. Each entry in `config.installed[]` (registry-managed stashes).
|
|
27
|
+
*
|
|
28
|
+
* Replaces the previous four-pass loop that walked `stashes[]` separately
|
|
29
|
+
* for each provider kind. Disabled entries (`enabled: false`) and entries
|
|
30
|
+
* whose disk path doesn't exist are filtered after deduplication.
|
|
17
31
|
*/
|
|
18
|
-
export function
|
|
32
|
+
export function resolveSourceEntries(overrideStashDir, existingConfig) {
|
|
19
33
|
const stashDir = overrideStashDir ?? resolveStashDir();
|
|
20
34
|
const config = existingConfig ?? loadConfig();
|
|
21
35
|
const sources = [{ path: stashDir }];
|
|
@@ -36,53 +50,86 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
|
|
|
36
50
|
});
|
|
37
51
|
}
|
|
38
52
|
};
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
// (1) + (2) Single pass over declared stashes — primary first if present,
|
|
54
|
+
// then the rest in declared order. The primary's directory is already
|
|
55
|
+
// injected as `sources[0]` above, so we only need to dedupe the source set.
|
|
56
|
+
const stashes = config.sources ?? config.stashes ?? [];
|
|
57
|
+
const primaryIdx = stashes.findIndex((entry) => entry.primary === true);
|
|
58
|
+
const ordered = [];
|
|
59
|
+
if (primaryIdx >= 0) {
|
|
60
|
+
ordered.push(stashes[primaryIdx]);
|
|
61
|
+
stashes.forEach((entry, i) => {
|
|
62
|
+
if (i !== primaryIdx)
|
|
63
|
+
ordered.push(entry);
|
|
64
|
+
});
|
|
44
65
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const entry of config.stashes ?? []) {
|
|
48
|
-
if (GIT_STASH_TYPES.has(entry.type) && entry.url && entry.enabled !== false) {
|
|
49
|
-
try {
|
|
50
|
-
const repo = parseGitRepoUrl(entry.url);
|
|
51
|
-
const cachePaths = getCachePaths(repo.canonicalUrl);
|
|
52
|
-
// The content/ subdirectory inside the extracted repo is the actual
|
|
53
|
-
// stash root containing DOC.md / SKILL.md files that the walker indexes.
|
|
54
|
-
const contentDir = path.join(cachePaths.repoDir, "content");
|
|
55
|
-
addSource(contentDir, entry.name, entry.wikiName);
|
|
56
|
-
}
|
|
57
|
-
catch (err) {
|
|
58
|
-
warn(`Warning: failed to resolve git stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
66
|
+
else {
|
|
67
|
+
ordered.push(...stashes);
|
|
61
68
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
warn(`Warning: failed to resolve website stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
69
|
+
for (const entry of ordered) {
|
|
70
|
+
if (entry.enabled === false)
|
|
71
|
+
continue;
|
|
72
|
+
const dir = resolveEntryContentDir(entry);
|
|
73
|
+
if (dir == null)
|
|
74
|
+
continue;
|
|
75
|
+
addSource(dir, entry.name, entry.wikiName);
|
|
74
76
|
}
|
|
75
|
-
// Installed
|
|
77
|
+
// (3) Installed stashes (registry-managed). Always last.
|
|
76
78
|
for (const entry of config.installed ?? []) {
|
|
77
79
|
addSource(entry.stashRoot, entry.id, entry.wikiName);
|
|
78
80
|
}
|
|
79
81
|
return sources;
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the content directory the indexer should walk for a given config
|
|
85
|
+
* entry. Returns `undefined` if the entry has no walkable content
|
|
86
|
+
* so the caller can skip it.
|
|
87
|
+
*
|
|
88
|
+
* Single source of truth: each provider owns its own path. We instantiate the
|
|
89
|
+
* registered {@link import("../sources/provider").SourceProvider} for the entry
|
|
90
|
+
* and call `provider.path()`. This replaces the old per-kind switch ladder
|
|
91
|
+
* (filesystem path / git cache / website cache) that lived here in 0.6.0 —
|
|
92
|
+
* see spec §10 step 4 and §7 "Removed from 0.6.0".
|
|
93
|
+
*
|
|
94
|
+
* The git case still does one extra step: the provider returns the cloned
|
|
95
|
+
* repo dir, but the indexer walks the `content/` subdirectory inside it.
|
|
96
|
+
* That convention is part of the akm content layout, not a provider concern,
|
|
97
|
+
* so it stays here.
|
|
98
|
+
*/
|
|
99
|
+
function resolveEntryContentDir(entry) {
|
|
100
|
+
const factory = resolveSourceProviderFactory(entry.type);
|
|
101
|
+
if (!factory)
|
|
102
|
+
return undefined;
|
|
103
|
+
let provider;
|
|
104
|
+
try {
|
|
105
|
+
provider = factory(entry);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
warn(`Warning: failed to construct ${entry.type} source provider for "${entry.name ?? entry.url ?? entry.path}": ${err instanceof Error ? err.message : String(err)}`);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
let dir;
|
|
112
|
+
try {
|
|
113
|
+
dir = provider.path();
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
warn(`Warning: failed to resolve ${entry.type} source path for "${entry.name ?? entry.url ?? entry.path}": ${err instanceof Error ? err.message : String(err)}`);
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
// Git providers expose the cloned repo root as their path. The akm content
|
|
120
|
+
// layout puts indexable files under `<repo>/content/`, so the walker needs
|
|
121
|
+
// that subdirectory. This is a content-layout convention, not a provider
|
|
122
|
+
// capability — keep it here.
|
|
123
|
+
if (GIT_STASH_TYPES.has(entry.type)) {
|
|
124
|
+
return path.join(dir, "content");
|
|
125
|
+
}
|
|
126
|
+
return dir;
|
|
127
|
+
}
|
|
81
128
|
/**
|
|
82
129
|
* Convenience: returns just the directory paths, preserving priority order.
|
|
83
130
|
*/
|
|
84
131
|
export function resolveAllStashDirs(overrideStashDir) {
|
|
85
|
-
return
|
|
132
|
+
return resolveSourceEntries(overrideStashDir).map((s) => s.path);
|
|
86
133
|
}
|
|
87
134
|
/**
|
|
88
135
|
* Find which source a file path belongs to.
|
|
@@ -168,15 +215,14 @@ function isValidDirectory(dir) {
|
|
|
168
215
|
return false;
|
|
169
216
|
}
|
|
170
217
|
}
|
|
171
|
-
// ──
|
|
172
|
-
const GIT_STASH_TYPES = new Set(["context-hub", "github", "git"]);
|
|
218
|
+
// ── Stash cache integration ─────────────────────────────────────────────────
|
|
173
219
|
/**
|
|
174
220
|
* Ensure all cache-backed stash providers are refreshed so their cache
|
|
175
221
|
* directories exist on disk. Must be called (async) before
|
|
176
|
-
* `
|
|
222
|
+
* `resolveSourceEntries()` so the content directories pass the
|
|
177
223
|
* `isValidDirectory()` check.
|
|
178
224
|
*/
|
|
179
|
-
export async function
|
|
225
|
+
export async function ensureSourceCaches(config) {
|
|
180
226
|
const cfg = config ?? loadConfig();
|
|
181
227
|
for (const entry of cfg.stashes ?? []) {
|
|
182
228
|
if (!GIT_STASH_TYPES.has(entry.type) || !entry.url || entry.enabled === false)
|
|
@@ -201,7 +247,3 @@ export async function ensureStashCaches(config) {
|
|
|
201
247
|
}
|
|
202
248
|
}
|
|
203
249
|
}
|
|
204
|
-
/** @deprecated Use ensureStashCaches instead. */
|
|
205
|
-
export const ensureGitCaches = ensureStashCaches;
|
|
206
|
-
/** @deprecated Use ensureStashCaches instead. */
|
|
207
|
-
export const ensureContextHubCaches = ensureStashCaches;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { getCacheDir, getSemanticStatusPath } from "../core/paths";
|
|
4
|
+
import { DEFAULT_LOCAL_MODEL } from "../llm/embedder";
|
|
5
5
|
export function deriveSemanticProviderFingerprint(embedding) {
|
|
6
6
|
if (embedding?.endpoint) {
|
|
7
7
|
return `remote:${embedding.endpoint}|${embedding.model}|${embedding.dimension ?? "default"}`;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
import { isRelevantAssetFile } from "
|
|
10
|
+
import { isRelevantAssetFile } from "../core/asset-spec";
|
|
11
11
|
import { buildFileContext } from "./file-context";
|
|
12
12
|
const SKIP_DIRS = new Set([".git", "node_modules", "bin", ".cache"]);
|
|
13
13
|
/**
|