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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic LLM-result cache wrapper shared across indexer passes.
|
|
3
|
+
*
|
|
4
|
+
* Each pass that calls an LLM and wants to skip re-processing unchanged
|
|
5
|
+
* content can delegate the cache check/write to `withLlmCache` instead of
|
|
6
|
+
* duplicating the hash-compute → lookup → write pattern inline.
|
|
7
|
+
*/
|
|
8
|
+
import { computeBodyHash, getLlmCacheEntry, upsertLlmCacheEntry } from "./db";
|
|
9
|
+
/**
|
|
10
|
+
* Generic LLM cache wrapper. Returns cached result if body unchanged,
|
|
11
|
+
* otherwise calls llmFn(), caches the result, and returns it.
|
|
12
|
+
* Returns undefined if llmFn() returns undefined or throws.
|
|
13
|
+
*
|
|
14
|
+
* @param db - SQLite database holding the LLM result cache.
|
|
15
|
+
* @param cacheKey - Stable identifier for this asset (typically its absolute path).
|
|
16
|
+
* @param body - The content being processed; its hash determines cache validity.
|
|
17
|
+
* @param reEnrich - When true the cache is bypassed and llmFn() is always called.
|
|
18
|
+
* @param llmFn - Async function that performs the actual LLM call.
|
|
19
|
+
* @param validate - Converts the raw parsed JSON back into the pass-specific type;
|
|
20
|
+
* returns undefined when the cached data is unusable.
|
|
21
|
+
*/
|
|
22
|
+
export async function withLlmCache(db, cacheKey, body, reEnrich, llmFn, validate) {
|
|
23
|
+
const bodyHash = computeBodyHash(body);
|
|
24
|
+
if (!reEnrich) {
|
|
25
|
+
try {
|
|
26
|
+
const cached = getLlmCacheEntry(db, cacheKey, bodyHash);
|
|
27
|
+
if (cached) {
|
|
28
|
+
const result = validate(JSON.parse(cached.resultJson));
|
|
29
|
+
if (result !== undefined)
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Cache corrupt — fall through
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const result = await llmFn();
|
|
38
|
+
if (result !== undefined) {
|
|
39
|
+
try {
|
|
40
|
+
upsertLlmCacheEntry(db, cacheKey, bodyHash, JSON.stringify(result));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Cache write failure is non-fatal
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
package/dist/indexer/matchers.js
CHANGED
|
@@ -1,193 +1,128 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in asset matchers for the akm file classification system.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - `extensionMatcher` (3) -- classifies any file by extension alone.
|
|
9
|
-
* Ensures every known file type is discoverable regardless of directory.
|
|
10
|
-
* - `directoryMatcher` (10) -- boosts specificity when an ancestor
|
|
11
|
-
* directory matches a known type name (e.g. `scripts/`, `agents/`).
|
|
12
|
-
* - `parentDirHintMatcher` (15) -- boosts specificity based on the
|
|
13
|
-
* immediate parent directory name.
|
|
14
|
-
* - `smartMdMatcher` (20 / 18 / 8 / 5) -- inspects markdown frontmatter
|
|
15
|
-
* and body content for agent/command signals; falls back to "knowledge"
|
|
16
|
-
* at specificity 5 when no signals are found. Command signals (`agent`
|
|
17
|
-
* frontmatter, `$ARGUMENTS`/`$1`-`$3` placeholders) return 18.
|
|
18
|
-
* - `wikiMatcher` (20) -- classifies any `.md` under `wikis/<name>/…` as
|
|
19
|
-
* `wiki`. Registered last so the later-wins tiebreaker beats agent at 20.
|
|
4
|
+
* Each private `classifyBy*` function encapsulates the classification logic for
|
|
5
|
+
* one heuristic. The public `*Matcher` exports compose those facts into the
|
|
6
|
+
* `MatchResult` shape expected by the rest of the indexer.
|
|
20
7
|
*/
|
|
8
|
+
import { defaultRendererRegistry } from "../core/asset-registry";
|
|
21
9
|
import { SCRIPT_EXTENSIONS } from "../core/asset-spec";
|
|
22
10
|
import { looksLikeWorkflow } from "../workflows/parser";
|
|
23
11
|
import { registerMatcher } from "./file-context";
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Private data
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const DIR_TYPE_MAP = [
|
|
16
|
+
{
|
|
17
|
+
dir: "scripts",
|
|
18
|
+
type: "script",
|
|
19
|
+
test: (ext) => SCRIPT_EXTENSIONS.has(ext),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
dir: "commands",
|
|
23
|
+
type: "command",
|
|
24
|
+
test: (ext) => ext === ".md",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
dir: "agents",
|
|
28
|
+
type: "agent",
|
|
29
|
+
test: (ext) => ext === ".md",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
dir: "knowledge",
|
|
33
|
+
type: "knowledge",
|
|
34
|
+
test: (ext) => ext === ".md",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
dir: "workflows",
|
|
38
|
+
type: "workflow",
|
|
39
|
+
test: (ext) => ext === ".md",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
dir: "memories",
|
|
43
|
+
type: "memory",
|
|
44
|
+
test: (ext) => ext === ".md",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
dir: "lessons",
|
|
48
|
+
type: "lesson",
|
|
49
|
+
test: (ext) => ext === ".md",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
dir: "vaults",
|
|
53
|
+
type: "vault",
|
|
54
|
+
test: (_, fileName) => fileName === ".env" || fileName.endsWith(".env"),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
dir: "tasks",
|
|
58
|
+
type: "task",
|
|
59
|
+
test: (ext) => ext === ".md",
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
const COMMAND_PLACEHOLDER_RE = /\$ARGUMENTS|\$[123]\b/;
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Private helpers
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
function matchDirectoryHint(dirName, ctx, specificity) {
|
|
67
|
+
if (dirName === "skills" && ctx.fileName === "SKILL.md") {
|
|
68
|
+
return { type: "skill", specificity };
|
|
69
|
+
}
|
|
70
|
+
for (const rule of DIR_TYPE_MAP) {
|
|
71
|
+
if (rule.dir === dirName && rule.test(ctx.ext, ctx.fileName)) {
|
|
72
|
+
return { type: rule.type, specificity };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function classifyByExtension(ctx) {
|
|
40
78
|
if (ctx.fileName === "SKILL.md" && !ctx.ancestorDirs.includes("wikis")) {
|
|
41
|
-
return { type: "skill", specificity: 25
|
|
79
|
+
return { type: "skill", specificity: 25 };
|
|
42
80
|
}
|
|
43
|
-
// Known script extensions (excluding .md, handled by smartMdMatcher)
|
|
44
81
|
if (SCRIPT_EXTENSIONS.has(ctx.ext)) {
|
|
45
|
-
return { type: "script", specificity: 3
|
|
82
|
+
return { type: "script", specificity: 3 };
|
|
46
83
|
}
|
|
47
84
|
return null;
|
|
48
85
|
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Directory-based matcher that boosts specificity when an ancestor
|
|
52
|
-
* directory segment from the stash root matches a known type name.
|
|
53
|
-
*
|
|
54
|
-
* The first matching type-like ancestor wins. This preserves intuitive
|
|
55
|
-
* behavior for nested stash layouts such as `agent-stash/agents/blog/foo.md`
|
|
56
|
-
* while still honoring earlier type roots like `commands/agents/foo.md`.
|
|
57
|
-
*/
|
|
58
|
-
export function directoryMatcher(ctx) {
|
|
59
|
-
const ext = ctx.ext;
|
|
86
|
+
function classifyByDirectory(ctx) {
|
|
60
87
|
for (const dir of ctx.ancestorDirs) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (dir === "skills" && ctx.fileName === "SKILL.md") {
|
|
65
|
-
return { type: "skill", specificity: 10, renderer: "skill-md" };
|
|
66
|
-
}
|
|
67
|
-
if (dir === "commands" && ext === ".md") {
|
|
68
|
-
return { type: "command", specificity: 10, renderer: "command-md" };
|
|
69
|
-
}
|
|
70
|
-
if (dir === "agents" && ext === ".md") {
|
|
71
|
-
return { type: "agent", specificity: 10, renderer: "agent-md" };
|
|
72
|
-
}
|
|
73
|
-
if (dir === "knowledge" && ext === ".md") {
|
|
74
|
-
return { type: "knowledge", specificity: 10, renderer: "knowledge-md" };
|
|
75
|
-
}
|
|
76
|
-
if (dir === "workflows" && ext === ".md") {
|
|
77
|
-
return { type: "workflow", specificity: 10, renderer: "workflow-md" };
|
|
78
|
-
}
|
|
79
|
-
if (dir === "memories" && ext === ".md") {
|
|
80
|
-
return { type: "memory", specificity: 10, renderer: "memory-md" };
|
|
81
|
-
}
|
|
82
|
-
if (dir === "vaults" && (ctx.fileName === ".env" || ctx.fileName.endsWith(".env"))) {
|
|
83
|
-
return { type: "vault", specificity: 10, renderer: "vault-env" };
|
|
84
|
-
}
|
|
88
|
+
const result = matchDirectoryHint(dir, ctx, 10);
|
|
89
|
+
if (result)
|
|
90
|
+
return result;
|
|
85
91
|
}
|
|
86
92
|
return null;
|
|
87
93
|
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Uses the immediate parent directory name as a hint. More specific than
|
|
91
|
-
* the ancestor-based directory matcher because the file might be nested
|
|
92
|
-
* several levels deep, yet its immediate parent can still carry strong
|
|
93
|
-
* naming conventions (e.g. `my-project/agents/planning.md`).
|
|
94
|
-
*/
|
|
95
|
-
export function parentDirHintMatcher(ctx) {
|
|
94
|
+
function classifyByParentDirHint(ctx) {
|
|
96
95
|
const { parentDir, ext, fileName } = ctx;
|
|
97
|
-
if (parentDir === "scripts" && SCRIPT_EXTENSIONS.has(ext)) {
|
|
98
|
-
return { type: "script", specificity: 15, renderer: "script-source" };
|
|
99
|
-
}
|
|
100
96
|
if (parentDir === "skills" && (fileName === "SKILL.md" || ext === ".md")) {
|
|
101
|
-
return { type: "skill", specificity: 15
|
|
102
|
-
}
|
|
103
|
-
if (parentDir === "agents" && ext === ".md") {
|
|
104
|
-
return { type: "agent", specificity: 15, renderer: "agent-md" };
|
|
105
|
-
}
|
|
106
|
-
if (parentDir === "commands" && ext === ".md") {
|
|
107
|
-
return { type: "command", specificity: 15, renderer: "command-md" };
|
|
108
|
-
}
|
|
109
|
-
if (parentDir === "knowledge" && ext === ".md") {
|
|
110
|
-
return { type: "knowledge", specificity: 15, renderer: "knowledge-md" };
|
|
111
|
-
}
|
|
112
|
-
if (parentDir === "workflows" && ext === ".md") {
|
|
113
|
-
return { type: "workflow", specificity: 15, renderer: "workflow-md" };
|
|
97
|
+
return { type: "skill", specificity: 15 };
|
|
114
98
|
}
|
|
115
|
-
|
|
116
|
-
return { type: "memory", specificity: 15, renderer: "memory-md" };
|
|
117
|
-
}
|
|
118
|
-
if (parentDir === "vaults" && (fileName === ".env" || fileName.endsWith(".env"))) {
|
|
119
|
-
return { type: "vault", specificity: 15, renderer: "vault-env" };
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
99
|
+
return matchDirectoryHint(parentDir, ctx, 15);
|
|
122
100
|
}
|
|
123
|
-
|
|
124
|
-
/** Pattern that matches OpenCode command placeholders in markdown body. */
|
|
125
|
-
const COMMAND_PLACEHOLDER_RE = /\$ARGUMENTS|\$[123]\b/;
|
|
126
|
-
/**
|
|
127
|
-
* Content-based matcher for `.md` files. Inspects frontmatter keys and body
|
|
128
|
-
* content to classify markdown as agent, command, or knowledge.
|
|
129
|
-
*
|
|
130
|
-
* Specificity levels:
|
|
131
|
-
* 20 -- agent-exclusive signals (`tools`, `toolPolicy`)
|
|
132
|
-
* 18 -- command content signals (`agent` frontmatter, `$ARGUMENTS`/`$1`-`$3`)
|
|
133
|
-
* 8 -- weak agent signal (`model` alone)
|
|
134
|
-
* 5 -- knowledge fallback (any unclassified `.md`)
|
|
135
|
-
*
|
|
136
|
-
* Command signals at 18 override directory hints (10/15) because the content
|
|
137
|
-
* unambiguously identifies a command template. Agent-exclusive signals at 20
|
|
138
|
-
* still win over command signals when both are present.
|
|
139
|
-
*/
|
|
140
|
-
export function smartMdMatcher(ctx) {
|
|
101
|
+
function classifyBySmartMd(ctx) {
|
|
141
102
|
if (ctx.ext !== ".md")
|
|
142
103
|
return null;
|
|
143
104
|
const body = ctx.content();
|
|
144
105
|
if (looksLikeWorkflow(body)) {
|
|
145
|
-
return { type: "workflow", specificity: 19
|
|
106
|
+
return { type: "workflow", specificity: 19 };
|
|
146
107
|
}
|
|
147
108
|
const fm = ctx.frontmatter();
|
|
148
109
|
if (fm) {
|
|
149
|
-
// Agent-exclusive indicators: toolPolicy or tools
|
|
150
|
-
// These return high specificity (20) to override everything else.
|
|
151
110
|
if ("toolPolicy" in fm || "tools" in fm) {
|
|
152
|
-
return { type: "agent", specificity: 20
|
|
111
|
+
return { type: "agent", specificity: 20 };
|
|
153
112
|
}
|
|
154
|
-
// Command signal: `agent` frontmatter key names a dispatch target.
|
|
155
|
-
// This is an OpenCode convention specific to commands.
|
|
156
113
|
if ("agent" in fm) {
|
|
157
|
-
return { type: "command", specificity: 18
|
|
114
|
+
return { type: "command", specificity: 18 };
|
|
158
115
|
}
|
|
159
116
|
}
|
|
160
|
-
// Command signal: body contains $ARGUMENTS or $1/$2/$3 placeholders.
|
|
161
|
-
// These are definitively command template patterns (OpenCode convention).
|
|
162
117
|
if (COMMAND_PLACEHOLDER_RE.test(body)) {
|
|
163
|
-
return { type: "command", specificity: 18
|
|
118
|
+
return { type: "command", specificity: 18 };
|
|
164
119
|
}
|
|
165
|
-
if (fm) {
|
|
166
|
-
|
|
167
|
-
// on commands too (OpenCode convention). Directory hints (10/15) win
|
|
168
|
-
// when the file lives in commands/, but model still classifies an .md
|
|
169
|
-
// as agent when no directory hint is present.
|
|
170
|
-
if ("model" in fm) {
|
|
171
|
-
return { type: "agent", specificity: 8, renderer: "agent-md" };
|
|
172
|
-
}
|
|
120
|
+
if (fm && "model" in fm) {
|
|
121
|
+
return { type: "agent", specificity: 8 };
|
|
173
122
|
}
|
|
174
|
-
|
|
175
|
-
return { type: "knowledge", specificity: 5, renderer: "knowledge-md" };
|
|
123
|
+
return { type: "knowledge", specificity: 5 };
|
|
176
124
|
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Classify any `.md` file that lives under `wikis/<name>/…` as `wiki`.
|
|
180
|
-
*
|
|
181
|
-
* Registered AFTER `smartMdMatcher` so the registered-later-wins tiebreaker
|
|
182
|
-
* puts wiki ahead of agent at specificity 20. That means a wiki page with
|
|
183
|
-
* agent-style frontmatter (e.g. `tools:`) still classifies as a wiki page,
|
|
184
|
-
* not an agent. That's intentional — the directory is the authoritative
|
|
185
|
-
* signal: files under `wikis/` are wiki content.
|
|
186
|
-
*
|
|
187
|
-
* Requires at least one path segment after `wikis/` (the wiki name) — a
|
|
188
|
-
* stray `.md` at the bare `wikis/` root is not a wiki page.
|
|
189
|
-
*/
|
|
190
|
-
export function wikiMatcher(ctx) {
|
|
125
|
+
function classifyByWiki(ctx) {
|
|
191
126
|
if (ctx.ext !== ".md")
|
|
192
127
|
return null;
|
|
193
128
|
const idx = ctx.ancestorDirs.indexOf("wikis");
|
|
@@ -195,10 +130,43 @@ export function wikiMatcher(ctx) {
|
|
|
195
130
|
return null;
|
|
196
131
|
if (idx + 1 >= ctx.ancestorDirs.length)
|
|
197
132
|
return null;
|
|
198
|
-
return { type: "wiki", specificity: 20
|
|
133
|
+
return { type: "wiki", specificity: 20 };
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Adapter: MatchFact → MatchResult
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
function toMatchResult(ctx, classify) {
|
|
139
|
+
const fact = classify(ctx);
|
|
140
|
+
if (!fact)
|
|
141
|
+
return null;
|
|
142
|
+
const renderer = defaultRendererRegistry.rendererNameFor(fact.type);
|
|
143
|
+
if (!renderer)
|
|
144
|
+
return null;
|
|
145
|
+
return {
|
|
146
|
+
type: fact.type,
|
|
147
|
+
specificity: fact.specificity,
|
|
148
|
+
renderer,
|
|
149
|
+
...(fact.meta ? { meta: fact.meta } : {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Public matchers (API unchanged)
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
export function extensionMatcher(ctx) {
|
|
156
|
+
return toMatchResult(ctx, classifyByExtension);
|
|
157
|
+
}
|
|
158
|
+
export function directoryMatcher(ctx) {
|
|
159
|
+
return toMatchResult(ctx, classifyByDirectory);
|
|
160
|
+
}
|
|
161
|
+
export function parentDirHintMatcher(ctx) {
|
|
162
|
+
return toMatchResult(ctx, classifyByParentDirHint);
|
|
163
|
+
}
|
|
164
|
+
export function smartMdMatcher(ctx) {
|
|
165
|
+
return toMatchResult(ctx, classifyBySmartMd);
|
|
166
|
+
}
|
|
167
|
+
export function wikiMatcher(ctx) {
|
|
168
|
+
return toMatchResult(ctx, classifyByWiki);
|
|
199
169
|
}
|
|
200
|
-
// ── Registration ────────────────────────────────────────────────────────────
|
|
201
|
-
/** All built-in matchers in registration order (later wins ties). */
|
|
202
170
|
const builtinMatchers = [
|
|
203
171
|
extensionMatcher,
|
|
204
172
|
directoryMatcher,
|
|
@@ -206,10 +174,6 @@ const builtinMatchers = [
|
|
|
206
174
|
smartMdMatcher,
|
|
207
175
|
wikiMatcher,
|
|
208
176
|
];
|
|
209
|
-
/**
|
|
210
|
-
* Register all built-in matchers with the file-context registry.
|
|
211
|
-
* Called once from the CLI entry point (or ensureBuiltinsRegistered).
|
|
212
|
-
*/
|
|
213
177
|
export function registerBuiltinMatchers() {
|
|
214
178
|
for (const matcher of builtinMatchers) {
|
|
215
179
|
registerMatcher(matcher);
|
|
@@ -33,11 +33,14 @@ import fs from "node:fs";
|
|
|
33
33
|
import path from "node:path";
|
|
34
34
|
import { stringify as yamlStringify } from "yaml";
|
|
35
35
|
import { parseAssetRef } from "../core/asset-ref";
|
|
36
|
+
import { concurrentMap } from "../core/concurrent";
|
|
36
37
|
import { parseFrontmatter, parseFrontmatterBlock } from "../core/frontmatter";
|
|
37
38
|
import { warn } from "../core/warn";
|
|
38
39
|
import { writeAssetToSource } from "../core/write-source";
|
|
39
40
|
import { resolveIndexPassLLM } from "../llm/index-passes";
|
|
40
|
-
import
|
|
41
|
+
import * as memoryInfer from "../llm/memory-infer";
|
|
42
|
+
import { withLlmCache } from "./llm-cache";
|
|
43
|
+
import { walkMarkdownFiles } from "./walker";
|
|
41
44
|
/**
|
|
42
45
|
* Frontmatter keys this pass cares about. Constants so a future rename only
|
|
43
46
|
* needs to touch one site.
|
|
@@ -60,7 +63,7 @@ const FM_SOURCE = "source";
|
|
|
60
63
|
* Both must allow the call for the pass to run. Either set to `false`
|
|
61
64
|
* short-circuits to a no-op result.
|
|
62
65
|
*/
|
|
63
|
-
export async function runMemoryInferencePass(config, sources, signal) {
|
|
66
|
+
export async function runMemoryInferencePass(config, sources, signal, db, reEnrich, onProgress, options = {}) {
|
|
64
67
|
const result = {
|
|
65
68
|
considered: 0,
|
|
66
69
|
splitParents: 0,
|
|
@@ -82,26 +85,75 @@ export async function runMemoryInferencePass(config, sources, signal) {
|
|
|
82
85
|
const primary = sources[0];
|
|
83
86
|
if (!primary)
|
|
84
87
|
return result;
|
|
85
|
-
const pending = collectPendingMemories(primary.path);
|
|
88
|
+
const pending = collectPendingMemories(primary.path).filter((record) => !options.candidateRefs || options.candidateRefs.has(record.ref));
|
|
86
89
|
result.considered = pending.length;
|
|
87
90
|
if (pending.length === 0)
|
|
88
91
|
return result;
|
|
89
|
-
|
|
92
|
+
let processed = 0;
|
|
93
|
+
const total = pending.length;
|
|
94
|
+
onProgress?.({ processed, total, writtenFacts: 0, skippedNoFacts: 0 });
|
|
95
|
+
const perRecordResults = await concurrentMap(pending, async (record) => {
|
|
90
96
|
if (signal?.aborted)
|
|
91
|
-
return
|
|
92
|
-
|
|
97
|
+
return undefined;
|
|
98
|
+
// Incremental cache: skip LLM call when body hash is unchanged and
|
|
99
|
+
// --re-enrich was not requested. The cache ref is the absolute file path.
|
|
100
|
+
const validate = (raw) => {
|
|
101
|
+
if (!raw || typeof raw !== "object")
|
|
102
|
+
return undefined;
|
|
103
|
+
const parsed = raw;
|
|
104
|
+
const title = typeof parsed.title === "string" ? parsed.title : "";
|
|
105
|
+
const description = typeof parsed.description === "string" ? parsed.description : "";
|
|
106
|
+
const content = typeof parsed.content === "string" ? parsed.content : "";
|
|
107
|
+
const tags = Array.isArray(parsed.tags) ? parsed.tags.filter((t) => typeof t === "string") : [];
|
|
108
|
+
const searchHints = Array.isArray(parsed.searchHints)
|
|
109
|
+
? parsed.searchHints.filter((h) => typeof h === "string")
|
|
110
|
+
: [];
|
|
111
|
+
if (title && description && content && tags.length > 0 && searchHints.length > 0) {
|
|
112
|
+
return { title, description, tags, searchHints, content };
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
};
|
|
116
|
+
const derived = db
|
|
117
|
+
? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
|
|
118
|
+
warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
|
|
119
|
+
}), validate)
|
|
120
|
+
: await memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
|
|
121
|
+
warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
|
|
122
|
+
});
|
|
93
123
|
if (!derived) {
|
|
94
|
-
|
|
95
|
-
// Intentionally NOT marked processed — a transient LLM failure should
|
|
96
|
-
// be retried on the next index run.
|
|
97
|
-
continue;
|
|
124
|
+
return { skipped: true };
|
|
98
125
|
}
|
|
99
126
|
const written = await writeDerivedMemory(record, derived);
|
|
100
127
|
if (written > 0) {
|
|
101
128
|
markParentProcessed(record);
|
|
129
|
+
return { skipped: false, splitParent: true, written };
|
|
130
|
+
}
|
|
131
|
+
return { skipped: false, splitParent: false, written: 0 };
|
|
132
|
+
},
|
|
133
|
+
// Default concurrency of 4 for cloud APIs. Set `llm.concurrency: 1`
|
|
134
|
+
// in config.json for local model servers (LM Studio, Ollama).
|
|
135
|
+
llmConfig.concurrency ?? 1);
|
|
136
|
+
for (let i = 0; i < perRecordResults.length; i++) {
|
|
137
|
+
const res = perRecordResults[i];
|
|
138
|
+
if (!res)
|
|
139
|
+
continue;
|
|
140
|
+
if (res.skipped) {
|
|
141
|
+
result.skippedNoFacts += 1;
|
|
142
|
+
// Intentionally NOT marked processed — a transient LLM failure should
|
|
143
|
+
// be retried on the next index run.
|
|
144
|
+
}
|
|
145
|
+
else if (res.splitParent) {
|
|
102
146
|
result.splitParents += 1;
|
|
103
|
-
result.writtenFacts += written;
|
|
147
|
+
result.writtenFacts += res.written;
|
|
104
148
|
}
|
|
149
|
+
processed++;
|
|
150
|
+
onProgress?.({
|
|
151
|
+
processed,
|
|
152
|
+
total,
|
|
153
|
+
writtenFacts: result.writtenFacts,
|
|
154
|
+
skippedNoFacts: result.skippedNoFacts,
|
|
155
|
+
currentRef: pending[i]?.ref,
|
|
156
|
+
});
|
|
105
157
|
}
|
|
106
158
|
return result;
|
|
107
159
|
}
|
|
@@ -155,24 +207,6 @@ export function isPendingMemory(frontmatter) {
|
|
|
155
207
|
return false;
|
|
156
208
|
return true;
|
|
157
209
|
}
|
|
158
|
-
function* walkMarkdownFiles(root) {
|
|
159
|
-
let entries;
|
|
160
|
-
try {
|
|
161
|
-
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
for (const entry of entries) {
|
|
167
|
-
const full = path.join(root, entry.name);
|
|
168
|
-
if (entry.isDirectory()) {
|
|
169
|
-
yield* walkMarkdownFiles(full);
|
|
170
|
-
}
|
|
171
|
-
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
172
|
-
yield full;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
210
|
function toMemoryName(memoriesDir, filePath) {
|
|
177
211
|
const rel = path.relative(memoriesDir, filePath);
|
|
178
212
|
if (!rel || rel.startsWith(".."))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const contributors = [];
|
|
2
|
+
let builtinsPromise;
|
|
3
|
+
async function ensureBuiltinMetadataContributorsRegistered() {
|
|
4
|
+
if (!builtinsPromise) {
|
|
5
|
+
builtinsPromise = (async () => {
|
|
6
|
+
await import("../output/renderers.js");
|
|
7
|
+
await import("../workflows/renderer.js");
|
|
8
|
+
})();
|
|
9
|
+
}
|
|
10
|
+
return builtinsPromise;
|
|
11
|
+
}
|
|
12
|
+
export function registerMetadataContributor(contributor) {
|
|
13
|
+
contributors.push(contributor);
|
|
14
|
+
}
|
|
15
|
+
export async function getMetadataContributors() {
|
|
16
|
+
await ensureBuiltinMetadataContributorsRegistered();
|
|
17
|
+
return [...contributors];
|
|
18
|
+
}
|
|
19
|
+
export async function applyMetadataContributors(entry, ctx) {
|
|
20
|
+
const activeContributors = await getMetadataContributors();
|
|
21
|
+
for (const contributor of activeContributors) {
|
|
22
|
+
if (!contributor.appliesTo(ctx))
|
|
23
|
+
continue;
|
|
24
|
+
contributor.contribute(entry, ctx);
|
|
25
|
+
}
|
|
26
|
+
}
|