akm-cli 0.7.5 → 0.8.0-rc.3
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 +1 -1
- package/dist/cli/parse-args.js +86 -0
- package/dist/cli.js +1023 -521
- package/dist/commands/agent-dispatch.js +107 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +812 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +218 -43
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +2 -23
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -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 +3 -30
- package/dist/commands/improve.js +1161 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +291 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +145 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/vault-key-rules.js +67 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +71 -28
- package/dist/commands/reflect.js +135 -35
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +54 -0
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +125 -20
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +168 -77
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/asset-registry.js +4 -16
- package/dist/core/asset-spec.js +10 -0
- package/dist/core/common.js +100 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +233 -133
- package/dist/core/events.js +73 -126
- package/dist/core/frontmatter.js +0 -6
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +731 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +52 -238
- package/dist/indexer/db.js +403 -54
- package/dist/indexer/ensure-index.js +61 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +409 -76
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +456 -290
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/matchers.js +124 -160
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +196 -197
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/builders.js +109 -0
- package/dist/integrations/agent/config.js +203 -3
- package/dist/integrations/agent/index.js +5 -2
- package/dist/integrations/agent/model-aliases.js +63 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +77 -72
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +93 -22
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +61 -122
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -62
- package/dist/llm/memory-infer.js +49 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -318
- package/dist/output/renderers.js +220 -256
- package/dist/output/shapes.js +101 -93
- package/dist/output/text.js +256 -17
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/registry/resolve.js +8 -16
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/filesystem.js +16 -23
- package/dist/sources/providers/git.js +4 -5
- package/dist/sources/providers/website.js +15 -22
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -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 +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +59 -91
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +5 -2
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +3 -2
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -11,6 +11,7 @@ import { loadConfig } from "../core/config";
|
|
|
11
11
|
import { UsageError } from "../core/errors";
|
|
12
12
|
import { warn } from "../core/warn";
|
|
13
13
|
import { SCOPE_KEYS } from "../indexer/metadata";
|
|
14
|
+
import { parseFlagValue } from "../output/context";
|
|
14
15
|
/**
|
|
15
16
|
* Parse a shorthand duration string to a number of milliseconds.
|
|
16
17
|
* Supports: `30d` (days), `12h` (hours), `6m` (months, approximated as 30d).
|
|
@@ -188,3 +189,56 @@ Return ONLY the JSON object, no prose, no markdown fences.`;
|
|
|
188
189
|
return { tags: [] };
|
|
189
190
|
}
|
|
190
191
|
}
|
|
192
|
+
// ── Content-arg disambiguation ───────────────────────────────────────────────
|
|
193
|
+
/**
|
|
194
|
+
* Guard against citty consuming a global flag value as the `content` positional.
|
|
195
|
+
*
|
|
196
|
+
* When the user runs `akm remember --format json` without a content argument,
|
|
197
|
+
* citty may assign `"json"` to the `content` positional because of how it
|
|
198
|
+
* handles flag order. This helper detects that case and returns `undefined`
|
|
199
|
+
* so `readMemoryContent` falls through to stdin.
|
|
200
|
+
*/
|
|
201
|
+
export function resolveRememberContentArg(content) {
|
|
202
|
+
if (content === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
const parsedFormat = parseFlagValue(process.argv, "--format");
|
|
205
|
+
if (parsedFormat !== undefined &&
|
|
206
|
+
content === parsedFormat &&
|
|
207
|
+
wasRememberFlagValueConsumedAsContent(content, parsedFormat, "--format")) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const parsedDetail = parseFlagValue(process.argv, "--detail");
|
|
211
|
+
if (parsedDetail !== undefined &&
|
|
212
|
+
content === parsedDetail &&
|
|
213
|
+
wasRememberFlagValueConsumedAsContent(content, parsedDetail, "--detail")) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
return content;
|
|
217
|
+
}
|
|
218
|
+
function wasRememberFlagValueConsumedAsContent(content, flagValue, flagName) {
|
|
219
|
+
const argv = process.argv.slice(2);
|
|
220
|
+
const rememberIndex = argv.indexOf("remember");
|
|
221
|
+
const tokens = rememberIndex >= 0 ? argv.slice(rememberIndex + 1) : argv;
|
|
222
|
+
let flagIndex = -1;
|
|
223
|
+
let flagConsumesNextToken = false;
|
|
224
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
225
|
+
const token = tokens[i];
|
|
226
|
+
if (token === flagName) {
|
|
227
|
+
flagIndex = i;
|
|
228
|
+
flagConsumesNextToken = true;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (token === `${flagName}=${flagValue}`) {
|
|
232
|
+
flagIndex = i;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (flagIndex === -1)
|
|
237
|
+
return false;
|
|
238
|
+
if (tokens.slice(0, flagIndex).includes(content))
|
|
239
|
+
return false;
|
|
240
|
+
const firstTokenAfterFlag = flagIndex + (flagConsumesNextToken ? 2 : 1);
|
|
241
|
+
if (tokens.slice(firstTokenAfterFlag).includes(content))
|
|
242
|
+
return false;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-repair pass for `akm improve`.
|
|
3
|
+
*
|
|
4
|
+
* Attempts to patch missing frontmatter fields (`description`, `when_to_use`)
|
|
5
|
+
* on assets that failed schema validation, using a single bounded in-tree LLM
|
|
6
|
+
* call per asset. Results are recorded as `schema_repair_invoked` events.
|
|
7
|
+
*
|
|
8
|
+
* This module is extracted from `improve.ts` to make the repair logic
|
|
9
|
+
* independently testable and to use the `tryLlmFeature` seam rather than raw
|
|
10
|
+
* `chatCompletion`.
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import { stringify as yamlStringify } from "yaml";
|
|
14
|
+
import { parseAssetRef } from "../core/asset-ref";
|
|
15
|
+
import { appendEvent, readEvents } from "../core/events";
|
|
16
|
+
import { parseFrontmatter } from "../core/frontmatter";
|
|
17
|
+
import { info } from "../core/warn";
|
|
18
|
+
import { resolveAssetPath } from "../indexer/path-resolver";
|
|
19
|
+
import { chatCompletion, parseEmbeddedJsonResponse } from "../llm/client";
|
|
20
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
21
|
+
/** Minimum gap between schema-repair attempts on the same asset. */
|
|
22
|
+
const SCHEMA_REPAIR_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
23
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Run the schema-repair loop for a batch of validation failures.
|
|
26
|
+
* Returns a list of per-asset outcome records and the set of refs that were
|
|
27
|
+
* successfully repaired (so the caller can exclude them from skip logic).
|
|
28
|
+
*/
|
|
29
|
+
export async function runSchemaRepairPass(failures, options) {
|
|
30
|
+
const repairs = [];
|
|
31
|
+
const repairedRefs = new Set();
|
|
32
|
+
const { startMs, budgetMs, llmConfig, stashDir, findFilePath = defaultFindFilePath, isLessonCandidateFn = defaultIsLessonCandidate, } = options;
|
|
33
|
+
for (const failure of failures) {
|
|
34
|
+
if (Date.now() - startMs >= budgetMs)
|
|
35
|
+
break;
|
|
36
|
+
// Cooldown: skip repair if we ran it successfully recently.
|
|
37
|
+
const recentRepairs = readEvents({ type: "schema_repair_invoked", ref: failure.ref });
|
|
38
|
+
const lastRepair = recentRepairs.events
|
|
39
|
+
.filter((e) => e.metadata?.outcome === "written")
|
|
40
|
+
.sort((a, b) => new Date(b.ts ?? 0).getTime() - new Date(a.ts ?? 0).getTime())[0];
|
|
41
|
+
if (lastRepair?.ts && Date.now() - new Date(lastRepair.ts).getTime() < SCHEMA_REPAIR_COOLDOWN_MS) {
|
|
42
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const filePath = await findFilePath(failure.ref, stashDir);
|
|
46
|
+
if (!filePath) {
|
|
47
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
52
|
+
const fm = parseFrontmatter(raw);
|
|
53
|
+
const missingFields = [];
|
|
54
|
+
if (!fm.data.description)
|
|
55
|
+
missingFields.push("description");
|
|
56
|
+
if (isLessonCandidateFn(failure.ref) && !fm.data.when_to_use)
|
|
57
|
+
missingFields.push("when_to_use");
|
|
58
|
+
if (missingFields.length === 0) {
|
|
59
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const fieldList = missingFields.join(" and ");
|
|
63
|
+
info(`[improve] schema-repair ${failure.ref} (${fieldList})`);
|
|
64
|
+
const bodyPreview = (fm.content ?? raw).slice(0, 2000);
|
|
65
|
+
const llmResponse = await chatCompletion(llmConfig, [
|
|
66
|
+
{
|
|
67
|
+
role: "system",
|
|
68
|
+
content: `You generate concise asset frontmatter fields. Respond with a JSON object containing only the missing fields. No prose, no markdown fences.`,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
role: "user",
|
|
72
|
+
content: `Generate the missing frontmatter fields (${fieldList}) for this ${parseAssetRef(failure.ref).type} asset. Return ONLY valid JSON like {"description": "...", "when_to_use": "..."}\n\n${bodyPreview}`,
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
const parsed = parseEmbeddedJsonResponse(llmResponse.trim());
|
|
76
|
+
if (!parsed) {
|
|
77
|
+
repairs.push({
|
|
78
|
+
ref: failure.ref,
|
|
79
|
+
reason: failure.reason,
|
|
80
|
+
outcome: "error",
|
|
81
|
+
error: "LLM returned unparseable JSON for schema repair",
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const newFm = { ...fm.data };
|
|
86
|
+
if (parsed.description)
|
|
87
|
+
newFm.description = parsed.description;
|
|
88
|
+
if (parsed.when_to_use)
|
|
89
|
+
newFm.when_to_use = parsed.when_to_use;
|
|
90
|
+
const fmStr = yamlStringify(newFm).trimEnd();
|
|
91
|
+
const newContent = `---\n${fmStr}\n---\n${fm.content}`;
|
|
92
|
+
fs.writeFileSync(filePath, newContent, "utf8");
|
|
93
|
+
info(`[improve] schema-repair written: ${failure.ref}`);
|
|
94
|
+
appendEvent({
|
|
95
|
+
eventType: "schema_repair_invoked",
|
|
96
|
+
ref: failure.ref,
|
|
97
|
+
metadata: { outcome: "written", reason: failure.reason },
|
|
98
|
+
});
|
|
99
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "written" });
|
|
100
|
+
repairedRefs.add(failure.ref);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
appendEvent({
|
|
104
|
+
eventType: "schema_repair_invoked",
|
|
105
|
+
ref: failure.ref,
|
|
106
|
+
metadata: { outcome: "error", reason: failure.reason, error: String(e) },
|
|
107
|
+
});
|
|
108
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "error", error: String(e) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { repairs, repairedRefs };
|
|
112
|
+
}
|
|
113
|
+
// ── Default seam implementations ─────────────────────────────────────────────
|
|
114
|
+
function defaultIsLessonCandidate(ref) {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = parseAssetRef(ref);
|
|
117
|
+
return parsed.type === "lesson";
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function defaultFindFilePath(ref, stashDir) {
|
|
124
|
+
return resolveAssetPath(ref, {
|
|
125
|
+
stashDir,
|
|
126
|
+
mode: "index-first",
|
|
127
|
+
directoryIndexNames: ["SKILL.md", "index.md", "README.md"],
|
|
128
|
+
preserveDirectNameFallback: true,
|
|
129
|
+
});
|
|
130
|
+
}
|
package/dist/commands/search.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { loadConfig } from "../core/config";
|
|
12
12
|
import { UsageError } from "../core/errors";
|
|
13
13
|
import { appendEvent } from "../core/events";
|
|
14
|
-
import { closeDatabase, openExistingDatabase } from "../indexer/db";
|
|
14
|
+
import { bumpUtilityScoresBatch, closeDatabase, openExistingDatabase } from "../indexer/db";
|
|
15
15
|
import { searchLocal } from "../indexer/db-search";
|
|
16
16
|
import { resolveSourceEntries } from "../indexer/search-source";
|
|
17
17
|
// Eagerly import source providers to trigger self-registration before the
|
|
@@ -48,6 +48,7 @@ export async function akmSearch(input) {
|
|
|
48
48
|
const stashDir = sources[0].path;
|
|
49
49
|
const filters = normalizeScopeFilters(input.filters);
|
|
50
50
|
const includeProposed = input.includeProposed === true;
|
|
51
|
+
const belief = input.belief ?? "all";
|
|
51
52
|
const localResult = source === "registry"
|
|
52
53
|
? undefined
|
|
53
54
|
: await searchLocal({
|
|
@@ -59,6 +60,7 @@ export async function akmSearch(input) {
|
|
|
59
60
|
config,
|
|
60
61
|
filters,
|
|
61
62
|
includeProposed,
|
|
63
|
+
beliefFilter: belief,
|
|
62
64
|
});
|
|
63
65
|
const registryResult = source === "stash" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
|
|
64
66
|
if (source === "stash") {
|
|
@@ -73,7 +75,7 @@ export async function akmSearch(input) {
|
|
|
73
75
|
warnings: localResult?.warnings?.length ? localResult.warnings : undefined,
|
|
74
76
|
timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
|
|
75
77
|
};
|
|
76
|
-
logSearchEvent(query, response);
|
|
78
|
+
logSearchEvent(query, response, undefined, localResult?.mode ?? "keyword");
|
|
77
79
|
return response;
|
|
78
80
|
}
|
|
79
81
|
const registryHits = (registryResult?.hits ?? []).map((hit) => {
|
|
@@ -124,7 +126,7 @@ export async function akmSearch(input) {
|
|
|
124
126
|
warnings: warnings.length ? warnings : undefined,
|
|
125
127
|
timing: { totalMs: Date.now() - t0 },
|
|
126
128
|
};
|
|
127
|
-
logSearchEvent(query, response);
|
|
129
|
+
logSearchEvent(query, response, undefined, localResult?.mode ?? "keyword");
|
|
128
130
|
return response;
|
|
129
131
|
}
|
|
130
132
|
/**
|
|
@@ -160,13 +162,13 @@ function resolveEntryIds(db, hits) {
|
|
|
160
162
|
* Per-entry events are recorded only for stash hits because registry hits
|
|
161
163
|
* have no local entry_id to reference.
|
|
162
164
|
*/
|
|
163
|
-
function logSearchEvent(query, response, existingDb) {
|
|
165
|
+
function logSearchEvent(query, response, existingDb, mode = "keyword") {
|
|
164
166
|
// Emit a structured event to events.jsonl so workflow-trace consumers
|
|
165
167
|
// detect akm search invocations without relying on stdout scraping.
|
|
166
168
|
const stashHits = response.hits.filter((h) => h.type !== "registry");
|
|
167
169
|
appendEvent({
|
|
168
170
|
eventType: "search",
|
|
169
|
-
metadata: { query, hitCount: stashHits.length, resultRefs: stashHits.map((h) => h.ref) },
|
|
171
|
+
metadata: { query, hitCount: stashHits.length, resultRefs: stashHits.map((h) => h.ref), mode },
|
|
170
172
|
});
|
|
171
173
|
try {
|
|
172
174
|
const db = existingDb ?? openExistingDatabase();
|
|
@@ -180,6 +182,12 @@ function logSearchEvent(query, response, existingDb) {
|
|
|
180
182
|
entry_ref: ref,
|
|
181
183
|
});
|
|
182
184
|
}
|
|
185
|
+
// Bump utility scores for all resolved entries (MemRL retrieval signal).
|
|
186
|
+
// The indexer overwrites these at next reindex; bumps are temporary hints.
|
|
187
|
+
const resolvedIds = resolved.map((r) => r.entryId).filter((id) => id !== undefined);
|
|
188
|
+
if (resolvedIds.length > 0) {
|
|
189
|
+
bumpUtilityScoresBatch(db, resolvedIds, 1.0);
|
|
190
|
+
}
|
|
183
191
|
// Count registry hits separately so registry-only searches record a
|
|
184
192
|
// non-zero resultCount. response.hits is always [] when source="registry".
|
|
185
193
|
const stashHitCount = response.hits.length;
|
|
@@ -192,6 +200,7 @@ function logSearchEvent(query, response, existingDb) {
|
|
|
192
200
|
stashHitCount,
|
|
193
201
|
registryHitCount,
|
|
194
202
|
resolvedCount: resolved.length,
|
|
203
|
+
mode,
|
|
195
204
|
}),
|
|
196
205
|
});
|
|
197
206
|
}
|
|
@@ -221,6 +230,13 @@ export function parseSearchSource(source) {
|
|
|
221
230
|
return "stash";
|
|
222
231
|
throw new UsageError(`Invalid value for --source: ${String(source)}. Expected one of: stash|registry|both`, "INVALID_SOURCE_VALUE");
|
|
223
232
|
}
|
|
233
|
+
export function parseBeliefFilterMode(value) {
|
|
234
|
+
if (value === undefined || value === "all")
|
|
235
|
+
return "all";
|
|
236
|
+
if (value === "current" || value === "historical")
|
|
237
|
+
return value;
|
|
238
|
+
throw new UsageError(`Invalid value for --belief: ${String(value)}. Expected one of: all|current|historical`, "INVALID_FLAG_VALUE");
|
|
239
|
+
}
|
|
224
240
|
/**
|
|
225
241
|
* Strip empty / non-string values from a scope filter object. Returns
|
|
226
242
|
* `undefined` when nothing meaningful remains, so callers don't pay for an
|
package/dist/commands/show.js
CHANGED
|
@@ -16,14 +16,17 @@
|
|
|
16
16
|
import fs from "node:fs";
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import { parseAssetRef } from "../core/asset-ref";
|
|
19
|
+
import { asNonEmptyString } from "../core/common";
|
|
19
20
|
import { loadConfig } from "../core/config";
|
|
20
21
|
import { NotFoundError, UsageError } from "../core/errors";
|
|
21
22
|
import { appendEvent, readEvents } from "../core/events";
|
|
22
|
-
import { parseFrontmatter
|
|
23
|
+
import { parseFrontmatter } from "../core/frontmatter";
|
|
23
24
|
import { closeDatabase, findEntryIdByRef, openExistingDatabase } from "../indexer/db";
|
|
24
25
|
import { ensureIndex } from "../indexer/ensure-index";
|
|
25
26
|
import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../indexer/file-context";
|
|
27
|
+
import { listRelatedPathsForFile } from "../indexer/graph-boost";
|
|
26
28
|
import { lookup } from "../indexer/indexer";
|
|
29
|
+
import { resolveAssetPath } from "../indexer/path-resolver";
|
|
27
30
|
import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries } from "../indexer/search-source";
|
|
28
31
|
import { insertUsageEvent } from "../indexer/usage-events";
|
|
29
32
|
import { resolveSourcesForOrigin } from "../registry/origin-resolve";
|
|
@@ -177,7 +180,7 @@ function enforceScopeOrThrow(filePath, ref, scope) {
|
|
|
177
180
|
for (const [key, expectedValue] of expected) {
|
|
178
181
|
if (expectedValue === undefined)
|
|
179
182
|
continue;
|
|
180
|
-
const actual =
|
|
183
|
+
const actual = asNonEmptyString(fm[`scope_${key}`]);
|
|
181
184
|
if (actual !== expectedValue) {
|
|
182
185
|
throw new NotFoundError(`Asset "${ref}" exists but is out of scope (expected scope_${key}="${expectedValue}").`);
|
|
183
186
|
}
|
|
@@ -201,6 +204,31 @@ function logShowEvent(ref, existingDb) {
|
|
|
201
204
|
// detect akm show invocations without relying on stdout scraping.
|
|
202
205
|
const parsed = parseAssetRef(ref);
|
|
203
206
|
appendEvent({ eventType: "show", ref, metadata: { type: parsed.type, name: parsed.name } });
|
|
207
|
+
// Detect if this show is a selection from a recent search result.
|
|
208
|
+
try {
|
|
209
|
+
const { events: recentSearches } = readEvents({ type: "search" });
|
|
210
|
+
const cutoffMs = Date.now() - 60_000;
|
|
211
|
+
const matchingSearch = [...recentSearches].reverse().find((e) => {
|
|
212
|
+
if (!e.ts || new Date(e.ts).getTime() < cutoffMs)
|
|
213
|
+
return false;
|
|
214
|
+
const refs = e.metadata?.resultRefs ?? [];
|
|
215
|
+
return refs.includes(ref);
|
|
216
|
+
});
|
|
217
|
+
if (matchingSearch) {
|
|
218
|
+
appendEvent({
|
|
219
|
+
eventType: "select",
|
|
220
|
+
ref,
|
|
221
|
+
metadata: {
|
|
222
|
+
query: matchingSearch.metadata?.query,
|
|
223
|
+
searchTs: matchingSearch.ts,
|
|
224
|
+
rankPosition: (matchingSearch.metadata?.resultRefs ?? []).indexOf(ref),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
/* fire-and-forget — select is best-effort */
|
|
231
|
+
}
|
|
204
232
|
try {
|
|
205
233
|
const db = existingDb ?? openExistingDatabase();
|
|
206
234
|
try {
|
|
@@ -219,18 +247,6 @@ function logShowEvent(ref, existingDb) {
|
|
|
219
247
|
/* fire-and-forget */
|
|
220
248
|
}
|
|
221
249
|
}
|
|
222
|
-
/**
|
|
223
|
-
* Resolve an asset path via the FTS5 index only. Spec §6.2's primary path.
|
|
224
|
-
*
|
|
225
|
-
* Returns `undefined` if the index has no matching row.
|
|
226
|
-
*/
|
|
227
|
-
async function resolvePathViaIndex(parsed) {
|
|
228
|
-
const entry = await lookup(parsed);
|
|
229
|
-
if (entry) {
|
|
230
|
-
return { assetPath: entry.filePath };
|
|
231
|
-
}
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
250
|
/** @internal Use akmShowUnified() for all external callers. */
|
|
235
251
|
export async function showLocal(input) {
|
|
236
252
|
const parsed = parseAssetRef(input.ref);
|
|
@@ -251,10 +267,11 @@ export async function showLocal(input) {
|
|
|
251
267
|
}
|
|
252
268
|
}
|
|
253
269
|
if (!assetPath) {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
270
|
+
const resolvedAssetPath = await resolveAssetPath(parsed, {
|
|
271
|
+
stashDir: input.stashDir,
|
|
272
|
+
mode: "index-first",
|
|
273
|
+
});
|
|
274
|
+
assetPath = resolvedAssetPath ?? undefined;
|
|
258
275
|
}
|
|
259
276
|
if (!assetPath && parsed.origin && searchSources.length === 0) {
|
|
260
277
|
const installCmd = `akm add ${parsed.origin}`;
|
|
@@ -293,8 +310,23 @@ export async function showLocal(input) {
|
|
|
293
310
|
origin: source?.registryId ?? null,
|
|
294
311
|
editable,
|
|
295
312
|
...(!editable ? { editHint: buildEditHint(assetPath, parsed.type, parsed.name, source?.registryId) } : {}),
|
|
313
|
+
related: (() => {
|
|
314
|
+
let db;
|
|
315
|
+
try {
|
|
316
|
+
db = openExistingDatabase();
|
|
317
|
+
const related = listRelatedPathsForFile(sourceStashDir, assetPath, 5, db);
|
|
318
|
+
return { total: related.length, hits: related };
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return { total: 0, hits: [] };
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
if (db)
|
|
325
|
+
closeDatabase(db);
|
|
326
|
+
}
|
|
327
|
+
})(),
|
|
296
328
|
};
|
|
297
|
-
const activeRun = getActiveWorkflowRun(getCurrentWorkflowScopeKey());
|
|
329
|
+
const activeRun = await getActiveWorkflowRun(getCurrentWorkflowScopeKey());
|
|
298
330
|
if (activeRun) {
|
|
299
331
|
fullResponse.activeRun = activeRun;
|
|
300
332
|
}
|
|
@@ -354,7 +386,7 @@ function buildSummaryResponse(full, assetPath) {
|
|
|
354
386
|
const textContent = full.content ?? full.template ?? full.prompt;
|
|
355
387
|
if (textContent && !description) {
|
|
356
388
|
const parsed = parseFrontmatter(textContent);
|
|
357
|
-
description =
|
|
389
|
+
description = asNonEmptyString(parsed.data.description);
|
|
358
390
|
}
|
|
359
391
|
}
|
|
360
392
|
const summary = {
|
|
@@ -371,3 +403,76 @@ function buildSummaryResponse(full, assetPath) {
|
|
|
371
403
|
};
|
|
372
404
|
return summary;
|
|
373
405
|
}
|
|
406
|
+
// ── argv normalisation ───────────────────────────────────────────────────────
|
|
407
|
+
const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
|
|
408
|
+
/**
|
|
409
|
+
* Normalize argv so positional view-mode arguments after the asset ref
|
|
410
|
+
* are rewritten into internal flags that citty can parse.
|
|
411
|
+
*
|
|
412
|
+
* Converts:
|
|
413
|
+
* akm show knowledge:guide.md toc → akm show knowledge:guide.md --akmView toc
|
|
414
|
+
* akm show knowledge:guide.md section Auth → akm show knowledge:guide.md --akmView section --akmHeading Auth
|
|
415
|
+
* akm show knowledge:guide.md lines 1 50 → akm show knowledge:guide.md --akmView lines --akmStart 1 --akmEnd 50
|
|
416
|
+
*
|
|
417
|
+
* Legacy `--view` is intentionally unsupported.
|
|
418
|
+
* Returns a new array; the input is never modified.
|
|
419
|
+
*/
|
|
420
|
+
export function normalizeShowArgv(argv) {
|
|
421
|
+
// argv[0]=bun argv[1]=script argv[2]=subcommand argv[3]=ref argv[4..]=rest
|
|
422
|
+
if (argv[2] !== "show")
|
|
423
|
+
return argv;
|
|
424
|
+
if (argv[3] === "proposal")
|
|
425
|
+
return argv;
|
|
426
|
+
if (argv.includes("--view") || argv.includes("--heading") || argv.includes("--start") || argv.includes("--end")) {
|
|
427
|
+
throw new UsageError('Legacy show flags are no longer supported. Use positional syntax like `akm show knowledge:guide toc` or `akm show knowledge:guide section "Auth"`.');
|
|
428
|
+
}
|
|
429
|
+
// Separate global flags from positional/show-specific args
|
|
430
|
+
const prefix = argv.slice(0, 3); // [bun, script, show]
|
|
431
|
+
const rest = argv.slice(3);
|
|
432
|
+
const globalFlags = [];
|
|
433
|
+
const showArgs = [];
|
|
434
|
+
for (let i = 0; i < rest.length; i++) {
|
|
435
|
+
const arg = rest[i];
|
|
436
|
+
if (arg === "--quiet" || arg === "-q" || arg === "--for-agent" || arg === "--for-agent=true") {
|
|
437
|
+
globalFlags.push(arg);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (arg.startsWith("--format=") || arg.startsWith("--detail=")) {
|
|
441
|
+
globalFlags.push(arg);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (arg === "--format" || arg === "--detail") {
|
|
445
|
+
globalFlags.push(arg);
|
|
446
|
+
if (rest[i + 1] !== undefined) {
|
|
447
|
+
globalFlags.push(rest[i + 1]);
|
|
448
|
+
i++;
|
|
449
|
+
}
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
showArgs.push(arg);
|
|
453
|
+
}
|
|
454
|
+
// showArgs[0] = ref, showArgs[1] = potential view mode, showArgs[2..] = view params
|
|
455
|
+
const ref = showArgs[0];
|
|
456
|
+
const viewMode = showArgs[1];
|
|
457
|
+
if (!ref || !viewMode || !SHOW_VIEW_MODES.has(viewMode)) {
|
|
458
|
+
return argv;
|
|
459
|
+
}
|
|
460
|
+
const result = [...prefix, ref, "--akmView", viewMode];
|
|
461
|
+
if (viewMode === "section") {
|
|
462
|
+
// Next arg is the heading name; pass empty string when missing so the
|
|
463
|
+
// show handler can produce a clear "section not found" error.
|
|
464
|
+
const heading = showArgs[2] ?? "";
|
|
465
|
+
result.push("--akmHeading", heading);
|
|
466
|
+
}
|
|
467
|
+
else if (viewMode === "lines") {
|
|
468
|
+
// Next two args are start and end
|
|
469
|
+
const start = showArgs[2];
|
|
470
|
+
const end = showArgs[3];
|
|
471
|
+
if (start)
|
|
472
|
+
result.push("--akmStart", start);
|
|
473
|
+
if (end)
|
|
474
|
+
result.push("--akmEnd", end);
|
|
475
|
+
}
|
|
476
|
+
result.push(...globalFlags);
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isHttpUrl, resolveStashDir } from "../core/common";
|
|
4
|
-
import { loadConfig, loadUserConfig, saveConfig } from "../core/config";
|
|
4
|
+
import { getSources, loadConfig, loadUserConfig, saveConfig } from "../core/config";
|
|
5
5
|
import { ConfigError, UsageError } from "../core/errors";
|
|
6
6
|
import { warn } from "../core/warn";
|
|
7
7
|
import { akmIndex } from "../indexer/indexer";
|
|
@@ -74,7 +74,7 @@ async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName)
|
|
|
74
74
|
// Derive the canonical name: explicit --name wins, then wiki name, then readable path.
|
|
75
75
|
const derivedName = explicitName ?? wikiName ?? toReadableId(resolvedPath);
|
|
76
76
|
// Check for duplicates in sources[]
|
|
77
|
-
const sources = [...(config
|
|
77
|
+
const sources = [...getSources(config)];
|
|
78
78
|
const existing = sources.find((s) => s.type === "filesystem" && s.path && path.resolve(s.path) === resolvedPath);
|
|
79
79
|
let persistedEntry;
|
|
80
80
|
if (!existing) {
|
|
@@ -85,7 +85,7 @@ async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName)
|
|
|
85
85
|
...(wikiName ? { wikiName } : {}),
|
|
86
86
|
};
|
|
87
87
|
sources.push(persistedEntry);
|
|
88
|
-
saveConfig({ ...config, sources
|
|
88
|
+
saveConfig({ ...config, sources });
|
|
89
89
|
}
|
|
90
90
|
else {
|
|
91
91
|
let changed = false;
|
|
@@ -99,7 +99,7 @@ async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName)
|
|
|
99
99
|
changed = true;
|
|
100
100
|
}
|
|
101
101
|
if (changed)
|
|
102
|
-
saveConfig({ ...config, sources
|
|
102
|
+
saveConfig({ ...config, sources });
|
|
103
103
|
persistedEntry = existing;
|
|
104
104
|
}
|
|
105
105
|
const index = await akmIndex({ stashDir });
|
|
@@ -116,7 +116,7 @@ async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName)
|
|
|
116
116
|
...(persistedEntry.wikiName ? { wiki: persistedEntry.wikiName } : {}),
|
|
117
117
|
},
|
|
118
118
|
config: {
|
|
119
|
-
sourceCount: (updatedConfig
|
|
119
|
+
sourceCount: getSources(updatedConfig).length,
|
|
120
120
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
121
121
|
},
|
|
122
122
|
index: {
|
|
@@ -131,7 +131,7 @@ async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName)
|
|
|
131
131
|
async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
|
|
132
132
|
const normalizedUrl = validateWebsiteInputUrl(ref);
|
|
133
133
|
const config = loadUserConfig();
|
|
134
|
-
const sources = [...(config
|
|
134
|
+
const sources = [...getSources(config)];
|
|
135
135
|
let entry = sources.find((stash) => stash.type === "website" && stash.url === normalizedUrl);
|
|
136
136
|
if (!entry) {
|
|
137
137
|
entry = {
|
|
@@ -142,7 +142,7 @@ async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
|
|
|
142
142
|
...(wikiName ? { wikiName } : {}),
|
|
143
143
|
};
|
|
144
144
|
sources.push(entry);
|
|
145
|
-
saveConfig({ ...config, sources
|
|
145
|
+
saveConfig({ ...config, sources });
|
|
146
146
|
}
|
|
147
147
|
else {
|
|
148
148
|
let changed = false;
|
|
@@ -155,7 +155,7 @@ async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
|
|
|
155
155
|
changed = true;
|
|
156
156
|
}
|
|
157
157
|
if (changed)
|
|
158
|
-
saveConfig({ ...config, sources
|
|
158
|
+
saveConfig({ ...config, sources });
|
|
159
159
|
}
|
|
160
160
|
const cachePaths = await ensureWebsiteMirror(entry, { requireStashDir: true });
|
|
161
161
|
const index = await akmIndex({ stashDir });
|
|
@@ -172,7 +172,7 @@ async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
|
|
|
172
172
|
...(entry.wikiName ? { wiki: entry.wikiName } : {}),
|
|
173
173
|
},
|
|
174
174
|
config: {
|
|
175
|
-
sourceCount: (updatedConfig
|
|
175
|
+
sourceCount: getSources(updatedConfig).length,
|
|
176
176
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
177
177
|
},
|
|
178
178
|
index: {
|
|
@@ -268,7 +268,7 @@ async function addRegistryStash(ref, stashDir, trustThisInstall, writable, wikiN
|
|
|
268
268
|
audit,
|
|
269
269
|
},
|
|
270
270
|
config: {
|
|
271
|
-
sourceCount: (updatedConfig
|
|
271
|
+
sourceCount: getSources(updatedConfig).length,
|
|
272
272
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
273
273
|
},
|
|
274
274
|
index: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { isRemoteUrl } from "../core/common";
|
|
3
|
+
import { getSources, loadConfig, loadUserConfig, saveConfig } from "../core/config";
|
|
3
4
|
import { ConfigError, UsageError } from "../core/errors";
|
|
4
5
|
import { resolveSourceEntries } from "../indexer/search-source";
|
|
5
6
|
// ── Operations ──────────────────────────────────────────────────────────────
|
|
@@ -19,14 +20,9 @@ export function addStash(opts) {
|
|
|
19
20
|
throw new ConfigError("writable: true is only supported on filesystem and git sources", "INVALID_CONFIG_FILE");
|
|
20
21
|
}
|
|
21
22
|
const config = loadUserConfig();
|
|
22
|
-
const sources = [...(config
|
|
23
|
-
const isRemoteUrl = target.startsWith("http://") ||
|
|
24
|
-
target.startsWith("https://") ||
|
|
25
|
-
target.startsWith("git@") ||
|
|
26
|
-
target.startsWith("ssh://") ||
|
|
27
|
-
target.startsWith("git://");
|
|
23
|
+
const sources = [...getSources(config)];
|
|
28
24
|
let entry;
|
|
29
|
-
if (isRemoteUrl) {
|
|
25
|
+
if (isRemoteUrl(target)) {
|
|
30
26
|
if (!providerType) {
|
|
31
27
|
throw new UsageError("--provider is required for URL sources (e.g. --provider git --provider website)");
|
|
32
28
|
}
|
|
@@ -53,7 +49,7 @@ export function addStash(opts) {
|
|
|
53
49
|
entry.name = name;
|
|
54
50
|
}
|
|
55
51
|
sources.push(entry);
|
|
56
|
-
saveConfig({ ...config, sources
|
|
52
|
+
saveConfig({ ...config, sources });
|
|
57
53
|
return { sources, added: true, entry };
|
|
58
54
|
}
|
|
59
55
|
/**
|
|
@@ -62,16 +58,12 @@ export function addStash(opts) {
|
|
|
62
58
|
*/
|
|
63
59
|
export function removeStash(target) {
|
|
64
60
|
const config = loadUserConfig();
|
|
65
|
-
const sources = [...(config
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
target.startsWith("git@") ||
|
|
69
|
-
target.startsWith("ssh://") ||
|
|
70
|
-
target.startsWith("git://");
|
|
71
|
-
const resolvedPath = !isUrl ? path.resolve(target) : undefined;
|
|
61
|
+
const sources = [...getSources(config)];
|
|
62
|
+
const isUrlTarget = isRemoteUrl(target);
|
|
63
|
+
const resolvedPath = !isUrlTarget ? path.resolve(target) : undefined;
|
|
72
64
|
// Try URL match first, then path, then name (most specific → least specific)
|
|
73
65
|
let idx = -1;
|
|
74
|
-
if (
|
|
66
|
+
if (isUrlTarget) {
|
|
75
67
|
idx = sources.findIndex((s) => s.url === target);
|
|
76
68
|
}
|
|
77
69
|
if (idx === -1 && resolvedPath) {
|
|
@@ -84,7 +76,7 @@ export function removeStash(target) {
|
|
|
84
76
|
return { sources, removed: false, message: "No matching source found" };
|
|
85
77
|
}
|
|
86
78
|
const removed = sources.splice(idx, 1)[0];
|
|
87
|
-
saveConfig({ ...config, sources
|
|
79
|
+
saveConfig({ ...config, sources });
|
|
88
80
|
return { sources, removed: true, entry: removed };
|
|
89
81
|
}
|
|
90
82
|
/**
|
|
@@ -93,6 +85,6 @@ export function removeStash(target) {
|
|
|
93
85
|
export function listStashes() {
|
|
94
86
|
const config = loadConfig();
|
|
95
87
|
const localSources = resolveSourceEntries();
|
|
96
|
-
const sources = config
|
|
88
|
+
const sources = getSources(config);
|
|
97
89
|
return { localSources, sources };
|
|
98
90
|
}
|