akm-cli 0.6.0-rc1 → 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 +21 -0
- package/README.md +9 -9
- package/dist/cli.js +181 -111
- package/dist/{completions.js → commands/completions.js} +1 -1
- package/dist/{config-cli.js → commands/config-cli.js} +109 -11
- package/dist/{curate.js → commands/curate.js} +8 -3
- package/dist/{info.js → commands/info.js} +15 -9
- package/dist/{init.js → commands/init.js} +4 -4
- package/dist/{install-audit.js → commands/install-audit.js} +4 -7
- package/dist/{installed-stashes.js → commands/installed-stashes.js} +77 -31
- package/dist/{migration-help.js → commands/migration-help.js} +2 -2
- package/dist/{registry-search.js → commands/registry-search.js} +8 -6
- package/dist/{remember.js → commands/remember.js} +55 -49
- 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} +103 -78
- package/dist/{stash-add.js → commands/source-add.js} +42 -32
- package/dist/{stash-clone.js → commands/source-clone.js} +12 -10
- 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} +1 -1
- package/dist/{asset-spec.js → core/asset-spec.js} +1 -1
- package/dist/{config.js → core/config.js} +79 -31
- package/dist/core/errors.js +90 -0
- package/dist/{frontmatter.js → core/frontmatter.js} +5 -3
- package/dist/core/write-source.js +280 -0
- package/dist/{db-search.js → indexer/db-search.js} +25 -19
- package/dist/{db.js → indexer/db.js} +70 -47
- package/dist/{file-context.js → indexer/file-context.js} +3 -3
- package/dist/{indexer.js → indexer/indexer.js} +123 -31
- package/dist/{manifest.js → indexer/manifest.js} +10 -10
- package/dist/{matchers.js → indexer/matchers.js} +3 -6
- package/dist/{metadata.js → indexer/metadata.js} +9 -5
- package/dist/{search-source.js → indexer/search-source.js} +52 -41
- 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} +1 -1
- package/dist/{llm-client.js → llm/client.js} +1 -1
- package/dist/{embedders → llm/embedders}/local.js +2 -2
- package/dist/{embedders → llm/embedders}/remote.js +1 -1
- package/dist/{embedders → llm/embedders}/types.js +1 -1
- package/dist/{metadata-enhance.js → llm/metadata-enhance.js} +2 -2
- package/dist/{cli-hints.js → output/cli-hints.js} +1 -0
- package/dist/{output-context.js → output/context.js} +21 -3
- package/dist/{renderers.js → output/renderers.js} +9 -65
- package/dist/{output-shapes.js → output/shapes.js} +18 -4
- package/dist/{output-text.js → output/text.js} +1 -1
- package/dist/{registry-build-index.js → registry/build-index.js} +16 -7
- 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/{providers → registry/providers}/index.js +1 -1
- package/dist/{providers → registry/providers}/skills-sh.js +59 -3
- package/dist/{providers → registry/providers}/static-index.js +80 -12
- package/dist/registry/providers/types.js +25 -0
- package/dist/{registry-resolve.js → registry/resolve.js} +3 -3
- 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} +16 -56
- package/dist/{stash-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 +53 -64
- package/dist/{stash-providers → sources/providers}/index.js +3 -4
- package/dist/sources/providers/install-types.js +14 -0
- package/dist/{stash-providers → sources/providers}/npm.js +42 -41
- package/dist/{stash-providers → sources/providers}/provider-utils.js +3 -3
- package/dist/{stash-providers → sources/providers}/sync-from-ref.js +2 -2
- package/dist/{stash-providers → sources/providers}/tar-utils.js +11 -8
- package/dist/{stash-providers → sources/providers}/website.js +29 -65
- package/dist/{stash-resolve.js → sources/resolve.js} +8 -7
- package/dist/{wiki.js → wiki/wiki.js} +12 -11
- package/dist/{workflow-authoring.js → workflows/authoring.js} +37 -14
- 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} +72 -28
- package/dist/workflows/schema.js +11 -0
- package/dist/workflows/validator.js +48 -0
- package/docs/migration/release-notes/0.6.0.md +69 -23
- package/package.json +1 -1
- package/dist/errors.js +0 -45
- package/dist/llm.js +0 -16
- package/dist/registry-factory.js +0 -19
- package/dist/ripgrep.js +0 -2
- package/dist/stash-provider-factory.js +0 -35
- package/dist/stash-provider.js +0 -3
- package/dist/stash-providers/filesystem.js +0 -71
- package/dist/stash-providers/openviking.js +0 -348
- package/dist/stash-types.js +0 -1
- package/dist/workflow-markdown.js +0 -260
- /package/dist/{common.js → core/common.js} +0 -0
- /package/dist/{markdown.js → core/markdown.js} +0 -0
- /package/dist/{paths.js → core/paths.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/{embedder.js → llm/embedder.js} +0 -0
- /package/dist/{embedders → llm/embedders}/cache.js +0 -0
- /package/dist/{registry-provider.js → registry/types.js} +0 -0
- /package/dist/{setup-steps.js → setup/steps.js} +0 -0
- /package/dist/{registry-types.js → sources/types.js} +0 -0
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* CLI entry point stays focused on argument parsing + output routing.
|
|
7
7
|
*/
|
|
8
8
|
import { stringify as yamlStringify } from "yaml";
|
|
9
|
-
import { tryReadStdinText } from "
|
|
10
|
-
import { loadConfig } from "
|
|
11
|
-
import { UsageError } from "
|
|
12
|
-
import { warn } from "
|
|
9
|
+
import { toErrorMessage, tryReadStdinText } from "../core/common";
|
|
10
|
+
import { loadConfig } from "../core/config";
|
|
11
|
+
import { UsageError } from "../core/errors";
|
|
12
|
+
import { warn } from "../core/warn";
|
|
13
13
|
/**
|
|
14
14
|
* Parse a shorthand duration string to a number of milliseconds.
|
|
15
15
|
* Supports: `30d` (days), `12h` (hours), `6m` (months, approximated as 30d).
|
|
@@ -17,7 +17,7 @@ import { warn } from "./warn";
|
|
|
17
17
|
export function parseDuration(s) {
|
|
18
18
|
const match = s.trim().match(/^(\d+)([dhm])$/i);
|
|
19
19
|
if (!match)
|
|
20
|
-
throw new UsageError(`Invalid --expires format "${s}". Use shorthand like 30d, 12h, or 6m
|
|
20
|
+
throw new UsageError(`Invalid --expires format "${s}". Use shorthand like 30d, 12h, or 6m.`, "INVALID_FLAG_VALUE");
|
|
21
21
|
const n = Number(match[1]);
|
|
22
22
|
const unit = match[2].toLowerCase();
|
|
23
23
|
if (unit === "d")
|
|
@@ -40,15 +40,15 @@ export function parseDuration(s) {
|
|
|
40
40
|
*/
|
|
41
41
|
export function buildMemoryFrontmatter(fields) {
|
|
42
42
|
const obj = {};
|
|
43
|
-
if (fields.description
|
|
43
|
+
if (fields.description?.trim())
|
|
44
44
|
obj.description = fields.description;
|
|
45
45
|
if (fields.tags && fields.tags.length > 0)
|
|
46
46
|
obj.tags = fields.tags;
|
|
47
|
-
if (fields.source
|
|
47
|
+
if (fields.source?.trim())
|
|
48
48
|
obj.source = fields.source;
|
|
49
|
-
if (fields.observed_at
|
|
49
|
+
if (fields.observed_at?.trim())
|
|
50
50
|
obj.observed_at = fields.observed_at;
|
|
51
|
-
if (fields.expires
|
|
51
|
+
if (fields.expires?.trim())
|
|
52
52
|
obj.expires = fields.expires;
|
|
53
53
|
if (fields.subjective)
|
|
54
54
|
obj.subjective = true;
|
|
@@ -86,38 +86,32 @@ export function runAutoHeuristics(body) {
|
|
|
86
86
|
const urlMatch = body.match(/https?:\/\/[^\s)>'"]+/);
|
|
87
87
|
const source = urlMatch ? urlMatch[0] : undefined;
|
|
88
88
|
// ISO date token or obvious relative date phrase → observed_at
|
|
89
|
-
|
|
90
|
-
const isoMatch = body.match(/\b(\d{4}-\d{2}-\d{2})\b/);
|
|
91
|
-
if (isoMatch) {
|
|
92
|
-
observed_at = isoMatch[1];
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
const relMatch = body.match(/\b(today|yesterday|last\s+week|last\s+month)\b/i);
|
|
96
|
-
if (relMatch) {
|
|
97
|
-
const phrase = relMatch[1].toLowerCase();
|
|
98
|
-
const now = new Date();
|
|
99
|
-
if (phrase === "today") {
|
|
100
|
-
observed_at = now.toISOString().slice(0, 10);
|
|
101
|
-
}
|
|
102
|
-
else if (phrase === "yesterday") {
|
|
103
|
-
const d = new Date(now);
|
|
104
|
-
d.setDate(d.getDate() - 1);
|
|
105
|
-
observed_at = d.toISOString().slice(0, 10);
|
|
106
|
-
}
|
|
107
|
-
else if (phrase.startsWith("last week")) {
|
|
108
|
-
const d = new Date(now);
|
|
109
|
-
d.setDate(d.getDate() - 7);
|
|
110
|
-
observed_at = d.toISOString().slice(0, 10);
|
|
111
|
-
}
|
|
112
|
-
else if (phrase.startsWith("last month")) {
|
|
113
|
-
const d = new Date(now);
|
|
114
|
-
d.setMonth(d.getMonth() - 1);
|
|
115
|
-
observed_at = d.toISOString().slice(0, 10);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
89
|
+
const observed_at = detectObservedAt(body);
|
|
119
90
|
return { tags, source, observed_at, subjective };
|
|
120
91
|
}
|
|
92
|
+
const RELATIVE_DATE_OFFSETS = {
|
|
93
|
+
today: () => { },
|
|
94
|
+
yesterday: (d) => d.setDate(d.getDate() - 1),
|
|
95
|
+
"last week": (d) => d.setDate(d.getDate() - 7),
|
|
96
|
+
"last month": (d) => d.setMonth(d.getMonth() - 1),
|
|
97
|
+
};
|
|
98
|
+
function detectObservedAt(body) {
|
|
99
|
+
const isoMatch = body.match(/\b(\d{4}-\d{2}-\d{2})\b/);
|
|
100
|
+
if (isoMatch)
|
|
101
|
+
return isoMatch[1];
|
|
102
|
+
const relMatch = body.match(/\b(today|yesterday|last\s+week|last\s+month)\b/i);
|
|
103
|
+
if (!relMatch)
|
|
104
|
+
return undefined;
|
|
105
|
+
// Normalise the matched phrase: lowercase, collapse internal whitespace,
|
|
106
|
+
// so "last week" matches the lookup table key.
|
|
107
|
+
const phrase = relMatch[1].toLowerCase().replace(/\s+/g, " ");
|
|
108
|
+
const offset = RELATIVE_DATE_OFFSETS[phrase];
|
|
109
|
+
if (!offset)
|
|
110
|
+
return undefined;
|
|
111
|
+
const d = new Date();
|
|
112
|
+
offset(d);
|
|
113
|
+
return d.toISOString().slice(0, 10);
|
|
114
|
+
}
|
|
121
115
|
/** Hard timeout for the `--enrich` LLM call. Write-path must not block on a misbehaving endpoint. */
|
|
122
116
|
const LLM_ENRICH_TIMEOUT_MS = 10_000;
|
|
123
117
|
/**
|
|
@@ -131,7 +125,8 @@ export async function runLlmEnrich(body) {
|
|
|
131
125
|
warn("Warning: --enrich requires an LLM to be configured. Run `akm config set llm` to configure one.");
|
|
132
126
|
return { tags: [] };
|
|
133
127
|
}
|
|
134
|
-
const
|
|
128
|
+
const llmConfig = config.llm;
|
|
129
|
+
const { chatCompletion, parseJsonResponse } = await import("../llm/client");
|
|
135
130
|
const prompt = `You are a memory tagger for a developer knowledge base.
|
|
136
131
|
Given the memory text below, return ONLY a JSON object with these fields:
|
|
137
132
|
- "tags": array of 1-5 short lowercase keyword tags
|
|
@@ -143,13 +138,25 @@ ${body.slice(0, 2000)}
|
|
|
143
138
|
|
|
144
139
|
Return ONLY the JSON object, no prose, no markdown fences.`;
|
|
145
140
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
141
|
+
let timeoutHandle;
|
|
142
|
+
const result = await (async () => {
|
|
143
|
+
try {
|
|
144
|
+
return await Promise.race([
|
|
145
|
+
chatCompletion(llmConfig, [
|
|
146
|
+
{ role: "system", content: "Return only valid JSON. No prose." },
|
|
147
|
+
{ role: "user", content: prompt },
|
|
148
|
+
], { maxTokens: 256, temperature: 0.1 }),
|
|
149
|
+
new Promise((_, reject) => {
|
|
150
|
+
timeoutHandle = setTimeout(() => reject(new Error("LLM enrichment timed out")), LLM_ENRICH_TIMEOUT_MS);
|
|
151
|
+
}),
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (timeoutHandle !== undefined) {
|
|
156
|
+
clearTimeout(timeoutHandle);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
})();
|
|
153
160
|
const parsed = parseJsonResponse(result);
|
|
154
161
|
if (!parsed) {
|
|
155
162
|
warn("Warning: --enrich received invalid JSON from the LLM. Writing memory without enrichment.");
|
|
@@ -165,8 +172,7 @@ Return ONLY the JSON object, no prose, no markdown fences.`;
|
|
|
165
172
|
return { tags, description, observed_at };
|
|
166
173
|
}
|
|
167
174
|
catch (err) {
|
|
168
|
-
|
|
169
|
-
warn(`Warning: --enrich failed (${msg}). Writing memory without enrichment.`);
|
|
175
|
+
warn(`Warning: --enrich failed (${toErrorMessage(err)}). Writing memory without enrichment.`);
|
|
170
176
|
return { tags: [] };
|
|
171
177
|
}
|
|
172
178
|
}
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `akm search` — entry point.
|
|
3
|
+
*
|
|
4
|
+
* Spec §6.1: search consults the local FTS5 index. There is one query path
|
|
5
|
+
* because there is one data store. Provider fan-out is gone.
|
|
6
|
+
*
|
|
7
|
+
* The orchestration here is thin: build the FTS query, optionally interleave
|
|
8
|
+
* a registry search behind `--source registry|both`, and log a usage event.
|
|
9
|
+
* Provider `search()` methods do not exist.
|
|
10
|
+
*/
|
|
11
|
+
import { loadConfig } from "../core/config";
|
|
12
|
+
import { UsageError } from "../core/errors";
|
|
13
|
+
import { closeDatabase, openDatabase } from "../indexer/db";
|
|
14
|
+
import { searchLocal } from "../indexer/db-search";
|
|
15
|
+
import { resolveSourceEntries } from "../indexer/search-source";
|
|
16
|
+
// Eagerly import source providers to trigger self-registration before the
|
|
17
|
+
// indexer or path-resolution code runs.
|
|
18
|
+
import "../sources/providers/index";
|
|
19
|
+
import { insertUsageEvent } from "../indexer/usage-events";
|
|
8
20
|
import { searchRegistry } from "./registry-search";
|
|
9
|
-
import { resolveStashSources } from "./search-source";
|
|
10
|
-
import { insertUsageEvent } from "./usage-events";
|
|
11
21
|
const DEFAULT_LIMIT = 20;
|
|
12
22
|
export async function akmSearch(input) {
|
|
13
23
|
const t0 = Date.now();
|
|
@@ -17,7 +27,7 @@ export async function akmSearch(input) {
|
|
|
17
27
|
const limit = normalizeLimit(input.limit);
|
|
18
28
|
const source = parseSearchSource(input.source ?? "stash");
|
|
19
29
|
const config = loadConfig();
|
|
20
|
-
const sources =
|
|
30
|
+
const sources = resolveSourceEntries(undefined, config);
|
|
21
31
|
if (sources.length === 0) {
|
|
22
32
|
// stashDir: "" is a safe sentinel here — the response carries zero hits
|
|
23
33
|
// and a warning, so no downstream code will try to use the empty path.
|
|
@@ -35,10 +45,6 @@ export async function akmSearch(input) {
|
|
|
35
45
|
// Primary stash directory — used for DB path lookups and as the default
|
|
36
46
|
// stash root. Safe because the empty-sources case is handled above.
|
|
37
47
|
const stashDir = sources[0].path;
|
|
38
|
-
// Resolve additional stash providers (e.g. OpenViking) from config.
|
|
39
|
-
// Exclude filesystem (handled by resolveStashSources) and git (content
|
|
40
|
-
// now indexed through the unified FTS5 pipeline).
|
|
41
|
-
const additionalStashProviders = resolveStashProviders(config).filter((p) => p.type !== "filesystem" && p.type !== "git");
|
|
42
48
|
const localResult = source === "registry"
|
|
43
49
|
? undefined
|
|
44
50
|
: await searchLocal({
|
|
@@ -49,35 +55,17 @@ export async function akmSearch(input) {
|
|
|
49
55
|
sources,
|
|
50
56
|
config,
|
|
51
57
|
});
|
|
52
|
-
// Pass original case to providers — FTS5 requires lowercase but remote providers handle case themselves
|
|
53
|
-
const additionalStashResults = source === "registry" || additionalStashProviders.length === 0
|
|
54
|
-
? []
|
|
55
|
-
: await Promise.all(additionalStashProviders.map(async (provider) => {
|
|
56
|
-
try {
|
|
57
|
-
return await provider.search({ query, type: searchType === "any" ? undefined : searchType, limit });
|
|
58
|
-
}
|
|
59
|
-
catch (err) {
|
|
60
|
-
return {
|
|
61
|
-
hits: [],
|
|
62
|
-
warnings: [`Stash ${provider.name}: ${err instanceof Error ? err.message : String(err)}`],
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
}));
|
|
66
|
-
// Merge stash hits from all providers
|
|
67
|
-
const additionalHits = additionalStashResults.flatMap((r) => r.hits);
|
|
68
|
-
const additionalWarnings = additionalStashResults.flatMap((r) => r.warnings ?? []);
|
|
69
58
|
const registryResult = source === "stash" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
|
|
70
59
|
if (source === "stash") {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const hasResults = allStashHits.length > 0;
|
|
60
|
+
const localHits = localResult?.hits ?? [];
|
|
61
|
+
const hasResults = localHits.length > 0;
|
|
74
62
|
const response = {
|
|
75
63
|
schemaVersion: 1,
|
|
76
64
|
stashDir,
|
|
77
65
|
source,
|
|
78
|
-
hits:
|
|
66
|
+
hits: localHits,
|
|
79
67
|
tip: hasResults ? undefined : localResult?.tip,
|
|
80
|
-
warnings:
|
|
68
|
+
warnings: localResult?.warnings?.length ? localResult.warnings : undefined,
|
|
81
69
|
timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
|
|
82
70
|
};
|
|
83
71
|
logSearchEvent(query, response);
|
|
@@ -116,14 +104,14 @@ export async function akmSearch(input) {
|
|
|
116
104
|
return response;
|
|
117
105
|
}
|
|
118
106
|
// source === "both"
|
|
119
|
-
const allStashHits =
|
|
120
|
-
const warnings = [...(localResult?.warnings ?? []), ...
|
|
107
|
+
const allStashHits = (localResult?.hits ?? []).slice(0, limit);
|
|
108
|
+
const warnings = [...(localResult?.warnings ?? []), ...(registryResult?.warnings ?? [])];
|
|
121
109
|
const hasResults = allStashHits.length > 0 || registryHits.length > 0;
|
|
122
110
|
const response = {
|
|
123
111
|
schemaVersion: 1,
|
|
124
112
|
stashDir,
|
|
125
113
|
source,
|
|
126
|
-
hits: allStashHits
|
|
114
|
+
hits: allStashHits,
|
|
127
115
|
registryHits,
|
|
128
116
|
tip: hasResults ? undefined : "No matching stash assets or registry entries were found.",
|
|
129
117
|
warnings: warnings.length ? warnings : undefined,
|
|
@@ -184,35 +172,6 @@ function logSearchEvent(query, response, existingDb) {
|
|
|
184
172
|
}
|
|
185
173
|
}
|
|
186
174
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
187
|
-
/**
|
|
188
|
-
* Merge local and additional stash hits into a single ranked list.
|
|
189
|
-
*
|
|
190
|
-
* Provider hits (e.g. OpenViking) keep their original scores and compete
|
|
191
|
-
* fairly alongside local hits. Duplicates are resolved in favour of the
|
|
192
|
-
* local version.
|
|
193
|
-
*
|
|
194
|
-
* 1. Build set of local hit keys for dedup.
|
|
195
|
-
* 2. Filter provider hits that aren't duplicates.
|
|
196
|
-
* 3. Combine local + non-duplicate provider hits.
|
|
197
|
-
* 4. Sort by score descending.
|
|
198
|
-
* 5. Slice to limit.
|
|
199
|
-
*/
|
|
200
|
-
export function mergeStashHits(localHits, additionalHits, limit) {
|
|
201
|
-
if (additionalHits.length === 0)
|
|
202
|
-
return localHits.slice(0, limit);
|
|
203
|
-
// Track local hits by a dedup key (path > ref > name)
|
|
204
|
-
const localKeys = new Set();
|
|
205
|
-
for (const h of localHits) {
|
|
206
|
-
localKeys.add(h.path ?? h.ref ?? h.name);
|
|
207
|
-
}
|
|
208
|
-
// Keep non-duplicate provider hits with their original scores
|
|
209
|
-
const providerOnly = additionalHits.filter((h) => {
|
|
210
|
-
const key = h.path ?? h.ref ?? h.name;
|
|
211
|
-
return !localKeys.has(key);
|
|
212
|
-
});
|
|
213
|
-
// Combine and sort by score descending
|
|
214
|
-
return [...localHits, ...providerOnly].sort((a, b) => (b.score ?? 0) - (a.score ?? 0)).slice(0, limit);
|
|
215
|
-
}
|
|
216
175
|
function normalizeLimit(limit) {
|
|
217
176
|
if (typeof limit !== "number" || Number.isNaN(limit) || limit <= 0) {
|
|
218
177
|
return DEFAULT_LIMIT;
|
|
@@ -227,7 +186,7 @@ export function parseSearchSource(source) {
|
|
|
227
186
|
return "stash";
|
|
228
187
|
if (typeof source === "undefined")
|
|
229
188
|
return "stash";
|
|
230
|
-
throw new UsageError(`Invalid value for --source: ${String(source)}. Expected one of: stash|registry|both
|
|
189
|
+
throw new UsageError(`Invalid value for --source: ${String(source)}. Expected one of: stash|registry|both`, "INVALID_SOURCE_VALUE");
|
|
231
190
|
}
|
|
232
191
|
/**
|
|
233
192
|
* Merge stash hits and registry hits via simple concatenation.
|
|
@@ -2,8 +2,8 @@ import * as childProcess from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { fetchWithRetry, IS_WINDOWS } from "
|
|
6
|
-
import { githubHeaders } from "
|
|
5
|
+
import { fetchWithRetry, IS_WINDOWS } from "../core/common";
|
|
6
|
+
import { githubHeaders } from "../integrations/github";
|
|
7
7
|
const REPO = "itlackey/akm";
|
|
8
8
|
const DEFAULT_PACKAGE_NAME = "akm-cli";
|
|
9
9
|
const NODE_MODULES_SEGMENT = "/node_modules/";
|
|
@@ -303,7 +303,7 @@ function normalizePathSeparators(value) {
|
|
|
303
303
|
}
|
|
304
304
|
function getInstalledPackageName() {
|
|
305
305
|
try {
|
|
306
|
-
const pkgPath = path.resolve(import.meta.dir ?? __dirname, "
|
|
306
|
+
const pkgPath = path.resolve(import.meta.dir ?? __dirname, "../../package.json");
|
|
307
307
|
if (fs.existsSync(pkgPath)) {
|
|
308
308
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
309
309
|
if (typeof pkg.name === "string" && pkg.name.trim()) {
|
|
@@ -1,31 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `akm show` — entry point.
|
|
3
|
+
*
|
|
4
|
+
* Spec §6.2:
|
|
5
|
+
*
|
|
6
|
+
* show(ref) → indexer.lookup(ref) → readFile(entry.filePath)
|
|
7
|
+
*
|
|
8
|
+
* The richer presentation logic (matchers, renderers, wiki-root handling,
|
|
9
|
+
* edit-hints, summary-detail truncation) lives below in this file. The flow:
|
|
10
|
+
*
|
|
11
|
+
* 1. Special-case wiki-root refs (`wiki:<name>` with no page path).
|
|
12
|
+
* 2. Ask `indexer.lookup(ref)` for the row in the FTS index.
|
|
13
|
+
* 3. Fall back to the on-disk type-dir resolver only when the index has
|
|
14
|
+
* no matching row — covers the "indexed yet?" gap when the user has
|
|
15
|
+
* just added a file and not run `akm index`.
|
|
16
|
+
* 4. Render the file via the matcher/renderer pipeline.
|
|
17
|
+
*
|
|
18
|
+
* Step (2) is the v1 spec change: reading is the indexer's job. Step (3) is a
|
|
19
|
+
* pragmatic safety net (NOT remote provider fallback, which the spec
|
|
20
|
+
* forbids — "Show: Local FTS5 index only. No remote provider fallback.").
|
|
21
|
+
*/
|
|
1
22
|
import fs from "node:fs";
|
|
2
23
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { NotFoundError, UsageError } from "
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
import "
|
|
24
|
+
import { parseAssetRef } from "../core/asset-ref";
|
|
25
|
+
import { loadConfig } from "../core/config";
|
|
26
|
+
import { NotFoundError, UsageError } from "../core/errors";
|
|
27
|
+
import { parseFrontmatter, toStringOrUndefined } from "../core/frontmatter";
|
|
28
|
+
import { closeDatabase, openDatabase } from "../indexer/db";
|
|
29
|
+
import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../indexer/file-context";
|
|
30
|
+
import { lookup } from "../indexer/indexer";
|
|
31
|
+
import { loadStashFile } from "../indexer/metadata";
|
|
32
|
+
import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries } from "../indexer/search-source";
|
|
33
|
+
import { resolveSourcesForOrigin } from "../registry/origin-resolve";
|
|
34
|
+
// Eagerly import source providers to trigger self-registration.
|
|
35
|
+
import "../sources/providers/index";
|
|
36
|
+
import { insertUsageEvent } from "../indexer/usage-events";
|
|
37
|
+
import { resolveAssetPath } from "../sources/resolve";
|
|
17
38
|
/**
|
|
18
39
|
* Show a wiki root (no page path) — returns the same payload as
|
|
19
40
|
* `akm wiki show <name>`.
|
|
20
|
-
*
|
|
21
|
-
* Called when `parseAssetRef` yields `type === "wiki"` and the name has no
|
|
22
|
-
* `/`, e.g. `wiki:research`.
|
|
23
41
|
*/
|
|
24
42
|
async function showWikiRoot(stashDir, wikiName) {
|
|
25
|
-
const { showWiki } = await import("
|
|
43
|
+
const { showWiki } = await import("../wiki/wiki.js");
|
|
26
44
|
const result = showWiki(stashDir, wikiName);
|
|
27
|
-
// Shape the WikiShowResult into a ShowResponse-compatible object.
|
|
28
|
-
// The payload mirrors what `akm wiki show <name>` returns.
|
|
29
45
|
return {
|
|
30
46
|
type: "wiki",
|
|
31
47
|
name: result.ref,
|
|
@@ -40,7 +56,7 @@ async function showWikiRoot(stashDir, wikiName) {
|
|
|
40
56
|
};
|
|
41
57
|
}
|
|
42
58
|
async function showWikiRootForSource(stashDir, source, wikiName) {
|
|
43
|
-
const { showWikiAtPath } = await import("
|
|
59
|
+
const { showWikiAtPath } = await import("../wiki/wiki.js");
|
|
44
60
|
if (source.wikiName === wikiName) {
|
|
45
61
|
const result = showWikiAtPath(wikiName, source.path);
|
|
46
62
|
return {
|
|
@@ -78,7 +94,9 @@ function resolveRegisteredWikiAssetPath(wikiRoot, wikiName, assetName) {
|
|
|
78
94
|
return realTarget;
|
|
79
95
|
}
|
|
80
96
|
/**
|
|
81
|
-
* Unified show:
|
|
97
|
+
* Unified show: queries the local FTS5 index, then falls back to on-disk
|
|
98
|
+
* type-dir resolution if the index has no row. Spec §6.2; no remote provider
|
|
99
|
+
* fallback.
|
|
82
100
|
*
|
|
83
101
|
* When `detail` is `"summary"`, the response omits content/template/prompt and
|
|
84
102
|
* returns only compact metadata (name, type, description, tags, parameters).
|
|
@@ -92,7 +110,7 @@ export async function akmShowUnified(input) {
|
|
|
92
110
|
{
|
|
93
111
|
const parsed = parseAssetRef(ref);
|
|
94
112
|
if (parsed.type === "wiki" && !parsed.name.includes("/")) {
|
|
95
|
-
const allSources =
|
|
113
|
+
const allSources = resolveSourceEntries();
|
|
96
114
|
const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
|
|
97
115
|
let lastError;
|
|
98
116
|
for (const source of searchSources) {
|
|
@@ -109,43 +127,11 @@ export async function akmShowUnified(input) {
|
|
|
109
127
|
new NotFoundError(`Wiki not found: ${parsed.name}. Run \`akm wiki create ${parsed.name}\` to create it.`));
|
|
110
128
|
}
|
|
111
129
|
}
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
logShowEvent(ref);
|
|
117
|
-
return result;
|
|
118
|
-
}
|
|
119
|
-
catch (err) {
|
|
120
|
-
// Only fall through to remote providers on NotFoundError
|
|
121
|
-
if (!(err instanceof NotFoundError))
|
|
122
|
-
throw err;
|
|
123
|
-
localError = err;
|
|
124
|
-
}
|
|
125
|
-
// 2. Try remote providers (e.g. OpenViking)
|
|
126
|
-
const config = loadConfig();
|
|
127
|
-
const providers = resolveStashProviders(config).filter((p) => p.type !== "filesystem" && p.canShow(ref));
|
|
128
|
-
for (const provider of providers) {
|
|
129
|
-
try {
|
|
130
|
-
const response = await provider.show(ref, input.view);
|
|
131
|
-
logShowEvent(ref);
|
|
132
|
-
if (input.detail === "summary") {
|
|
133
|
-
return buildSummaryResponse(response);
|
|
134
|
-
}
|
|
135
|
-
return response;
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
if (!(err instanceof NotFoundError))
|
|
139
|
-
throw err;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// Nothing found anywhere — rethrow the original local error with its specific message
|
|
143
|
-
throw localError;
|
|
130
|
+
// Try local filesystem (FTS5 index lookup, then on-disk fallback)
|
|
131
|
+
const result = await showLocal(input);
|
|
132
|
+
logShowEvent(ref);
|
|
133
|
+
return result;
|
|
144
134
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Fire-and-forget: log a show event to the usage_events table.
|
|
147
|
-
* Never blocks the caller; errors are silently ignored.
|
|
148
|
-
*/
|
|
149
135
|
function logShowEvent(ref, existingDb) {
|
|
150
136
|
try {
|
|
151
137
|
const db = existingDb ?? openDatabase();
|
|
@@ -170,14 +156,48 @@ function logShowEvent(ref, existingDb) {
|
|
|
170
156
|
/* fire-and-forget */
|
|
171
157
|
}
|
|
172
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolve an asset path to a file via:
|
|
161
|
+
* 1. `indexer.lookup(ref)` — the spec's primary path (§6.2).
|
|
162
|
+
* 2. On-disk type-dir traversal — fallback for files not yet indexed.
|
|
163
|
+
*
|
|
164
|
+
* Returns `undefined` if neither path finds a match.
|
|
165
|
+
*/
|
|
166
|
+
async function resolvePathViaIndexThenDisk(parsed, searchSourceDirs) {
|
|
167
|
+
// Step 1: indexer
|
|
168
|
+
try {
|
|
169
|
+
const entry = await lookup(parsed);
|
|
170
|
+
if (entry) {
|
|
171
|
+
return { assetPath: entry.filePath };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
// Index unavailable (e.g. DB doesn't exist yet) — fall back to disk walk.
|
|
176
|
+
if (!(err instanceof NotFoundError)) {
|
|
177
|
+
// continue to disk fallback
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Step 2: on-disk type-dir traversal
|
|
181
|
+
let lastError;
|
|
182
|
+
for (const dir of searchSourceDirs) {
|
|
183
|
+
try {
|
|
184
|
+
const assetPath = await resolveAssetPath(dir, parsed.type, parsed.name);
|
|
185
|
+
return { assetPath, lastError };
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return lastError ? { assetPath: "", lastError } : undefined;
|
|
192
|
+
}
|
|
173
193
|
/** @internal Use akmShowUnified() for all external callers. */
|
|
174
194
|
export async function showLocal(input) {
|
|
175
195
|
const parsed = parseAssetRef(input.ref);
|
|
176
196
|
const displayType = parsed.type;
|
|
177
197
|
const config = loadConfig();
|
|
178
|
-
const allSources =
|
|
198
|
+
const allSources = resolveSourceEntries(input.stashDir);
|
|
179
199
|
const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
|
|
180
|
-
const
|
|
200
|
+
const allSourceDirs = searchSources.map((s) => s.path);
|
|
181
201
|
let assetPath;
|
|
182
202
|
const matchedSource = parsed.type === "wiki" ? searchSources.find((source) => parsed.name.startsWith(`${source.wikiName}/`)) : undefined;
|
|
183
203
|
let lastError;
|
|
@@ -189,15 +209,13 @@ export async function showLocal(input) {
|
|
|
189
209
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
190
210
|
}
|
|
191
211
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
assetPath = await resolveAssetPath(dir, parsed.type, parsed.name);
|
|
197
|
-
break;
|
|
212
|
+
if (!assetPath) {
|
|
213
|
+
const resolved = await resolvePathViaIndexThenDisk(parsed, allSourceDirs);
|
|
214
|
+
if (resolved?.assetPath) {
|
|
215
|
+
assetPath = resolved.assetPath;
|
|
198
216
|
}
|
|
199
|
-
|
|
200
|
-
lastError =
|
|
217
|
+
else if (resolved?.lastError) {
|
|
218
|
+
lastError = resolved.lastError;
|
|
201
219
|
}
|
|
202
220
|
}
|
|
203
221
|
if (!assetPath && parsed.origin && searchSources.length === 0) {
|
|
@@ -211,7 +229,7 @@ export async function showLocal(input) {
|
|
|
211
229
|
"Check the name with `akm search` or verify the asset exists in your stash."));
|
|
212
230
|
}
|
|
213
231
|
const source = matchedSource ?? findSourceForPath(assetPath, allSources);
|
|
214
|
-
const sourceStashDir = source?.path ??
|
|
232
|
+
const sourceStashDir = source?.path ?? allSourceDirs[0];
|
|
215
233
|
if (!sourceStashDir) {
|
|
216
234
|
throw new UsageError(`Could not determine stash root for asset: ${displayType}:${parsed.name}. ` +
|
|
217
235
|
"Run `akm init` to create the stash directory, or check `akm stash list` for configured paths.");
|
|
@@ -229,7 +247,7 @@ export async function showLocal(input) {
|
|
|
229
247
|
if (!renderer) {
|
|
230
248
|
throw new UsageError(`Renderer "${match.renderer}" not found for asset: ${displayType}:${parsed.name}`);
|
|
231
249
|
}
|
|
232
|
-
const renderCtx = buildRenderContext(fileCtx, match,
|
|
250
|
+
const renderCtx = buildRenderContext(fileCtx, match, allSourceDirs, source?.registryId);
|
|
233
251
|
const response = renderer.buildShowResponse(renderCtx);
|
|
234
252
|
const editable = isEditable(assetPath, config);
|
|
235
253
|
const fullResponse = {
|
|
@@ -243,6 +261,20 @@ export async function showLocal(input) {
|
|
|
243
261
|
}
|
|
244
262
|
return fullResponse;
|
|
245
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Minimal `show`: ref → indexer lookup → file contents. Used by callers that
|
|
266
|
+
* just need the raw file (e.g. clone, write-source) and don't want the full
|
|
267
|
+
* renderer graph. Spec §6.2's literal flow.
|
|
268
|
+
*/
|
|
269
|
+
export async function showByRef(ref) {
|
|
270
|
+
const parsed = parseAssetRef(ref);
|
|
271
|
+
const entry = await lookup(parsed);
|
|
272
|
+
if (!entry) {
|
|
273
|
+
throw new NotFoundError(`Asset not found for ref: ${parsed.type}:${parsed.name}`);
|
|
274
|
+
}
|
|
275
|
+
const body = await fs.promises.readFile(entry.filePath, "utf8");
|
|
276
|
+
return { filePath: entry.filePath, body };
|
|
277
|
+
}
|
|
246
278
|
/**
|
|
247
279
|
* Build a compact summary response from a full ShowResponse.
|
|
248
280
|
*
|
|
@@ -250,24 +282,17 @@ export async function showLocal(input) {
|
|
|
250
282
|
* type, name, path, description, tags, parameters, action.
|
|
251
283
|
* Enriches description and tags from frontmatter or .stash.json when available.
|
|
252
284
|
*
|
|
253
|
-
* Enrichment via frontmatter and .stash.json is only performed when `assetPath`
|
|
254
|
-
* is supplied (local assets). Remote provider responses (e.g. OpenViking) rely
|
|
255
|
-
* on the provider having already populated description and tags.
|
|
256
|
-
*
|
|
257
285
|
* The resulting JSON should be under 200 tokens.
|
|
258
286
|
*/
|
|
259
287
|
function buildSummaryResponse(full, assetPath) {
|
|
260
|
-
// Try to enrich metadata from .stash.json if description or tags are missing
|
|
261
288
|
let description = full.description;
|
|
262
289
|
let tags = full.tags;
|
|
263
290
|
if (assetPath) {
|
|
264
|
-
// Try frontmatter extraction from content fields
|
|
265
291
|
const textContent = full.content ?? full.template ?? full.prompt;
|
|
266
292
|
if (textContent && !description) {
|
|
267
293
|
const parsed = parseFrontmatter(textContent);
|
|
268
294
|
description = toStringOrUndefined(parsed.data.description);
|
|
269
295
|
}
|
|
270
|
-
// Try .stash.json for richer metadata (tags especially)
|
|
271
296
|
const dir = path.dirname(assetPath);
|
|
272
297
|
const stashFile = loadStashFile(dir);
|
|
273
298
|
if (stashFile) {
|