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,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { getAssetTypes } from "
|
|
4
|
+
import { getAssetTypes } from "../core/asset-spec";
|
|
5
5
|
// ── Known flag values ────────────────────────────────────────────────────────
|
|
6
6
|
const FLAG_VALUES = {
|
|
7
7
|
"--format": ["json", "text", "yaml", "jsonl"],
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { DEFAULT_CONFIG, } from "
|
|
2
|
-
import { UsageError } from "
|
|
1
|
+
import { DEFAULT_CONFIG, } from "../core/config";
|
|
2
|
+
import { UsageError } from "../core/errors";
|
|
3
|
+
// ── Merge helpers for LLM/embedding subkey set ───────────────────────────────
|
|
4
|
+
function mergeLlmLike(base, patch) {
|
|
5
|
+
return { endpoint: "", model: "", ...(base ?? {}), ...patch };
|
|
6
|
+
}
|
|
7
|
+
function mergeLlmLikeEmbedding(base, patch) {
|
|
8
|
+
return { endpoint: "", model: "", ...(base ?? {}), ...patch };
|
|
9
|
+
}
|
|
3
10
|
export function parseConfigValue(key, value) {
|
|
4
11
|
switch (key) {
|
|
5
12
|
case "stashDir":
|
|
6
13
|
return { stashDir: requireNonEmptyString(value, key) };
|
|
14
|
+
case "defaultWriteTarget":
|
|
15
|
+
return { defaultWriteTarget: requireNonEmptyString(value, key) };
|
|
7
16
|
case "semanticSearchMode":
|
|
8
17
|
// Accept legacy boolean-style strings from CLI
|
|
9
18
|
if (value === "true")
|
|
@@ -16,12 +25,26 @@ export function parseConfigValue(key, value) {
|
|
|
16
25
|
return { semanticSearchMode: value };
|
|
17
26
|
case "embedding":
|
|
18
27
|
return { embedding: parseEmbeddingConnectionValue(value) };
|
|
28
|
+
case "embedding.endpoint":
|
|
29
|
+
return { embedding: mergeLlmLikeEmbedding(undefined, { endpoint: requireNonEmptyString(value, key) }) };
|
|
30
|
+
case "embedding.model":
|
|
31
|
+
return { embedding: mergeLlmLikeEmbedding(undefined, { model: requireNonEmptyString(value, key) }) };
|
|
32
|
+
case "embedding.apiKey":
|
|
33
|
+
return { embedding: mergeLlmLikeEmbedding(undefined, { apiKey: requireNonEmptyString(value, key) }) };
|
|
19
34
|
case "llm":
|
|
20
35
|
return { llm: parseLlmConnectionValue(value) };
|
|
36
|
+
case "llm.endpoint":
|
|
37
|
+
return { llm: mergeLlmLike(undefined, { endpoint: requireNonEmptyString(value, key) }) };
|
|
38
|
+
case "llm.model":
|
|
39
|
+
return { llm: mergeLlmLike(undefined, { model: requireNonEmptyString(value, key) }) };
|
|
40
|
+
case "llm.apiKey":
|
|
41
|
+
return { llm: mergeLlmLike(undefined, { apiKey: requireNonEmptyString(value, key) }) };
|
|
21
42
|
case "registries":
|
|
22
43
|
return { registries: parseRegistriesValue(value) };
|
|
44
|
+
case "sources":
|
|
23
45
|
case "stashes":
|
|
24
|
-
|
|
46
|
+
// "stashes" is kept as an alias for backwards-compat; both write to `sources`.
|
|
47
|
+
return { sources: parseStashesValue(value) };
|
|
25
48
|
case "output.format":
|
|
26
49
|
return { output: { format: parseOutputFormat(value) } };
|
|
27
50
|
case "output.detail":
|
|
@@ -46,16 +69,32 @@ export function getConfigValue(config, key) {
|
|
|
46
69
|
switch (key) {
|
|
47
70
|
case "stashDir":
|
|
48
71
|
return config.stashDir ?? null;
|
|
72
|
+
case "defaultWriteTarget":
|
|
73
|
+
return config.defaultWriteTarget ?? null;
|
|
49
74
|
case "semanticSearchMode":
|
|
50
75
|
return config.semanticSearchMode;
|
|
51
76
|
case "embedding":
|
|
52
77
|
return config.embedding ?? null;
|
|
78
|
+
case "embedding.endpoint":
|
|
79
|
+
return config.embedding?.endpoint ?? null;
|
|
80
|
+
case "embedding.model":
|
|
81
|
+
return config.embedding?.model ?? null;
|
|
82
|
+
case "embedding.apiKey":
|
|
83
|
+
return config.embedding?.apiKey ?? null;
|
|
53
84
|
case "llm":
|
|
54
85
|
return config.llm ?? null;
|
|
86
|
+
case "llm.endpoint":
|
|
87
|
+
return config.llm?.endpoint ?? null;
|
|
88
|
+
case "llm.model":
|
|
89
|
+
return config.llm?.model ?? null;
|
|
90
|
+
case "llm.apiKey":
|
|
91
|
+
return config.llm?.apiKey ?? null;
|
|
55
92
|
case "registries":
|
|
56
93
|
return config.registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
94
|
+
case "sources":
|
|
57
95
|
case "stashes":
|
|
58
|
-
|
|
96
|
+
// "stashes" is an alias for "sources" for backwards-compat.
|
|
97
|
+
return config.sources ?? config.stashes ?? [];
|
|
59
98
|
case "output.format":
|
|
60
99
|
return config.output?.format ?? null;
|
|
61
100
|
case "output.detail":
|
|
@@ -85,6 +124,7 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
85
124
|
case "embedding":
|
|
86
125
|
case "llm":
|
|
87
126
|
case "registries":
|
|
127
|
+
case "sources":
|
|
88
128
|
case "stashes":
|
|
89
129
|
case "output.format":
|
|
90
130
|
case "output.detail":
|
|
@@ -95,6 +135,38 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
95
135
|
case "security.installAudit.registryWhitelist":
|
|
96
136
|
case "security.installAudit.allowedFindings":
|
|
97
137
|
return mergeConfigValue(config, parseConfigValue(key, rawValue));
|
|
138
|
+
// Subkey setters use deep-merge so sibling fields are preserved
|
|
139
|
+
case "embedding.endpoint":
|
|
140
|
+
return {
|
|
141
|
+
...config,
|
|
142
|
+
embedding: mergeLlmLikeEmbedding(config.embedding, { endpoint: requireNonEmptyString(rawValue, key) }),
|
|
143
|
+
};
|
|
144
|
+
case "embedding.model":
|
|
145
|
+
return {
|
|
146
|
+
...config,
|
|
147
|
+
embedding: mergeLlmLikeEmbedding(config.embedding, { model: requireNonEmptyString(rawValue, key) }),
|
|
148
|
+
};
|
|
149
|
+
case "embedding.apiKey":
|
|
150
|
+
return {
|
|
151
|
+
...config,
|
|
152
|
+
embedding: mergeLlmLikeEmbedding(config.embedding, { apiKey: requireNonEmptyString(rawValue, key) }),
|
|
153
|
+
};
|
|
154
|
+
case "llm.endpoint":
|
|
155
|
+
return { ...config, llm: mergeLlmLike(config.llm, { endpoint: requireNonEmptyString(rawValue, key) }) };
|
|
156
|
+
case "llm.model":
|
|
157
|
+
return { ...config, llm: mergeLlmLike(config.llm, { model: requireNonEmptyString(rawValue, key) }) };
|
|
158
|
+
case "llm.apiKey":
|
|
159
|
+
return { ...config, llm: mergeLlmLike(config.llm, { apiKey: requireNonEmptyString(rawValue, key) }) };
|
|
160
|
+
case "defaultWriteTarget": {
|
|
161
|
+
const name = requireNonEmptyString(rawValue, key);
|
|
162
|
+
const knownNames = (config.sources ?? config.stashes ?? [])
|
|
163
|
+
.map((s) => s.name)
|
|
164
|
+
.filter((n) => typeof n === "string");
|
|
165
|
+
if (knownNames.length > 0 && !knownNames.includes(name)) {
|
|
166
|
+
throw new UsageError(`Unknown source name "${name}" for defaultWriteTarget; configured source names: ${knownNames.map((n) => `"${n}"`).join(", ")}`);
|
|
167
|
+
}
|
|
168
|
+
return { ...config, defaultWriteTarget: name };
|
|
169
|
+
}
|
|
98
170
|
default:
|
|
99
171
|
throw new UsageError(`Unknown config key: ${key}`);
|
|
100
172
|
}
|
|
@@ -103,14 +175,38 @@ export function unsetConfigValue(config, key) {
|
|
|
103
175
|
switch (key) {
|
|
104
176
|
case "stashDir":
|
|
105
177
|
return { ...config, stashDir: undefined };
|
|
178
|
+
case "defaultWriteTarget":
|
|
179
|
+
return { ...config, defaultWriteTarget: undefined };
|
|
106
180
|
case "embedding":
|
|
107
181
|
return { ...config, embedding: undefined };
|
|
182
|
+
case "embedding.endpoint":
|
|
183
|
+
return { ...config, embedding: mergeLlmLikeEmbedding(config.embedding, { endpoint: "" }) };
|
|
184
|
+
case "embedding.model":
|
|
185
|
+
return { ...config, embedding: mergeLlmLikeEmbedding(config.embedding, { model: "" }) };
|
|
186
|
+
case "embedding.apiKey": {
|
|
187
|
+
if (!config.embedding)
|
|
188
|
+
return config;
|
|
189
|
+
const { apiKey: _a, ...rest } = config.embedding;
|
|
190
|
+
return { ...config, embedding: rest };
|
|
191
|
+
}
|
|
108
192
|
case "llm":
|
|
109
193
|
return { ...config, llm: undefined };
|
|
194
|
+
case "llm.endpoint":
|
|
195
|
+
return { ...config, llm: mergeLlmLike(config.llm, { endpoint: "" }) };
|
|
196
|
+
case "llm.model":
|
|
197
|
+
return { ...config, llm: mergeLlmLike(config.llm, { model: "" }) };
|
|
198
|
+
case "llm.apiKey": {
|
|
199
|
+
if (!config.llm)
|
|
200
|
+
return config;
|
|
201
|
+
const { apiKey: _b, ...restLlm } = config.llm;
|
|
202
|
+
return { ...config, llm: restLlm };
|
|
203
|
+
}
|
|
110
204
|
case "registries":
|
|
111
205
|
return { ...config, registries: undefined };
|
|
206
|
+
case "sources":
|
|
112
207
|
case "stashes":
|
|
113
|
-
|
|
208
|
+
// "stashes" is kept as an alias for backwards-compat; both clear `sources`.
|
|
209
|
+
return { ...config, sources: undefined, stashes: undefined };
|
|
114
210
|
case "output.format":
|
|
115
211
|
return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
|
|
116
212
|
case "output.detail":
|
|
@@ -155,8 +251,10 @@ export function listConfig(config) {
|
|
|
155
251
|
output: mergeOutputConfig(DEFAULT_CONFIG.output, config.output) ?? null,
|
|
156
252
|
stashDir: config.stashDir ?? null,
|
|
157
253
|
installed: config.installed ?? [],
|
|
158
|
-
|
|
254
|
+
sources: config.sources ?? config.stashes ?? [],
|
|
159
255
|
};
|
|
256
|
+
if (config.defaultWriteTarget)
|
|
257
|
+
result.defaultWriteTarget = config.defaultWriteTarget;
|
|
160
258
|
if (config.embedding)
|
|
161
259
|
result.embedding = config.embedding;
|
|
162
260
|
if (config.llm)
|
|
@@ -349,7 +447,7 @@ function parseJsonObject(value, key, example) {
|
|
|
349
447
|
}
|
|
350
448
|
catch {
|
|
351
449
|
throw new UsageError(`Invalid value for ${key}: expected JSON object with endpoint and model` +
|
|
352
|
-
` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')
|
|
450
|
+
` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')`, "INVALID_JSON_CONFIG_VALUE");
|
|
353
451
|
}
|
|
354
452
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
355
453
|
throw new UsageError(`Invalid value for ${key}: expected a JSON object`);
|
|
@@ -388,18 +486,18 @@ function parseStashesValue(value) {
|
|
|
388
486
|
parsed = JSON.parse(value);
|
|
389
487
|
}
|
|
390
488
|
catch {
|
|
391
|
-
throw new UsageError(`Invalid value for
|
|
489
|
+
throw new UsageError(`Invalid value for sources: expected JSON array of {type, path?, url?, name?, enabled?, options?} objects`);
|
|
392
490
|
}
|
|
393
491
|
if (!Array.isArray(parsed)) {
|
|
394
|
-
throw new UsageError(`Invalid value for
|
|
492
|
+
throw new UsageError(`Invalid value for sources: expected a JSON array`);
|
|
395
493
|
}
|
|
396
494
|
return parsed.map((entry, i) => {
|
|
397
495
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
398
|
-
throw new UsageError(`Invalid value for
|
|
496
|
+
throw new UsageError(`Invalid value for sources[${i}]: expected an object with a "type" field`);
|
|
399
497
|
}
|
|
400
498
|
const obj = entry;
|
|
401
499
|
if (typeof obj.type !== "string" || !obj.type) {
|
|
402
|
-
throw new UsageError(`Invalid value for
|
|
500
|
+
throw new UsageError(`Invalid value for sources[${i}]: "type" is required`);
|
|
403
501
|
}
|
|
404
502
|
const result = { type: obj.type };
|
|
405
503
|
if (typeof obj.path === "string" && obj.path)
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curate logic for `akm curate`.
|
|
3
|
+
*
|
|
4
|
+
* Given a query (and optional type filter / source / limit), pick a small,
|
|
5
|
+
* high-signal set of stash + registry hits and enrich each with the data
|
|
6
|
+
* needed to act (ref, run, parameters, follow-up command).
|
|
7
|
+
*
|
|
8
|
+
* The exported `akmCurate()` API is the single entry point. Internal
|
|
9
|
+
* helpers stay private. Tests can drive the public API or call the smaller
|
|
10
|
+
* pure helpers (`curateSearchResults`, `orderCuratedTypes`,
|
|
11
|
+
* `deriveCurateFallbackQueries`) by importing them directly.
|
|
12
|
+
*/
|
|
13
|
+
import { UsageError } from "../core/errors";
|
|
14
|
+
import { truncateDescription } from "../output/shapes";
|
|
15
|
+
import { akmSearch, parseSearchSource } from "./search";
|
|
16
|
+
import { akmShowUnified } from "./show";
|
|
17
|
+
const CURATE_FALLBACK_FILTER_WORDS = new Set([
|
|
18
|
+
"a",
|
|
19
|
+
"an",
|
|
20
|
+
"and",
|
|
21
|
+
"for",
|
|
22
|
+
"how",
|
|
23
|
+
"i",
|
|
24
|
+
"in",
|
|
25
|
+
"of",
|
|
26
|
+
"or",
|
|
27
|
+
"the",
|
|
28
|
+
"to",
|
|
29
|
+
"with",
|
|
30
|
+
]);
|
|
31
|
+
const CURATED_TYPE_FALLBACK_ORDER = ["skill", "command", "script", "knowledge", "agent", "memory"];
|
|
32
|
+
const CURATED_TYPE_FALLBACK_INDEX = new Map(CURATED_TYPE_FALLBACK_ORDER.map((type, index) => [type, index]));
|
|
33
|
+
const MIN_CURATE_FALLBACK_TOKEN_LENGTH = 3;
|
|
34
|
+
const MAX_CURATE_FALLBACK_KEYWORDS = 6;
|
|
35
|
+
export const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
|
|
36
|
+
export const MIN_CURATE_SEARCH_LIMIT = 12;
|
|
37
|
+
const DEFAULT_CURATE_LIMIT = 4;
|
|
38
|
+
/**
|
|
39
|
+
* Public curate entry point. Performs the search itself when
|
|
40
|
+
* `options.searchResponse` is not supplied.
|
|
41
|
+
*/
|
|
42
|
+
export async function akmCurate(options) {
|
|
43
|
+
const trimmedQuery = options.query.trim();
|
|
44
|
+
if (!trimmedQuery) {
|
|
45
|
+
throw new UsageError('A curation query is required. Usage: akm curate "<task or prompt>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT");
|
|
46
|
+
}
|
|
47
|
+
const limit = options.limit && options.limit > 0 ? options.limit : DEFAULT_CURATE_LIMIT;
|
|
48
|
+
const source = options.source ?? parseSearchSource("stash");
|
|
49
|
+
const searchResponse = options.searchResponse ??
|
|
50
|
+
(await searchForCuration({
|
|
51
|
+
query: options.query,
|
|
52
|
+
type: options.type,
|
|
53
|
+
// Search deeper than the final curated count so we can pick one strong
|
|
54
|
+
// match per type and still have room for fallback retries.
|
|
55
|
+
limit: Math.max(limit * CURATE_SEARCH_LIMIT_MULTIPLIER, MIN_CURATE_SEARCH_LIMIT),
|
|
56
|
+
source,
|
|
57
|
+
}));
|
|
58
|
+
return curateSearchResults(options.query, searchResponse, limit, options.type);
|
|
59
|
+
}
|
|
60
|
+
export async function curateSearchResults(query, result, limit, selectedType) {
|
|
61
|
+
const stashHits = result.hits.filter((hit) => hit.type !== "registry");
|
|
62
|
+
const registryHits = result.registryHits ?? [];
|
|
63
|
+
let selectedStashHits;
|
|
64
|
+
if (selectedType && selectedType !== "any") {
|
|
65
|
+
selectedStashHits = stashHits.slice(0, limit);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const bestByType = new Map();
|
|
69
|
+
for (const hit of stashHits) {
|
|
70
|
+
if (!bestByType.has(hit.type))
|
|
71
|
+
bestByType.set(hit.type, hit);
|
|
72
|
+
}
|
|
73
|
+
const orderedTypes = orderCuratedTypes(query, Array.from(bestByType.keys()));
|
|
74
|
+
selectedStashHits = orderedTypes
|
|
75
|
+
.map((type) => bestByType.get(type))
|
|
76
|
+
.filter((hit) => Boolean(hit));
|
|
77
|
+
}
|
|
78
|
+
const selectedRegistryHits = selectedStashHits.length >= limit ? [] : registryHits.slice(0, Math.min(2, limit - selectedStashHits.length));
|
|
79
|
+
const items = [
|
|
80
|
+
...(await Promise.all(selectedStashHits.slice(0, limit).map((hit) => enrichCuratedStashHit(query, hit)))),
|
|
81
|
+
...selectedRegistryHits.map((hit) => buildCuratedRegistryItem(query, hit)),
|
|
82
|
+
].slice(0, limit);
|
|
83
|
+
return {
|
|
84
|
+
query,
|
|
85
|
+
summary: buildCurateSummary(query, items),
|
|
86
|
+
items,
|
|
87
|
+
...(result.warnings?.length ? { warnings: result.warnings } : {}),
|
|
88
|
+
...(result.tip ? { tip: result.tip } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function orderCuratedTypes(query, types) {
|
|
92
|
+
const lower = query.toLowerCase();
|
|
93
|
+
const boosts = new Map();
|
|
94
|
+
const addBoost = (type, amount) => boosts.set(type, (boosts.get(type) ?? 0) + amount);
|
|
95
|
+
if (/(run|script|bash|shell|cli|execute|automation|deploy|build|test|lint)/.test(lower)) {
|
|
96
|
+
addBoost("script", 6);
|
|
97
|
+
addBoost("command", 4);
|
|
98
|
+
}
|
|
99
|
+
if (/(guide|docs?|readme|reference|how|explain|learn|why)/.test(lower)) {
|
|
100
|
+
addBoost("knowledge", 6);
|
|
101
|
+
addBoost("skill", 4);
|
|
102
|
+
}
|
|
103
|
+
if (/(agent|assistant|planner|review|analy[sz]e|architect|prompt)/.test(lower)) {
|
|
104
|
+
addBoost("agent", 6);
|
|
105
|
+
addBoost("skill", 3);
|
|
106
|
+
}
|
|
107
|
+
if (/(config|template|release|generate|command)/.test(lower)) {
|
|
108
|
+
addBoost("command", 5);
|
|
109
|
+
}
|
|
110
|
+
if (/(memory|context|recall|remember)/.test(lower)) {
|
|
111
|
+
addBoost("memory", 6);
|
|
112
|
+
}
|
|
113
|
+
return [...types].sort((a, b) => {
|
|
114
|
+
const boostDiff = (boosts.get(b) ?? 0) - (boosts.get(a) ?? 0);
|
|
115
|
+
if (boostDiff !== 0)
|
|
116
|
+
return boostDiff;
|
|
117
|
+
return ((CURATED_TYPE_FALLBACK_INDEX.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
|
118
|
+
(CURATED_TYPE_FALLBACK_INDEX.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async function enrichCuratedStashHit(query, hit) {
|
|
122
|
+
let shown;
|
|
123
|
+
try {
|
|
124
|
+
shown = await akmShowUnified({ ref: hit.ref });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
shown = undefined;
|
|
128
|
+
}
|
|
129
|
+
const description = shown?.description ?? hit.description;
|
|
130
|
+
const preview = buildCuratedPreview(shown, hit);
|
|
131
|
+
return {
|
|
132
|
+
source: "stash",
|
|
133
|
+
type: shown?.type ?? hit.type,
|
|
134
|
+
name: shown?.name ?? hit.name,
|
|
135
|
+
ref: hit.ref,
|
|
136
|
+
...(description ? { description } : {}),
|
|
137
|
+
...(preview ? { preview } : {}),
|
|
138
|
+
...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
|
|
139
|
+
...(shown?.run ? { run: shown.run } : {}),
|
|
140
|
+
followUp: `akm show ${hit.ref}`,
|
|
141
|
+
reason: buildCuratedReason(query, shown?.type ?? hit.type),
|
|
142
|
+
...(hit.score !== undefined ? { score: hit.score } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function buildCuratedRegistryItem(query, hit) {
|
|
146
|
+
return {
|
|
147
|
+
source: "registry",
|
|
148
|
+
type: "registry",
|
|
149
|
+
name: hit.name,
|
|
150
|
+
id: hit.id,
|
|
151
|
+
...(hit.description ? { description: hit.description } : {}),
|
|
152
|
+
followUp: hit.action ?? `akm add ${hit.id}`,
|
|
153
|
+
reason: `Useful external source to explore for ${query}.`,
|
|
154
|
+
...(hit.score !== undefined ? { score: hit.score } : {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function firstNonEmpty(values) {
|
|
158
|
+
return values.find((value) => typeof value === "string" && value.trim().length > 0);
|
|
159
|
+
}
|
|
160
|
+
function buildCuratedPreview(shown, hit) {
|
|
161
|
+
if (shown?.run)
|
|
162
|
+
return truncateDescription(`run ${shown.run}`, 160);
|
|
163
|
+
const payload = firstNonEmpty([shown?.template, shown?.prompt, shown?.content, hit.description])
|
|
164
|
+
?.replace(/\s+/g, " ")
|
|
165
|
+
.trim();
|
|
166
|
+
return payload ? truncateDescription(payload, 160) : undefined;
|
|
167
|
+
}
|
|
168
|
+
function buildCuratedReason(query, type) {
|
|
169
|
+
switch (type) {
|
|
170
|
+
case "script":
|
|
171
|
+
return `Best runnable script match for "${query}".`;
|
|
172
|
+
case "command":
|
|
173
|
+
return `Best reusable command/template match for "${query}".`;
|
|
174
|
+
case "knowledge":
|
|
175
|
+
return `Best reference document match for "${query}".`;
|
|
176
|
+
case "skill":
|
|
177
|
+
return `Best instructions/workflow match for "${query}".`;
|
|
178
|
+
case "agent":
|
|
179
|
+
return `Best specialized agent prompt match for "${query}".`;
|
|
180
|
+
case "memory":
|
|
181
|
+
return `Best saved context match for "${query}".`;
|
|
182
|
+
default:
|
|
183
|
+
return `Best ${type} match for "${query}".`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function buildCurateSummary(query, items) {
|
|
187
|
+
if (items.length === 0) {
|
|
188
|
+
return `No curated assets were selected for "${query}".`;
|
|
189
|
+
}
|
|
190
|
+
const labels = items.map((item) => `${item.type}:${item.name}`);
|
|
191
|
+
return `Selected ${items.length} high-signal result${items.length === 1 ? "" : "s"}: ${labels.join(", ")}.`;
|
|
192
|
+
}
|
|
193
|
+
function hasSearchResults(result) {
|
|
194
|
+
return result.hits.length > 0 || (result.registryHits?.length ?? 0) > 0;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Extract a small set of fallback keywords when a prompt-style curate query
|
|
198
|
+
* returns no hits as a whole phrase.
|
|
199
|
+
*
|
|
200
|
+
* We keep up to MAX_CURATE_FALLBACK_KEYWORDS distinct keywords and drop short
|
|
201
|
+
* or common filler words so follow-up searches stay inexpensive while focusing
|
|
202
|
+
* on higher-signal terms.
|
|
203
|
+
*/
|
|
204
|
+
export function deriveCurateFallbackQueries(query) {
|
|
205
|
+
return Array.from(new Set(query
|
|
206
|
+
.toLowerCase()
|
|
207
|
+
.split(/[^a-z0-9]+/)
|
|
208
|
+
.map((token) => token.trim())
|
|
209
|
+
// Keep longer tokens so fallback stays focused on higher-signal terms
|
|
210
|
+
// and avoids broad one- and two-letter matches that overwhelm curation.
|
|
211
|
+
.filter((token) => token.length >= MIN_CURATE_FALLBACK_TOKEN_LENGTH && !CURATE_FALLBACK_FILTER_WORDS.has(token)))).slice(0, MAX_CURATE_FALLBACK_KEYWORDS);
|
|
212
|
+
}
|
|
213
|
+
export function mergeCurateSearchResponses(base, extras) {
|
|
214
|
+
const hitsByRef = new Map();
|
|
215
|
+
for (const hit of base.hits.filter((entry) => entry.type !== "registry")) {
|
|
216
|
+
hitsByRef.set(hit.ref, hit);
|
|
217
|
+
}
|
|
218
|
+
for (const result of extras) {
|
|
219
|
+
for (const hit of result.hits.filter((entry) => entry.type !== "registry")) {
|
|
220
|
+
const existing = hitsByRef.get(hit.ref);
|
|
221
|
+
if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
|
|
222
|
+
hitsByRef.set(hit.ref, hit);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const registryById = new Map();
|
|
227
|
+
for (const hit of base.registryHits ?? []) {
|
|
228
|
+
registryById.set(hit.id, hit);
|
|
229
|
+
}
|
|
230
|
+
for (const result of extras) {
|
|
231
|
+
for (const hit of result.registryHits ?? []) {
|
|
232
|
+
const existing = registryById.get(hit.id);
|
|
233
|
+
if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
|
|
234
|
+
registryById.set(hit.id, hit);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const warnings = Array.from(new Set([...(base.warnings ?? []), ...extras.flatMap((result) => result.warnings ?? [])]));
|
|
239
|
+
const mergedHits = [...hitsByRef.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
240
|
+
const mergedRegistryHits = [...registryById.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
241
|
+
return {
|
|
242
|
+
...base,
|
|
243
|
+
hits: mergedHits,
|
|
244
|
+
...(mergedRegistryHits.length > 0 ? { registryHits: mergedRegistryHits } : {}),
|
|
245
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
246
|
+
...(mergedHits.length > 0 || mergedRegistryHits.length > 0 ? { tip: undefined } : {}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
export async function searchForCuration(input) {
|
|
250
|
+
const initial = await akmSearch(input);
|
|
251
|
+
if (hasSearchResults(initial))
|
|
252
|
+
return initial;
|
|
253
|
+
const fallbackQueries = deriveCurateFallbackQueries(input.query);
|
|
254
|
+
if (fallbackQueries.length <= 1)
|
|
255
|
+
return initial;
|
|
256
|
+
const fallbackResults = await Promise.all(fallbackQueries.map((token) => akmSearch({
|
|
257
|
+
query: token,
|
|
258
|
+
type: input.type,
|
|
259
|
+
limit: input.limit,
|
|
260
|
+
source: input.source,
|
|
261
|
+
})));
|
|
262
|
+
return mergeCurateSearchResponses(initial, fallbackResults);
|
|
263
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { getAssetTypes } from "
|
|
3
|
-
import { loadConfig } from "
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { getEffectiveSemanticStatus, readSemanticStatus } from "
|
|
7
|
-
import { pkgVersion } from "
|
|
2
|
+
import { getAssetTypes } from "../core/asset-spec";
|
|
3
|
+
import { loadConfig } from "../core/config";
|
|
4
|
+
import { getDbPath } from "../core/paths";
|
|
5
|
+
import { closeDatabase, getEntryCount, getMeta, isVecAvailable, openDatabase } from "../indexer/db";
|
|
6
|
+
import { getEffectiveSemanticStatus, readSemanticStatus } from "../indexer/semantic-status";
|
|
7
|
+
import { pkgVersion } from "../version";
|
|
8
8
|
/**
|
|
9
9
|
* Assemble system info describing the current capabilities, configuration,
|
|
10
10
|
* and index state. Used by `akm info`.
|
|
@@ -13,8 +13,8 @@ import { pkgVersion } from "./version";
|
|
|
13
13
|
*/
|
|
14
14
|
export function assembleInfo(options) {
|
|
15
15
|
const config = loadConfig();
|
|
16
|
-
// Asset types
|
|
17
|
-
const assetTypes = getAssetTypes();
|
|
16
|
+
// Asset types (copy into a mutable array — `getAssetTypes()` returns readonly)
|
|
17
|
+
const assetTypes = [...getAssetTypes()];
|
|
18
18
|
const semanticRuntime = readSemanticStatus();
|
|
19
19
|
const semanticStatus = getEffectiveSemanticStatus(config, semanticRuntime);
|
|
20
20
|
// Search modes
|
|
@@ -29,8 +29,14 @@ export function assembleInfo(options) {
|
|
|
29
29
|
...(r.provider ? { provider: r.provider } : {}),
|
|
30
30
|
...(r.enabled !== undefined ? { enabled: r.enabled } : {}),
|
|
31
31
|
}));
|
|
32
|
-
// Stash providers
|
|
33
|
-
|
|
32
|
+
// Stash providers — prefer `sources[]`; fall back to `stashDir` when the
|
|
33
|
+
// user has not yet migrated to the sources[] config shape so that info
|
|
34
|
+
// always reflects at least one provider when a stash is configured.
|
|
35
|
+
const configuredSources = config.sources ?? config.stashes ?? [];
|
|
36
|
+
const stashesList = configuredSources.length === 0 && config.stashDir
|
|
37
|
+
? [{ type: "filesystem", path: config.stashDir, name: "primary" }]
|
|
38
|
+
: configuredSources;
|
|
39
|
+
const sourceProviders = stashesList.map((s) => ({
|
|
34
40
|
type: s.type,
|
|
35
41
|
...(s.name ? { name: s.name } : {}),
|
|
36
42
|
...(s.path ? { path: s.path } : {}),
|
|
@@ -51,7 +57,7 @@ export function assembleInfo(options) {
|
|
|
51
57
|
...(semanticRuntime?.message ? { message: semanticRuntime.message } : {}),
|
|
52
58
|
},
|
|
53
59
|
registries,
|
|
54
|
-
|
|
60
|
+
sourceProviders,
|
|
55
61
|
indexStats,
|
|
56
62
|
};
|
|
57
63
|
}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
import { TYPE_DIRS } from "
|
|
11
|
-
import { getConfigPath, loadUserConfig, saveConfig } from "
|
|
12
|
-
import { getBinDir, getDefaultStashDir } from "
|
|
13
|
-
import { ensureRg } from "
|
|
10
|
+
import { TYPE_DIRS } from "../core/asset-spec";
|
|
11
|
+
import { getConfigPath, loadUserConfig, saveConfig } from "../core/config";
|
|
12
|
+
import { getBinDir, getDefaultStashDir } from "../core/paths";
|
|
13
|
+
import { ensureRg } from "../setup/ripgrep-install";
|
|
14
14
|
export async function akmInit(options) {
|
|
15
15
|
const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
|
|
16
16
|
let created = false;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { filterNonEmptyStrings } from "
|
|
3
|
+
import { filterNonEmptyStrings, toPosix } from "../core/common";
|
|
4
4
|
const DEFAULT_INSTALL_AUDIT_CONFIG = {
|
|
5
5
|
enabled: true,
|
|
6
6
|
blockOnCritical: true,
|
|
@@ -346,16 +346,28 @@ function splitAllowedFindings(findings, ref, allowedFindings) {
|
|
|
346
346
|
return { findings: active, waivedFindings: waived };
|
|
347
347
|
}
|
|
348
348
|
function matchesAllowedFinding(finding, ref, allowedFindings) {
|
|
349
|
+
// Normalize paths so a waiver written against `scripts/setup.sh` matches
|
|
350
|
+
// a finding emitted as `./scripts/setup.sh` or `scripts//setup.sh`. On
|
|
351
|
+
// Windows we also fold case, mirroring `isWithin`'s comparison rules.
|
|
352
|
+
const findingPathNormalized = normalizeWaiverPath(finding.file);
|
|
349
353
|
return allowedFindings.some((allowed) => {
|
|
350
354
|
if (allowed.id !== finding.id)
|
|
351
355
|
return false;
|
|
352
356
|
if (allowed.ref && allowed.ref !== ref)
|
|
353
357
|
return false;
|
|
354
|
-
if (allowed.path && allowed.path !==
|
|
358
|
+
if (allowed.path && normalizeWaiverPath(allowed.path) !== findingPathNormalized)
|
|
355
359
|
return false;
|
|
356
360
|
return true;
|
|
357
361
|
});
|
|
358
362
|
}
|
|
363
|
+
function normalizeWaiverPath(value) {
|
|
364
|
+
if (!value)
|
|
365
|
+
return value;
|
|
366
|
+
// Strip a leading `./` and POSIX-ify after path.normalize so Windows path
|
|
367
|
+
// separators don't trigger spurious mismatches.
|
|
368
|
+
const normalized = toPosix(path.normalize(value)).replace(/^\.\/+/, "");
|
|
369
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
370
|
+
}
|
|
359
371
|
function addUrlLabels(labels, rawUrl) {
|
|
360
372
|
if (!rawUrl)
|
|
361
373
|
return;
|