akm-cli 0.7.5 → 0.8.0-rc.6
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/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
- package/README.md +20 -4
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli.js +1995 -551
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +1531 -0
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +990 -75
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +5 -23
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +400 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +77 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-profiles.js +146 -0
- package/dist/commands/improve-result-file.js +103 -0
- package/dist/commands/improve.js +2175 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/index.js +183 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/vault-key-rules.js +139 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal.js +66 -12
- package/dist/commands/propose.js +86 -31
- package/dist/commands/reflect.js +1119 -73
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember.js +69 -6
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +144 -25
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +438 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/commands/vault.js +130 -77
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +7 -0
- package/dist/core/asset-registry.js +7 -16
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +22 -0
- package/dist/core/common.js +157 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +625 -0
- package/dist/core/config-schema.js +501 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +327 -987
- package/dist/core/errors.js +40 -19
- package/dist/core/events.js +91 -138
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +3 -6
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +326 -14
- package/dist/core/proposal-quality-validators.js +364 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +498 -42
- package/dist/core/state-db.js +927 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/warn.js +62 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +152 -253
- package/dist/indexer/db.js +933 -103
- package/dist/indexer/ensure-index.js +64 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -124
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +506 -291
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +148 -160
- package/dist/indexer/memory-inference.js +99 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +255 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +5 -16
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +150 -74
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +118 -23
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +68 -0
- package/dist/integrations/session-logs/providers/claude-code.js +59 -0
- package/dist/integrations/session-logs/providers/opencode.js +55 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +72 -124
- package/dist/llm/embedder.js +3 -19
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +3 -0
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +89 -48
- package/dist/llm/graph-extract.js +676 -70
- package/dist/llm/index-passes.js +9 -23
- package/dist/llm/memory-infer.js +52 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +281 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +5 -318
- package/dist/output/context.js +3 -0
- package/dist/output/renderers.js +223 -256
- package/dist/output/shapes.js +150 -105
- package/dist/output/text.js +318 -30
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +3 -0
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +70 -49
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17307 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +7 -5
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +211 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +62 -91
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +9 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +20 -8
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -381
- package/dist/templates/wiki-templates.js +0 -100
package/dist/indexer/metadata.js
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import fs from "node:fs";
|
|
2
5
|
import path from "node:path";
|
|
3
6
|
import { deriveCanonicalAssetName, deriveCanonicalAssetNameFromStashRoot, isRelevantAssetFile, } from "../core/asset-spec";
|
|
4
|
-
import { isAssetType } from "../core/common";
|
|
5
|
-
import { parseFrontmatter
|
|
7
|
+
import { asNonEmptyString, isAssetType, writeFileAtomic } from "../core/common";
|
|
8
|
+
import { parseFrontmatter } from "../core/frontmatter";
|
|
6
9
|
import { isVerbose, warn } from "../core/warn";
|
|
7
10
|
import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "./file-context";
|
|
11
|
+
import { applyMetadataContributors } from "./metadata-contributors";
|
|
8
12
|
export const SCOPE_KEYS = ["user", "agent", "run", "channel"];
|
|
9
13
|
// ── Load / Write ────────────────────────────────────────────────────────────
|
|
10
14
|
const STASH_FILENAME = ".stash.json";
|
|
11
15
|
// ── Quality semantics (v1 spec §4.2) ────────────────────────────────────────
|
|
12
16
|
/**
|
|
13
|
-
* Well-known quality values. `generated` and `
|
|
17
|
+
* Well-known quality values. `generated`, `curated`, and `enriched` are included in
|
|
14
18
|
* default search; `proposed` is excluded by default and opt-in via
|
|
15
19
|
* `--include-proposed`. Unknown values warn once and remain searchable.
|
|
16
20
|
*/
|
|
17
|
-
export const KNOWN_QUALITY_VALUES = new Set(["generated", "curated", "proposed"]);
|
|
21
|
+
export const KNOWN_QUALITY_VALUES = new Set(["generated", "curated", "enriched", "proposed"]);
|
|
18
22
|
/** Tracks unknown quality values we've already warned about (one warn per value per process). */
|
|
19
23
|
const warnedUnknownQualityValues = new Set();
|
|
20
24
|
/**
|
|
@@ -80,20 +84,7 @@ export function loadStashFile(dirPath, options) {
|
|
|
80
84
|
}
|
|
81
85
|
export function writeStashFile(dirPath, stash) {
|
|
82
86
|
const filePath = stashFilePath(dirPath);
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
fs.writeFileSync(tmpPath, `${JSON.stringify(stash, null, 2)}\n`, "utf8");
|
|
86
|
-
fs.renameSync(tmpPath, filePath);
|
|
87
|
-
}
|
|
88
|
-
catch (err) {
|
|
89
|
-
try {
|
|
90
|
-
fs.unlinkSync(tmpPath);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
/* ignore cleanup failure */
|
|
94
|
-
}
|
|
95
|
-
throw err;
|
|
96
|
-
}
|
|
87
|
+
writeFileAtomic(filePath, `${JSON.stringify(stash, null, 2)}\n`);
|
|
97
88
|
}
|
|
98
89
|
/**
|
|
99
90
|
* Validate and normalize a raw object into a `StashEntry`.
|
|
@@ -185,19 +176,38 @@ export function validateStashEntry(entry) {
|
|
|
185
176
|
if (typeof e.pageKind === "string" && e.pageKind.trim().length > 0) {
|
|
186
177
|
result.pageKind = e.pageKind.trim();
|
|
187
178
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
179
|
+
const xrefs = normalizeNonEmptyStringList(e.xrefs);
|
|
180
|
+
if (xrefs)
|
|
181
|
+
result.xrefs = xrefs;
|
|
182
|
+
const sources = normalizeNonEmptyStringList(e.sources);
|
|
183
|
+
if (sources)
|
|
184
|
+
result.sources = sources;
|
|
185
|
+
if (typeof e.beliefState === "string" && e.beliefState.trim().length > 0) {
|
|
186
|
+
result.beliefState = e.beliefState.trim();
|
|
194
187
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
188
|
+
const supersededBy = normalizeNonEmptyStringList(e.supersededBy);
|
|
189
|
+
if (supersededBy)
|
|
190
|
+
result.supersededBy = supersededBy;
|
|
191
|
+
const contradictedBy = normalizeNonEmptyStringList(e.contradictedBy);
|
|
192
|
+
if (contradictedBy)
|
|
193
|
+
result.contradictedBy = contradictedBy;
|
|
194
|
+
const currentBeliefRefs = normalizeNonEmptyStringList(e.currentBeliefRefs);
|
|
195
|
+
if (currentBeliefRefs)
|
|
196
|
+
result.currentBeliefRefs = currentBeliefRefs;
|
|
197
|
+
if (e.captureMode === "hot" || e.captureMode === "background") {
|
|
198
|
+
result.captureMode = e.captureMode;
|
|
199
|
+
}
|
|
200
|
+
if (typeof e.whenToUse === "string" && e.whenToUse.trim().length > 0) {
|
|
201
|
+
result.whenToUse = e.whenToUse.trim();
|
|
202
|
+
}
|
|
203
|
+
if (typeof e.lessonStrength === "number" && Number.isFinite(e.lessonStrength) && e.lessonStrength >= 0) {
|
|
204
|
+
result.lessonStrength = Math.floor(e.lessonStrength);
|
|
205
|
+
}
|
|
206
|
+
const evidenceSources = normalizeNonEmptyStringList(e.evidenceSources);
|
|
207
|
+
if (evidenceSources)
|
|
208
|
+
result.evidenceSources = evidenceSources;
|
|
209
|
+
if (typeof e.derivedFrom === "string" && e.derivedFrom.trim().length > 0) {
|
|
210
|
+
result.derivedFrom = e.derivedFrom.trim();
|
|
201
211
|
}
|
|
202
212
|
if (typeof e.scope === "object" && e.scope !== null && !Array.isArray(e.scope)) {
|
|
203
213
|
const scope = normalizeScopeObject(e.scope);
|
|
@@ -275,9 +285,9 @@ function normalizeIntent(value) {
|
|
|
275
285
|
return undefined;
|
|
276
286
|
const raw = value;
|
|
277
287
|
const intent = {};
|
|
278
|
-
const when =
|
|
279
|
-
const input =
|
|
280
|
-
const output =
|
|
288
|
+
const when = asNonEmptyString(raw.when);
|
|
289
|
+
const input = asNonEmptyString(raw.input);
|
|
290
|
+
const output = asNonEmptyString(raw.output);
|
|
281
291
|
if (when)
|
|
282
292
|
intent.when = when;
|
|
283
293
|
if (input)
|
|
@@ -290,7 +300,7 @@ function normalizeStringListOrUndefined(value) {
|
|
|
290
300
|
return normalizeNonEmptyStringList(value);
|
|
291
301
|
}
|
|
292
302
|
export function applyCuratedFrontmatter(entry, fmData) {
|
|
293
|
-
const description =
|
|
303
|
+
const description = asNonEmptyString(fmData.description);
|
|
294
304
|
if (description) {
|
|
295
305
|
entry.description = description;
|
|
296
306
|
entry.source = "frontmatter";
|
|
@@ -311,18 +321,72 @@ export function applyCuratedFrontmatter(entry, fmData) {
|
|
|
311
321
|
const examples = normalizeStringListOrUndefined(fmData.examples);
|
|
312
322
|
if (examples)
|
|
313
323
|
entry.examples = examples;
|
|
314
|
-
const run =
|
|
324
|
+
const run = asNonEmptyString(fmData.run);
|
|
315
325
|
if (run)
|
|
316
326
|
entry.run = run;
|
|
317
|
-
const setup =
|
|
327
|
+
const setup = asNonEmptyString(fmData.setup);
|
|
318
328
|
if (setup)
|
|
319
329
|
entry.setup = setup;
|
|
320
|
-
const cwd =
|
|
330
|
+
const cwd = asNonEmptyString(fmData.cwd);
|
|
321
331
|
if (cwd)
|
|
322
332
|
entry.cwd = cwd;
|
|
323
|
-
const quality =
|
|
333
|
+
const quality = asNonEmptyString(fmData.quality);
|
|
324
334
|
if (quality)
|
|
325
335
|
entry.quality = normalizeQuality(quality);
|
|
336
|
+
const beliefState = asNonEmptyString(fmData.beliefState);
|
|
337
|
+
if (beliefState)
|
|
338
|
+
entry.beliefState = beliefState;
|
|
339
|
+
const supersededBy = normalizeStringListOrUndefined(fmData.supersededBy);
|
|
340
|
+
if (supersededBy)
|
|
341
|
+
entry.supersededBy = supersededBy;
|
|
342
|
+
const contradictedBy = normalizeStringListOrUndefined(fmData.contradictedBy);
|
|
343
|
+
if (contradictedBy)
|
|
344
|
+
entry.contradictedBy = contradictedBy;
|
|
345
|
+
const currentBeliefRefs = normalizeStringListOrUndefined(fmData.currentBeliefRefs);
|
|
346
|
+
if (currentBeliefRefs)
|
|
347
|
+
entry.currentBeliefRefs = currentBeliefRefs;
|
|
348
|
+
// captureMode: "hot" | "background" — strict whitelist; unknown values are ignored.
|
|
349
|
+
if (fmData.captureMode === "hot" || fmData.captureMode === "background") {
|
|
350
|
+
entry.captureMode = fmData.captureMode;
|
|
351
|
+
}
|
|
352
|
+
// when_to_use → whenToUse — free-form guidance for retrieval/intent matching.
|
|
353
|
+
const whenToUse = asNonEmptyString(fmData.when_to_use);
|
|
354
|
+
if (whenToUse)
|
|
355
|
+
entry.whenToUse = whenToUse;
|
|
356
|
+
// lessonStrength: array → length, number → direct. Negative numbers clamp to 0.
|
|
357
|
+
if (Array.isArray(fmData.lessonStrength)) {
|
|
358
|
+
entry.lessonStrength = fmData.lessonStrength.length;
|
|
359
|
+
}
|
|
360
|
+
else if (typeof fmData.lessonStrength === "number" && Number.isFinite(fmData.lessonStrength)) {
|
|
361
|
+
entry.lessonStrength = Math.max(0, Math.floor(fmData.lessonStrength));
|
|
362
|
+
}
|
|
363
|
+
const evidenceSources = normalizeStringListOrUndefined(fmData.evidenceSources);
|
|
364
|
+
if (evidenceSources)
|
|
365
|
+
entry.evidenceSources = evidenceSources;
|
|
366
|
+
// Phase 5A / Advantage D5: capture parent ref for derived memories.
|
|
367
|
+
// Memory-inference writes `source: "memory:<parent>"` and `inferred: true`
|
|
368
|
+
// (and a derived child name suffix `.derived`). We mirror that source ref
|
|
369
|
+
// into `entry.derivedFrom` so the indexer can populate the dedicated
|
|
370
|
+
// `derived_from` column. Non-derived entries leave this field unset.
|
|
371
|
+
if (entry.type === "memory") {
|
|
372
|
+
const isDerivedByName = entry.name.toLowerCase().endsWith(".derived");
|
|
373
|
+
const isDerivedByFm = fmData.inferred === true;
|
|
374
|
+
if (isDerivedByName || isDerivedByFm) {
|
|
375
|
+
const sourceStr = asNonEmptyString(fmData.source);
|
|
376
|
+
if (sourceStr?.includes(":")) {
|
|
377
|
+
entry.derivedFrom = sourceStr;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// Fallback: some legacy renderings store only `derivedFrom: <name>`
|
|
381
|
+
// (a bare parent name). Promote it to a `memory:` ref so the lookup
|
|
382
|
+
// column stays consistent.
|
|
383
|
+
const derivedFromName = asNonEmptyString(fmData.derivedFrom);
|
|
384
|
+
if (derivedFromName) {
|
|
385
|
+
entry.derivedFrom = derivedFromName.includes(":") ? derivedFromName : `memory:${derivedFromName}`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
326
390
|
const intent = normalizeIntent(fmData.intent);
|
|
327
391
|
if (intent)
|
|
328
392
|
entry.intent = intent;
|
|
@@ -416,6 +480,12 @@ export function shouldIndexStashFile(stashRoot, file, options) {
|
|
|
416
480
|
const segments = relPath.split(/[\\/]+/).filter(Boolean);
|
|
417
481
|
if (segments.length === 0)
|
|
418
482
|
return true;
|
|
483
|
+
// Skip vault .env files that have a sibling .sensitive marker file.
|
|
484
|
+
if (segments[0] === "vaults" && (file.endsWith(".env") || path.basename(file) === ".env")) {
|
|
485
|
+
const markerPath = file.replace(/\.env$/, ".sensitive");
|
|
486
|
+
if (fs.existsSync(markerPath))
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
419
489
|
if (options?.treatStashRootAsWikiRoot) {
|
|
420
490
|
return !(segments.length === 1 && WIKI_INFRA_FILES.has(segments[0]));
|
|
421
491
|
}
|
|
@@ -681,7 +751,137 @@ function mergeAliases(existing, generated) {
|
|
|
681
751
|
const merged = normalizeTerms([...(existing ?? []), ...generated]);
|
|
682
752
|
return merged.length > 0 ? merged : undefined;
|
|
683
753
|
}
|
|
754
|
+
// ── Enrichment Completeness ─────────────────────────────────────────────────
|
|
755
|
+
/**
|
|
756
|
+
* Returns `true` when a stash entry already has enough LLM-quality metadata
|
|
757
|
+
* that calling the LLM would produce no meaningful improvement.
|
|
758
|
+
*
|
|
759
|
+
* An entry is considered complete when ALL of the following hold:
|
|
760
|
+
* - `description` is a non-empty string
|
|
761
|
+
* - `tags` is a non-empty array
|
|
762
|
+
* - `searchHints` is a non-empty array
|
|
763
|
+
*
|
|
764
|
+
* This predicate is used by `enhanceDirsWithLlm` to skip the LLM call for
|
|
765
|
+
* entries that were previously enriched and already carry all three fields.
|
|
766
|
+
* Pass `reEnrich = true` in the caller to bypass this check.
|
|
767
|
+
*/
|
|
768
|
+
export function isEnrichmentComplete(entry) {
|
|
769
|
+
const hasDescription = typeof entry.description === "string" && entry.description.trim().length > 0;
|
|
770
|
+
const hasTags = Array.isArray(entry.tags) && entry.tags.length > 0;
|
|
771
|
+
const hasSearchHints = Array.isArray(entry.searchHints) && entry.searchHints.length > 0;
|
|
772
|
+
return hasDescription && hasTags && hasSearchHints;
|
|
773
|
+
}
|
|
684
774
|
// ── Metadata Generation ─────────────────────────────────────────────────────
|
|
775
|
+
/**
|
|
776
|
+
* Shared pipeline (steps 2-6) for building a single StashEntry from a file.
|
|
777
|
+
*
|
|
778
|
+
* Both `generateMetadata` and `generateMetadataFlat` perform identical work
|
|
779
|
+
* once the initial `entry` object has been seeded with type and canonical name.
|
|
780
|
+
* This helper encapsulates that shared pipeline so the two callers only differ
|
|
781
|
+
* in how they determine the asset type and canonical name (step 1):
|
|
782
|
+
*
|
|
783
|
+
* - `generateMetadata` — explicit `assetType` arg + `deriveCanonicalAssetName`
|
|
784
|
+
* - `generateMetadataFlat` — type from `runMatchers()` + `deriveCanonicalAssetNameFromStashRoot`
|
|
785
|
+
*
|
|
786
|
+
* @param file Absolute path to the file being processed.
|
|
787
|
+
* @param assetType Resolved asset type string (already validated by caller).
|
|
788
|
+
* @param canonicalName Resolved canonical name (already computed by caller).
|
|
789
|
+
* @param dirPath Directory containing the file (used for tag fallback).
|
|
790
|
+
* @param pkgMeta Pre-loaded package.json metadata for this directory (may be null/undefined).
|
|
791
|
+
* @param stashRoot Stash root used for renderer search hints context.
|
|
792
|
+
* @param ctx FileContext for the file (may be pre-built by the caller).
|
|
793
|
+
* @param match Pre-resolved MatchResult when available (from `generateMetadataFlat`).
|
|
794
|
+
* @returns The populated entry, or `{ skip: true, warning: string }` when the
|
|
795
|
+
* renderer throws and the file should be dropped.
|
|
796
|
+
*/
|
|
797
|
+
async function buildEntryFromFile(file, assetType, canonicalName, dirPath, pkgMeta, stashRoot, ctx, match) {
|
|
798
|
+
const ext = path.extname(file).toLowerCase();
|
|
799
|
+
const baseName = path.basename(file, ext);
|
|
800
|
+
const entry = {
|
|
801
|
+
name: canonicalName,
|
|
802
|
+
type: assetType,
|
|
803
|
+
quality: "generated",
|
|
804
|
+
confidence: 0.55,
|
|
805
|
+
source: "filename",
|
|
806
|
+
};
|
|
807
|
+
// Priority 1: Package.json metadata
|
|
808
|
+
if (pkgMeta) {
|
|
809
|
+
if (pkgMeta.description && !entry.description) {
|
|
810
|
+
entry.description = pkgMeta.description;
|
|
811
|
+
entry.source = "package";
|
|
812
|
+
entry.confidence = 0.8;
|
|
813
|
+
}
|
|
814
|
+
if (pkgMeta.keywords && pkgMeta.keywords.length > 0)
|
|
815
|
+
entry.tags = normalizeTerms(pkgMeta.keywords);
|
|
816
|
+
}
|
|
817
|
+
// Priority 2: Frontmatter (for .md files -- overrides package.json description)
|
|
818
|
+
if (ext === ".md") {
|
|
819
|
+
const content = ctx.content();
|
|
820
|
+
const parsed = parseFrontmatter(content);
|
|
821
|
+
applyCuratedFrontmatter(entry, parsed.data);
|
|
822
|
+
// Extract parameters from frontmatter params: key
|
|
823
|
+
const fmParams = extractFrontmatterParameters(parsed.data);
|
|
824
|
+
if (fmParams)
|
|
825
|
+
entry.parameters = fmParams;
|
|
826
|
+
// Pass wiki-pattern frontmatter through onto the entry
|
|
827
|
+
applyWikiFrontmatter(entry, parsed.data);
|
|
828
|
+
// Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
|
|
829
|
+
if (entry.type === "command") {
|
|
830
|
+
const cmdParams = extractCommandParameters(parsed.content);
|
|
831
|
+
if (cmdParams) {
|
|
832
|
+
entry.parameters = mergeParameters(entry.parameters, cmdParams);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
// Extract @param from script files.
|
|
837
|
+
// Vault files (.env) are deliberately excluded — their contents are secrets
|
|
838
|
+
// and must never be parsed for @param or any other metadata that could
|
|
839
|
+
// embed a value into the entry.
|
|
840
|
+
if (ext !== ".md" && assetType !== "vault") {
|
|
841
|
+
const content = ctx.content();
|
|
842
|
+
const scriptParams = extractScriptParameters(file, content);
|
|
843
|
+
if (scriptParams)
|
|
844
|
+
entry.parameters = scriptParams;
|
|
845
|
+
applyCommentMetadata(entry, extractCommentMetadata(file, content));
|
|
846
|
+
}
|
|
847
|
+
// Priority 3: Renderer metadata extraction
|
|
848
|
+
// When no pre-resolved match is available (generateMetadata path), run
|
|
849
|
+
// matchers now so the renderer can extract type-specific metadata.
|
|
850
|
+
const resolvedMatch = match ?? (await runMatchers(ctx));
|
|
851
|
+
if (resolvedMatch) {
|
|
852
|
+
const renderer = await getRenderer(resolvedMatch.renderer);
|
|
853
|
+
if (renderer) {
|
|
854
|
+
const renderCtx = buildRenderContext(ctx, resolvedMatch, [stashRoot]);
|
|
855
|
+
try {
|
|
856
|
+
await applyMetadataContributors(entry, {
|
|
857
|
+
rendererName: renderer.name,
|
|
858
|
+
renderContext: renderCtx,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
return {
|
|
863
|
+
skip: true,
|
|
864
|
+
warning: buildMetadataSkipWarning(file, assetType, error),
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// Priority 4: Filename heuristics (fallback)
|
|
870
|
+
if (!entry.description) {
|
|
871
|
+
entry.description = fileNameToDescription(baseName);
|
|
872
|
+
entry.source = "filename";
|
|
873
|
+
entry.confidence = Math.min(entry.confidence ?? 0.55, 0.55);
|
|
874
|
+
}
|
|
875
|
+
if (!entry.tags || entry.tags.length === 0) {
|
|
876
|
+
entry.tags = extractTagsFromPath(file, dirPath);
|
|
877
|
+
}
|
|
878
|
+
entry.tags = normalizeTerms(entry.tags ?? []);
|
|
879
|
+
entry.aliases = mergeAliases(entry.aliases, buildAliases(canonicalName, entry.tags));
|
|
880
|
+
// Search hints are only generated when LLM is configured (via enhanceStashWithLlm)
|
|
881
|
+
// Heuristic search hints are too noisy to be useful for search quality
|
|
882
|
+
entry.filename = path.basename(file);
|
|
883
|
+
return entry;
|
|
884
|
+
}
|
|
685
885
|
export async function generateMetadata(dirPath, assetType, files, typeRoot = dirPath) {
|
|
686
886
|
const entries = [];
|
|
687
887
|
const warnings = [];
|
|
@@ -694,84 +894,16 @@ export async function generateMetadata(dirPath, assetType, files, typeRoot = dir
|
|
|
694
894
|
if (!isRelevantAssetFile(assetType, fileName))
|
|
695
895
|
continue;
|
|
696
896
|
const canonicalName = deriveCanonicalAssetName(assetType, typeRoot, file) ?? baseName;
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
type: assetType,
|
|
700
|
-
quality: "generated",
|
|
701
|
-
confidence: 0.55,
|
|
702
|
-
source: "filename",
|
|
703
|
-
};
|
|
704
|
-
// Priority 1: Package.json metadata
|
|
705
|
-
if (pkgMeta) {
|
|
706
|
-
if (pkgMeta.description && !entry.description) {
|
|
707
|
-
entry.description = pkgMeta.description;
|
|
708
|
-
entry.source = "package";
|
|
709
|
-
entry.confidence = 0.8;
|
|
710
|
-
}
|
|
711
|
-
if (pkgMeta.keywords && pkgMeta.keywords.length > 0)
|
|
712
|
-
entry.tags = normalizeTerms(pkgMeta.keywords);
|
|
713
|
-
}
|
|
714
|
-
// Priority 2: Frontmatter (for .md files -- overrides package.json description)
|
|
715
|
-
if (ext === ".md") {
|
|
716
|
-
const content = fs.readFileSync(file, "utf8");
|
|
717
|
-
const parsed = parseFrontmatter(content);
|
|
718
|
-
applyCuratedFrontmatter(entry, parsed.data);
|
|
719
|
-
// Extract parameters from frontmatter params: key
|
|
720
|
-
const fmParams = extractFrontmatterParameters(parsed.data);
|
|
721
|
-
if (fmParams)
|
|
722
|
-
entry.parameters = fmParams;
|
|
723
|
-
// Pass wiki-pattern frontmatter through onto the entry
|
|
724
|
-
applyWikiFrontmatter(entry, parsed.data);
|
|
725
|
-
// Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
|
|
726
|
-
if (entry.type === "command") {
|
|
727
|
-
const cmdParams = extractCommandParameters(parsed.content);
|
|
728
|
-
if (cmdParams) {
|
|
729
|
-
entry.parameters = mergeParameters(entry.parameters, cmdParams);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
// Extract @param from script files.
|
|
734
|
-
// Vault files (.env) are deliberately excluded — their contents are secrets
|
|
735
|
-
// and must never be parsed for @param or any other metadata that could
|
|
736
|
-
// embed a value into the entry.
|
|
737
|
-
if (ext !== ".md" && assetType !== "vault") {
|
|
738
|
-
const content = fs.readFileSync(file, "utf8");
|
|
739
|
-
const scriptParams = extractScriptParameters(file, content);
|
|
740
|
-
if (scriptParams)
|
|
741
|
-
entry.parameters = scriptParams;
|
|
742
|
-
applyCommentMetadata(entry, extractCommentMetadata(file, content));
|
|
743
|
-
}
|
|
744
|
-
// Priority 3: Type-specific metadata extraction (e.g. TOC for knowledge, comments for scripts)
|
|
897
|
+
// Build file context with typeRoot as the stash root so renderer context
|
|
898
|
+
// and search hints are scoped to the type directory.
|
|
745
899
|
const fileCtx = buildFileContext(typeRoot, file);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
try {
|
|
752
|
-
renderer.extractMetadata(entry, renderCtx);
|
|
753
|
-
}
|
|
754
|
-
catch (error) {
|
|
755
|
-
warnings.push(buildMetadataSkipWarning(file, assetType, error));
|
|
756
|
-
continue;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
// Priority 4: Filename heuristics (fallback)
|
|
761
|
-
if (!entry.description) {
|
|
762
|
-
entry.description = fileNameToDescription(baseName);
|
|
763
|
-
entry.source = "filename";
|
|
764
|
-
entry.confidence = Math.min(entry.confidence ?? 0.55, 0.55);
|
|
765
|
-
}
|
|
766
|
-
if (!entry.tags || entry.tags.length === 0) {
|
|
767
|
-
entry.tags = extractTagsFromPath(file, dirPath);
|
|
900
|
+
// Step 1: type is explicit; delegate steps 2-6 to the shared pipeline.
|
|
901
|
+
const result = await buildEntryFromFile(file, assetType, canonicalName, dirPath, pkgMeta, typeRoot, fileCtx, null);
|
|
902
|
+
if ("skip" in result) {
|
|
903
|
+
warnings.push(result.warning);
|
|
904
|
+
continue;
|
|
768
905
|
}
|
|
769
|
-
|
|
770
|
-
entry.aliases = mergeAliases(entry.aliases, buildAliases(canonicalName, entry.tags));
|
|
771
|
-
// Search hints are only generated when LLM is configured (via enhanceStashWithLlm)
|
|
772
|
-
// Heuristic search hints are too noisy to be useful for search quality
|
|
773
|
-
entry.filename = path.basename(file);
|
|
774
|
-
entries.push(entry);
|
|
906
|
+
entries.push(result);
|
|
775
907
|
}
|
|
776
908
|
return warnings.length > 0 ? { entries, warnings } : { entries };
|
|
777
909
|
}
|
|
@@ -789,6 +921,7 @@ export async function generateMetadataFlat(stashRoot, files) {
|
|
|
789
921
|
for (const file of files) {
|
|
790
922
|
if (!shouldIndexStashFile(stashRoot, file))
|
|
791
923
|
continue;
|
|
924
|
+
// Step 1: determine type and canonical name via the matcher system.
|
|
792
925
|
const ctx = buildFileContext(stashRoot, file);
|
|
793
926
|
const match = await runMatchers(ctx);
|
|
794
927
|
if (!match)
|
|
@@ -802,83 +935,20 @@ export async function generateMetadataFlat(stashRoot, files) {
|
|
|
802
935
|
const ext = path.extname(file).toLowerCase();
|
|
803
936
|
const baseName = path.basename(file, ext);
|
|
804
937
|
const canonicalName = deriveCanonicalAssetNameFromStashRoot(assetType, stashRoot, file) ?? baseName;
|
|
805
|
-
|
|
806
|
-
name: canonicalName,
|
|
807
|
-
type: assetType,
|
|
808
|
-
quality: "generated",
|
|
809
|
-
confidence: 0.55,
|
|
810
|
-
source: "filename",
|
|
811
|
-
};
|
|
812
|
-
// Package.json metadata
|
|
938
|
+
// Resolve package.json metadata with a per-directory cache.
|
|
813
939
|
const dirPath = path.dirname(file);
|
|
814
940
|
if (!pkgMetaCache.has(dirPath)) {
|
|
815
941
|
pkgMetaCache.set(dirPath, extractPackageMetadata(dirPath));
|
|
816
942
|
}
|
|
817
943
|
const pkgMeta = pkgMetaCache.get(dirPath);
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (pkgMeta.keywords?.length)
|
|
825
|
-
entry.tags = normalizeTerms(pkgMeta.keywords);
|
|
826
|
-
}
|
|
827
|
-
// Frontmatter
|
|
828
|
-
if (ext === ".md") {
|
|
829
|
-
const content = ctx.content();
|
|
830
|
-
const parsed = parseFrontmatter(content);
|
|
831
|
-
applyCuratedFrontmatter(entry, parsed.data);
|
|
832
|
-
// Extract parameters from frontmatter params: key
|
|
833
|
-
const fmParams = extractFrontmatterParameters(parsed.data);
|
|
834
|
-
if (fmParams)
|
|
835
|
-
entry.parameters = fmParams;
|
|
836
|
-
// Pass wiki-pattern frontmatter through onto the entry
|
|
837
|
-
applyWikiFrontmatter(entry, parsed.data);
|
|
838
|
-
// Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
|
|
839
|
-
if (entry.type === "command") {
|
|
840
|
-
const cmdParams = extractCommandParameters(parsed.content);
|
|
841
|
-
if (cmdParams) {
|
|
842
|
-
entry.parameters = mergeParameters(entry.parameters, cmdParams);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
// Extract @param from script files.
|
|
847
|
-
// Vault files (.env) are deliberately excluded — their contents are secrets
|
|
848
|
-
// and must never be parsed for @param or any other metadata that could
|
|
849
|
-
// embed a value into the entry.
|
|
850
|
-
if (ext !== ".md" && assetType !== "vault") {
|
|
851
|
-
const content = ctx.content();
|
|
852
|
-
const scriptParams = extractScriptParameters(file, content);
|
|
853
|
-
if (scriptParams)
|
|
854
|
-
entry.parameters = scriptParams;
|
|
855
|
-
applyCommentMetadata(entry, extractCommentMetadata(file, content));
|
|
856
|
-
}
|
|
857
|
-
// Renderer metadata extraction
|
|
858
|
-
const renderer = await getRenderer(match.renderer);
|
|
859
|
-
if (renderer?.extractMetadata) {
|
|
860
|
-
const renderCtx = buildRenderContext(ctx, match, [stashRoot]);
|
|
861
|
-
try {
|
|
862
|
-
renderer.extractMetadata(entry, renderCtx);
|
|
863
|
-
}
|
|
864
|
-
catch (error) {
|
|
865
|
-
warnings.push(buildMetadataSkipWarning(file, assetType, error));
|
|
866
|
-
continue;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
// Filename heuristics fallback
|
|
870
|
-
if (!entry.description) {
|
|
871
|
-
entry.description = fileNameToDescription(baseName);
|
|
872
|
-
entry.source = "filename";
|
|
873
|
-
entry.confidence = Math.min(entry.confidence ?? 0.55, 0.55);
|
|
874
|
-
}
|
|
875
|
-
if (!entry.tags || entry.tags.length === 0) {
|
|
876
|
-
entry.tags = extractTagsFromPath(file, dirPath);
|
|
944
|
+
// Steps 2-6: delegate to the shared pipeline; pass the pre-resolved match
|
|
945
|
+
// so we don't run matchers a second time.
|
|
946
|
+
const result = await buildEntryFromFile(file, assetType, canonicalName, dirPath, pkgMeta, stashRoot, ctx, match);
|
|
947
|
+
if ("skip" in result) {
|
|
948
|
+
warnings.push(result.warning);
|
|
949
|
+
continue;
|
|
877
950
|
}
|
|
878
|
-
|
|
879
|
-
entry.aliases = mergeAliases(entry.aliases, buildAliases(canonicalName, entry.tags));
|
|
880
|
-
entry.filename = path.basename(file);
|
|
881
|
-
entries.push(entry);
|
|
951
|
+
entries.push(result);
|
|
882
952
|
}
|
|
883
953
|
return warnings.length > 0 ? { entries, warnings } : { entries };
|
|
884
954
|
}
|
|
@@ -980,17 +1050,6 @@ export function extractDescriptionFromComments(filePath) {
|
|
|
980
1050
|
return hashLines.join(" ");
|
|
981
1051
|
return null;
|
|
982
1052
|
}
|
|
983
|
-
export function extractFrontmatterDescription(filePath) {
|
|
984
|
-
let content;
|
|
985
|
-
try {
|
|
986
|
-
content = fs.readFileSync(filePath, "utf8");
|
|
987
|
-
}
|
|
988
|
-
catch {
|
|
989
|
-
return null;
|
|
990
|
-
}
|
|
991
|
-
const parsed = parseFrontmatter(content);
|
|
992
|
-
return toStringOrUndefined(parsed.data.description) ?? null;
|
|
993
|
-
}
|
|
994
1053
|
export function extractPackageMetadata(dirPath) {
|
|
995
1054
|
const pkgPath = path.join(dirPath, "package.json");
|
|
996
1055
|
if (!fs.existsSync(pkgPath))
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parseAssetRef } from "../core/asset-ref";
|
|
7
|
+
import { resolveAssetPathFromName, TYPE_DIRS } from "../core/asset-spec";
|
|
8
|
+
import { isWithin } from "../core/common";
|
|
9
|
+
import { resolveSourcesForOrigin } from "../registry/origin-resolve";
|
|
10
|
+
import { lookup } from "./indexer";
|
|
11
|
+
import { resolveSourceEntries } from "./search-source";
|
|
12
|
+
function normalizeRef(ref) {
|
|
13
|
+
return typeof ref === "string" ? parseAssetRef(ref) : ref;
|
|
14
|
+
}
|
|
15
|
+
function buildDiskCandidates(sourcePath, ref, preserveDirectNameFallback) {
|
|
16
|
+
const typeDir = path.join(sourcePath, TYPE_DIRS[ref.type] ?? `${ref.type}s`);
|
|
17
|
+
const candidates = [
|
|
18
|
+
resolveAssetPathFromName(ref.type, typeDir, ref.name),
|
|
19
|
+
path.join(sourcePath, ref.type, `${ref.name}.md`),
|
|
20
|
+
path.join(sourcePath, ref.type, ref.name),
|
|
21
|
+
];
|
|
22
|
+
if (preserveDirectNameFallback) {
|
|
23
|
+
candidates.push(path.join(sourcePath, `${ref.name}.md`), path.join(sourcePath, ref.name));
|
|
24
|
+
}
|
|
25
|
+
return candidates;
|
|
26
|
+
}
|
|
27
|
+
function resolveDirectoryEntry(filePath, directoryIndexNames) {
|
|
28
|
+
let stat;
|
|
29
|
+
try {
|
|
30
|
+
stat = fs.statSync(filePath);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (stat.isFile())
|
|
36
|
+
return filePath;
|
|
37
|
+
if (!stat.isDirectory())
|
|
38
|
+
return null;
|
|
39
|
+
for (const indexName of directoryIndexNames) {
|
|
40
|
+
const candidate = path.join(filePath, indexName);
|
|
41
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
async function resolveViaIndex(ref) {
|
|
47
|
+
try {
|
|
48
|
+
const entry = await lookup(ref);
|
|
49
|
+
return entry?.filePath ?? null;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function resolveViaDisk(ref, options) {
|
|
56
|
+
let sources = resolveSourceEntries(options.stashDir);
|
|
57
|
+
if (options.honorOrigin !== false) {
|
|
58
|
+
sources = resolveSourcesForOrigin(ref.origin, sources);
|
|
59
|
+
}
|
|
60
|
+
const directoryIndexNames = options.directoryIndexNames ?? ["SKILL.md"];
|
|
61
|
+
const preserveDirectNameFallback = options.preserveDirectNameFallback ?? true;
|
|
62
|
+
for (const source of sources) {
|
|
63
|
+
if (options.writableDirSet && !options.writableDirSet.has(path.resolve(source.path)))
|
|
64
|
+
continue;
|
|
65
|
+
const candidates = buildDiskCandidates(source.path, ref, preserveDirectNameFallback);
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
if (!fs.existsSync(candidate))
|
|
68
|
+
continue;
|
|
69
|
+
const resolved = resolveDirectoryEntry(candidate, directoryIndexNames);
|
|
70
|
+
if (!resolved)
|
|
71
|
+
continue;
|
|
72
|
+
const resolvedRoot = fs.realpathSync(source.path);
|
|
73
|
+
const realTarget = fs.realpathSync(resolved);
|
|
74
|
+
if (!isWithin(realTarget, resolvedRoot))
|
|
75
|
+
continue;
|
|
76
|
+
return realTarget;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
export async function resolveAssetPath(ref, options = {}) {
|
|
82
|
+
const parsed = normalizeRef(ref);
|
|
83
|
+
const mode = options.mode ?? "index-first";
|
|
84
|
+
if (mode !== "disk-only") {
|
|
85
|
+
const indexed = await resolveViaIndex(parsed);
|
|
86
|
+
if (indexed)
|
|
87
|
+
return indexed;
|
|
88
|
+
if (mode === "index-only")
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return resolveViaDisk(parsed, options);
|
|
92
|
+
}
|